昨天我們發現打從一開始就沒有思考過遊戲應該要在按 q 切回選單後暫停還是結束遊戲,而且 AI 為了要暫停遊戲做了很多遊戲邏輯的修改,讓邏輯變得沒那麼乾淨,我不太喜歡。
那就是時候來想想有沒有更適合的方式,來列一下我心中理想的樣子:
最後這點應該是我最在意的,我覺得如果為了暫停或中止遊戲,而在很多細節上加上一些零碎的調整,會導致我在未來要修改一個地方就牽動到很多地方,我蠻不喜歡這樣。另一方面,如果我每個遊戲都要為了放入這個共用入口而做很多修改,那這就不是一個容易實現的架構,最容易的架構應該是寫好遊戲之後,做幾個簡單的設定就可以接進這個入口。
所以我們今天的主題是要讓 AI 來提供不同的做法,然後我們來比較看看哪一個更符合期待。
我們會從這幾個面向來比較:
而為了讓每一次修改都可以看到跟原版的差異,我決定要開三個以 Day21 分支為基礎的新分支,讓他們有一個共同的起點,另外也為了便於評估,每次 AI 開發都會採用一樣的流程:
最後再來決定要使用哪一個版本
每次切換到遊戲,都是新的遊戲(可以看到地圖都有重新產出、兩個遊戲重複執行時箱子和角色的位置都不一樣),不會因為遊戲的按鍵而導致選單在遊戲時渲染,這是對的。
我先聲明一下這不是實際程式碼由上而下的順序,而是我為了方便理解而整理的閱讀順序。
先從兩個遊戲的改動開始看:
兩邊都是把遊戲的物件的產生都收進一個 resetGame 裡面。
遊戲開始前去執行一遍,以產生遊戲的物件,這樣就確保遊戲內容每次都是新的。
再來看 index.js 怎麼寫:
把 process.stdin.on 改成函式 handleMenuInput 。
在檔案最後面才使用 process.stdin.on 並傳入 handleMenuInput 給它。這樣就完成監聽器的改造。
在按 Enter 進入遊戲的時候,用 process.stdin.off 關掉監聽。
backToMenu 函式內在返回到選單的時候重啟監聽。
功能和流程的部分是沒問題,但在程式碼方面,因為我們 Day21 做的改動其實也不少,每次新增一個遊戲,單支檔案的工作量還是蠻大的,比方說這個按下 q 回到選單的邏輯,就是昨天我們為了共用架構做了許多擴充。這一版沒辦法讓他回到我理想中精簡的樣子。
要如何在每次加新的檔案時都會記得要修改哪些地方,是蠻困難的。
跟上一版一樣沒有什麼明顯的問題。
首先遊戲的部分,它把整個遊戲檔案都包成一個函式了,我直接收合起來給你們看:
檔案最後可以看到 createBoxGame 這個函式最終會提供三個方法出來給 inex.js 引用。
如果大家有興趣可以再去閱讀遊戲和 index.js 的程式碼,我這邊只講重點:
在這一版裡面有兩個重要的變數
let onExit; // 用來儲存從 index.js 回傳的 backToMenu 方法備用
let started = false; // 儲存遊戲狀態
start 函式負責開始這個遊戲,包含將 started 狀態改成 true ,並將從 index.js 傳過來的 backToMenu 儲存成 onExit ,然後渲染遊戲畫面。
handleInput 負責監聽,當按到 q 的時候就執行 onExit 回到 index.js 。
stop 的作用在清除狀態,把 started 改為 false ,並將 onExit 改回 null ,這樣 handleInput 在偵測到 start 為 false 的時候,就不會繼續執行這個遊戲。
這邊特別想帶大家注意的是遊戲端拿掉了這些設定終端機的邏輯:
原因是這些都已經由 index.js 處理掉了。
那麼我們最關心的問題:目前都還沒回答到如何重新開啟新的遊戲,而不是暫停遊戲,這就要來簡單讀一下 index.js 的這一段:
大家有回想起來剛剛是整個遊戲檔案被封裝成一個 createBoxGame 嗎,然後這邊在遊戲註冊的時候,把它放進了 create 裡面,後面再用 .create() 來執行這一個函式,這樣就讓整個遊戲全部重來一遍了,包含遊戲物件的重新生成。
這一版兩個遊戲之間的改動差異比較小,但是對於整體架構做了更大的調整,其實我覺得這是可以接受的,因為我只要現在改一次,之後的遊戲都 follow 這個規則,讓每一款遊戲都只負責提供 index.js 可以呼叫的方法就好了。
不過還是可以看到為了處理計時器,爆爆王遊戲比推箱子要多了一些改動,你可以看到圖片中多了 enemyTimer 的設計
也不是說不能接受,但會不會有更好的解決辦法呢?
這一版的狀況也是完全 OK 的,關鍵還是在程式碼。
首先特別帶大家來看到爆爆王的這一段,q 按鍵被清除掉了大量多餘的流程,恢復乾淨了:
然後檔案最底部開始執行是這樣寫的:
由於 startBox 傳入的參數是 backToMenu ,所以這裡最主要的差別就是 backToMenu 不是 index.js 傳進來的函式,而是直接讓子程序結束;父程序偵測到子程序正常結束後,再重新顯示選單。
0 則是狀態碼,意思是正常結束遊戲這個子程序,回到 index.js 時,狀態碼正常,所以顯示選單。
但是在 Ctrl + C 的部分就使用到 130 這個狀態碼,代表直接中止,對應到父程序中這一段,如果 130 狀態碼回傳到 index.js 時,由 index.js 把整個程序給中止掉:
那麼我們來看看在 index.js 的部分,是相對改動較多,我們只看最關鍵的幾個部分就好:
引入的地方改成直接引入整個檔案,用一個 activeChild 來記錄現在正在執行哪一個子程序。
我畫了一個流程圖給大家參考:
這個版本對 index.js 改動較大,但只要改變這一次,後面每個遊戲都只需要做非常微幅的調整,就可以順利的接給 index.js 做使用,相較於前兩個版本是更精簡的做法。
這幾個版本都可以順利切換遊戲,而且監聽不會互相打架,所以關鍵差異在:程式碼好不好維護,如果要接入新的遊戲,誰對遊戲邏輯的改動最小會是最終的評斷標準。
版本一:每次開始遊戲時重設狀態,對我們原本的程式碼改動最小,可是日後維護最不容易。
版本二:把遊戲封裝成方法,由 index.js 統一來管理遊戲流程,未來所有的遊戲都可以比照這個規格去封裝並引入 index.js,但仍然需要額外去處理計時器等細節。
版本三:對 index.js 的改動最大,但對遊戲的改動最小,未來開發一款新遊戲只需要把整個檔案引入到 index.js 並依據監聽回傳對應的狀態碼。
如果是你,你會選哪一個呢?我最終選擇的是版本三,最符合我理想中的模式。
回顧這三天的共用入口開發,我們從已經知道怎麼做的地方先下手,寫著寫著最後發現這個架構完全不是我們要的,原因是因為一開始就沒想過要暫停還是中止遊戲的問題,於是我們在最後一天重新考慮了這個情境之後,改用子程序的作法來解決。
我想在最後來反思一下這個流程,談談你們認為一個好的架構從哪裡來的呢?為什麼資深工程師總是知道怎樣寫更精簡?你認為他們是:
從這三天的體驗裡,我認識到我不是那種天才工程師,第一次就什麼都想到、什麼都顧到,也沒辦法在第一次寫這個功能就可以很精煉而且符合需求,我是需要藉由踩坑 → 發現 → 重新構想 → 嘗試新的架構,逐漸找到更適合的作法的人。
但你有想過如果今天不是因為 AI 的話,你會願意寫三種完全不同的方法再來比較優劣,還是會想要妥協於已經寫好的東西,試著在原有的架構裡面掙扎?如果沒有 AI 的話,我可能會沒有勇氣做這麼繁重的工作,相信大家經歷前面三個比較之後也會跟我有共鳴吧!!但由於 AI 的出現,我們得以快速的寫出第一版、第二版、第三版,用不同的方式去找到更精簡的方法,我覺得這就是 AI 的價值所在。
所以我想我們不應該去無條件接受 AI 提供的第一版做法,而是可以試著從各種角度,比如從功能面、從程式碼好不好維護、從流程設計等角度去思考,這個架構是我要的嗎?不是的話,就試著讓 AI 做出更符合期待的作法,或許是對於作品更負責任的方式。
最後最後,你可能會發現其實我們還沒處理遊戲結束的時候直接結束程序的問題,這就留給讀者們自行來找方法處理了XD 我相信前面分享了那麼多的方法,你一定可以的!
那麼明天,我們要來開發新的遊戲了!!你有猜到是什麼遊戲嗎?也許你可以試著列看看 CLI 還可以開發哪些遊戲呢?你想藉由這些遊戲練習怎樣的技巧呢?
我們明天見!
如果你想要瀏覽完整的程式碼,可以參考這裡: