曾經(jīng)想知道編寫自己的復(fù)古游戲需要多少工作? Pong為Arduino編寫代碼有多容易?和我一起,向我展示如何構(gòu)建Arduino供電的迷你復(fù)古游戲機,以及如何從頭開始編寫Pong。最終結(jié)果如下:
構(gòu)建計劃
這是一個相當(dāng)簡單的電路。 電位器(電位器)將控制游戲,而Arduino將會驅(qū)動OLED顯示屏。這將在面包板上生產(chǎn),但是您可能希望將其制成永久性電路并將其安裝在箱子中。之前我們已經(jīng)寫過有關(guān)重新創(chuàng)建Pong的文章,但是今天我將向您展示如何從頭開始編寫代碼,并分解每個部分。
您需要的內(nèi)容
這是您需要的:
1 x Arduino(任何型號)
1 x 10k電位器
1 x 0.96英寸I2C OLED顯示屏
1 x面包板
各種公頭》公連接線
任何Arduino都應(yīng)該工作,請查看我們的購買指南如果您不確定要購買哪種型號。
這些OLED顯示器非???。通常可以購買白色,藍色,黃色或這三種的混合物。它們確實是全彩色的,但是它們又增加了該項目的復(fù)雜性和成本。
電路
這是一個非常簡單的電路。如果您對Arduino沒有太多的經(jīng)驗,請先查看這些初學(xué)者項目。
在這里是:
在鍋的前面,將左引腳連接到 + 5V ,將右引腳連接到接地。將中間引腳連接到模擬引腳0 (A0)。
使用I2C協(xié)議連接OLED顯示器。將 VCC 和 GND 連接到Arduino + 5V 和接地。將 SCL 連接到模擬五( A5 )。將 SDA 連接到模擬4 ( A4 )。它連接到模擬引腳的原因很簡單。這些引腳包含I2C協(xié)議所需的電路。確保它們正確連接,并且沒有交叉。確切的引腳會因型號而異,但是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);
}
現(xiàn)在打開串行監(jiān)視器(右上》 串行監(jiān)視器)并轉(zhuǎn)動鍋。您應(yīng)該看到在串行監(jiān)視器上顯示的值。完全逆時針應(yīng)為零,完全逆時針應(yīng)為 1023 :
您稍后會對此進行調(diào)整,但現(xiàn)在就可以了。如果什么也沒有發(fā)生,或者您不做任何事情就改變了值,請斷開并仔細檢查電路。
OLED測試
OLED顯示的配置稍微復(fù)雜一些。您需要安裝兩個庫才能首先驅(qū)動顯示。從Github下載Adafruit_SSD1306和Adafruit-GFX庫。將文件復(fù)制到您的庫文件夾中。這取決于您的操作系統(tǒng):
Mac OS:/用戶/用戶名/Documents/Arduino/libraries
Linux:/home/Username/Sketchbook
Windows:/Users/Arduino/libraries
現(xiàn)在上傳測試草圖。轉(zhuǎn)到文件》 示例》 Adafruit SSD1306 》 ssd1306_128x64_i2c 。這應(yīng)該給您一個包含大量圖形的大草圖:
如果上傳后沒有任何反應(yīng),請斷開連接并再次檢查您的連接。如果示例不在菜單中,則可能需要重新啟動Arduino IDE。
代碼
現(xiàn)在是時候編寫代碼了。我將解釋每個步驟,所以如果您只想使其運行,請?zhí)阶詈蟆_@是相當(dāng)數(shù)量的代碼,因此,如果您不確定,請查看以下10個免費資源以學(xué)習(xí)編碼。
首先包括必要的庫:
#include
#include
#include
#include
SPI 和 WIRE 是用于處理I2C通信的兩個Arduino庫。 Adafruit_GFX 和 Adafruit_SSD1306 是您先前安裝的庫。
下一步,配置顯示:
Adafruit_SSD1306 display(4);
然后設(shè)置運行游戲所需的所有變量:
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;
這些變量存儲運行游戲所需的所有數(shù)據(jù)。其中一些存儲球的位置,屏幕的大小,球員的位置等。請注意其中的一些是 const 的意思,它們是恒定的,并且永遠不會改變。
屏幕分辨率和焊球位置存儲在數(shù)組中。數(shù)組是相似事物的集合,對于球,存儲坐標( X 和 Y )。訪問數(shù)組中的元素很容易(不要在文件中包含此代碼):
resolution[1];
由于數(shù)組從零開始,因此將返回分辨率數(shù)組中的第二個元素( 64 )。更新元素甚至更容易(同樣,不包含此代碼):
ball[1] = 15;
在 void setup()內(nèi),配置顯示:/p》 void setup() {
display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
display.display();
}
第一行告訴Adafruit庫,您的顯示器正在使用什么尺寸和通訊協(xié)議(在這種情況下,為 128 x 64 和 I2C )。第二行( display.display())告訴屏幕顯示緩沖區(qū)中存儲的內(nèi)容(無內(nèi)容)。
創(chuàng)建兩個名為 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 。嘗試更改此設(shè)置,看看會發(fā)生什么。此drawCircle方法接受像素顏色-黑色或白色。因為這是單色顯示(一種顏色),所以白色表示像素處于打開狀態(tài),黑色表示像素處于關(guān)閉狀態(tài)。
現(xiàn)在創(chuàng)建一種稱為 moveAi 的方法:
void moveAi() {
eraseAiPaddle(aiPos);
if (ball[1] 》 aiPos) {
++aiPos;
}
else if (ball[1] 《 aiPos) {
--aiPos;
}
drawAiPaddle(aiPos);
}
此方法處理移動人工智能或 AI 播放器。這是一個非常簡單的計算機對手-如果球在槳上方,請向上移動。它在槳下面,向下移動。很簡單,但是效果很好。增量和減量符號( ++ aiPos 和 –aiPos )用于從aiPosition中添加或減去一個。您可以添加或減去更大的數(shù)字以使AI更快地移動,因此更難以克服。這樣做的方法如下:
aiPos += 2;
并且:
aiPos -= 2;
加號等于和負號符號是aiPos當(dāng)前值加/減兩個的簡寫。這是另一種方法:
aiPos = aiPos + 2;
和
aiPos = aiPos - 1;
注意此方法如何首先擦除槳,并且然后再次繪制。必須這樣做。如果繪制了新的槳葉位置,則屏幕上將有兩個重疊的槳葉。
drawNet 方法使用兩個循環(huán)繪制球網(wǎng):
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 變量來設(shè)置其大小。
創(chuàng)建名為 drawPixels 和的方法擦除像素。就像球形方法一樣,兩者之間的唯一區(qū)別是像素的顏色:
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);
}
}
}
再次,這兩種方法都使用兩個 》循環(huán)繪制一組像素。循環(huán)不必使用庫 drawPixel 方法繪制每個像素,而是根據(jù)給定的尺寸繪制一組像素。
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 對應(yīng),將像素設(shè)置為黑色或關(guān)閉。
最后四種方法非常相似。他們繪制并擦除了玩家和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);
}
注意他們?nèi)绾握{(diào)用之前創(chuàng)建的 erasePixel 方法。這些方法會繪制并擦除適當(dāng)?shù)臉?/p>
主循環(huán)中還有更多邏輯。這是完整的代碼:
#include
#include
#include
#include
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);
}
這是您最終得到的結(jié)果:
對代碼很有信心,您可以進行許多修改:
添加難度級別菜單(更改AI和球速)。
向其中添加一些隨機移動
為兩個玩家添加另一個底池。
添加一個暫停按鈕。
現(xiàn)在看看這些復(fù)古游戲Pi Zero項目。
責(zé)任編輯:wv
-
游戲機
+關(guān)注
關(guān)注
9文章
299瀏覽量
33412 -
Arduino
+關(guān)注
關(guān)注
187文章
6464瀏覽量
186681
發(fā)布評論請先 登錄
相關(guān)推薦
評論