更新於 2023/06/03閱讀時間約 49 分鐘

【TBoy】無線連線,雙人對戰!重現Pong遊戲新面貌

Hello!大家好我是茄諾,今天要來幫自主研發的掌機TBoy寫款可以連線對戰的遊戲,不知道TBoy的朋友可以連至以下網址看看:
因為要連線對戰,所以需要準備兩台TBoy掌機,請參考以上文章製作;接下來要挑選適合連線對戰的遊戲,個人偏愛Pong,這款由Atari開發的投幣式大型電玩對戰遊戲非常適合與家人朋友一起同樂,以下擷取自ChatGPT針對Pong所作的簡介:
Pong是電子遊戲的先驅,由Atari公司於1972年推出,是艾倫·艾爾柯尼設計。這款遊戲簡單而吸引人,它模仿了桌面乒乓球,玩家操作垂直移動的槓,反射來回移動的球,以防止它進入自己的得分區。Pong的簡單設計和直觀的遊戲規則使它迅速取得成功,開創了電子遊戲行業的全新紀元。它不僅改變了人們的娛樂方式,而且也開啟了電子遊戲的商業可能性。

關於Pong玩法整理
也請ChatGPT幫我們將Pong的玩法整理出來了:
1. 遊戲場地 Pong遊戲的場地是一個矩形,有兩個玩家,一個在上方,一個在下方。每個玩家都有一個橫條(也稱為”板子”或”球拍”),可在水平方向移動。
2. 遊戲物件:遊戲中的主要物件是一個小球,它會在遊戲場地內以一定的速度移動,並且在撞到邊界或玩家的橫條時反彈。
3. 玩家控制:玩家可以左右移動他們的橫條,目的是讓小球反彈回對方的半場。玩家不能超過遊戲場地的邊界。
4. 得分規則:當一方玩家未能成功用橫條接住小球,使其反彈回對方半場時,對方就會得分。也就是說,如果小球碰到你的場地邊界,對方就獲得一分。獲得一定分數的玩家(例如10分)將獲得勝利。
5. 物理規則:小球撞擊到橫條的不同位置會產生不同的反彈角度。例如,如果小球撞到橫條的邊緣,它將以較大的角度反彈,如果小球撞到橫條的中心,它將幾乎垂直反彈。
6. 遊戲流程 遊戲開始時,小球從場地中央或隨機的一個點發出,向任一方向移動。遊戲繼續進行,直到一名玩家贏得遊戲,然後開始新的一局。
以上就是Pong遊戲的基本規則和玩法。為了讓遊戲更有趣,你也可以加入更多的元素,比如變化小球的速度,或者讓橫條有不同的大小和移動速度等。

兩台TBoy掌機互相通訊方式
TBoy使用ESP32微控制器作為核心晶片,可以使用多種方法來與另一個ESP32進行通訊。以下是一些常見的方式:
  1. Wi-Fi: ESP32支援Wi-Fi通訊,可以設定為Access Point模式或Station模式。在Access Point模式下,一個ESP32可以創建自己的Wi-Fi網絡,讓其他的ESP32連接。在Station模式下,ESP32可以連接到現有的Wi-Fi網絡。透過Wi-Fi,兩個或更多的ESP32可以互相傳送資料。
  2. 藍牙: ESP32支援藍牙和藍牙低能耗(BLE)通訊。藍牙是一種短距離無線通訊技術,適用於需要低功耗的應用。
  3. 串列通訊(Serial Communication): 如果兩個ESP32在物理上足夠接近,它們可以使用串列通訊(如UART,SPI,或I2C)來互相傳送資料。這需要硬體連線,例如使用跳線來連接兩個ESP32的相應引腳。
  4. LoRa: 如果你需要長距離通訊,你可以考慮使用LoRa模塊。LoRa是一種長距離無線通訊技術,適合於低功耗的物聯網應用。請注意,這需要額外的LoRa模塊才能實現。
Pong遊戲的通訊方式是使用Wi-Fi的進行溝通。

Github原始碼下載

關於Pong遊戲的架構與檔案說明

安裝套件
  • Bodmer/TFT_eSPI

Pong遊戲主程式
以下列出Pong遊戲主程式,程式碼內以寫上詳細註解
  • PongOnline.ino
//========================================================
// 功能:Pong連線遊戲 - 主程式.
//
//========================================================
#include <TFT_eSPI.h>
#include <SPI.h>

#include "Free_Fonts.h"
#include "esp32_wifi_tcp.h"
#include "Joystick.h"
// 0:FPS.
// 1:計算FPS.
// 2:閃爍字.
#include "ClockSystem.h"

#define PADDLE_WIDTH 32           // 板子寬長.
#define PADDLE_HEIGHT 10

#define BALL_WIDTH 4              // 球寬長.
#define BALL_HEIGHT 4

#define WIN_SCORE 10              // 贏幾局結束遊戲.

// 遊戲模式.
typedef enum{
 GAME_SELECT=0,    // 選1P、2P.  
 GAME_CONNECTING,  // 等待連線.
 GAME_BEGIN,       // 準備開始遊戲.
 GAME_PLAY,        // 開始遊戲.
 GAME_OVER         // 遊戲結束.
} GameMode;

// box結構.
typedef struct {
   int x;
   int y;
   int w;
   int h;    
} Box;

// 球結構.
typedef struct {
   double x;
   double y;
   double prevX;
   double prevY;
   int w;
   int h;        
} Ball;

// 建立wifi物件.
esp32_wifi_tcp tcp( true, "ESP32-AP", "12345678");
// SPI.
TFT_eSPI tft = TFT_eSPI();
// 備頁.
TFT_eSprite doubleBuffer = TFT_eSprite(&tft);

// 搖桿1P.
Joystick joystick_1P(2, 15, 13, 12, 32, 33, 35, 0);
// 時脈系統.
ClockSystem clockSystem;

// 板子(1P、2P).
Box board1P;
Box board2P;
// 球.
Ball ball;

// fps.
uint8_t fps=0;
uint8_t fpsTemp=0;

// 是否為Server模式(1P).
bool isServer = true;
// 閃爍字.
bool flashFont = true;

// 分數.
uint8_t score1P = 0;
uint8_t score2P = 0;
// 球-移動速度.
double speed = 2.0;
// 球-設定初始反射角度.
double reflection_angle = 45.0;  

// 行進時間(時間越長球速越快).
int travelTime = 0;
// 無敵.
bool invincible= false;
// 自動玩遊戲.
bool autoPlay = false;
// 板子移動速度.
int boardSpeed = 4;

// 字串.
char buf[64];

// 設定初始狀態.
GameMode gameMode = GAME_SELECT;

//-----------------------------------------------------
// 拆解字串.
//-----------------------------------------------------
void split(const char *data, char separator, char result[][100], int *resultCount, int maxResultCount) {
 int len = strlen(data);
 int start = 0;
 int end = 0;
 *resultCount = 0;
 for (int i = 0; i < len; i++) {
   if (data[i] == separator) {
     end = i;
     int length = end - start;
     if (length > 0) {
       strncpy(result[*resultCount], &data[start], length);
       result[*resultCount][length] = '\0';
       (*resultCount)++;
       if (*resultCount >= maxResultCount) {
         break;
       }
     }
     start = i + 1;
   }
 }  
 if (*resultCount < maxResultCount) {
   int length = len - start;
   if (length > 0) {
     strncpy(result[*resultCount], &data[start], length);
     result[*resultCount][length] = '\0';
     (*resultCount)++;
   }
 }
}

//-----------------------------------------------------
// 判斷球與板子碰撞.
//-----------------------------------------------------
bool didCollide(Box p, Ball b){
 // 首先檢查球是否在板子的左邊或右邊之外,如果是,則不可能發生碰撞
 if ((b.x + BALL_WIDTH / 2.0) < p.x || (b.x - BALL_WIDTH / 2.0) > (p.x + PADDLE_WIDTH)) {
   return false;
 }
 // 檢查球是否在板子的上邊或下邊之外,如果是,則不可能發生碰撞
 if ((b.y + BALL_HEIGHT / 2.0) < p.y || (b.y - BALL_HEIGHT / 2.0) > (p.y + PADDLE_HEIGHT)) {
   return false;
 }
 // 如果以上條件都不成立,則表示球與板子有重疊,即發生了碰撞
 return true;
}

//-----------------------------------------------------
// 更新球移動.
//-----------------------------------------------------
void updateBall(Ball* position, double* speed, double* reflection_angle) {
   // 遊戲中才進入處理球移動.
   if(gameMode != GAME_PLAY)
     return;
   
   // 計算球在x軸和y軸上的速度成分
   double dx = *speed * cos(*reflection_angle);
   double dy = *speed * sin(*reflection_angle);

   // 備份球舊座標(判斷碰撞用).
   position->prevX = position->x;
   position->prevY = position->y;

   // 更新球的位置
   position->x += dx;
   position->y += dy;

   // 檢查是否碰到邊界,並反彈
   if (position->x < 4  || position->x > 127) {
     // 球碰到左右邊界,將反射角度反轉
     *reflection_angle = M_PI - *reflection_angle;
   }
   if (position->y < 0 || position->y > 240) {
     // 球碰到上下邊界,將反射角度反轉
     *reflection_angle = -(*reflection_angle);
     // 不是無敵才進入執行.
     if(!invincible){
       // 1P加分.
       if(position->y < 0){
         score1P++;        
       // 2P加分.
       }else if(position->y > 240){
         score2P++;
       }  
       // 初始球位置.
       position->x = position->prevX = 70;
       position->y = position->prevY = 120;
       
       // 放慢球速.
       *speed = 2.0;
       //Serial.println((String)position->x+":"+position->y);      
     }      
   }
}

//-----------------------------------------------------
// 更新畫面.
//-----------------------------------------------------
void updateScreen(){
 // 牆壁-左.
 doubleBuffer.fillRect (  0, 0,  4,  240, 0xFFFF);    
 // 牆壁-右.
 doubleBuffer.fillRect (131, 0,  4,  240, 0xFFFF);
 // 牆壁-中.      
 for(int i=4; i<131; i+=8){
   doubleBuffer.fillRect (i, 118, 4,  4, 0xFFFF);
 }
 // 顯示分數.
 doubleBuffer.setFreeFont(FSB18);
 doubleBuffer.setTextColor(TFT_WHITE, TFT_BLACK);      
 sprintf(buf, "%d", score2P);
 doubleBuffer.drawCentreString(buf, 105,  80, GFXFF); // 2P.
 sprintf(buf, "%d", score1P);
 doubleBuffer.drawCentreString(buf, 105, 135, GFXFF); // 1P.  

 // 板子1P.
 doubleBuffer.fillRect ( board1P.x, board1P.y, board1P.w,  board1P.h, 0xFFFF);
 // 板子2P.
 doubleBuffer.fillRect ( board2P.x, board2P.y, board2P.w,  board2P.h, 0xFFFF);   
}

//-----------------------------------------------------
// 顯示連線訊息.
//-----------------------------------------------------
void connecting(){
 // 用全黑清除螢幕
 doubleBuffer.fillScreen(TFT_BLACK);
 doubleBuffer.setFreeFont(FSB9);
 doubleBuffer.setTextColor(TFT_WHITE, TFT_RED);
 doubleBuffer.drawCentreString("connecting...", 66, 40, GFXFF);
 // 更新畫面.
 updateScreen();  
}

//-----------------------------------------------------
// 板子超界修正.
//-----------------------------------------------------
void boardOverstep(){  
 if(board1P.x<4)
   board1P.x = 4;          
 if(board2P.x<4)
   board2P.x = 4;        
 if(board1P.x>99)
   board1P.x = 99;          
 if(board2P.x>99)
   board2P.x = 99;
}

//-----------------------------------------------------
// 移動板子.
//-----------------------------------------------------
void boardMove(){
 // 左.
 if (joystick_1P.GetButtonLeft(false)) {
   // 1P.
   if(isServer){
     board1P.x -= boardSpeed;
   // 2P.
   }else{
     board2P.x -= boardSpeed;
   }
   
 // 右.
 }else if (joystick_1P.GetButtonRight(false)) {
   // 1P.
   if(isServer){
     board1P.x += boardSpeed;
   // 2P.
   }else{
     board2P.x += boardSpeed;
   }        

 // 開關無敵.
 }else if (joystick_1P.GetButtonStart(true)) {
   invincible = !invincible;
   
 // 開關自動玩.
 }else if (joystick_1P.GetButtonSelect(true)) {
   autoPlay = !autoPlay;
       
 }

 // 快速移動板子.
 if (joystick_1P.GetButtonA(false)){
   boardSpeed = 12;
 }else{
   boardSpeed = 4;
 }
 
 // 板子超界修正.
 boardOverstep();  
}

//-----------------------------------------------------
// 1P、2P相互溝通.
//-----------------------------------------------------
void communicate(){
 // Server模式(1P).
 if(isServer){
   // 【Server傳送指令給Client】1P板子座標 - c1|x|bx|by|score1P|score2P|gameMode|
   sprintf(buf, "c1|%d|%f|%f|%d|%d|%d|", board1P.x, ball.x, ball.y, score1P, score2P, gameMode);
   tcp.send(buf);
         
   // 更新球.
   updateBall(&ball, &speed, &reflection_angle);
     
   // 如果球與板子發生碰撞就處理反彈.       
   if( didCollide(board1P, ball) || didCollide(board2P, ball)){
     // 反彈球.
     reflection_angle = -(reflection_angle);
     // 復原座標.
     ball.x = ball.prevX; ball.y = ball.prevY;
     // 開球時放慢速度,等碰到板子後恢復正常速度.
     if(speed == 2.0) speed = 4.0;
   }    
   //Serial.println(str);

   // 自動玩.
   if(autoPlay){
     board1P.x = ball.x -(PADDLE_WIDTH - BALL_WIDTH)/2;
   }
          
 // Client模式(2P).
 }else{
   //【Client傳送指令給Server】2P板子座標 - s1|x|
   sprintf(buf, "s1|%d|", board2P.x);
   tcp.send(buf);

   // 自動玩.
   if(autoPlay){
     board2P.x = ball.x-(PADDLE_WIDTH - BALL_WIDTH)/2;
   }    
 }
 // 板子超界修正.
 boardOverstep();  
}

//-----------------------------------------------------
// 重新開始遊戲.
//-----------------------------------------------------
void resetGame(){
 // 球.
 ball.x = ball.prevX = 70; ball.y = ball.prevY = 120;
 // 分數.
 score1P = 0;
 score2P = 0;
 // 球-移動速度.
 speed = 2.0;
 // 行進時間(時間越長球速越快).
 travelTime = 0;
 // 板子移動速度.
 boardSpeed = 4;  
}

//-----------------------------------------------------
// 解指令字串.
//-----------------------------------------------------
void decodingReceived(String received){
 char result[10][100];   // 用來存放分割後的字串,最多容納 10 個字串,每個字串長度最多 100 個字元.
 int resultCount = 0;    // 儲存分割後的字串數量.
 int n = 0;
 double d = 0.0;
 
 if(received=="")
   return;
   
 // 需要多一個字元來容納結尾的 null 字元
 char charArray[received.length() + 1];  

 // 將 String 轉換為 char 字元陣列
 received.toCharArray(charArray, sizeof(charArray));
 // 解指令字串.
 split( charArray, '|', result, &resultCount, 10);

 //【Client(2p)傳送指令給Server(1p)】板子座標.
 // 指令格式:s1|x|
 if(String(result[0])=="s1"){
   n = String(result[1]).toInt();
   board2P.x = n;
   
 //【Server(1p)傳送指令給Client(2p)】板子座標、球資料.
 // 指令格式:c1|x|bx|by|score1P|score2P|gameMode|
 }else if(String(result[0])=="c1"){
   // x.
   n = String(result[1]).toInt();
   board1P.x = n;
   // bx.
   d = String(result[2]).toDouble();
   ball.x = d;    
   // by.
   d = String(result[3]).toDouble();
   ball.y = d;        
   // score1P.
   n = String(result[4]).toInt();
   score1P = n;
   // score2P.
   n = String(result[5]).toInt();
   score2P = n;
   // gameMode.
   n = String(result[6]).toInt();
   gameMode = (GameMode)n;   
 }  
}

//-----------------------------------------------------
// 初始.
//-----------------------------------------------------
void setup() {
 Serial.begin(115200);
 
 // 初始化LCD
 tft.begin();                  
 tft.setSwapBytes(true);

 // 螢幕方向(0:正 2:反).
 tft.setRotation(0);

 // 初始時脈系統.
 clockSystem.initClock();

 // 建立備頁(精靈).
 doubleBuffer.setColorDepth(16);
 doubleBuffer.createSprite(135, 240);      
   
 // 板子1P.
 board1P.x = 54; board1P.y =  16; board1P.w = PADDLE_WIDTH;board1P.h = PADDLE_HEIGHT;
 // 板子2P.
 board2P.x = 54; board2P.y = 216; board2P.w = PADDLE_WIDTH;board2P.h = PADDLE_HEIGHT;
 // 球.  
 ball.x = ball.prevX = 70; ball.y = ball.prevY = 120;
 ball.w = BALL_WIDTH; ball.h = BALL_HEIGHT;
}

//-----------------------------------------------------
// 主迴圈.
//-----------------------------------------------------
void loop() {  
 // 計算FPS.
 if (clockSystem.checkClock(1, 1000)) {
   fps=fpsTemp;
   fpsTemp=0;
   travelTime++;   
 }

 // FPS 60.
 if (clockSystem.checkClock(0, 17)) {
   fpsTemp++;
       
   // 解指令字串.
   decodingReceived(tcp.receive());    
   // 清除螢幕.
   doubleBuffer.fillScreen(TFT_BLACK);
   
   // 選1P、2P.
   if(gameMode == GAME_SELECT){
     //--------------------------------------------
     // 顯示.
     //--------------------------------------------    
     // 更新畫面.
     updateScreen();
     
     // 顯示字.
     doubleBuffer.setFreeFont(FSSBO24);
     doubleBuffer.setTextColor(TFT_WHITE, TFT_BLACK);
     doubleBuffer.drawCentreString("1P", 50,  35, GFXFF);
     doubleBuffer.drawCentreString("2P", 50, 160, GFXFF);
   
     // 顯示選到的模式(1P Server , 2P Client).
     doubleBuffer.setTextColor(TFT_WHITE, TFT_RED);
     if(isServer)
       doubleBuffer.drawCentreString("1P", 50,  35, GFXFF);
     else
       doubleBuffer.drawCentreString("2P", 50, 160, GFXFF);
   
     //--------------------------------------------
     // 輸入.
     //--------------------------------------------
     // 上.
     if (joystick_1P.GetButtonUp(true)) {
       isServer = true;
     // 下.
     }else if (joystick_1P.GetButtonDown(true)) {
       isServer = false;
     }
     // A.
     if (joystick_1P.GetButtonA(true)||joystick_1P.GetButtonB(true)){

       // 連線訊息.
       connecting();
       // 更新備頁.
       doubleBuffer.pushSprite(0, 0, 0x07E0);

       // 進入等待連線.
       gameMode = GAME_CONNECTING;
       
       // 設定建立Server或Client.
       tcp.isServer = isServer;
       tcp.begin();
     }    

   // 等待連線.
   }else if(gameMode == GAME_CONNECTING){    
     // 連線訊息.
     connecting();     
     
     // 設定進入準備開始遊戲.
     if(tcp.isConnect()){
       // tcp.send("conn");
       gameMode = GAME_BEGIN;
     }
           
   // 準備開始遊戲.
   }else if(gameMode == GAME_BEGIN){
     // 更新畫面.
     updateScreen();  
     
     // 顯示訊息.
     doubleBuffer.setFreeFont(FSB9);
     if(flashFont)
       doubleBuffer.setTextColor(TFT_WHITE);
     else
       doubleBuffer.setTextColor(TFT_BLACK);

     // 1P.
     if(isServer){
       // A.
       if (joystick_1P.GetButtonA(true)){
         // 球.  
         ball.x = 70; ball.y = 120;
         // 開始遊戲.
         gameMode = GAME_PLAY;
       }
       doubleBuffer.drawCentreString("press A start", 66, 40, GFXFF);        
     // 2P.
     }else{
       doubleBuffer.drawCentreString("waiting...", 66, 40, GFXFF);
     }
                  
     // 閃爍字.
     if (clockSystem.checkClock(2, 1000))
       flashFont =!flashFont;

     // 判斷輸入移動板子.
     boardMove();
     
     // 1P、2P相互溝通.
     communicate();
                 
   // 開始遊戲.
   }else if(gameMode == GAME_PLAY){    
     // 更新畫面.
     updateScreen();
     // 判斷輸入移動板子.
     boardMove();
     // 1P、2P相互溝通.
     communicate();

     // 顯示球.
     doubleBuffer.fillRect ( ball.x, ball.y, ball.w,  ball.h, 0xFFFF);

     // 遊戲結束.
     if(score1P >= WIN_SCORE || score2P >= WIN_SCORE)
       gameMode = GAME_OVER;

     // 增加球移動速度.
     if((travelTime%10)==0){
       travelTime++;
       speed++;        
       if(speed>8)
         speed = 8;
       //Serial.println(speed);
     }
       
   // 遊戲結束.
   }else if(gameMode == GAME_OVER){
     // 更新畫面.
     updateScreen();      
     // 1P.
     if(isServer){
       // A.
       if (joystick_1P.GetButtonA(true)){
         // 重新開始遊戲.  
         resetGame();          
         // 開始遊戲.
         gameMode = GAME_BEGIN;
       }        
     }
     // 判斷輸入移動板子.
     boardMove();
     // 1P、2P相互溝通.
     communicate();

     // 顯示字.
     doubleBuffer.setFreeFont(FSSO12);
     doubleBuffer.setTextColor(TFT_WHITE, TFT_RED);
     if(score1P >= WIN_SCORE){
       doubleBuffer.drawCentreString("LOSE", 50,  45, GFXFF);
       doubleBuffer.drawCentreString("WIN", 50, 170, GFXFF);
     }else{
       doubleBuffer.drawCentreString("WIN", 50,  45, GFXFF);
       doubleBuffer.drawCentreString("LOSE", 50, 170, GFXFF);        
     }
   }
   
   /*
   // FPS.
   sprintf(buf, "FPS:%d", fps);
   doubleBuffer.setFreeFont(FSB9);
   doubleBuffer.setTextColor(TFT_WHITE);    
   doubleBuffer.drawCentreString(buf, 40, 100, GFXFF);    
   */    
   // 將備頁(精靈)複製到顯示區.
   doubleBuffer.pushSprite(0, 0, 0x07E0);
 }     

}

遊戲原理與重點程式碼說明
關於Server、Client同步技巧與原理
在連線遊戲中Server、Client之間的同步訊息非常重要,尤其是需要快速反應的遊戲,以本次製作的Pong為例,我們所使用的同步作法是把所有運作邏輯(包括球的移動)都放在Server運算然後在即時傳送給Client,這有點像Server是電視台,Client是家裡的電視機只要收到電視台的訊息在撥出,然後再來聊聊Server傳輸訊息給Client時的編碼字串,以下是Pong遊戲1P(Server)傳給2P(Client)的傳輸編碼:
指令|1P板子X座標|球X座標|球Y座標|1P分數|2P分數|遊戲模式|

範例:
c1|100|50|20|1|2|3|
其中每個部分的數值以|分隔,Client收到指令後會以|為分隔解碼出各區段的數值來使用
建立1P(Server)與2P(Client)連線物件程式碼說明
// 建立wifi物件.
esp32_wifi_tcp tcp( true, "ESP32-AP", "12345678");

// 是否為Server模式(1P).
bool isServer = true;

void loop() {



   // 選1P、2P.
   if(gameMode == GAME_SELECT){
     // 上.
     if (joystick_1P.GetButtonUp(true)) {
       isServer = true;
     // 下.
     }else if (joystick_1P.GetButtonDown(true)) {
       isServer = false;
     }
     // A.
     if (joystick_1P.GetButtonA(true)||joystick_1P.GetButtonB(true)){




       // 進入等待連線.
       gameMode = GAME_CONNECTING;
       
       // 設定建立Server或Client.
       tcp.isServer = isServer;
       tcp.begin();
     }
   }

}
以上程式碼主要是在判斷輸入上下按鈕選擇1P或2P並在按下A按鈕後設定成1P(變數isServer=true)或2P(變數isServer=false),isServer變數很重要,初始esp32_wifi_tcp物件時就需要傳入這個變數來確認建立的esp32_wifi_tcp物件是1P(Server)或2P(Client)物件,然後程式內也會參考這個變數,來運作1P(Server)或2P(Client)端的遊戲邏輯
1P(Server)、2P(Client)相互傳送訊息程式碼說明
void communicate(){
 // Server模式(1P).
 if(isServer){
   // 【Server傳送指令給Client】1P板子座標 - c1|x|bx|by|score1P|score2P|gameMode|
   sprintf(buf, "c1|%d|%f|%f|%d|%d|%d|", board1P.x, ball.x, ball.y, score1P, score2P, gameMode);
   tcp.send(buf);
         
   // 更新球.
   updateBall(&ball, &speed, &reflection_angle);
     
   // 如果球與板子發生碰撞就處理反彈.       
   if( didCollide(board1P, ball) || didCollide(board2P, ball)){
     // 反彈球.
     reflection_angle = -(reflection_angle);
     // 復原座標.
     ball.x = ball.prevX; ball.y = ball.prevY;
     // 開球時放慢速度,等碰到板子後恢復正常速度.
     if(speed == 2.0) speed = 4.0;
   }    




          
 // Client模式(2P).
 }else{
   //【Client傳送指令給Server】2P板子座標 - s1|x|
   sprintf(buf, "s1|%d|", board2P.x);
   tcp.send(buf);




 }
}
communicate()函數主要在處理1P(Server)、2P(Client)相互傳送訊息,傳送的字串編碼說明如下圖:
1P(Server)、2P(Client)接收字串後解碼使用程式碼說明
void decodingReceived(String received){
 char result[10][100];   // 用來存放分割後的字串,最多容納 10 個字串,每個字串長度最多 100 個字元.
 int resultCount = 0;    // 儲存分割後的字串數量.
 int n = 0;
 double d = 0.0;
 
 if(received=="")
   return;
   
 // 需要多一個字元來容納結尾的 null 字元
 char charArray[received.length() + 1];  

 // 將 String 轉換為 char 字元陣列
 received.toCharArray(charArray, sizeof(charArray));
 // 解指令字串.
 split( charArray, '|', result, &resultCount, 10);

 //【Client(2p)傳送指令給Server(1p)】板子座標.
 // 指令格式:s1|x|
 if(String(result[0])=="s1"){
   n = String(result[1]).toInt();
   board2P.x = n;
   
 //【Server(1p)傳送指令給Client(2p)】板子座標、球資料.
 // 指令格式:c1|x|bx|by|score1P|score2P|gameMode|
 }else if(String(result[0])=="c1"){
   // x.
   n = String(result[1]).toInt();
   board1P.x = n;
   // bx.
   d = String(result[2]).toDouble();
   ball.x = d;    
   // by.
   d = String(result[3]).toDouble();
   ball.y = d;        
   // score1P.
   n = String(result[4]).toInt();
   score1P = n;
   // score2P.
   n = String(result[5]).toInt();
   score2P = n;
   // gameMode.
   n = String(result[6]).toInt();
   gameMode = (GameMode)n;   
 }  
}
decodingReceived函數的功能主要是解碼收到的字串,並將解碼後的數值放入相關變數

操作說明
  • 上下按鈕:選1P或2P
  • 左右按鈕:左右移動板子
  • A按鈕:確定、開始遊戲、遊戲中按住加速板子左右移動
  • Start按鈕:無敵(球掉落底部自動反彈)
  • Select按鈕:自動玩(板子自動追著球的X座標移動)

實測影片

後記

睽違兩年的TBoy掌機更新,帶來了連線新體驗,快找好友一起同樂享受對戰新樂趣吧!
這是在幻想如果TBoy是上市掌機的話經過兩年沒更新忽然有連線功能發表因該會下這標題,兩年沒更新真的有點久,有空真的要幫TBoy掌機多寫幾款遊戲或多作幾個周邊配備,以上便是這次為大家帶來的連線遊戲Pong教學內容希望大家會喜歡,有任何建議歡迎以下留言,我們下次見。
【Youtube】【TBoy】無線連線,雙人對戰!重現Pong遊戲新面貌

【無限升級紛絲團】

分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.