带有OLED显示屏的Arduino复古游戏

曾经想知道编写自己的复古游戏需要多少工作? Pong为Arduino编写代码有多容易?和我一起,向我展示如何构建Arduino供电的迷你复古游戏机,以及如何从头开始编写Pong。这是最终结果:

构建计划

这是一个相当简单的电路。 电位器(电位器)将控制游戏,而Arduino将会驱动OLED显示屏。这将在面包板上生产,但是您可能希望将其制成永久性电路并将其安装在箱子中。我们之前曾经写过关于重新创建Pong的文章,但是今天我将向您展示如何从头开始编写代码,并分解每个部分。

您需要什么

这里是什么您需要:

  • 1 x Arduino(任何型号)
  • 1 x 10k电位器
  • 1 x 0.96英寸I2C OLED显示屏
  • 1 x面包板
  • 各种公头>公头连接线
  • 任何Arduino都可以使用,因此如果您不确定要购买哪种型号,请参阅我们的购买指南。

    这些OLED显示器非常酷。通常可以购买白色,蓝色,黄色或这三种的混合物。它们确实是全彩色的,但是它们又增加了该项目的复杂性和成本。

    电路

    这是一个非常简单的电路。如果您没有Arduino的丰富经验,请先查看这些初学者项目。

    在这里是:

    看锅的前面,将左引脚连接到 + 5V ,右引脚连接到接地。将中间引脚连接到模拟引脚0 (A0)。

    使用I2C协议连接OLED显示器。将 VCC GND 连接到Arduino + 5V 接地。将 SCL 连接到模拟五 A5 )。将 SDA 连接到模拟4 A4 )。它连接到模拟引脚的原因很简单。这些引脚包含I2C协议所需的电路。确保它们正确连接,并且没有交叉。确切的引脚会因型号而异,但是Nano和Uno会使用A4和A5。如果您未使用Arduino或Nano,请查看模型的Wire库文档。

    电位器测试

    上传此测试代码(请确保从中选择正确的电路板和端口工具>面板工具>端口菜单):

    void setup() {    // put your setup code here, to run once:    Serial.begin(9600); // setup serial}void loop() {    // put your main code here, to run repeatedly:    Serial.println(analogRead(A0)); // print the value from the pot    delay(500);}

    现在打开串行监视器(右上>串行监视器),然后转动电位器。您应该看到在串行监视器上显示的值。完全逆时针方向应为,完全逆时针方向应为 1023

    稍后您将对此进行调整,但是现在还可以。如果什么也没发生,或者您不做任何事情就改变了值,请断开连接并再次检查电路。

    OLED测试

    The OLED display is slightly more complex to configure. You need to install two libraries to drive the display first. Download the Adafruit_SSD1306 and Adafruit-GFX libraries from Github. Copy the files into your libraries folder. This varies depending on your operating system:

  • Mac OS: / Users / Username / Documents / Arduino /库
  • Linux: / home / Username / Sketchbook
  • Windows: / Users / Arduino / libraries
  • 现在上传测试草图。转到文件>示例> Adafruit SSD1306 > ssd1306_128x64_i2c 。这应该为您提供一个包含大量图形的大草图:

    如果上传后没有任何反应,请断开连接并再次检查您的连接。如果菜单中没有这些示例,则可能需要重新启动Arduino IDE。

    代码

    现在是时候编写代码了。我将解释每个步骤,所以如果您只想使其运行,请跳到最后。这是相当数量的代码,因此,如果您不确定,请查看以下10个免费资源以学习编码。

    首先包括必要的库:

    #include <SPI.h>#include <Wire.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h>

    SPI WIRE 是用于处理I2C通信的两个Arduino库。 Adafruit_GFX Adafruit_SSD1306 是您先前安装的库。

    Next, configure the display:

    Adafruit_SSD1306 display(4);

    然后设置运行游戏所需的所有变量:

    int resolution[2] = {128, 64}, ball[2] = {20, (resolution[1] / 2)};const int PIXEL_SIZE = 8, WALL_WIDTH = 4, PADDLE_WIDTH = 4, BALL_SIZE = 4, SPEED = 3;int playerScore = 0, aiScore = 0, playerPos = 0, aiPos = 0;char ballDirectionHori = 'R', ballDirectionVerti = 'S';boolean inProgress = true;

    这些存储了运行游戏所需的所有数据。其中一些存储球的位置,屏幕的大小,球员的位置等。请注意其中的一些是 const 的意思,它们是恒定的,并且永远不会改变。

    屏幕分辨率和焊球位置存储在数组中。数组是相似事物的集合,对于球,存储坐标( X Y )。访问数组中的元素很容易(不要在文件中包含此代码):

    resolution[1];

    由于数组从零开始,这将返回分辨率数组中的第二个元素( 64 >)。更新元素甚至更加容易(再次,不包括此代码):

    ball[1] = 15;

    Inside void setup(), configure the display:

    void setup()   {  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);  display.display();}

    第一行告诉Adafruit库您的显示器正在使用什么尺寸和通讯协议(在这种情况下, 128 x 64 I2C )。第二行( display.display())告诉屏幕显示缓冲区中存储的内容(无内容)。

    创建两个名为 drawBall eraseBall

    void drawBall(int x, int y) {  display.drawCircle(x, y, BALL_SIZE, WHITE);}void eraseBall(int x, int y) {  display.drawCircle(x, y, BALL_SIZE, BLACK);}

    这些取球的 x y 坐标并将其绘制在屏幕上使用显示库中的 drawCircle 方法。这使用了前面定义的常量 BALL_SIZE 。尝试更改此设置,看看会发生什么。此drawCircle方法接受像素颜色-黑色白色。因为这是单色显示(一种颜色),所以白色表示像素处于打开状态,黑色表示像素处于关闭状态。

    现在创建一种称为 moveAi 的方法:

    void moveAi() {  eraseAiPaddle(aiPos);  if (ball[1] > aiPos) {    ++aiPos;  }  else if (ball[1] < aiPos) {    --aiPos;  }  drawAiPaddle(aiPos);}

    此方法处理移动人工智能 AI 播放器。这是一个非常简单的计算机对手-如果球在桨上方,请向上移动。它在桨下面,向下移动。很简单,但是效果很好。增量和减量符号( ++ aiPos –aiPos )用于从aiPosition中添加或减去一个。您可以添加或减去更大的数字以使AI更快地移动,因此更难以克服。以下是您的操作方法:

    aiPos += 2;

    aiPos -= 2;

    加号减号是加法的简写或从aiPos的当前值中减去两个。这是另一种实现方法:

    aiPos = aiPos + 2;

    aiPos = aiPos - 1;

    请注意,此方法首先擦除划板,然后再次绘制。必须这样做。如果绘制了球拍的新位置,则屏幕上将有两个重叠的球拍。

    drawNet 方法使用两个循环绘制球网:

    void drawNet() {  for (int i = 0; i < (resolution[1] / WALL_WIDTH); ++i) {    drawPixel(((resolution[0] / 2) - 1), i * (WALL_WIDTH) + (WALL_WIDTH * i), WALL_WIDTH);  }}

    这使用 WALL_WIDTH 变量来设置大小。

    创建名为 drawPixels erasePixels 的方法。就像球形方法一样,两者之间的唯一区别是像素的颜色:

    void drawPixel(int posX, int posY, int dimensions) {  for (int x = 0; x < dimensions; ++x) {    for (int y = 0; y < dimensions; ++y) {      display.drawPixel((posX + x), (posY + y), WHITE);    }  }}void erasePixel(int posX, int posY, int dimensions) {  for (int x = 0; x < dimensions; ++x) {    for (int y = 0; y < dimensions; ++y) {      display.drawPixel((posX + x), (posY + y), BLACK);    }  }}

    再次,这两种方法都使用两个 for 循环绘制一组像素。循环不必使用库 drawPixel 方法绘制每个像素,而是根据给定的尺寸绘制一组像素。

    drawScore 方法使用库的文本功能将播放器和AI得分写入屏幕。它们存储在 playerScore aiScore 中:

    void drawScore() {  display.setTextSize(2);  display.setTextColor(WHITE);  display.setCursor(45, 0);  display.println(playerScore);  display.setCursor(75, 0);  display.println(aiScore);}

    此方法还具有 eraseScore 对应项,可将像素设置为黑色或关闭。

    最后四种方法非常相似。他们绘制并擦除了玩家和AI球拍:

    void erasePlayerPaddle(int row) {  erasePixel(0, row - (PADDLE_WIDTH * 2), PADDLE_WIDTH);  erasePixel(0, row - PADDLE_WIDTH, PADDLE_WIDTH);  erasePixel(0, row, PADDLE_WIDTH);  erasePixel(0, row + PADDLE_WIDTH, PADDLE_WIDTH);  erasePixel(0, row + (PADDLE_WIDTH + 2), PADDLE_WIDTH);}

    注意如何调用之前创建的 erasePixel 方法。这些方法会绘制并擦除适当的桨。

    主循环中还有更多逻辑。这是整个代码:

    #include <SPI.h>#include <Wire.h>#include <Adafruit_GFX.h>#include <Adafruit_SSD1306.h>Adafruit_SSD1306 display(4);int resolution[2] = {128, 64}, ball[2] = {20, (resolution[1] / 2)};const int PIXEL_SIZE = 8, WALL_WIDTH = 4, PADDLE_WIDTH = 4, BALL_SIZE = 4, SPEED = 3;int playerScore = 0, aiScore = 0, playerPos = 0, aiPos = 0;char ballDirectionHori = 'R', ballDirectionVerti = 'S';boolean inProgress = true;void setup()   {  display.begin(SSD1306_SWITCHCAPVCC, 0x3C);  display.display();}void loop() {  if (aiScore > 9 || playerScore > 9) {    // check game state    inProgress = false;  }  if (inProgress) {    eraseScore();    eraseBall(ball[0], ball[1]);    if (ballDirectionVerti == 'U') {      // move ball up diagonally      ball[1] = ball[1] - SPEED;    }    if (ballDirectionVerti == 'D') {      // move ball down diagonally      ball[1] = ball[1] + SPEED;    }    if (ball[1] <= 0) { // bounce the ball off the top ballDirectionVerti = 'D'; } if (ball[1] >= resolution[1]) {      // bounce the ball off the bottom      ballDirectionVerti = 'U';    }    if (ballDirectionHori == 'R') {      ball[0] = ball[0] + SPEED; // move ball      if (ball[0] >= (resolution[0] - 6)) {        // ball is at the AI edge of the screen        if ((aiPos + 12) >= ball[1] && (aiPos - 12) <= ball[1]) { // ball hits AI paddle if (ball[1] > (aiPos + 4)) {            // deflect ball down            ballDirectionVerti = 'D';          }          else if (ball[1] < (aiPos - 4)) {            // deflect ball up            ballDirectionVerti = 'U';          }          else {            // deflect ball straight            ballDirectionVerti = 'S';          }          // change ball direction          ballDirectionHori = 'L';        }        else {          // GOAL!          ball[0] = 6; // move ball to other side of screen          ballDirectionVerti = 'S'; // reset ball to straight travel          ball[1] = resolution[1] / 2; // move ball to middle of screen          ++playerScore; // increase player score        }      }    }    if (ballDirectionHori == 'L') {      ball[0] = ball[0] - SPEED; // move ball      if (ball[0] <= 6) { // ball is at the player edge of the screen if ((playerPos + 12) >= ball[1] && (playerPos - 12) <= ball[1]) { // ball hits player paddle if (ball[1] > (playerPos + 4)) {            // deflect ball down            ballDirectionVerti = 'D';          }          else if (ball[1] < (playerPos - 4)) { // deflect ball up ballDirectionVerti = 'U'; } else { // deflect ball straight ballDirectionVerti = 'S'; } // change ball direction ballDirectionHori = 'R'; } else { ball[0] = resolution[0] - 6; // move ball to other side of screen ballDirectionVerti = 'S'; // reset ball to straight travel ball[1] = resolution[1] / 2; // move ball to middle of screen ++aiScore; // increase AI score } } } drawBall(ball[0], ball[1]); erasePlayerPaddle(playerPos); playerPos = analogRead(A2); // read player potentiometer playerPos = map(playerPos, 0, 1023, 8, 54); // convert value from 0 - 1023 to 8 - 54 drawPlayerPaddle(playerPos); moveAi(); drawNet(); drawScore(); } else { // somebody has won display.clearDisplay(); display.setTextSize(4); display.setTextColor(WHITE); display.setCursor(0, 0); // figure out who if (aiScore > playerScore) {      display.println("YOU  LOSE!");    }    else if (playerScore > aiScore) {      display.println("YOU  WIN!");    }  }  display.display();}void moveAi() {  // move the AI paddle  eraseAiPaddle(aiPos);  if (ball[1] > aiPos) {    ++aiPos;  }  else if (ball[1] < aiPos) {    --aiPos;  }  drawAiPaddle(aiPos);}void drawScore() {  // draw AI and player scores  display.setTextSize(2);  display.setTextColor(WHITE);  display.setCursor(45, 0);  display.println(playerScore);  display.setCursor(75, 0);  display.println(aiScore);}void eraseScore() {  // erase AI and player scores  display.setTextSize(2);  display.setTextColor(BLACK);  display.setCursor(45, 0);  display.println(playerScore);  display.setCursor(75, 0);  display.println(aiScore);}void drawNet() {  for (int i = 0; i < (resolution[1] / WALL_WIDTH); ++i) {    drawPixel(((resolution[0] / 2) - 1), i * (WALL_WIDTH) + (WALL_WIDTH * i), WALL_WIDTH);  }}void drawPixel(int posX, int posY, int dimensions) {  // draw group of pixels  for (int x = 0; x < dimensions; ++x) {    for (int y = 0; y < dimensions; ++y) {      display.drawPixel((posX + x), (posY + y), WHITE);    }  }}void erasePixel(int posX, int posY, int dimensions) {  // erase group of pixels  for (int x = 0; x < dimensions; ++x) {    for (int y = 0; y < dimensions; ++y) {      display.drawPixel((posX + x), (posY + y), BLACK);    }  }}void erasePlayerPaddle(int row) {  erasePixel(0, row - (PADDLE_WIDTH * 2), PADDLE_WIDTH);  erasePixel(0, row - PADDLE_WIDTH, PADDLE_WIDTH);  erasePixel(0, row, PADDLE_WIDTH);  erasePixel(0, row + PADDLE_WIDTH, PADDLE_WIDTH);  erasePixel(0, row + (PADDLE_WIDTH + 2), PADDLE_WIDTH);}void drawPlayerPaddle(int row) {  drawPixel(0, row - (PADDLE_WIDTH * 2), PADDLE_WIDTH);  drawPixel(0, row - PADDLE_WIDTH, PADDLE_WIDTH);  drawPixel(0, row, PADDLE_WIDTH);  drawPixel(0, row + PADDLE_WIDTH, PADDLE_WIDTH);  drawPixel(0, row + (PADDLE_WIDTH + 2), PADDLE_WIDTH);}void drawAiPaddle(int row) {  int column = resolution[0] - PADDLE_WIDTH;  drawPixel(column, row - (PADDLE_WIDTH * 2), PADDLE_WIDTH);  drawPixel(column, row - PADDLE_WIDTH, PADDLE_WIDTH);  drawPixel(column, row, PADDLE_WIDTH);  drawPixel(column, row + PADDLE_WIDTH, PADDLE_WIDTH);  drawPixel(column, row + (PADDLE_WIDTH * 2), PADDLE_WIDTH);}void eraseAiPaddle(int row) {  int column = resolution[0] - PADDLE_WIDTH;  erasePixel(column, row - (PADDLE_WIDTH * 2), PADDLE_WIDTH);  erasePixel(column, row - PADDLE_WIDTH, PADDLE_WIDTH);  erasePixel(column, row, PADDLE_WIDTH);  erasePixel(column, row + PADDLE_WIDTH, PADDLE_WIDTH);  erasePixel(column, row + (PADDLE_WIDTH * 2), PADDLE_WIDTH);}void drawBall(int x, int y) {  display.drawCircle(x, y, BALL_SIZE, WHITE);}void eraseBall(int x, int y) {  display.drawCircle(x, y, BALL_SIZE, BLACK);}

    这是您最终得到的结果:

    一旦您对代码充满信心,就可以进行许多修改:

  • 向球或AI中添加一些随机运动。
  • 为两个玩家添加另一个底池。
  • 添加一个暂停按钮。
  • 现在看看这些复古游戏Pi Zero项目。

    您是否使用此代码对Pong进行了编码?您做了哪些修改?在下面的评论中让我知道,我希望能看到一些图片!

    标签: Arduino 电子产品 复古游戏