其實這個遊戲的目標到目前為止已經完成了,但如同昨天所說的,包含如何顯示背包、如果有多個任務或是隨遊戲進展有多段對話、多個 NPC,目前的架構似乎會太難以維護。
今天我想要先來完成背包的部分,自己先思考哪邊會是卡關的地方,再由 AI 來提供建議,並從他的設計中學到一些想法。
那麼我們就先定義一下今天的工作邊界:完成顯示背包以及管理背包內容物的邏輯。
途中如果又發現新的問題,就記錄下來,但是不馬上處理以免失焦了。
我們就開始吧!
這一段因為是先前沒有設計過的內容,我還是會想要自己來思考一遍,這樣才知道自己目前卡關的位置是在哪裡,之後才可以讓 AI 來協助。
所以首先要來描述一下我們期待的遊戲流程以及畫面會長怎樣:
背包
--------
草藥 2
雞湯 1
一樣我想要自己練習這個狀態機的設計

其實切換開關這個是我們熟悉的,知道該怎麼清除畫面、渲染出背包的畫面。
但是背包內的物品該怎麼紀錄,是要有一個完整的陣列來記錄所有的項目嗎,還是該動態的從各個物件中計算出來?
這就是我們可以來詢問 AI 的部分了。
一樣也先試著拆解步驟:
那麼在步驟的第一點,其實就是我們卡關的地方,這時候就先來解決最重要的問題:背包的資料結構,包含 status 和 內容物該如何設計?這一項先確認下來,後續的工作才會有依據。
所以我們就把這個問題拿來詢問 AI ,結果他提出了一個不僅僅是解決背包問題,同時也解決了對話和地圖切換的渲染問題的做法:

利用 currentScreen 來決定目前的畫面是地圖、背包還是對話,好處是接下來可以利用這個來決定在不同的畫面開放哪些按鍵可以按。

另外一個好處是未來除了這三個畫面之外,還可能新增其他的畫面,所以利用這個方法來管理不同的畫面就可以無痛新增更多畫面了!
本來我是想到這邊交由 AI 來實作的,但我試著讓他按照上方的建議修改之後,意識到 AI 大幅的修改掉太多的地方,包含我們前面寫好的渲染流程。
但是我的目的只是找到一個最適合管理背包資料的方案,其餘的坑還是想要自己慢慢地踩過、痛過之後才懂。
所以我決定要參考他的資料結構,不參考畫面渲染的方法,然後自己從頭實作一次。
制定背包變數,這裡由於我們目前的渲染機制還是會靠這個狀態,所以我會在背包內加上 status 欄位,並且參考 AI 的 inventory 設計擺到我自己制定的 content 的位置:
let bag = {
status: 'open', //open close
content: {
herbs:{
name: "草藥",
num: 0,
},
chickenSoup:{
name:'雞湯',
num:0,
}
}
};
監聽 O ,需要 return 避免重新渲染
if ((key === 'o' || key === 'O')) {
showBag();
return;
}
開啟和關閉背包的邏輯,這邊主要是因為我們先前都使用陣列去計算行數,但是這次背包本身是物件,所以需要使用 Object.keys 和 Object.value 這樣的方法來處理行數、計算內容的問題。
function showBag() {
// 計算背包畫面需要多少空間
const bagHeight = 2 + Object.keys(bag.content).length;
if (bag.status === 'close') {
bag.status = 'open'
// 清除畫面
process.stdout.write(`\x1b[${viewHeight}A`); // 上移游標
process.stdout.write('\x1b[J'); // 清除畫面
const itemLines = Object.values(bag.content)
.map(item => `${item.name} ${item.num}`);
const frame = [
'背包',
'--------',
...itemLines,
].join('\n')+'\n';
process.stdout.write("\n".repeat(bagHeight));//推出空間
process.stdout.write(`\x1b${bagHeight}A`); //上移游標
process.stdout.write(frame);//印出
} else if (bag.status === 'open') {
bag.status = 'close';
// 清除畫面
process.stdout.write(`\x1b[${bagHeight}A`); // 上移游標
process.stdout.write('\x1b[J'); // 清除畫面
process.stdout.write('\n'.repeat(viewHeight));
render();
}
}
測試一下背包是不是可以正常印出:
但是這時候背包的數量變成了 [object object] ,因為我們把剛剛的純數字改成了物件,所以現在必須要改寫一下這個邏輯,改成計算內容物數量的加總:
const bagTotal = Object.values(bag.content)
.reduce((total, item) => total + item.num, 0);
row += ` 背包:${ bagTotal}`;
另外撿拾藥草的地方也要把 bag 數量的計算修改一下
bag.content.herbs.num += 1;
bag.content.herbs.num -= 1;
最後是獲得雞精的時候,我們要加上雞精的數量:
// pushNPCStatus 內
else if (npcStatus === "forthTalk") {
npcStatus = "complete";
task -= 1;
bag.content.chickenSoup.num += 1; // 加上雞精數量
// 清除畫面
process.stdout.write(`\x1b${fourthTalk.length}A`); // 上移游標
process.stdout.write('\x1b[J'); // 清除畫面
process.stdout.write('\n'.repeat(viewHeight));//推出空間
render();
}
寫到這邊,我自己實測的時候發現在完成任務時出現了錯誤

仔細看原來是因為我們還在用 bag 純數字的邏輯來判斷能不能完成任務,所以我們要改成草藥數量
if (bag.content.herbs.num < 3) {
return;
}
npcStatus = "forthTalk";
bag.content.herbs.num -= 3;
最後再實測一下完整遊戲:
終於整個遊戲大功告成啦!當然這裡面還有很多 bug 需要修,比方說現在畫面渲染的邏輯四散在多個地方,沒有一個統一管理全部畫面的狀態機。另外遊戲也需要可以擴充多個 NPC、多個任務、血量條或者是打怪等等的玩法,那在這些玩法之下要如何來優化架構,才不會像現在這樣管理得很辛苦,就會是未來的課題了。
不過大家應該也都很清楚我們這個 RPG 遊戲的練習目的和開發邊界了,所以我們的這個系列文章,會開發到這邊為止。真的很謝謝大家閱讀到這邊,明天我想要留一天的時間來好好回顧過去 30 天的這趟旅程,那麼我們就明天見啦!
如果你想要瀏覽完整的程式碼,可以參考這裡:第 29 天程式碼