Photo by Joshua Doherty on Unsplash
開通ChatGPT PLUS
話說ChatGPT+SGDK能不能蹦出MD(SEGA Mega Drive)新遊戲呢?這幾天開通了ChatGPT PLUS想說來試試使用ChatGPT作款能在MD主機上玩的小遊戲,順便記錄一下製作這款遊戲的過程,至於要作哪款遊戲呢?上篇文章惡搞了Google Chrome小恐龍,那就選Google Chrome小恐龍好了,不囉嗦說作就作。。。
簡單企劃文件與環境安裝
在開始動手寫Google Chrome小恐龍執行企劃的時候,忽然想到可以動動手請ChatGPT寫,二話不說開啟ChatGPT直接輸入"請幫我整理Google Chrome恐龍遊戲簡單製作企畫說明文件",ChatGPT也立馬回應:
唉呦!不錯喔,有模有樣的,雖然沒比我寫的好(心虛),最少寫的比我快,那我們就勉強著參考這份文件開始製作遊戲了,首先來建構開發環境,請連至以下網址下載工具一共有三個檔案:
- KMod v0.7.3模擬器 (檔案名稱:Gens_KMod_v0.7.3.7z)
- Visual Studio Code (檔案名稱:VSCodeUserSetup-x64–1.78.0-insider.exe)
下載完後建立目錄MegaDrive將三個檔案複製進去,先將sgdk180.7z與Gens_KMod_v0.7.3.7z解壓縮然後安裝VSCodeUserSetup-x64–1.78.0-insider.exe,待Visual Studio Code安裝完成請將之開啟,參考下圖找到Genesis Code外掛並安裝:
接下來設定Genesis Code,開啟Settings選項
依照下圖進行設定
設定完畢將Visual Studio Code關閉,接下來進入sgdk180目錄,執行build_adv.bat與build_lib.bat
完成後重新開啟Visual Studio Code,按下熱鍵Ctrl+Shift+P,依照下圖步驟建立一個測試專案:
建立test目錄並選擇此目錄
開啟Terminal
依照下圖步驟執行設定與編譯程式
編譯後如果看到以下視窗跳出恭喜!我們開發環境就設定完成了
開始與ChatGPT溝通製作遊戲
再來就要請出我們月薪六百台幣的ChatGPT PLUS,開門見山就跟他說"使用請用SGDK 1.70的寫一個類似Google Chrome恐龍的遊戲",因為太緊張還有結巴,原本想說因該要跑個一小時吧,正準備轉身去茶水間泡個咖啡偷懶一下時,ChatGPT已經啪啦啪啦將程式碼寫一半就停了,只好叫他繼續我看著呢!
前後大概不到二分鐘的時間就完成了,好吧!這個產出速度個人是自認沒法比了,只能安慰自己寫的快內容不見的是對的,依照ChatGPT說的我們先建立一個sgdk_dino專案(作法前面有教跟建立test專案的步驟一樣),建立完成後開啟main.c並將裡面資料全部清除,並將ChatGPT寫好的程式碼全部複製貼上,然後開始理解程式碼,發現只有兩個錯誤#include <resources.h>用不到先刪除,加上沒有驅動搖桿的程式碼,幫忙加上:
u8 value;
value = JOY_getPortType(PORT_1);
switch (value)
{
case PORT_TYPE_MENACER:
JOY_setSupport(PORT_1, JOY_SUPPORT_MENACER);
break;
case PORT_TYPE_JUSTIFIER:
JOY_setSupport(PORT_1, JOY_SUPPORT_JUSTIFIER_BOTH);
break;
case PORT_TYPE_MOUSE:
JOY_setSupport(PORT_1, JOY_SUPPORT_MOUSE);
break;
case PORT_TYPE_TEAMPLAYER:
JOY_setSupport(PORT_1, JOY_SUPPORT_TEAMPLAYER);
break;
}
其他程式碼看上去沒問題,整合進我們程式內並編譯執行,按下手把A可以控制恐龍方塊跳起,按下手把Start重新開始遊戲,以下是執行影片:
老實說當時我害怕極了,真的可以玩,想當初學SGDK搞了一個禮拜才看到Hello SEGA!!我們繼續,想說每次都要自己玩有點累,加上也想知道恐龍方塊在不死的狀況下持續往右跑會不會出現其他問題,所以接下來我們繼續對ChatGPT下指令"請修改讓恐龍遇到障礙物自動跳起":
看了一下程式碼,沒大問題直接整合到我們程式內,編譯執行後也確實能正常執行:
在遊戲自動玩了一小段時間後果然發現恐龍方塊在往上跳下降後會掉落到地板下,繼續指揮ChatGPT說"主角在往上跳落下後會掉落地板下,請修正這個問題":
看程式碼,也沒大問題直接整合進去,編譯執行後也都正常運作:
高興地玩了一陣子後發現障礙物都只有一格,只好繼續指揮ChatGPT讓障礙物每次出現有不同高度,請輸入"請修改obstacle變數,讓障礙物每次出現時都有不同高度":
繼續理解程式碼發現多處有問題,所以就不叫ChatGPT修改了,我們自行修正,首先frameCounter變數用不到,所以直接刪除,將障礙物初始高度設定為1
obstacle.height = OBSTACLE_HEIGHT
修改
obstacle.height = 1
繪製障礙物迴圈內繪製障礙方塊的參數有誤請作以下修改
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL2, FALSE, FALSE, FALSE, 1),
obstacle.x / 8, (obstacle.y - i * 8) / 8, obstacle.width, 2);
修改
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL2, FALSE, FALSE, FALSE, 1),
obstacle.x / 8, (obstacle.y - i * 8) / 8, 1, 2);
修正完畢後繼續編譯執行:
影片中可以看出障礙物已經會出現不同高度,可是恐龍方塊碰到障礙物不會結束遊戲,繼續對ChatGPT說道"經過以上修改恐龍碰到障礙物不會遊戲結束了,請修正這個問題":
看了一下程式碼除了障礙物高度錯誤外其他沒大問題,先整合進去,然後障礙物高度部分修正如下:
if (dino.x + 16 < obstacle.x || dino.x > obstacle.x + obstacle.width * 8) {
修改
if (dino.x + 16 < obstacle.x || dino.x > obstacle.x + obstacle.width * 2) {
對了,因該要加個Game Over顯示,讓恐龍方塊碰到障礙物後可以顯示,這個也先不麻煩ChatGPT,自己動手改一下,將主迴圈修改如下:
while(1)
{
SYS_doVBlankProcess();
// 更新恐龍與障礙物.
updateDino();
updateObstacle();
// 修改.
if (checkCollision()) {
// 清除障礙物.
VDP_setTileMapDataRect ( BG_A, TILE_ATTR_FULL(PAL0, FALSE, FALSE, FALSE, 0), 0, 0, 40, 25, 1, DMA);
// 當遊戲結束時,顯示文字提示
VDP_drawTextBG( BG_A, "Game Over!", 15, 12);
while (1) {
SYS_doVBlankProcess();
u16 keys = JOY_readJoypad(JOY_1);
if (keys & BUTTON_A){
// 清除畫面.
VDP_clearPlane (BG_A, TRUE);
// 初始遊戲.
createDino();
createObstacle();
break;
}
VDP_waitVSync();
}
}
VDP_waitVSync();
}
修改完畢直接編譯執行:
開始有點像樣了,繼續來加點分數顯示,直接跟ChatGPT說"請加分數功能,分數計算方法為每往右移動一格(obstacle.x — )分數就加1":
看了一下程式碼沒大問題,將之整合進去,不過整合的時候忽然想到如果每往右移動1Pixel分數就加1這樣分數會過大,所以順手改成每移動一個方塊(8Pixel)分數就加1這樣會比較好一點,然後也加了分數越高移動速度越快的設定:
void updateObstacle() {
// ... 保持代碼不變
// CHANNEL修改.
// 更新障礙物位置,依照分數設定障礙物一動速度.
if(score>1200){
obstacle.x-=8;
}else if(score>500){
obstacle.x-=4;
}else if(score>50){
obstacle.x-=2;
}else{
obstacle.x--;
}
// 讓恐龍往右每走一格(8像素)就加1分.
if(obstacle.x%8 == 0){
score++;
}
// ... 保持代碼不變,直到主迴圈
}
完成後編譯執行
開始有模有樣了,我們在來加個最高分數紀錄,直接跟ChatGPT說"請加入每局結束後判斷最高分數功能,並顯示在畫面中間上方位置"
看了下程式碼也沒什麼大問題將之整合進去,感覺畫面有點不明顯,所以對畫面區塊顏色作了一些調整:
int main() {
// ... 保持代碼不變
// 設定背景顏色.
PAL_setColor (0, RGB24_TO_VDPCOLOR(0xa1adff));
// 設定障礙物顏色.
PAL_setColor (1, RGB24_TO_VDPCOLOR(0xde5718));
// 設定恐龍顏色.
PAL_setColor (2, RGB24_TO_VDPCOLOR(0x0600ff));
// 設定寶物顏色.
PAL_setColor (3, RGB24_TO_VDPCOLOR(0xff0000));
// 設定雲顏色.
PAL_setColor (4, RGB24_TO_VDPCOLOR(0xc9c9c9));
// ... 保持代碼不變
}
為了讓遊戲更有趣,來加個由右往左移動的方塊,恐龍方塊可以跳起來吃掉並增加分數,一樣對著ChatGPT說"請增加判斷score變數每增加100分就從右邊生出一個方塊往左移動,移動到左邊盡頭就消失,並判斷如果恐龍碰到此方塊就將score加20同時關閉方塊"
看了一下程式碼發現由右往左移動的方塊那部分沒將移動過的方塊清除,所以會有殘留方塊,繼續跟ChatGPT說"方塊往左移動後會殘留舊方塊請修改將之清除"
看完程式碼沒問題,將之整合進去,並修正了一些邏輯問題,最後編譯執行
遊戲大致完成,玩了幾十分鐘,感覺可以在加兩朵雲來修飾一下畫面,所以跟ChatGPT說"請在上方製作2朵雲使其從最右往左慢慢移動,移動到最左邊後使其在回到右邊在繼續往左移動,反覆以上動作,雲移動過後要清除舊的雲不要留下殘影"
程式碼沒大問題將之整合進去,編譯執行
影片中為了測試方便將恐龍方塊改成無敵了。
完成,整理程式碼
好的!到此Google Chrome小恐龍遊戲已經完成,將程式碼重頭到尾整理一遍加入註解如下:
#include <genesis.h>
#include "pal.h"
// 常數設定.
#define GROUND_Y 200
#define DINO_Y 180
#define DINO_WIDTH 16
#define DINO_HEIGHT 16
#define OBSTACLE_WIDTH 8
#define OBSTACLE_HEIGHT 16
#define JUMP_HEIGHT 40
#define BONUS_START_X 320
#define BONUS_Y 120
#define BONUS_SPEED 4
#define CLOUD1_START_X 320
#define CLOUD1_Y 16
#define CLOUD2_START_X 400
#define CLOUD2_Y 24
#define CLOUD_SPEED 1
// 恐龍方塊結構.
typedef struct {
s16 x, y;
u16 width, height;
u8 isJumping;
s16 jumpCounter;
} Dino;
// 障礙物結構.
typedef struct {
s16 x, y;
u16 width, height;
u8 isActive;
} Obstacle;
// 寶物結構.
typedef struct {
s16 x;
s16 y;
bool isActive;
} Bonus;
// 雲結構.
typedef struct {
s16 x;
s16 y;
} Cloud;
Dino dino;
Obstacle obstacle;
Bonus bonus;
Cloud cloud1;
Cloud cloud2;
// 在main函數開頭添加分數變數.
int score = 0;
// 在main函數開頭添加最高分數變數.
int highScore = 0;
void createDino();
void createObstacle();
void updateDino();
void updateObstacle();
bool checkCollision();
void updateScore();
void updateHighScore();
//----------------------------------------------------------------------------
// 雲 - 初始.
//----------------------------------------------------------------------------
void initClouds() {
cloud1.x = CLOUD1_START_X;
cloud1.y = CLOUD1_Y;
cloud2.x = CLOUD2_START_X;
cloud2.y = CLOUD2_Y;
}
//----------------------------------------------------------------------------
// 雲 - 更新.
//----------------------------------------------------------------------------
void updateCloud(Cloud *cloud) {
// 清除舊雲位置
VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL0, FALSE, FALSE, FALSE, 0), cloud->x / 8, cloud->y / 8);
cloud->x -= CLOUD_SPEED;
// 如果雲超出螢幕左邊邊界,將其重置到右側
if (cloud->x < -16) {
cloud->x = 320;
}
// 繪製新雲位置
VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL0, FALSE, FALSE, FALSE, 4), cloud->x / 8, cloud->y / 8);
}
//----------------------------------------------------------------------------
// 初始寶物方塊.
//----------------------------------------------------------------------------
void spawnBonus() {
bonus.x = BONUS_START_X;
bonus.y = BONUS_Y;
bonus.isActive = TRUE;
}
//----------------------------------------------------------------------------
// 更新寶物方塊.
//----------------------------------------------------------------------------
void updateBonus() {
if (!bonus.isActive) {
return;
}
// 清除舊方塊位置
VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL0, FALSE, FALSE, FALSE, 0), bonus.x / 8, bonus.y / 8);
bonus.x -= BONUS_SPEED;
// 如果方塊超出螢幕左邊邊界,將 isActive 設為 FALSE
if (bonus.x < -8) {
bonus.isActive = FALSE;
return;
}
// 檢查恐龍與方塊之間的碰撞
if (dino.y + 16 >= bonus.y && dino.y <= bonus.y + 8 && dino.x + 16 >= bonus.x && dino.x <= bonus.x + 8) {
bonus.isActive = FALSE;
score += 100;
return;
}
// 繪製方塊
if (bonus.isActive) {
VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL0, FALSE, FALSE, FALSE, 3), bonus.x / 8, bonus.y / 8);
} else {
VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL0, FALSE, FALSE, FALSE, 0), bonus.x / 8, bonus.y / 8);
}
}
//----------------------------------------------------------------------------
// 初始障礙物.
//----------------------------------------------------------------------------
void initObstacle() {
obstacle.x = 320;
obstacle.y = GROUND_Y - 8;
obstacle.width = 2;
}
//----------------------------------------------------------------------------
// 亂數障礙物高低.
//----------------------------------------------------------------------------
void spawnObstacle() {
obstacle.x = 320;
// 生成隨機高度(1到5格)
obstacle.height = 1 + (random() % 5);
// 繪製障礙物
for (int i = 0; i < obstacle.height; i++) {
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL2, FALSE, FALSE, FALSE, 1), obstacle.x / 8, (obstacle.y - i * 8) / 8, obstacle.width, 1);
}
}
//----------------------------------------------------------------------------
// 程式進入點.
//----------------------------------------------------------------------------
int main() {
// 初始化螢幕
VDP_init();
// 設置解析度
VDP_setScreenWidth320();
VDP_setScreenHeight224();
// 設定背景顏色.
PAL_setColor (0, RGB24_TO_VDPCOLOR(0xa1adff));
// 設定障礙物顏色.
PAL_setColor (1, RGB24_TO_VDPCOLOR(0xde5718));
// 設定恐龍顏色.
PAL_setColor (2, RGB24_TO_VDPCOLOR(0x0600ff));
// 設定寶物顏色.
PAL_setColor (3, RGB24_TO_VDPCOLOR(0xff0000));
// 設定雲顏色.
PAL_setColor (4, RGB24_TO_VDPCOLOR(0xc9c9c9));
// 加入搖桿判斷.
u8 value;
value = JOY_getPortType(PORT_1);
switch (value)
{
case PORT_TYPE_MENACER:
JOY_setSupport(PORT_1, JOY_SUPPORT_MENACER);
break;
case PORT_TYPE_JUSTIFIER:
JOY_setSupport(PORT_1, JOY_SUPPORT_JUSTIFIER_BOTH);
break;
case PORT_TYPE_MOUSE:
JOY_setSupport(PORT_1, JOY_SUPPORT_MOUSE);
break;
case PORT_TYPE_TEAMPLAYER:
JOY_setSupport(PORT_1, JOY_SUPPORT_TEAMPLAYER);
break;
}
createDino();
createObstacle();
// 初始化雲.
initClouds();
while(1)
{
SYS_doVBlankProcess();
// 更新恐龍與障礙物.
updateDino();
updateObstacle();
updateScore(); // 更新分數
updateHighScore();
updateBonus(); // 更新方塊
updateCloud(&cloud1); // 更新雲1
updateCloud(&cloud2); // 更新雲2
if (checkCollision()) {
// 當遊戲結束時,顯示文字提示
VDP_drawTextBG( BG_A, "Game Over!", 15, 12);
while (1) {
SYS_doVBlankProcess();
u16 keys = JOY_readJoypad(JOY_1);
if (keys & BUTTON_START){
// 清除畫面.
VDP_clearPlane (BG_A, TRUE);
// 初始遊戲.
createDino();
createObstacle();
// 初始分數.
score = 0;
break;
}
VDP_waitVSync();
}
}
VDP_waitVSync();
}
return 0;
}
//----------------------------------------------------------------------------
// 建立恐龍方塊.
//----------------------------------------------------------------------------
void createDino() {
dino.x = 32;
dino.y = DINO_Y;
dino.width = DINO_WIDTH;
dino.height = DINO_HEIGHT;
dino.isJumping = FALSE;
dino.jumpCounter = 0;
}
//----------------------------------------------------------------------------
// 建立障礙物.
//----------------------------------------------------------------------------
void createObstacle() {
obstacle.x = 320;
obstacle.y = GROUND_Y - OBSTACLE_HEIGHT;
obstacle.width = OBSTACLE_WIDTH;
obstacle.height = 1;
obstacle.isActive = TRUE;
}
//----------------------------------------------------------------------------
// 更新恐龍方塊.
//----------------------------------------------------------------------------
void updateDino() {
// 清除恐龍之前的位置
VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL0, FALSE, FALSE, FALSE, 0), dino.x / 8, dino.y / 8);
VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL0, FALSE, FALSE, FALSE, 0), dino.x / 8 + 1, dino.y / 8);
VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL0, FALSE, FALSE, FALSE, 0), dino.x / 8, dino.y / 8 + 1);
VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL0, FALSE, FALSE, FALSE, 0), dino.x / 8 + 1, dino.y / 8 + 1);
if (dino.isJumping) {
dino.jumpCounter++;
if (dino.jumpCounter < JUMP_HEIGHT) {
dino.y -= 2;
} else if (dino.jumpCounter < 2 * JUMP_HEIGHT) {
dino.y += 2;
} else {
dino.jumpCounter = 0;
dino.isJumping = FALSE;
}
}
// 根據按鍵狀態更新恐龍
u16 keys = JOY_readJoypad(JOY_1);
if (keys & BUTTON_A && !dino.isJumping) {
dino.isJumping = TRUE;
}
// 根據恐龍與障礙物的距離自動跳起
//if (!dino.isJumping && dino.y == DINO_Y && obstacle.x - (dino.x + DINO_WIDTH) < 32) {
// dino.isJumping = TRUE;
//}
// 恐龍落地後將 dino.isJumping 設定為 FALSE
if (dino.y > DINO_Y) {
dino.y = DINO_Y;
dino.isJumping = FALSE;
}
// 繪製新的恐龍位置
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, FALSE, FALSE, FALSE, 2), dino.x / 8, dino.y / 8, 2, 2);
}
//----------------------------------------------------------------------------
// 更新障礙物.
//----------------------------------------------------------------------------
void updateObstacle() {
// 清除障礙物之前的位置
for (int i = 0; i < obstacle.height; i++) {
VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL0, FALSE, FALSE, FALSE, 0), obstacle.x / 8, (obstacle.y - i * 8) / 8);
}
// 更新障礙物位置,依照分數設定障礙物一動速度.
if(score>1200){
obstacle.x-=8;
}else if(score>500){
obstacle.x-=4;
}else if(score>50){
obstacle.x-=2;
}else{
obstacle.x--;
}
// 讓恐龍往右每走一格(8像素)就加1分.
if(obstacle.x%8 == 0){
score++;
}
// 檢查是否需要重新生成障礙物
if (obstacle.x + 8 * obstacle.width < 0) {
spawnObstacle();
}
// 繪製新的障礙物位置
for (int i = 0; i < obstacle.height; i++) {
VDP_fillTileMapRect(BG_A, TILE_ATTR_FULL(PAL0, FALSE, FALSE, FALSE, 1), obstacle.x / 8, (obstacle.y - i * 8) / 8, 1, 2);
}
}
//----------------------------------------------------------------------------
// 判斷恐龍方塊與障礙物碰撞.
//----------------------------------------------------------------------------
bool checkCollision() {
// 如果恐龍在空中並且足夠高以避免碰撞,則返回FALSE.
if (dino.isJumping && dino.y <= (obstacle.y - obstacle.height * 8)) {
return FALSE;
}
// 如果恐龍和障礙物在水平位置上沒有接觸,則返回FALSE.
if (dino.x + 16 < obstacle.x || dino.x > obstacle.x + obstacle.width * 2) {
return FALSE;
}
return TRUE;
}
//----------------------------------------------------------------------------
// 更新分數.
//----------------------------------------------------------------------------
void updateScore() {
static int prevScore = 0;
// 新增修改方塊顯示中就不要再進入執行(!bonus.isActive).
if (score % 100 == 0 && score != prevScore && !bonus.isActive) {
spawnBonus();
prevScore = score;
}
char scoreStr[10];
sprintf(scoreStr, "Score: %d", score);
VDP_drawText(scoreStr, 2, 0);
}
//----------------------------------------------------------------------------
// 更新最高分數.
//----------------------------------------------------------------------------
void updateHighScore() {
if (score > highScore) {
highScore = score;
}
char highScoreStr[15];
sprintf(highScoreStr, "High Score: %d", highScore);
VDP_drawText(highScoreStr, 16, 0);
}
所有程式碼與ChatGPT聊天內容也上傳到Github上
Github
實機試玩影片
後記
大家如果詳細看與ChatGPT聊天內容(文件以上傳至GitHub)會發現有很多ChatGPT寫出來的程式碼無法使用或過多Bugs,所以沒有採納,這部分有很大的原因是因為所輸入的指令不夠精準,文章為了順暢度所以沒將這部分寫入,總歸首次的ChatGPT體驗讓我非常驚訝,AI也走到了所謂的奇異點,依照這個進展相信未來AI也將成為我們生活中的標配,好的!本次的MD小遊戲製作教學也到了尾聲,我們下次見。
對了,如果您熟悉SGDK也可以將遊戲內方塊換成圖片,這樣可以讓畫面看起來更專業,非常期待您的作品。
更新日誌