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

閱讀時間約 49 分鐘
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遊戲新面貌

【無限升級紛絲團】

為什麼會看到廣告
avatar-img
5會員
16內容數
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
無限升級的沙龍 的其他內容
前言 嗨,各位懷舊遊戲愛好者!今天要跟大家分享一個有趣的主題:如何利用React和Pixi.js這兩大神兵利器,重塑我們那個年代的經典紅白機打磚塊遊戲! 先跟大家簡單科普一下,React是一個超級火爆的前端框架,能讓我們輕鬆創建可重用的UI組件,組件間的狀態管理也相當方便。。。
想要在復古遊戲機上享受無線遊戲體驗,同時又能炫耀自己現代的遊戲控制器嗎?BlueRetro 來拯救你的童年!這款開源的藍牙遊戲控制器轉接器讓你可以把最新款的 PlayStation、Xbox 和 Switch Pro 控制器連接到那些古老的遊戲機上。誰說過復古不能與現代搭配?
話說ChatGPT+SGDK能不能蹦出MD(SEGA Mega Drive)新遊戲呢?這幾天開通了ChatGPT PLUS想說來試試使用ChatGPT作款能在MD主機上玩的小遊戲,順便記錄一下製作這款遊戲的過程,至於要作哪款遊戲呢?
Hi!大家好,今天來教大家如何玩壞Google Chrome小恐龍,這款Chrome離線小恐龍跑酷遊戲,一般是在開啟Chrome瀏覽器並在離線狀態下可以玩到的遊戲,其實也是可以在連線狀態下玩的。。。
前陣子在某線上拍賣平台買了一支超便宜的有線超任USB搖桿,結果玩不到一個禮拜按鈕就被按壞了,而且發現還沒家裡那台老舊超級任天堂搖桿順手,所以就開始打起老舊超任搖桿的主意。。。。
在那Nokia手機風靡全球的年代,因該有不少人玩過手機內建的貪吃蛇遊戲,記得當時年紀小在還是學生的那個年代就經常利用上電腦課的時候偷偷用我那隻好不容易打工購買的Nokia手機玩這款遊戲,玩到最後還利用電腦課的時間用BASIC寫出了一款簡易版的貪吃蛇遊戲。。。
前言 嗨,各位懷舊遊戲愛好者!今天要跟大家分享一個有趣的主題:如何利用React和Pixi.js這兩大神兵利器,重塑我們那個年代的經典紅白機打磚塊遊戲! 先跟大家簡單科普一下,React是一個超級火爆的前端框架,能讓我們輕鬆創建可重用的UI組件,組件間的狀態管理也相當方便。。。
想要在復古遊戲機上享受無線遊戲體驗,同時又能炫耀自己現代的遊戲控制器嗎?BlueRetro 來拯救你的童年!這款開源的藍牙遊戲控制器轉接器讓你可以把最新款的 PlayStation、Xbox 和 Switch Pro 控制器連接到那些古老的遊戲機上。誰說過復古不能與現代搭配?
話說ChatGPT+SGDK能不能蹦出MD(SEGA Mega Drive)新遊戲呢?這幾天開通了ChatGPT PLUS想說來試試使用ChatGPT作款能在MD主機上玩的小遊戲,順便記錄一下製作這款遊戲的過程,至於要作哪款遊戲呢?
Hi!大家好,今天來教大家如何玩壞Google Chrome小恐龍,這款Chrome離線小恐龍跑酷遊戲,一般是在開啟Chrome瀏覽器並在離線狀態下可以玩到的遊戲,其實也是可以在連線狀態下玩的。。。
前陣子在某線上拍賣平台買了一支超便宜的有線超任USB搖桿,結果玩不到一個禮拜按鈕就被按壞了,而且發現還沒家裡那台老舊超級任天堂搖桿順手,所以就開始打起老舊超任搖桿的主意。。。。
在那Nokia手機風靡全球的年代,因該有不少人玩過手機內建的貪吃蛇遊戲,記得當時年紀小在還是學生的那個年代就經常利用上電腦課的時候偷偷用我那隻好不容易打工購買的Nokia手機玩這款遊戲,玩到最後還利用電腦課的時間用BASIC寫出了一款簡易版的貪吃蛇遊戲。。。
你可能也想看
Google News 追蹤
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
昨天,看到小朋友的動態,是一個「你以為的研究所」影片, 原本以為是美好的地方,誰知一腳踩進去,水很深…… 這個我也深有感觸啊,真的,這一行,好辛苦。 不過,說到研究所, 我想起小時候的卡通,「無敵鐵金剛」,裡面的「原子光研究所」。 很久之後,才知道日文原版叫光子力研究所。 原
Thumbnail
2023對我來說是個很特別的一年,這一年我特別有意識的慢下來,並不是特別刻意的,就是很自然而然的就放慢了速度,而透過這個慢我似乎活了起來,好好的品嚐自己的生活。
Thumbnail
如今的社會,把一切甚至是時間都交給科技,就像交出時間寶石的這一刻。這一刻我們能再次點亮內心中相信人性的光芒嗎?
各位大家好,這是我最近幾天遇到的事情。 情形如下: 1、其他行動裝置皆能"正常使用",如平板、手機等。 2、桌面右下角音量圖示(喇叭)旁的電腦變成"地球"。 3、重開機或網路偵測均無法解決。 解決方法: 請更換"新的網路線"! 流程1 我是用買新的,一條2米長39元。(非
Thumbnail
不管你之前是否是超級英雄迷,都會從這部電影裡獲得一些東西。所以千萬別讓沒看過《索爾3》或《美隊3》,成為阻止你看這部電影的原因XD 看完我真的心神澎湃,充滿各種難以言喻的情感,歷經了許多最後居然得到這樣一個結果,雖然心裡明知一切都會好起來的,漫威絕對不敢真的殺了他們
Thumbnail
【原創TBoy掌機】 DIY了幾台網路大神們開源的掌機後,想想也該是時候來弄台原創掌機回饋一下社群了,也就是說新作的這台掌機會開源,希望對有興趣製作掌機的同好有所幫助。
Thumbnail
漫威十年磨一劍,真的不是嘴上說說,集合了幾乎全部的漫威角色,一起集結到這部鉅作,它,值得我們的期待嗎?
Thumbnail
烏鴉(cos星爵)提到他被朋友不說分由地揍一拳,雖然莫名其妙,但他還是說:「等等、什麼都別跟我說」
Thumbnail
  導演羅素兄弟讓各組故事如同多顆衛星般,圍繞在以反派薩諾斯(Thanos)為核心的敘事模式,使得《復仇者聯盟3:無限之戰》的電影敘事模式和過去的Marvel作品完全不同,成為首部以反派作為主核心,英雄們作為附屬衛星的影像敘事模式。這是Marvel影業十年來的重要突破,也展現Russo兄弟的敘事功力。
Thumbnail
這個秋,Chill 嗨嗨!穿搭美美去賞楓,裝備款款去露營⋯⋯你的秋天怎麼過?秋日 To Do List 等你分享! 秋季全站徵文,我們準備了五個創作主題,參賽還有機會獲得「火烤兩用鍋」,一起來看看如何參加吧~
Thumbnail
11/20日NVDA即將公布最新一期的財報, 今天Sell Side的分析師, 開始調高目標價, 市場的股價也開始反應, 未來一週NVDA將重新回到美股市場的焦點, 今天我們要分析NVDA Sell Side怎麼看待這次NVDA的財報預測, 以及實際上Buy Side的倉位及操作, 從
Thumbnail
Hi 大家好,我是Ethan😊 相近大家都知道保濕是皮膚保養中最基本,也是最重要的一步。無論是在畫室裡長時間對著畫布,還是在旅途中面對各種氣候變化,保持皮膚的水分平衡對我來說至關重要。保濕化妝水不僅能迅速為皮膚補水,還能提升後續保養品的吸收效率。 曾經,我的保養程序簡單到只包括清潔和隨意上乳液
昨天,看到小朋友的動態,是一個「你以為的研究所」影片, 原本以為是美好的地方,誰知一腳踩進去,水很深…… 這個我也深有感觸啊,真的,這一行,好辛苦。 不過,說到研究所, 我想起小時候的卡通,「無敵鐵金剛」,裡面的「原子光研究所」。 很久之後,才知道日文原版叫光子力研究所。 原
Thumbnail
2023對我來說是個很特別的一年,這一年我特別有意識的慢下來,並不是特別刻意的,就是很自然而然的就放慢了速度,而透過這個慢我似乎活了起來,好好的品嚐自己的生活。
Thumbnail
如今的社會,把一切甚至是時間都交給科技,就像交出時間寶石的這一刻。這一刻我們能再次點亮內心中相信人性的光芒嗎?
各位大家好,這是我最近幾天遇到的事情。 情形如下: 1、其他行動裝置皆能"正常使用",如平板、手機等。 2、桌面右下角音量圖示(喇叭)旁的電腦變成"地球"。 3、重開機或網路偵測均無法解決。 解決方法: 請更換"新的網路線"! 流程1 我是用買新的,一條2米長39元。(非
Thumbnail
不管你之前是否是超級英雄迷,都會從這部電影裡獲得一些東西。所以千萬別讓沒看過《索爾3》或《美隊3》,成為阻止你看這部電影的原因XD 看完我真的心神澎湃,充滿各種難以言喻的情感,歷經了許多最後居然得到這樣一個結果,雖然心裡明知一切都會好起來的,漫威絕對不敢真的殺了他們
Thumbnail
【原創TBoy掌機】 DIY了幾台網路大神們開源的掌機後,想想也該是時候來弄台原創掌機回饋一下社群了,也就是說新作的這台掌機會開源,希望對有興趣製作掌機的同好有所幫助。
Thumbnail
漫威十年磨一劍,真的不是嘴上說說,集合了幾乎全部的漫威角色,一起集結到這部鉅作,它,值得我們的期待嗎?
Thumbnail
烏鴉(cos星爵)提到他被朋友不說分由地揍一拳,雖然莫名其妙,但他還是說:「等等、什麼都別跟我說」
Thumbnail
  導演羅素兄弟讓各組故事如同多顆衛星般,圍繞在以反派薩諾斯(Thanos)為核心的敘事模式,使得《復仇者聯盟3:無限之戰》的電影敘事模式和過去的Marvel作品完全不同,成為首部以反派作為主核心,英雄們作為附屬衛星的影像敘事模式。這是Marvel影業十年來的重要突破,也展現Russo兄弟的敘事功力。