如何製作 Pong 遊戲-hyperPong

**注意:**本教學的完成專案檔案可在本文末尾找到。
簡介
在本教學中,我們將從頭開始製作一款類似 Pong 的遊戲。我們會介紹 hyperPad 中一些較進階的功能與行為,因此本教學假設你已具備一些 hyperPad 編輯器的基本知識。如果你想先熟悉如何從零開始製作遊戲,可以先參閱完整指南-製作平台遊戲教學。
Pong 是一款簡單的雙人遊戲,只由兩個球拍和一顆球組成,可以視為桌球的簡易數位版本。每位玩家各自控制位於球桌兩側的球拍。球拍只能前後移動,將球擊向對手。如果玩家沒接到球,另一位玩家就得一分。率先得到 11 分的玩家獲勝!
我們會為 Pong 遊戲加入三種遊玩模式:1 人模式(玩家對 AI)、2 人模式(玩家對玩家),以及 0 人「觀戰」模式(AI 對 AI)。
第 1 部分:設定專案與主選單
讓我們先建立一個新專案。
開啟 hyperPad 中的「Projects」選單,並點選「New Project」。為專案命名,然後點選「Next」。方向保持預設的橫向模式即可。本教學的「Device Support」設定為 iPad 4:3。在下一個畫面中,選取「Bird's Eye View」,其餘設定維持預設值。
現在新專案已準備就緒,在開始製作遊戲本身之前,先快速建立主選單。首先,將場景重新命名為「Main Menu」。
點選「Project/Scene Menu」按鈕,再點選「Scene 1」旁邊的齒輪按鈕。
點選「Settings」選項,將場景重新命名為「Main Menu」。點選「Save」以儲存並關閉設定視窗。
接著,為了保留早期復古遊戲(例如街機版 Pong)的傳統風格,可以將背景設為黑色。
點選「Scene Settings」按鈕並選取「Background」。點選「Change Colour」按鈕,將顏色設為黑色(#000000FF)。
現在在主選單加入標題標籤。點選「Special Objects」按鈕並選取「Label」。
在文字欄位中輸入遊戲名稱(本教學輸入的是「hyperPong」),並將字體大小設為大一點,但不要太大。若標題是「hyperPong」,72 點大小就很合適。將顏色改為白色(#FFFFFFFF),並將標題放在選單上方附近的中央位置。
現在主選單場景已經準備好,可以加入不同遊玩模式的按鈕了。暫時先讓選單保持原樣,等遊戲可以遊玩後再完成它。
第 2 部分:設定遊戲場景與球拍
點選「Project Menu」按鈕,再點選「Add Scene」。將場景命名為「Game」,然後點選「Create」。現在進入 Game 場景後,應該會看到空白畫布。和 Main Menu 場景一樣,再次點選「Scene Settings」按鈕,並將背景設為黑色(#000000FF)。
接著開始製作玩家球拍。前往「Special Objects」按鈕並選取「Empty Object」,建立一個空物件。將顏色改為白色(#FFFFFFFF),並確認不透明度滑桿已調至最滿。
將物件重新命名為「Player-1」,並將其物理模式設為「Physics」,同時保持「Passable」啟用。
保留「Passable」啟用的原因,是這樣我們就能更精確地控制球碰到球拍時的反應。我們會手動控制球的速度和方向。
在 Player-1 物件屬性中,前往「Transform」分頁,將 X 比例設為 25%,並將物件移至遊戲場地左側。
接著為球拍加入控制,讓它可以移動。選取球拍後,返回「Physics」分頁,開啟它的行為編輯器。
在「Interaction」區段中放置「Joystick Analog」行為(如果找不到,可能需要點選「Advanced」分頁)。這會自動在場景編輯器中建立一個搖桿。
不過,我們不希望球拍只能透過搖桿移動,也希望 AI 能夠控制它移動。
在「Custom」區段中放置一個「Box Container」。它會儲存球拍要移動的方向。如此一來,無論是玩家操作搖桿,還是 AI 進行計算,都可以透過修改同一個值來移動球拍。它的預設值應設為 0。最好也替它取個容易辨識的名稱,例如「Move Direction」。
選取「Box Container」後,按下「Change Input Field」按鈕並點選「Yes」。這會為方塊容器建立一個「Set Input Field」行為,讓我們可以在需要時修改它的值。將「Set Input Field」連接到「Joystick Analog」。
選取「Set Input Field」行為後,將「Joystick Analog」的輸出值拖曳至「New Value」欄位。選取「y」屬性。現在,「Move Direction」方塊容器會追蹤玩家將搖桿往上或往下推的幅度(y 軸)。
現在我們會追蹤搖桿往上或往下移動的幅度;如果玩家放開搖桿,也必須將這個值重設為 0。否則球拍會一直朝上次記錄的方向移動。
在「Interaction」區段中加入「Stopped Touching」行為,然後點選它以開啟屬性。預設情況下,它會追蹤玩家何時停止觸碰球拍,但我們要追蹤的是玩家何時停止觸碰搖桿。
點選物件屬性以修改,然後選取搖桿。你可能需要先選取「Global UI」圖層,才能選到搖桿。選好後,點選勾選按鈕。此時行為應會顯示搖桿為追蹤物件。
點選稍早建立的「Set Input Field」並複製它。將複製出的「Set Input Field」連接到「Stopped Touching」行為,並將它的「New Value」屬性設為 0。
現在,「Move Direction」方塊容器變數會追蹤搖桿的上下移動,並在放開搖桿時重設為 0。這是讓球拍知道該如何移動的好方法,接下來就為球拍加入移動功能。
在「Custom」區段中放置「Timer」行為。在它的屬性中,將間隔設為 0。這個計時器會盡可能頻繁地執行(每個影格執行一次),讓我們有機會頻繁更新球拍的移動。
設定球拍速度之前,我們要先確認「Move Direction」方塊容器的值限制在合理範圍內,以免它太高或太低,否則在某些情況下球拍會移動得太快。
在「Logic」區段中放置「Minimum」和「Maximum」行為。將「Minimum」連接到計時器,再將「Maximum」連接到「Minimum」。完成後應該會像這樣:
在「Minimum」行為的屬性中,將第一個值設為「Move Direction」行為的輸出值,並將第二個值設為 1。
「Minimum」行為會輸出輸入值中最小的數字。因此,如果「Move Direction」的值大於 1,它就會輸出 1。否則,它會直接輸出「Move Direction」的值。
在「Maximum」行為的屬性中,將第一個值設為「Minimum」的結果,並將第二個值設為 -1。
「Maximum」行為會輸出輸入值中最大的數字。因此,如果「Move Direction」的值小於 -1,它就會輸出 -1。否則,它會輸出「Minimum」的值(也就是 1 或「Move Direction」中較小的那個值)。
這表示「Maximum」的結果實際上就是介於 -1 和 +1 之間的「Move Direction」值。這讓我們可以完全控制球拍的最大移動速度!
-1 到 +1 的範圍很容易使用,但如果直接將它當作球拍速度,移動會太慢。
再次前往「Logic」區段,放置「Multiply Values」行為並連接到「Maximum」行為。第一個值設為「Maximum」行為的輸出結果。第二個值則可以設為一個合適的「最大速度」。
目前先設為 20。如果稍後覺得球拍移動太慢或太快,只要將這個數字調高或調低即可。
現在,「Multiply Values」行為會提供好用的速度範圍,從低至 -20 到高至 +20。
接下來可以設定球拍速度了。在「Physics」區段中加入「Set Velocity」行為,並將它連接到「Multiply Values」行為。將「x」欄位設為 0,並將「y」欄位設為「Multiply Values」行為的輸出結果。
試玩看看!遊戲開始後,球拍應該會隨著你上下移動搖桿而移動。
測試時你可能已經注意到,沒有任何東西能阻止球拍移出畫面。下一部分中,我們會加入邊界,讓所有物件都留在視野內。
第 3 部分:遊戲邊界
繼續之前,先預想一下良好的遊戲設計。隨著我們逐步完成遊戲,玩家球拍會越來越靠近畫面邊緣,而不是位於畫面中央。這會造成問題,因為我們的 UI 元素(例如搖桿)會與遊戲元素(例如球拍)重疊。
我們不應將整個畫面都設為可玩區域,而是應將邊界往內縮,避免球拍跑到搖桿下方、被玩家手指遮住,讓玩家看不清楚。這樣就不會在可玩區域底部附近找不到球拍。我們也可以將這些內縮的邊界做為硬性邊界,防止物件離開可玩場地。
點選「Special Objects」按鈕並建立新的「Empty Object」。點選新的空物件以開啟它的屬性。
將物理模式保持為「Wall」,並關閉「Passable」。設定顏色,讓我們在畫面上看得見它;不透明的深灰色(#1F1F1FFF)就很合適。將摩擦力設為 0%,彈性設為 100%。
接著,在牆的「Transform」分頁中,將 Y 比例設為 200%,並將它移到畫面區域的頂端。再拉伸牆面,使它符合畫面區域的完整寬度。你也可以開啟格線吸附模式,協助精確定位。
現在複製這面牆,並將副本移至底部。
再複製一次,這次將 X 比例設為 200%。接著拉伸它的高度,讓它符合畫面,並將它放在畫面左側。
再複製一次垂直牆,並將它移至畫面右側。現在應該已經有完整封閉的遊戲場地:
現在開始遊戲時,你可能會立刻發現球拍可以直接穿過牆,就像牆不存在一樣。這有兩個原因:我們將球拍設為「Passable」,而且每個影格都會強制設定球拍速度。這沒有問題,只要加入一種方式,阻止球拍移入牆內即可。
我們會檢查玩家的位置與牆的位置;若玩家的位置超出邊界,就將玩家移回可玩場地。
為了讓這個行為更容易實作,點選上方的牆,並將 Y 錨點設為 0%。這會將牆的位置錨定在它的最底端。你可能需要點選鎖定圖示來解除錨點欄位的鎖定。
同樣地,將底部牆的 Y 錨點設為 100%。這會將底部牆的位置錨定在它的最頂端。
現在可以開始實作邊界檢查行為了。點選球拍並開啟它的行為編輯器。
先從「Logic」區段放置「Get Bounding Box」。預設應該會使用球拍。此行為會提供球拍的底部/左側座標以及尺寸。
接著從「Logic」區段放置「Add Values」,並將它連接到「Get Bounding Box」行為。在「Add Values」屬性中,將第一個值設為「Get Bounding Box」結果的「y」,第二個值設為結果的「height」。
相加的結果就是球拍頂端的位置。為了方便辨識,你可以將行為重新命名為更有意義的名稱,例如「Add Values - Paddle Top」,這樣就能清楚知道它的用途。
現在要檢查球拍是否移入上方或下方的牆。在「Transform」區段中放置兩個「Get Position」行為,並將它們平行連接到「Add Values」行為。
將第一個「Get Position」行為設為追蹤底部牆,第二個設為追蹤頂部牆。你也可以重新命名這些行為,方便辨識。完成後的結構應該類似這樣:
對每個「Get Position」行為,從「Logic」區段放置一個「Subtract Values」行為並連接。
對於連接底部牆「Get Position」的「Subtract Values」,在屬性中將第一個值設為底部牆「Get Position」結果的「position_y」,第二個值設為「Get Bounding Box」的「y」。
接著,對於連接頂部牆「Get Position」的另一個「Subtract Values」,將第一個值設為頂部牆「Get Position」結果的「position_y」,第二個值設為「Add Values - Paddle Top」的結果。
這些減法會告訴我們必須將球拍「推回」多遠,才能讓它留在遊戲場地內。現在只需要檢查是否需要這麼做。
在「Logic」區段中放置一個「If」行為,並將它連接到兩個行為分支。
對於連接底部牆分支的「If」行為,將第一個值設為相連「Subtract Values」行為的結果。條件設為 >(「Is Greater Than」),第二個值設為 0。
對於連接頂部牆分支的「If」行為,將第一個值設為相連「Subtract Values」行為的結果。條件設為 <(「Is Less Than」),第二個值設為 0。
這兩個「If」陳述式會分別在球拍越過對應的牆時繼續執行。
如果球拍越過牆,我們要強制停止它繼續移動:將球拍速度重設為 0,然後再把它推回場地內。
在「Physics」區段中為每個分支放置並連接「Set Velocity」行為,並將兩者的 Velocity X/Y 值都設為 0。
現在可以將球拍推回遊戲場地了。在「Transform」區段中,為每個分支放置一個「Move By」行為。
在每個「Move By」行為中,將 X 值保留為 0,並將 Y 值設為該分支中「Subtract Values」的結果。接著將持續時間設為 0 秒(立即移動)。
執行這整個區段時,會檢查球拍是否進入牆內、進入多深,並在需要時將它推出牆外。現在只需要在球拍每次移動時執行這串行為。
為了簡化流程,在這串行為的最上方,從「Custom」區段加入「Behaviour Bundle」,並將它連接到「Get Bounding Box」之前。你可以將它重新命名為容易辨識的名稱,例如「Check Bounds」。
注意:「Behaviour Bundle」行為本身不會執行任何操作,在此處僅用來標示檢查球拍是否超出邊界的「起點」。
接著,在「Custom」區段中放置「Execute Behaviour」。在它的屬性中,點選「Select Behaviour」欄位,選取剛建立的「Check Bounds」行為。現在將「Execute Behaviour」移到原本設定球拍移動速度的位置下方,並將它連接到球拍移動行為鏈中的「Set Velocity」。
現在,每次球拍更新移動速度時,「Execute Behaviour」就會將執行流程「跳轉」至新的「Check Bounds」行為鏈,檢查球拍是否移出邊界!
試玩看看,看看效果如何!
繼續之前還有最後一件事:將球拍移到場地左側。
現在有了足夠空間,來製作球吧!
第 4 部分:製作球
返回場景編輯器,建立一個空物件。將它的物理模式設為「Physics」,並關閉「Passable」,讓球可以移動並與邊界牆產生實體碰撞。將摩擦力設為 0%,彈性設為 100%,顏色設為白色(#FFFFFFFF)。為了方便起見,也可以將物件重新命名為「Ball」。
在「Transform」分頁中,將比例設為 X=25%、Y=25%,位置設為 X=50%、Y=50%。
接著開啟球的行為編輯器,為它加入移動功能。在「Custom」區段中放置「Wait」行為,並將時間設為 3 秒。
接著從「Logic」區段放置「Random Number」行為,並將它連接到「Wait」行為。第一個值輸入 -1.0,第二個值輸入 1.0。請務必包含「.0」,這樣才會隨機挑選 -1.0 到 1.0 之間、包含小數位的數字;否則只會挑選整數。這會做為球初次發球時的隨機「垂直方向」。
接著從「Physics」分頁放置「Apply Force」行為並連接。將 Force X 設為 -1,Force Y 設為「Random Number」行為的結果。這會決定球移動的方向,但 1 牛頓的力並不大,因此還需要調整倍率。將倍率設為 150,並測試遊戲,看看速度是否太快或太慢。如果是,可以依照喜好調整倍率。遊戲開始後,球應該會在 3 秒後開始移動,就像剛發球一樣。
測試時,球應該會在牆壁之間反彈,但會穿過球拍。這是正常的,因為我們還沒實作球撞上球拍時的行為。你可能也注意到球有時會開始旋轉,但我們不希望這樣,所以來修正它。
回到球的行為編輯器,在「Transform」區段中任意位置放置「Lock Rotation」行為,再次測試。現在球就不會失控旋轉了。
加入球拍碰撞行為之前,先返回場景編輯器,點選球拍,並在球拍屬性中前往「Tags」分頁。在文字欄位中輸入「Paddle」,並按下「+」按鈕,將標籤加入球拍。
現在回到球的行為編輯器。在「Object」區段中任意位置放置「Collided」行為。在該行為的屬性中,點選「Tags」分頁,並選取剛建立的 Paddle 標籤。現在每當球碰到任何帶有「Paddle」標籤的物件時,就會執行此事件。
我們要取得球目前的速度,並反轉它的 X 速度。如此一來,如果球原本向左移動,就會開始向右移動,反之亦然。我們也希望球的速度比之前稍微快一點,這樣隨著回合進行,球會逐漸加速。
將「Get Velocity」行為從「Physics」區段放置並連接到「Collided」行為。接著從「Logic」分頁連接「Multiply Values」行為。第一個值設為「Get Velocity」的「velocity_x」結果,第二個值設為 -1.03。這次乘法會得出球的負 X 速度,並使球速增加 3%。接下來套用這項變更。
將「Set Velocity」行為從「Physics」區段放置並連接到「Multiply Values」行為。將它的 X 速度設為「Multiply Values」的結果,並讓 Y 速度保持目前的速度。
現在開始遊戲時,球應該會四處反彈、撞上球拍並反向移動,速度還會有微乎其微、幾乎察覺不到的增幅。
遊戲逐漸成形了!球和球拍現在都能順暢移動,但球的垂直方向不會改變。球撞上球拍時,垂直方向也應該改變:如果球撞上球拍靠近頂端的位置,就應該往上飛;如果撞上靠近底端的位置,就應該往下飛。這會為遊戲增加更多變化,也讓玩家有機會控制球的去向,增添技巧性。
要決定球的垂直方向應該改變多少,我們需要取得球和球拍位置之間的差值。在「Transform」區段中放置兩個「Get Position」行為,並將它們串接在「Set Velocity」行為之後。第一個「Get Position」預設會追蹤 Ball。將第二個「Get Position」的物件改為「Collided」事件行為的輸出「ObjectB_ID」。
**注意:**此處的「ObjectB」是發生碰撞的球拍,因為它是碰撞事件相關的第二個物件;第一個物件(「ObjectA」)則是球。
從「Logic」區段放置「Subtract Values」行為並連接。第一個值設為追蹤球的第一個「Get Position」之「position_y」結果。第二個值設為追蹤碰撞球拍的第二個「Get Position」之「position_y」結果。
接著從「Physics」區段放置「Apply Force」行為並連接。將 X 力設為 0,Y 力設為「Subtract Values」的結果。單靠位置差異產生的力不夠強,所以還需要調整倍率。試試 50,看看遊戲中的感覺如何。
測試時,球現在應該會依照你擊中球拍的位置改變垂直方向。如果球撞上球拍靠近頂端的位置,就應該往上飛;如果撞上靠近底端的位置,就應該往下飛。如果球的方向相反,試著交換「Subtract Values」行為中的值,應該就能解決問題。
球的移動功能幾乎全部完成了!還有一個小問題:球速沒有上限!事實上,如果持續玩一段時間而不重設,球會快到直接穿過物件。我們應該限制球的速度,避免它達到極高的速度。
在「Apply Force」行為之後,從「Physics」區段放置並連接「Get Velocity」,取得球的新速度。在它下方,從「Logic」區段依序放置並連接「Maximum」和「Minimum」行為。
在「Maximum」屬性中,將第一個值設為「Get Velocity」的「velocity_x」結果,第二個值設為 -25。這會得到兩個值中較大的那一個。
在「Minimum」屬性中,將第一個值設為「Maximum」的結果,第二個值設為 25。這會得到兩個值中較小的那一個,實際上就是將球的 X 速度限制在 -25 到 25 之間,不能更高或更低!
現在也要對球的 Y 速度做相同處理。再依序放置並連接一組「Maximum」和「Minimum」。在新的「Maximum」屬性中,將第一個值設為「Get Velocity」的「velocity_y」結果,第二個值設為 -10。
在新的「Minimum」屬性中,將第一個值設為新的「Maximum」結果,第二個值設為 10。這會將 Y 速度限制在 -10 到 10 之間。你也可以稍後依照喜好調整這些數值!
現在已取得受限的球速值,可以套用它們了。從「Physics」分頁放置並連接「Set Velocity」行為。在它的屬性中,將 X 速度設為第一個限制 X 速度的「Minimum」結果,再將 Y 速度設為第二個限制 Y 速度的「Minimum」結果。
現在,無論比賽進行多久,球都不會達到極端速度。Y 速度也受到限制,因此球不會偏斜得太厲害。
第 5 部分:球拍人工智慧
雖然複製球拍並製作第二個可操作的球拍就可以了,但能選擇與 AI 對戰,而不只是雙人遊戲,豈不是更棒?當然!在本部分中,我們會讓球拍具備 AI,並讓玩家可以選擇 2 人模式或與 AI 對戰。
利用我們為玩家移動設計的系統,也就是修改「Move Direction」方塊容器,就能相當直接地實作 AI。球拍不再使用搖桿,而是依照球在它上方或下方來修改自己的「Move Direction」。
我們也會在「Move Direction」中加入一些熵,換句話說,就是隨機性。這會讓 AI 帶有一點類似人類的特性,不會那麼像機器人,也不會完美地玩遊戲。
以下是我們為 AI 設定的規則:
- 只有當球朝 AI 移動時,AI 才會移動。
- 「Move Direction」就是球和球拍的 Y(垂直)位置差,再加上一些熵。
- 每當球撞到任何物件(球拍、牆或其他物件)時,球拍 AI 的熵就會改變。
- 熵會根據球的速度而定:球移動越快,熵值越高,因此球拍的準確度越低。
聽起來很簡單,開始動手吧。
點選球拍並進入它的行為編輯器。找一塊空白處,從「Custom」區段放置新的「Timer」行為。將計時器設為 0 秒。為了方便辨識,可以將它重新命名為「AI Think」。
**注意:**如果此時或之後出現訊息,指出 0 秒計時器數量過多,可以放心忽略。我們的專案不會用到太多這類計時器。
首先,將「Move Direction」設為 0,這樣當球拍不該移動時就會停止移動。點選我們在第 2 部分建立的「Move Direction」方塊行為,再點選「Change Input Field」按鈕,建立新的「Set Input Field」行為。將「Set Input Field」連接到「AI Think」計時器,並將它的「New Value」屬性設為 0。
在「Transform」區段中放置兩個「Get Position」行為,並從「AI Think」計時器的第二個執行連接點依序連接。將其中一個設為追蹤 Ball,另一個保持追蹤球拍。為了更清楚易懂,也可以重新命名這些行為。完成後應該會像這樣:
這樣一來,先將「Move Direction」設為 0,之後才會繼續執行其餘的 AI 行為。
接著從「Physics」區段放置「Get Velocity」行為並連接,將它設為追蹤 Ball。這三個行為提供了我們判斷球是否朝球拍移動所需的資料。
從「Logic」區段放置「If」行為並連接。第一個值設為球拍「Get Position」行為的「position_x」結果。條件設為「Is Less Than」(<),第二個值設為球「Get Position」行為的「position_x」結果。
如果此條件成立,代表球拍位於球的左側。換句話說,球在球拍右側。
我們也需要一個分支來處理球在球拍右側的情況。再放置一個「If」行為,並將它吸附到前一個「If」行為右側。這會將它轉換成「Else If」行為,並在第一個「If」行為為 false 時執行。它的屬性維持原樣即可。
現在我們已經有兩個分支,分別處理球位於球拍左側或右側的情況,接下來要檢查球是否正朝球拍移動。
再放置一個「If」行為並連接到第一個「If」行為。第一個值設為球「Get Velocity」行為的「velocity_x」結果。條件設為「Is Less Than」(<),第二個值設為 0。如果球正向左移動,此「If」行為就會成立。這正是我們要的,因為前一個「If」行為會在球位於球拍右側時成立!因此球必定正朝球拍移動。
現在對「Else If」分支重複相同步驟。點選新的「If」行為並複製它,再連接到「Else If」行為。將條件設為「Is Greater Than」(>)。現在,如果球位於球拍左側,這個「If」行為就會執行,檢查球是否向右移動。如果是,球也就正朝球拍移動。
如果執行流程能通過這一連串的「If」陳述式,就表示 AI 可以移動了!現在需要依照球的垂直位置更新「Move Direction」。
在「Logic」區段中放置「Subtract Values」行為。將兩個「If」行為都連接到同一個「Subtract Values」行為。第一個值設為球「Get Position」的「position_y」結果,第二個值設為球拍「Get Position」的「position_y」結果。
完成後應該會像這樣:
這會得出球與球拍在垂直軸上的距離。如果使用這個值,當球遠在球拍上方或下方時,球拍就會嘗試移動得更快。現在只要將這個值設定到「Move Direction」方塊容器中即可!
點選「Move Direction」方塊容器,並按下「Change Input Field」按鈕,建立另一個「Set Input Field」行為。將它移到「Subtract Values」行為旁並連接,將它的「New Value」屬性設為「Subtract Values」行為的結果。
試玩看看,球拍現在應該會自己動起來了!
你可能已經注意到,AI 玩得非常好。目前的 AI 設定幾乎能完美遊玩;如果想讓人類玩家有合理的勝算,這就成了問題。這時候就需要加入熵:讓 AI 帶有隨機性,可以降低它的準確度,讓它不那麼像機器人,也不會讓人類玩家覺得太難纏。
回到球拍的行為編輯器,從「Custom」區段放置另一個「Box Container」,並將它命名為「Entropy」,方便辨識。預設值設為 0。接著從「Logic」區段放置「Add Values」行為。
將最近建立的「Set Input Field」從「Subtract Values」斷開,改為在兩者之間連接「Add Values」。在「Add Values」屬性中,第一個值設為「Subtract Values」的結果,第二個值設為「Entropy」方塊容器的結果。
在「Set Input Field」屬性中,將「New Value」屬性從「Subtract Values」的結果改成「Add Values」的結果。
現在球拍的「Move Direction」會加上「Entropy」方塊容器的值,因此只要在「Entropy」中儲存一些隨機值,就能將它加到球拍的「Move Direction」中,降低準確度。繼續吧。
如本部分前面所述,我們希望球撞到任何物件(例如球拍或牆)時,熵都會改變。這樣可以模擬人類每次需要在球路改變時,重新評估球的去向(而且不一定準確)。
找一塊空白處,從「Object」區段放置「Collided」事件行為。第一個物件選取 Ball 做為追蹤物件。第二個物件保持原樣。其他設定也都維持不變。每當球與任何物件碰撞時,就會執行此事件行為。
本部分前面也提到,熵應該依據球的速度決定。這可以模擬人類在球移動得越快時,對球路的判斷也越不準確。
我們要將基本熵設為介於球的負 Y 軸速度和正 Y 軸速度之間的數值。從「Physics」區段放置「Get Velocity」行為並連接,將它設為追蹤 Ball 物件。
從「Logic」區段放置「Subtract Values」並連接。第一個值設為 0,第二個值設為「Get Velocity」的「velocity_y」結果。這會得到球的負 Y 軸速度。
接著從「Logic」區段放置「Random Number」行為並連接。第一個值設為「Get Velocity」的「velocity_y」結果,第二個值設為「Subtract Values」的結果。這會產生介於負「velocity_y」和正「velocity_y」之間的數字。
我們可以直接將隨機數的結果當作熵值,但這會讓球拍變得非常不準。因此,我們需要稍微縮小它的數值。
從「Logic」區段放置「Divide Values」並連接。第一個值設為「Random Number」的結果,第二個值設為 5。第二個值越大,球拍就越準確;反之,第二個值越小,球拍就越不準確。
**注意:**你可以依照喜好修改這個值,調整 AI 的難度。也可以根據選取的難度等級修改此值。
現在更新「Entropy」方塊容器。點選它,再點選「Change Input Field」按鈕,為「Entropy」建立「Set Input Field」。將「Set Input Field」連接到「Divide Values」,並將它的「New Value」設為「Divide Values」的結果。
現在開始遊戲時,AI 就不會那麼像機器人了。它會犯更多錯誤,也不會只想把球打到球拍正中央。
至此,我們已完成製作 AI 所需的所有項目。下一部分中,我們會複製球拍並設定不同的遊玩模式。
第 6 部分:第二個球拍與遊玩模式
在本部分中,我們會設定不同遊玩模式的選項(例如玩家對玩家、玩家對 AI,以及 AI 對 AI),並製作第二個球拍。
在場景編輯器中選取「Global UI」圖層。點選「Create Special Object」按鈕,建立一個文字為「Game Settings」的標籤物件。將標籤移到畫面外,讓玩家看不到它。
我們會使用這個物件來儲存一些遊戲設定。由於標籤位於「Global UI」上,切換場景時它會自動刪除。但儲存在此標籤上的屬性不會因場景切換而重設,因此很適合用來儲存要保留的資料。
**注意:**某些場景載入行為中的「Refresh」設定會使所有物件重新載入,包括「Global UI」物件及其屬性。在自己的專案中建立「Global UI」物件時,請記住這一點!
選取「Game Settings」物件後,前往「Attributes」分頁並建立兩個屬性。第一個命名為「Player 1 AI」,第二個命名為「Player 2 AI」。將「Player 1 AI」屬性設為 0,「Player 2 AI」屬性設為 1。
我們會使用這些設定,將球拍設為「Player mode」或「AI mode」。例如,如果「Player 1 AI」設為 0,就表示 Player 1 是玩家而非 AI。如果「Player 2 AI」設為 1,就表示 Player 2 是 AI。我們設定這些預設值,是為了能直接從編輯器測試遊戲。
現在回到「Main」圖層,再次進入球拍的行為編輯器。我們會加入一些行為,依照球拍應該是玩家還是 AI,停用某些球拍行為。
找一塊空白處,從「Objects」區段放置「Get Attribute」。在它的屬性中,將物件欄位設為追蹤「Game Settings」標籤。你需要在物件選取畫面中選取「Global UI」圖層,才能選到它。
將「Attribute Key Type」設為「Predefined」,並將鍵值設為「Player 1 AI」。此行為會取得遊戲設定中為玩家 1 設定的值。
接著從「Logic」區段放置「If」行為並連接。第一個值設為「Get Attribute」的「attributeValue」結果。如果尚未設定,將條件改為「Is Equal To」(=),並將第二個值設為 0。如果此球拍是玩家而不是 AI,就會執行此「If」分支。
在「Custom」區段中放置「Behaviour Off」行為並連接。將選取的行為設為「AI Think」計時器。此行為執行時會停用「AI Think」計時器,這表示只有搖桿會修改球拍的「Move Direction」。
現在也要為 AI 模式設定類似行為。這需要使用「Else If」行為。再放置一個「If」行為,並將它吸附到上一個「If」行為的右側,轉換為「Else If」。不需要修改任何屬性。
再次前往「Custom」區段,放置並連接兩個「Behaviour Off」行為到「Else If」。其中一個設為「Joystick Analog」行為,另一個設為「Stopped Touching」行為。
現在,如果球拍設定為 AI,就會停用搖桿移動事件,只允許「AI Think」計時器控制移動。這已經很接近我們想要的效果,但搖桿既然不會做任何事,也應該將它隱藏。
在「Transform」區段中放置「Hide Graphic」行為並連接。將物件設為搖桿。你可能需要先選取「Global UI」圖層,才能選到它。
既然這裡會隱藏圖像,也應該加入一種方式,以便球拍切回玩家模式時重新顯示搖桿。從「Transform」區段放置「Show Graphic」行為,並將它連接到停用「AI Think」計時器的「Behaviour Off」行為。將物件設為同一個搖桿。
現在終於可以建立第二個球拍了。點選 Player 1 球拍,按下複製按鈕,並將它移到可玩區域右側。開啟格線,確保位置對齊。將它命名為「Player-2」。
現在要複製搖桿,讓它擁有獨立控制。選取「Global UI」圖層並點選搖桿。複製它,也將副本移到右側。
接著要更新 Player-2 球拍中的幾個行為。回到「Main」圖層,點選 Player-2 球拍並開啟它的行為編輯器。
點選剛建立的「Get Attribute」。在屬性中,將鍵值從「Player 1 AI」改為「Player 2 AI」。
接著,將剛建立的「Show Graphic」和「Hide Graphic」都改為追蹤新建立的第二個搖桿。
最後,將我們早在第 2 部分建立的「Joystick Analog」和「Stopped Touching」事件行為都改為追蹤第二個搖桿。
現在開始遊戲時,應該可以和另一側的 AI 互相擊球了!(記得我們在遊戲設定中將「Player 2 AI」設為 1,因此它會啟用 AI 模式並停用玩家控制。)
有了將球拍設定為玩家操作或 AI 操作的功能後,現在可以更新主選單按鈕了。前往場景選單並選取「Main Menu」,開啟我們在第 1 部分建立的選單場景。
點選「Create Special Object」按鈕並建立新的標籤。將它的顏色改為白色(#FFFFFFFF),以符合主題,並將它置於畫面中央。將文字改為「Play 1 Player」,並將文字對齊方式設為置中。
我們需要加入一些行為,讓它使用正確的設定載入遊戲。這個按鈕要啟用玩家對 AI 模式,也就是 Player-1 是玩家,Player-2 是 AI。選取新的標籤後,開啟它的行為編輯器。
先從「Interaction」區段加入「Started Touching」事件行為。它應該已經在追蹤這個標籤。
接著從「Object」區段放置「Set Attribute」行為並連接。將追蹤物件設為「Game Settings」標籤,你可能需要先選取「Global UI」圖層才能選到它。將屬性鍵值類型設為「Predefined」,並選取「Player 1 AI」屬性。由於這個按鈕會以玩家對 AI 模式開始遊戲,將屬性值設為 0,避免 Player-1 球拍變成 AI。
接著複製「Set Attribute」行為並連接。將它的鍵值設為「Player 2 AI」,屬性值改為 1。這會讓 Player-2 球拍由 AI 控制。
最後,從「Scene」區段放置「Load Scene」行為並連接。將要載入的場景設為 Game 場景。關閉「Refresh」選項,因為它會重設「Game Settings」屬性。你也可以依喜好選擇轉場動畫。
離開該標籤的行為編輯器,返回選單場景。選取「Play 1 Player」按鈕,複製它並將副本移到另一個標籤下方。將文字改為「Play 2 Player」。
開啟新標籤的行為編輯器,將屬性值改為「Player 1 AI」和「Player 2 AI」都設為 0。這個按鈕會以兩支球拍都由玩家操作的模式開始遊戲。
離開該標籤的行為編輯器,返回選單場景。選取「Play 2 Player」按鈕,複製它並將副本移到另一個標籤下方。將文字改為「Watch AI」。
開啟新標籤的行為編輯器,將「Player 1 AI」和「Player 2 AI」的屬性值都設為 1。這個按鈕會讓兩支球拍都由 AI 操控,你可以觀賞它們彼此對戰。
離開該標籤的行為編輯器,返回選單場景。測試遊戲,確認所有遊玩模式都正常運作。
「Play 1 Player」應該將左側球拍設為玩家操作,右側球拍設為 AI。
「Play 2 Player」應該將左右兩側球拍都設為玩家操作。
「Watch AI」應該將左右兩側球拍都設為 AI。
第 7 部分:計分與勝利條件
目前我們的 Pong 仿作看起來相當有希望。在本部分中,我們會加入計分與比賽獲勝的機制,讓玩家(或 AI)可以互相得分並贏得比賽。開啟場景選單並進入 Game 場景。
點選球並開啟它的行為編輯器。我們要追蹤球何時撞到左側或右側的牆,然後將分數給予得分的玩家。
在「Object」區段中放置兩個「Collided」行為。其中一個的第一個物件預設應該是 Ball,將第二個物件設為左側的牆。另一個「Collided」事件行為的第一個物件也應該預設為 Ball,將第二個物件設為右側的牆。
接著從「Custom」區段放置兩個「Broadcast Message」行為。分別將它們連接到各自的「Collided」行為。對於連接到左側牆「Collided」事件的「Broadcast Message」,將它的事件鍵值改為「player 2 scored」。另一個則設為「player 1 scored」。
稍後我們會使用這些訊息為玩家加分。
現在要停止球的移動,並將球重設回原位。在「Physics」區段中放置「Set Velocity」行為。將它連接到兩個廣播訊息,並將速度屬性設為 X=0、Y=0。完成後應該會像這樣:
從「Transform」區段放置「Move to Point」行為並連接。將位置類型設為「Relative Position (%)」,將點的位置設為 X=50%、Y=50%,並啟用「screen coordinates」。接著將持續時間設為 0 秒,讓球立即移動。
我們也需要發送遊戲需要重設的訊息,這樣玩家得分時就能重設發球計時器。
放置並連接「Broadcast Message」,將事件鍵值設為「reset」。
在球的行為編輯器中,找到我們在第 4 部分建立、用於遊戲開始時發球的「Wait」(3 秒)行為。在它上方從「Custom」區段放置並連接「Receive Message」行為,將事件鍵值設為「reset」。
現在每次遊戲重設時,球都會重新發球。不過,這也造成比賽剛開始時球不會發球。我們需要在球建立時也執行發球行為。可以在「Wait」行為旁連接非事件行為來完成。從「Custom」區段放置「Behaviour Bundle」,並將它和「Receive Message」行為一起連接到「Wait」行為。完成後應該會像這樣:
測試遊戲時,每當球撞到左側或右側的牆,球就應該會重設,並在 3 秒後發球。
現在我們會在玩家得分時廣播訊息,接著來製作計分板。關閉行為編輯器並返回場景編輯器。
選取「Scene UI」圖層,點選「Create Special Object」按鈕並建立新的標籤。將顏色改為白色(#FFFFFFFF),並將它移至場地左上方。文字設為「0」,大小設為 72 點,對齊方式設為置中。
此標籤會追蹤 Player-1 的分數。進入它的行為編輯器,加入一些計分行為。
在「Custom」區段中放置「Receive Message」,將事件鍵值設為「player 1 scored」。接著在「UI」區段中放置並連接「Add to Score」行為。將「Add By」屬性設為 1,並將「Event When Score Reaches」屬性改為 11。
在「Scene」區段中放置並連接「Load Overlay」行為。將要載入的疊加畫面設為預先製作的 Game Over 疊加畫面。分數達到 11 時,「Add to Score」行為會繼續執行並載入 Game Over 畫面,代表比賽結束。
離開行為編輯器並複製計分標籤,將它移到右側,做為 Player-2 的計分標籤。
在新標籤的行為編輯器中,將「Receive Message」事件改為「player 2 scored」。
測試遊戲時,玩家得分後,計分板應該會增加 1 分。如果球撞到左側的牆,就是 Player-2 得分;如果撞到右側的牆,就是 Player-1 得分。分數達到 11 時,就會顯示 Game Over 畫面。
接著加入畫面倒數計時器,倒數球的發球時間。
再建立一個標籤,並將它放在畫面中央。將顏色設為白色(#FFFFFFFF),並清空文字欄位。
開啟標籤的行為編輯器。在「Custom」區段中放置「Receive Message」,並將事件鍵值設為「reset」。接著在「UI」區段中放置並連接「Count Down」行為,將時間設為 3 秒。
最後,再從「UI」區段放置並連接「Set Label」,並讓文字欄位保持空白,讓倒數結束後不顯示任何文字。
球重設時,我們會看到 3、2、1 倒數後發球。我們也希望比賽剛開始時顯示倒數。就像處理球的發球行為一樣,放置「Bundle Behaviour」,並將它和「Receive Message」行為一起連接到「Count Down」行為。完成後應該會像這樣:
測試遊戲時,球發球前應該會顯示 3、2、1 倒數。
接著修改 Game Over 選單,讓它顯示比「Game Over」更有獎勵感的訊息,並做些其他調整。在場景編輯器中開啟場景選單,展開「Overlays」區段。你應該會看到預先建立的「Game Over」疊加畫面。點選齒輪按鈕並進入「Settings」,將它重新命名為「Player 1 Wins」。
接著在場景選單中點選疊加畫面的名稱,開啟它。
首先點選「Restart」標籤並刪除它。接著點選「Main Menu」標籤,將文字改為「End Match」。這樣玩家只能返回主選單,因為重新開始場景也會重設遊戲設定,而我們不希望這樣。
接著點選大的「Game Over」標籤,將文字改為「Player 1 Wins!」。最後將所有標籤的顏色改為白色(#FFFFFFFF),符合遊戲其他部分的主題。疊加畫面現在應該會像這樣:
現在為 Player 2 建立疊加畫面。開啟場景選單,點選「Player 1 Wins」疊加畫面旁的齒輪按鈕。選擇「Duplicate」複製疊加畫面。再次回到場景選單,點選複製出的疊加畫面旁的齒輪按鈕,將它重新命名為「Player 2 Wins」。
在「Player 2 Wins」疊加畫面中,將大型標籤的文字從「Player 1 Wins!」改為「Player 2 Wins!」。
現在要更新計分行為,讓它開啟正確的疊加畫面。在場景選單中展開「Scenes」區段,並開啟 Game 場景。
點選 Player-1 計分板標籤並開啟它的行為編輯器。確認「Load Overlay」已設為開啟「Player 1 Wins」疊加畫面。
返回場景編輯器,並開啟 Player-2 計分板標籤的行為編輯器。點選「Load Overlay」行為,將選取的疊加畫面改為「Player 2 Wins」。
現在玩遊戲時,Player-1 得到 11 分會顯示「Player 1 Wins!」,Player-2 得到 11 分則會顯示「Player 2 Wins!」。
本教學的這部分快完成了!還需要加入一個細節:改變球的發球方向。目前球總是朝 Player-1 發球。比賽開始時,球應該隨機朝左或朝右發球;玩家得分時,球則應朝得分的玩家發球。
返回場景編輯器,點選球並開啟它的行為編輯器。從「Custom」區段任意位置放置「Box Container」,並將它重新命名為「Serve Direction」,方便辨識。
接著從「Logic」區段任意位置放置「Execute Sequence」行為。在它的屬性中,將「Sequence Type」改為「Random」。
這會用來決定遊戲開始時球的發球方向。點選「Serve Direction」方塊容器,並按兩次「Change Input Field」按鈕,建立兩個「Set Input Field」行為。將兩者平行連接到「Execute Sequence」。其中一個「Set Input Field」行為的「New Value」改為 -1。
另一個則將「New Value」設為 1。
遊戲開始時,「Serve Direction」會隨機設為 -1(向左發球)或 1(向右發球)。找到球的發球行為鏈中的「Apply Force」行為,將它的 Force-X 值設為「Serve Direction」方塊容器的結果。
多測試幾次遊戲,球在遊戲開始時應該會隨機向左或向右發球。
現在只需要依據得分者改變發球方向。從「Serve Direction」方塊容器再建立一個「Set Input Field」,找到球撞上左右牆的行為。騰出空間,將它連接到「player 2 scored」的「Broadcast Message」行為。在「Set Input Field」屬性中,將「New Value」設為 1。這會讓球朝 Player-2 發球。
複製「Set Input Field」,將它連接到另一個「player 1 scored」的「Broadcast Message」行為,並將「New Value」改為 -1。這會讓球朝 Player-1 發球。
本教學的這部分就完成了!Player-1 得分時,球應該會向左朝他發球;Player-2 得分時,球應該會向右朝他發球。此外,玩家現在可以互相得分,並最終贏得或輸掉比賽。
第 8 部分:暫停選單、退出與最後調整
到目前為止,遊戲已經具備完成所需的大部分內容,只差幾個小功能,例如遊戲中的暫停按鈕,以及從主選單妥善退出遊戲的方式。最後,我們還應該在主選單隱藏搖桿,因為主選單不需要它們。做完這些,就大功告成了。
在 Game 場景的場景編輯器中,選取「Scene UI」圖層,並點選「Create Special Object」按鈕建立新的標籤。將顏色設為白色(#FFFFFFFF),並將它放在畫面頂端。將文字改為「Pause」,並將對齊方式設為置中。
開啟它的行為編輯器,從「Interaction」區段放置「Started Touching」行為。在它下方放置「Load Overlay」行為,並將選取的疊加畫面設為預先製作的 Pause Menu。確認「Pause」選項已啟用。你也可以依喜好選擇轉場樣式。
也稍微調整一下 Pause Menu。返回場景編輯器並開啟場景選單。展開「Overlays」區段並開啟 Pause Menu 疊加畫面。
如同玩家獲勝疊加畫面的處理方式,點選「Restart」標籤並刪除它。將所有標籤的顏色改為白色(#FFFFFFFF),並將「Main Menu」標籤的文字改為「End Match」。
測試遊戲時,遊戲中的「Pause」按鈕應該會開啟 Pause Menu 疊加畫面,同時暫停背景中的遊戲。按下「Resume」會從暫停處繼續遊戲,而按下「End Match」則會返回主選單場景。
接著為 Main Menu 場景做最後調整。回到場景編輯器,開啟場景選單並展開「Scenes」區段。選取 Main Menu 並開啟它。
建立「Quit」按鈕。先為另一個按鈕騰出空間,再複製其中一個遊玩模式按鈕。將它移到列表底部,並將文字改為「Quit Game」。完成後開啟它的行為編輯器。
在行為編輯器中,刪除除「Started Touching」行為以外的所有行為,讓它成為唯一剩下的行為。
在「Scene」區段中放置並連接「Quit Project」行為。測試主選單時,「Quit Game」按鈕現在應該會結束遊戲。
最後,在主選單中隱藏搖桿。點選標題標籤並開啟它的行為編輯器。我們會在這裡加入幾個行為,隱藏搖桿。
在行為編輯器中,從「Transform」區段放置兩個「Hide Graphic」行為。其中一個設為其中一支搖桿,另一個則設為另一支搖桿。選取搖桿時,你可能需要先選取「Global UI」圖層,才能選到它們。
現在測試選單時,就不會再看到搖桿了!開始遊戲後,球拍會依據自己是 AI 還是玩家操作,顯示或繼續隱藏搖桿,因此遊戲中的搖桿應該會恢復顯示。
結語
恭喜!你已經製作出一款相當像樣的知名 Pong 遊戲仿作。
內容確實不少,但如果你一路完成到這裡,應該已經了解許多遊戲設計主題,從製作人工智慧,到依照設定動態調整場景等。這些知識可以套用到各種類型的遊戲中,讓你的作品更加出色。
我們還可以繼續改進目前的遊戲……
你可以加入自己的圖像和音效,繼續練習新技能。也可以把實作難度等級(簡單、普通、困難等)做為練習:使用我們為 AI 建立的基礎,在選擇 AI 球拍時加入難度設定。(提示:參閱本教學第 4 部分的結尾!)
本教學的完成專案檔案可在此取得:下載教學專案

