Construct 2教學-第5節
HJ Online Learning Center
APP開發線上教學
  • Register

Construct 2 教學-第5節

第5節-進階主角設計(上)

 

大家好,我是傅老師。歡迎來到我的Construct 2教學。

上一節我們設計了第一個敵人小史,為小史設計了兩套AI,這一節我們則要武裝蛋蛋老師,讓她按下攻擊鍵能夠投擲武器攻擊小史。但是她如果不幸 撞到小史,就要做個痛痛的表情喔~本節我們會深入介紹"狀態機"的概念,使用狀態機來實作蛋蛋老師。來吧~向小史 開戰啦!

 
 
 
 


 

5-1 以狀態機控制Sprite

在第2節中,我們以簡單的方式增加了5組動畫控制,成功的切換蛋蛋老師行走與停止兩套動畫。事實上第2節的做法並不完備,因為平台遊戲主角除了基本行走之外, 還需具備進行攻擊與被攻擊兩種狀態。行走攻擊被攻擊,此三狀態是主角必備狀態。將以上概念用圖像畫出,可得到下面狀態圖:

蛋蛋老師的控制流程

以下是狀態圖的解說:

1. 遊戲開始,蛋蛋老師初始化為行走狀態("move")
 A. 此時若使用者按下WASD,則執行行走動畫控制,狀態不切換,維持行走狀態。
 B. 此時若使用者按下發射鍵,則將狀態切換為攻擊("attack")
 C. 此時若被擊中,則將狀態切換為被擊中("hit")
2. 進入攻擊狀態("attack")
 A. 將動畫切為攻擊動畫
 B. 此時若被擊中,則將狀態切換為被擊中("hit")
 C. 當攻擊動畫播放完畢,則切換回行走狀態("move")
3. 進入被擊中狀態("hit")
 A. 將動畫切為被擊中動畫
 B. 此時若按下發射鍵,必須忽略(圖中以淺灰色表示)
 C. 當被擊中動畫播放完畢,則切換回行走狀態("move")

如何?簡單吧~這些特性都符合常見的平台遊戲。其中特別要請大家注意的地方是項目3.B,此項目所隱含的是:主角被攻擊時無法立刻反擊,得要等到被攻擊動畫播完才可反擊。這是很常見的合理設定,所以狀態圖中傅老師故意塗成淺灰色來提醒大家,千萬別多寫了這條項目喔~

上面這個狀態圖,在軟體編程的領域裡他有一個響亮的名號,稱為"有限狀態機" (Finite State Machine,簡稱FSM)。如果您唸的是電子電機或是資訊科系,應該在一年級的時候老師就會教授FSM的設計。不過大部份的同學在當下應該無法悟出 FSM有啥用途...(還有人可能聽到一半就睡著了),得要等到畢業進了公司才會知道,原來FSM可以用來設計電子錶、熱水瓶、微波爐,當然還有我們平台 遊戲中的主角蛋蛋老師呀!

具體實作FSM的方式是採用一個無限迴圈(C語言實作可用while()),在迴圈內將所有的狀態以條件判斷式串接(C語言實作可用switch())。在C2下實作FSM,可比照第4節所教的For each狀態控制迴圈(*1),圖示如下:

以狀態實作蛋蛋老師的控制流程

OK~底下開始實作程式碼。

添加實件變數與無限迴圈

請為[Player]添加一個實件變數,將其命名為"state",用來存放[Player]的狀態。比照4.3之步驟2,為[Player]新增一個名為"state"的實件變數,將其初始值設為"move",如下圖。

新增實件變數

其次增加<For each>迴圈。點選<Add event> -> <System> -> <For each>打開設定視窗,將篩選物件設為 [Player]。

篩選物件設為Player

完成如下圖。

成功加入For each迴圈

實作move狀態

滑鼠左鍵點擊第27條事件前端綠色箭頭部份,將此事件選取。按下鍵盤"s"鍵,加入子事件。選擇[Player] -> <Compare instance variable>,增加一條條件判斷式。在對話框中選擇"state"變數,條件式設為<= Equal to>,數值設為"move"(注意雙引號不可省略)。詳細步驟亦可參照4.3節為小史設定<For each>迴圈的部份。

加入條件判斷式

接著開始實作1.A ~ 1.C。

實作1.A

 

1.A. 此時若使用者按下WASD,則執行行走動畫控制,狀態不切換,維持行走狀態。

滑鼠左鍵點選第9條事件前方綠箭頭,再按住鍵盤<ctrl>鍵不放,以滑鼠左鍵再加選第10、11、12、13條事件,選好以上5條事件後鬆開<ctrl>,將選取事件拖曳接至第28內。完成如下圖,C2會自動重排各事件的號碼。

移動動畫控制

好~做完按<F5>,預覽看看。疑?怎麼一直報錯呢?以前都不會的呀!

是這樣的,C2的事件分為三種:普通事件(normal,又稱every tick)、觸發事件(trigger)、迴圈事件(loop)。普通事件的事件左前方留有一小塊空白;觸發事件左前端是一個綠色箭頭;迴圈事件左前端則 是一個綠色迴旋箭頭。剛才報錯的原因是一個初學者可能會犯的錯誤,就是:

觸發型事件不得做為子事件

由於觸發事件的特性,他只能被放在事件表中的第一層事件。搬動這5條事件前,他們的確位於事件表中第一層;不過搬到第23條子事件內之後,他們就不 再位於事件表第一層了。子事件內是不允許有觸發型事件的,所以剛剛報了一堆錯。修正的方法就是不用觸發型事件,改用普通事件來重新設計1.A動畫控制。我們使用二元樹邏輯來重新設計,動畫控制流程如下:

動畫切換流程

 

刪除第24~28條事件,點選第23條事件前方空白處,按下"s"鍵新增子事件。點選[Player] -> <Is on floor>事件,該事件會添加為第24條。 完成後再重覆一次,點選第23條,按"s",點選[Player] -> <Is on foor>,添加為第25條。完成如下:

動畫切換流程

添加普通型事件

我們要將第二條事件邏輯反轉(Invert),用以偵測 is NOT on floor的狀況。以滑鼠右鍵點擊第25條事件,選取<invert>

邏輯反轉

邏輯反轉

OK~您剛剛實作出了第一層二元判斷式,可判斷出[Player]是否在地面。若為是,則跳進第24條事件;反之若否,則跳進第25條事件。接著使用一樣的操作方式,實作第二層二元判斷式。

首先實作第24條在地面上這組。點選第24條前方空白處,按下鍵盤"s",選擇[Player] -> <Is moving>;完成後再重覆一次,並把重覆這次所產生的事件設為反向邏輯(Invert)。

實作第二層二元樹

接著實作第27條不在地面這組。點選第27條前方空白處,按下鍵盤"s",選擇[Player] -> <Is jumping>;完成後再重覆一次,並把重覆這次所產生的事件設為反向邏輯(Invert)。

二元樹架構完工

OK~二元樹流程架構完工,再來依動畫控制流程圖填入控制用的程式。依動畫控制流程圖,第25條事件代表[Player]在地面上,而且正在行走當 中,理應播放行走動畫。點選第25條事件右方<Add action>,點選[Player] -> <Set animation> -> "walk" + "from beginning",結束。完成後依同樣步驟,為第26條添加"stand"動畫;為第28、29條添加"walk"動畫。

項目1.A完工

完成後按<F5>預覽,動畫控制一切恢復正常。項目1.A完工。

實作1.B

 

1.B. 此時若使用者按下發射鍵,則將狀態切換為攻擊("attack")

傅老師想將發射鈕定為"Shift"鍵,一但偵測到"SHIFT"鍵被按下就將[Player] 的"state"切換為"attack",離開"move"狀態。

點選第23條事件<state = "move">,按下鍵盤"s"新增子事件。選擇<Keyboard> -> <Key is down>,點選<Click to choose>叫出設定視窗來選擇欲偵測按鍵,按下鍵盤右方<Shift>鍵,C2會自動偵測並填入視窗中。


偵測Shift鍵

OK,Done,新增了第30條鍵盤偵測事件。按下其右方<Add action>,點選[Player] -> <Set value> -> "state" + "attack"。項目1.B完工。

項目1.B完工

實作1.C

 

1.C. 此時若被擊中,則將狀態切換為被擊中("hit")

所謂的擊中,就是蛋蛋老師跟小史發生碰撞。碰撞的檢測在第4節我們是使用<On collision with another object>事件來做的,那時用來檢測小史與左右界限的碰撞。可是仔細看一看,疑?這個事件前面有個綠箭頭,他是一個觸發型事件啊~觸發型事件是無法當 成子事件來用的。所以在這裡,我們要換成對應的普通型事件:<Is overlapping another object>。

點選第23條事件<state = "move">,按下鍵盤"s"新增子事件。選擇[Player] -> <Is overlapping another object>叫出設定視窗,選擇[E1]為目標偵測物件。 OK,新增了第31條碰撞偵測事件。

點選右方<Add action>,點選[Player] -> <Set value> -> "state" + "hit"。項目1.C完工。

項目1.C完工

實作attack狀態

首先新增狀態判斷式,滑鼠左鍵點擊第22條事件前端綠色箭頭部份,將此事件選取。按下鍵盤"s"鍵,加入子事件。選擇[Player] -> <Compare instance variable>,增加一條條件判斷式。在對話框中選擇"state"變數,條件式設為<= Equal to>,數值設為"attack"。

新增attack狀態

新增了第32條事件,接著實作2.A~2.C

實作2.A

 2.A. 將動畫切為攻擊動畫

我們已經為蛋蛋老師做了一套"throw"的動畫,這組動畫是一個投球的動作,非常的可愛。所以本項目的意思就是:當目前動畫不為"throw"時,我們要將[Player]的動畫切換為"throw"。

點選第32條事件,按下"s"增加子事件。選取[Player] -> <Is playing> -> 將Animation設為"throw"(雙引號不可省),OK後即新增第33條事件。由於我們要偵測的是"throw"沒被播放的狀況,因此需將此事件 邏輯反轉。滑鼠右擊第33條事件,選擇<Invert>。

事件設置完後點選右方<Add action>,選擇[Player] -> <Set animation> -> "throw" + "from beginning"。

項目2.A完工

OK,項目2.A完工。

實作2.B

 2.B. 此時若被擊中,則將狀態切換為被擊中("hit")

項目2.B與項目1.C根本一模一樣,直接copy過來即可。(Copy的方式可參考4.3節之步驟2)

項目2.B完工

實作2.C

 C. 當攻擊動畫播放完畢,則切換回行走狀態("move")

"throw"動畫一共有17幀,最後一幀標號為16。因此我們可以追蹤目前[Player]播放的幀標號,假如等於16,即代表動畫播放完畢。

throw動畫共有17幀

點選第32條事件,按下"s"增加子事件。選取[Player] -> <Compare frame> -> "Equal to" + "16"

偵測目前播放幀標號

點選右方<Add action>,點選[Player] -> <Set value> -> "state" + "move"。項目2.C完工。


項目2.C完工

實作hit狀態

首先新增狀態判斷式,滑鼠左鍵點擊第22條事件前端綠色箭頭部份,將此事件選取。按下鍵盤"s"鍵,加入子事件。選擇[Player] -> <Compare instance variable>,增加一條條件判斷式。在對話框中選擇"state"變數,條件式設為<= Equal to>,數值設為"hit"。

新增hit狀態

新增了第36條事件,接著實作3.A~3.C

實作3.A

 3.A. 將動畫切為被擊中動畫

3.A與2.A非常相似,將2.A copy過來修改即可。Copy過來以後,將原本標為"throw"動畫的部份改為"dead"動畫,3.A完工。

項目3.A完工

實作3.B

 3.B. 此時若按下發射鍵,必須忽略

3.B不需實作,依原訂計劃忽略即可。

實作3.C

 3.C. 當被擊中動畫播放完畢,則切換回行走狀態("move"). 將動畫切為被擊中動畫

3.C與2.C非常相似,將2.C copy過來修改即可。"dead"動畫只有14幀,最後一幀標號為13。因此要將偵測事件改為標號13。

dead動畫共有14幀

項目3.C完工

完工!按<F5>預覽試試。嗯~動畫非常的流暢,按下<Shift>也會做出投擲的動作,被撞到的時候蛋蛋老師會嚇一跳,此時也無法投擲。很好,蛋蛋老師的FSM狀態機試作成功!

救命呀~有人欺負我~~

 


 

4-2 加入子彈

請下載下面的Sprite包,於C2中新增Sprite,將包中的bullet/b1.png讀入。將Sprite命名為b1,結束。

http://www.memoryabc.com/c2_tutorial/L5_sprite.zip

加入子彈b1

在C2下實做子彈實在是一件非常簡單的事~只要幫物件加上一個<Bullet>行為特性,他就具備子彈的物理特性了。選取b1,在左方參數區中點擊<Behaviors>叫出行為特性視窗。 選擇並增加<Bullet>特性。另外,一般來說子彈超出畫面就應該自動消失,這個C2也已經幫我們設計好了,只需要加入<Destroy outside layout>行為特性即可。

加入後至左方參數區,將<Bullet>行為特性參數按下圖設置:

設置Bullet參數

 

參數 功能
Speed 子彈射速。
Acceleration 加速度。
Gravity 子彈所受的地心引力。
Bounce off solids 子彈碰到固體是否回彈回?
Set angle 是否依照射出方向旋轉子彈圖形?
Initial state 子彈特性之初始狀態。

 

傅老師刻意把子彈放在佈局外側,只是一個放置的習慣。類似像子彈、寶物、特別角色,都可以先放在佈局外側。一來編輯時搜索方便,二來若添加了<Destroy outside layout>,則遊戲一開始該物件就會被刪除,也就不會對遊戲產生進一步的干擾。

我們觀察蛋蛋老師的投擲動畫,發現大概在第4幀把子彈放上蛋蛋老師的手,再於第8幀時將子彈丟出,這樣最順暢。因此這裡要做的項目有以下兩項:

1. 於第4幀產生子彈,放在蛋蛋老師手上。
2. 於第8幀開啟子彈行為特性,讓子彈飛。

以下開始實作。

項目1

滑鼠左鍵雙擊物件區之[Player],打開Sprite編輯畫面,選擇"throw"動畫,選擇標號為4之第4幀,可以看到編輯區中出現了蛋蛋老師仰角投擲的畫面。選擇錨點(image point)調整器。在錨點列表裡點擊新增鈕,會自動新增一個新錨點, 請將其改名為"b1"。改名後將滑鼠移至蛋蛋老師頭上,會出現一個十字型,約略找一下蛋蛋老師左手掌的位置(座標(48,34),目前被她的頭擋住),按下滑鼠左鍵,可以看到該位置出現一個小方框。 b1錨點設置完成,未來產生子彈時,只需要指定子彈產生在b1錨點上即可。完成後在b1上點選右鍵,選取<Apply to whole animation>,讓C2為每一幀自動設置b1錨點。完成後按<Esc>跳出。

新增b1錨點

點擊頁籤選擇器上的<Event Sheet1>,跳回到事件表。找到第32條事件,該事件是"attack"狀態的進入點。點選第32條事件前方空白處選取事件,按下"s"新增子事件。 選擇[Player] -> <Compare frame> -> "equal to" + "4",新增為第36條事件。

新增第4幀檢查事件

於第36條事件右方按下<Add action>,選擇<System> -> <Create object>叫出設定視窗,依下圖設置:

新增子彈

項目2

回頭點選第32條事件,按下"s"新增子事件。選擇[Player] -> <Compare frame> -> "equal to" + "8",新增為第37條事件。按下右方<Add action>,選擇[b1] -> <Bullet:Set enable> ,將子彈行為特性設為"Enabled"。

啟動子彈

設定子彈角度,當[Player]面向左時(也就是鏡向時),子彈運動角度應為240度;反之,當[Player]面向右時(也就是無鏡向時),子彈運動角度應為-60度。 點選第37條事件,按"s"加子事件。選擇[Player] -> <Is mirrored>,加入。於其右方點擊<Add action>,點選[b1] -> <Set angle of motion> -> "240"。

設定子彈角度

以相同方法製作另一側的子彈投擲。點選第37條事件,按"s"加子事件。選擇[Player] -> <Is mirrored>,加入。於其上點擊滑鼠右鍵,選擇邏輯反轉(Invert)。 再於其右方點擊<Add action>,點選[b1] -> <Set angle of motion> -> "-60"。完成如下圖:

兩面發射完成

OK!<F5>預覽一下~疑?好像有機個問題發生了...

1. 連續丟子彈的話,新子彈發射瞬間,畫面上全部的子彈會就地重新發射。
2. 一次會丟出不只一顆子彈。
3. 子彈在脫手前不會跟著蛋蛋老師移動。
4. 子彈擋住蛋蛋老師的頭。

小問題~我們來做些修正吧!

 



5-3 子彈特性修正

會造成上述四個問題的原因,一個關鍵在於我們未追蹤待發射的子彈,導致啟動子彈及調整角度時把整個佈局上的子彈全部變動了。除此之外都是小問題。解決的方式逐項列出如下:

1. 讓蛋蛋老師記得自己手上產生的子彈是哪一顆,等到子彈即將脫手時,選取正確的那一顆子彈來啟用並調整。
2. 播放第4幀時,若蛋蛋老師手上已有子彈,則不可再創造新子彈。
3. 將產生的新子彈在脫手前必須黏住蛋蛋老師。
4. 將產生的新子彈擺放至蛋蛋老師後方。

修正1+修正2

在C2下,每個實件都有自己的唯一識別碼,我們稱之為UID(Unique ID)。我們為蛋蛋老師新增一個實件變數,讓她紀錄自己手上子彈的UID。若UID為0,代表手上沒有待發射子彈;反之若不為0,代表手上正抱持著待發射 子彈。看到物件區,滑鼠左鍵單擊[Player],到參數設定區點擊<Instance variables>,新增一個實例變數,命名為"bullet_uid",類型設為number,初始值設為0。

新增實件變數

首先處理第4幀的部份。找到第36條事件,看到右方已經存在一條創造子彈的程序,點選其下方<Add action>,選擇[Player] -> <Set value> -> "bullet_uid" + "b1.UID"。

儲存子彈UID-1

儲存子彈UID-2

點選第36條事件,按"s"加子事件。選擇[Player] -> <Compare instance variable> -> "bullet_uid" + "Equal to" + "0",產生新的第37條事件。將原本第36條事件後的動作搬移至第37條後,如下圖:

儲存子彈UID-3

第4幀調整完畢,接著處理第8幀。點選第38條事件,按"s"加子事件。選取[b1] -> <Pick by unique ID> -> "Player.bullet_uid",產生第41條事件。產生完畢後依下圖重擺程式。

選取手上的子彈

重擺程式

蛋蛋老師將子彈脫手後,我們應該將"bullet_uid"設回為"0"。請找到第39條事件,點選右方<Add action>,選擇[Player] -> <Set value> -> "bullet_uid" + "0"。 完成後按<F5>預覽,第一及第二個問題正確解掉了。

問題1、2解決

修正3

若想要讓子彈黏附在蛋蛋老師上,造成連動的效果,最簡單的方式就是逐幀調整子彈的位置。子彈於投擲動畫的第4幀產生,於第8幀投出,因此我們需要在 第5~7幀之間調整子彈的位置。點選第32條事件,按"s"加入子事件。選擇[System] -> <Is between values>叫出設定視窗,依下圖之設置將其增加為第42條事件。

設定第5~7幀

<Is between values>可以判斷指定的表達式其數值是否位於上下限之間(Upper/Lower bound),檢查的界限包括上下限值,在本例中,當"Player.AnimationFrame"參數其值為5、6、7三者之一時,此判斷事件都會滿 足成立,進而執行此事件右方之動作。

點選第42條事件,按"s"增加子事件。選擇[b1] -> <Pick by unique ID> -> "Player.bullet_uid"。點選右方<Add action>,[b1] -> <Set position>叫出設置視窗,設置如下:

調整b1位置使其黏附蛋蛋老師

問題3解決

OK,按<F5>預覽。問題三正確解決。

修正4

在C2中,較晚創造的實件會位於較高位置,這就是為什麼產生的子彈會把蛋蛋老師的臉蓋住。我們可以使用<Move to object>動作,把子彈放到蛋蛋老師背後。點選第37條事件,在該事件右方可找到創造子彈的程序。我們就是要在子彈創造後將其擺放至蛋蛋老師身後,加 在此事件後是正確的選擇。

點選第37條事件右方的<Add action>,選擇[b1] -> <Move to object> 叫出設置視窗,設置如下:

將b1擺放至player後面
參數 功能
Where 放置於目標物件前方(in front)或後方(behind)。
Object 目標物件。

問題4解決

OK~按<F5>預覽,四個問題已經全部解決。優呼~!將檔案儲存為L5.capx,下課休息囉~

蛋蛋老師FSM狀態機大功告成!

 



5-4 結語

在本節裡我們學會了有限狀態機(FSM)的設計方法,並使用FSM成功修改了蛋蛋老師的程序。下一節我們將會實作我方子彈攻擊與敵人攻擊。

有限狀態機是一個非常重要的設計技術,所以傅老師用了整整一節來教這個題目,其中包括:如何構思狀態圖、將狀態圖轉為C2程式碼、預覽找出問題逐一返修。採用這樣的設計思路,可以讓程式更具結構化,而不是想到哪兒寫到哪兒。

學會了嗎?我們下次見!

 



5-5 傅老師鼓勵你...

 

  1. 參考本節開始的狀態圖,加入一個會自動撿起寶物的狀態,將修改過的狀態圖手繪出來。
  2. 以傳統 的紅白任天堂機來說,使用者除了十字方向鍵外(D-pad),還有A、B兩鍵。若將本遊戲WASD鍵對應到D-pad,將Shift鍵對應到A鍵,則我們 目前有一顆B鍵沒派上用場。請將這個B鍵設計為新的主角動作及其狀態,參考本節開始的狀態圖,將此新狀態加入狀態圖,手繪出來。

 



附註

 

*1。C2的事件表本身就是一個巨大的無限迴圈,每次執完事件表中最後一則事件後,C2會自動捲回到第一條事件。
*2。Pin原本應翻譯為"釘附",這裡依傅老師的習慣翻譯為黏附。