GameMaker: Studio 中文教程 #6: 人物與技能
編者按
indienova 會員青銅的幻想為希望了解學習 GameMaker: Studio 的中文讀者專門撰寫了本系列教程。
本欄目已經有了專門的專題頁面,請參看這裡。根據教程四的結尾處的小調查,吃瓜群眾們表示對人物的攻擊技能和敵人 AI 十分有興趣,因此接下來的幾次教程將圍繞這個相關方向展開。
查看部分動圖請點擊圖片,由於知乎專欄不支持代碼高亮,推薦移步原文閱讀。
歡迎讀者朋友在文章後留言,以便作者能夠繼續針對性地安排接下來的教程內容。
教程目標
添加伊瑟拉的普通攻擊和技能。
準備工作
這個系列教程的項目/代碼及原始美術素材全部都在 GitHub 項目庫。這個教程的內容基於目錄 GMS_TUT_04 下的內容開始,完成後的項目文件放在目錄 GMS_TUT_05。此次教程所需的美術資源可以在這裡下載。
導入美術資源
首先需要導入相關的人物動畫,講到這裡我很開心。因為呢,我對我們這個遊戲《冰杖秘聞》的像素美術是十分欣賞的,也很希望能夠分享更多的人物動畫出來(更多視頻及原畫請移步官網欣賞)。這裡是本次教程將要用到的兩套動畫——伊瑟拉的普通攻擊(側面、正面和背面)以及技能攻擊:
在策划上,普通攻擊是用手裡的法杖發出一個魔法球,而技能攻擊是一個傷害較低的範圍減速,於是我們還需要一個飛行的魔法球以及範圍減速效果的動畫:
以上就是這次教程里用到的所有美術資源的動圖,現在需要把它們導入 GMS 中去。
具體的導入操作在教程二中有詳細的描述,特別要記得正確的把 Sprite 的原點設置在人物的腳下。下面是我所採用的 Sprite 命名,我建議的是所有的 Sprite 資源名稱都以 spr_ 開頭,同時盡量讓名稱能夠表示該資源的用途或含義:
- 普通攻擊側面: spr_ysera_attack_side
- 普通攻擊正面: spr_ysera_attack_front
- 普通攻擊背面: spr_ysera_attack_back
- 特殊技能: spr_ysera_skill
- 魔法球: spr_ysera_magic_bullet
- 特殊技能效果: spr_ysera_skill_effect
將這些資源都放入 Sprites 分類里之前建立的 Character 目錄中(紅框中的是這次新導入的 Sprite 資源):
溫馨小提示如果你覺得在左側的資源導航欄中,所顯示的資源圖片太小不容易看清,你可以通過以下設置來顯示資源的大圖標。在 File(文件)菜單的 Preferences(選項)中,勾選 Big Resource Tree Icons(資源樹中顯示大圖標):
然後重啟 GMS,圖標就會放大一些,更容易看清對應的人物動作一點:
但這樣也使得每屏能顯示的資源數量變少了,所以路怎麼走,你們自己選。
建立人物控制的腳本
在開始敲代碼之前先回顧一下之前的人物控制相關的腳本,我們打開obj_ysera,可以看到當前她響應三個事件Create、Step以及與obj_scene_base的碰撞:
其中 Create 事件對應的腳本是:
phy_fixed_rotation = 1; //為了不讓人物發生旋轉n
Step 事件對應的腳本是:
event_inherited();nYseraStep();n
而碰撞事件只是為了註冊人物和物體間的碰撞,暫時並沒有腳本需要運行。
關於這幾個腳本我要做的第一件事是在資源導航欄的 Scripts 分類中建立一個新的名為 YseraCreate 的腳本文件,並將phy_fixed_rotation = 1 這句代碼從 Create 事件的腳本移入這個腳本,然後將 Create 事件的腳本修改至如下:
event_inherited();nYseraCreate();n
這個改動的目的如下:
- obj_ysera 作為「場景中的動態物體」(obj_scene_dynamic)的子類,理應執行任何在父類的 Create 事件中執行的腳本(通過調用 event_inherited);
- 將腳本內容移至 YseraCreate 腳本文件中,這樣可以避免一部分代碼放在事件窗口中,一部分代碼放在腳本文件里的混亂。在這樣調整之後,我們就可以很清楚的知道,所有和伊瑟拉這個人物相關的腳本是 YseraCreate 和 YseraStep 這兩個腳本文件,下面的改動也都在這兩個腳本文件中進行。
定義人物的朝向
再回顧一下 YseraCreate 和 YseraStep 這兩個腳本的內容,是這樣的。
YseraCreate:
phy_fixed_rotation = 1;n
YseraStep:
image_speed = 0.25;nif(keyboard_check(ord(A))){n phy_position_x = phy_position_x - 4;n sprite_index = spr_ysera_walk_side;n image_xscale = 1;n}nelse if(keyboard_check(ord(D))){n phy_position_x = phy_position_x + 4;n sprite_index = spr_ysera_walk_side; n image_xscale = -1;n}nelse if(keyboard_check(ord(W))){n phy_position_y = phy_position_y - 4;n sprite_index = spr_ysera_walk_back; n}nelse if(keyboard_check(ord(S))){n phy_position_y = phy_position_y + 4;n sprite_index = spr_ysera_walk_front;n}nelse{n sprite_index = spr_ysera_idle;n}n
接下來,首先我想要在YseraCreate腳本中加一個枚舉(enum)來定義人物當前的朝向,因為在目前的代碼中,人物移動時 phy_position_x 和 phy_position_y 的加減、人物在 x 軸的縮放 image_xscale、移動時需要採用的動畫這幾個因素都是和人物的朝向有關的,而且可以想像將要添加的攻擊動畫、魔法球的生成位置、魔法球的飛行方向還與朝向有關。
因為我們只有四個方向的動畫,所以朝向定義為四個——上、下、左、右,然後在枚舉定義後添加用於記錄人物朝向的變數 m_playerDirection:
enum PlayerDirection{n UP,n DOWN,n LEFT,n RIGHTn}nnm_playerDirection = PlayerDirection.DOWN;n
(注釋:為便於查看,此次教程中新增加的代碼用淺橙色高亮顯示)
默認的朝向設置成下方,因為我們設置的人物的初始圖像是朝下的。然後我們在 YseraStep 腳本中根據玩家按鍵,將朝向設置成對應的值,例如玩家按A鍵時方嚮應設置為向左:
if(keyboard_check(ord(A)))n{n phy_position_x = phy_position_x - 4;n sprite_index = spr_ysera_walk_side;n image_xscale = 1;n m_playerDirection = PlayerDirection.LEFT;n}n
播放攻擊與技能動畫
在記錄了人物的朝向以後,我們就可以正確的選擇當前的攻擊動畫了,所以接下來加入的功能是在玩家按鍵時播放攻擊和技能的動畫。
我不記得有沒有在之前的教程里說過這個小提示,就是假如你當前還不清楚一個功能要怎麼下手實現,你可以先嘗試做些簡單的改動讓你的遊戲看起來有這個功能。在嘗試的過程中你就會更清楚下一步要怎樣做。例如現在我們要做的就是這樣,如果還不清楚人物的攻擊過程一共包含哪些要做的步驟,那至少可以先播放一個人物動畫。
還可以舉一個例子,比如你在做遊戲的背包系統,如果你覺得不知從何開始,那麼不如先做一個最簡單的功能就是當玩家按下打開背包的按鍵時,顯示一個背包的 UI 出來。接下來再看這個 UI 需要哪些玩家數據以及如何讓這個UI正確關聯到玩家數據。至少這種方法對我個人在開發過程中一直有很好的效果。
接下來回到我們教程的項目,我們選擇 J和 K 鍵分別作為攻擊和技能的按鍵,那麼在 YseraStep 腳本中響應的代碼如下:
if(keyboard_check(ord(J)))n{n switch(m_playerDirection)n {n case PlayerDirection.UP: sprite_index = spr_ysera_attack_back;n break;n case PlayerDirection.DOWN: sprite_index = spr_ysera_attack_front;n break;n case PlayerDirection.LEFT: sprite_index = spr_ysera_attack_side;n break;n case PlayerDirection.RIGHT: sprite_index = spr_ysera_attack_side;n break;n }n image_index = 0;n}else if(keyboard_check(ord(K)))n{n sprite_index = spr_ysera_skill;n image_index = 0;n}n
為不同的情況設置 sprite_index,即當前需要播放的動畫。其中語句 image_index=0 的意思是每次攻擊和技能的動畫的從第一幀開始播放。
增加人物狀態變數
但如果僅僅是把這段代碼加進 YseraStep 腳本中,你會發現人物還是沒法正確的播放攻擊和技能動畫,這是因為之前控制人物移動的代碼中的存在一個 else 語句會在玩家沒有任何按鍵的時候播放玩家的站立動畫,這樣會導致在按下 J 鍵後,玩家正要播放攻擊動畫時,這個站立動畫會立刻取代攻擊動畫。
所以這裡需要再次整理一下我們想要的邏輯。之前的行走控制代碼的邏輯是,當玩家按下四個方向的行走按鍵時,移動角色並播放相應的行走動畫,若玩家沒有按下任何按鍵,則播放站立動畫。在加入了攻擊和技能動畫後,這個邏輯被打破了,因為在播放攻擊和技能動畫的過程中,即使這時玩家沒有再按鍵,也應當等到動畫播放完畢才能回到站立動畫。與此同時,在攻擊和釋放技能的過程中,人物也應該無法移動。(在攻擊的過程中無法移動會影響操作手感,此處為了教程內容的安排簡化處理)
根據以上的邏輯,需要再為人物增加兩個新的變數用來表示人物是否正在攻擊過程中(m_isAttacking)和人物是否正在釋放技能過程中(m_isInSkill)。在 YseraCreate 腳本中初始化這兩個變數:
m_isAttacking = false;nm_isInSkill = false;n
同時在YseraStep腳本處理攻擊和技能按鍵的代碼處設置它們的值為 true:
if(keyboard_check(ord(J))){m_isAttacking = true;…...
}else if(keyboard_check(ord(K))){m_isInSkill = true;…...}那麼如何將這兩個變數的值在動畫播放完畢後設置成 false呢?這裡需要引入一個新的事件類型——動畫結束(Animation end)。這裡對事件與腳本的處理與之前一致,我們需要做的是建立一個名為 YseraAnimationEnd 的腳本,並與 Animation end 事件關聯:然後在 YseraAnimationEnd 腳本中添加如下代碼:
if(m_isAttacking && sprite_index == spr_ysera_attack_siden || sprite_index == spr_ysera_attack_frontn || sprite_index == spr_ysera_attack_back){n m_isAttacking = false;n}if(m_isInSkill && sprite_index == spr_ysera_skill){n m_isInSkill = false;n}n
這裡的含義是,如果角色處在攻擊狀態且攻擊的動畫結束了,那麼取消角色的攻擊狀態,對釋放技能這裡同理。到此時為止,m_isAttacking(是否在攻擊)和 m_isInSkill(是否在施法)這兩個變數的值已經能夠正確的反映角色狀態了。
在具有了這兩個變數以後,就可以用來改寫攻擊與技能動畫的播放代碼了,由於代碼過長因此這裡只寫出簡化的代碼結構:
if(m_isAttacking == false && m_isInSkill == false)n{nif(按下攻擊鍵)n 播放攻擊動畫nelse if(按下技能鍵)n 播放技能動畫nelse if(按下A鍵)n 播放向左行走動畫nelse if(按下D鍵)n 播放向右行走動畫nelse if(按下W鍵)n 播放向上行走動畫nelse if(按下S鍵)n 播放向下行走動畫nelsen 播放站立動畫n}n
在完成這部分代碼後,人物應該可以正常的播放攻擊和技能動畫了:
生成魔法球與技能效果
接下來要做的是配合動畫,在合適的時間釋放出魔法球和技能效果。首先為它們建立兩個 Object:obj_ysera_magic_bullet 和 obj_ysera_skill_effect,並分別將 spr_ysera_magic_bullet 和spr_ysera_skill_effect 作為它們的 Sprite。
根據美術上的設定,釋放魔法球和技能效果的出現都是在動畫的第二幀,因此在 YseraStep 腳本的最後加上這段代碼來生成它們:
if(sprite_index == spr_ysera_attack_siden|| sprite_index == spr_ysera_attack_frontn|| sprite_index == spr_ysera_attack_back){n if(image_index > 2 && m_fired == false){n instance_create(x, y, obj_ysera_magic_bullet);n m_fired = true;n }n}if(sprite_index == spr_ysera_skill){n if(image_index > 2 && m_fired == false){n instance_create(x, y, obj_ysera_skill_effect);n m_fired = truen }n}n
這段代碼的意思是,如果當前在播放攻擊動畫,那麼在大於第二幀的時候,就在玩家的位置生成一個魔法球,對於技能動畫同理。這裡用到了一個新的函數 instance_create,它的作用是在指定的位置生成一個 Object。
其實當初我在實現這個功能的時候踩了一個坑,我最早的代碼是這樣寫的:
if(在播放攻擊動畫時){n if(image_index == 2){n 生成魔法球n }n}n
然後這段代碼的結果是什麼都沒有出現。為什麼呢?秘密在這裡,因為 image_index 並不是一個整數,這意味著image_index==2 這個條件可能永遠都不會實現。所以應該把條件改成 image_index > 2。但如果只是把這個條件改掉:
if(在播放攻擊動畫時){n if(image_index > 2){n 生成魔法球n }n}n
那結果會在第二幀以後產生許多個魔法球,因此還需要添加一個變數 m_fired 來標記這次攻擊動畫過程中是否已經生成過魔法球了,這樣就變成了上面那段完整代碼所表達的邏輯。
另外在代碼實現上還需要在 YseraCreate 腳本里初始化變數:
m_fired = false;n
然後在每次按鍵開始播放攻擊動畫的時候將這個變數重置為 false:
if(keyboard_check(ord(J)))n {n ...省略n m_fired = false;n }n else if(keyboard_check(ord(K)))n {n ...省略n m_fired = false;n }n
好吧,敲完了這麼多代碼終於又可以運行測試一下了:
這個結果是不是你預想之中的呢?因為我們只是生成了魔法球和技能效果,就沒有再管它們了啊。在遊戲開發中,對於每一個在場景中動態生成的物體,由於是我們通過代碼創建的,因此我們就需要我們自己來關注在何時銷毀它們。而與之相對的是,通過關卡編輯器預先放置在場景中的牆、木桶什麼的,就不需要我們操心了,引擎會負責它們的創建和銷毀。
那麼對於這兩個物體來說,它們的邏輯又各有不同。其中魔法球應該在生成後沿著射出的方向飛行,並在飛出屏幕以後自動消除;而技能效果只需要在播放完一遍以後消失就可以了。
魔法球的飛行與消除
首先為物體 obj_ysera_magic_bullet 的 Create、Step 和 Outside Room 事件分別建立三個腳本 MagicBulletCreate、MagicBulletStep 和 MagicBulletOutsideRoom。你們看,一切都是套路,和之前對 obj_ysera 做的事情差不多,唯一的不同是多了一個 Outside Room 事件,這個事件是指物體移動到了房間以外。
魔法球的飛行過程需要兩個變數:飛行方向和速度。在實現上,其實這兩個變數可以合併在一起變成m_speedX和m_speedY——飛行速度在X軸和Y軸的分量。在MagicBulletCreate腳本中初始化這兩個變數:
m_speedX = 0;m_speedY = 0;n
另外談到初始化,實際上對於 GML 編程語言來說並不是一個必要的步驟,也就是說你可以在任何時候寫一個新的變數來保存你的數值,並給後面使用。唯一會出錯的情形是,如果你嘗試讀取一個變數,而這個變數之前沒有任何地方給它賦值過,那 GMS 會報出以下錯誤信息:
Variable xxx(yyy, zzz) not set before reading it.n
所以我總是在 Create 事件對應的腳本中給我想要用的變數設置一個初始值,以免後面出錯。另外能在 Create 腳本中看到這個物體需要用到的所有變數也是一件好事對不對。
飛行的過程就很簡單了,在 MagicBulletStep 腳本中根據速度更新物體的位置就好了:
x = x + m_speedX;y = y + m_speedY;n
在MagicBulletOutsideRoom腳本中銷毀物體:
instance_destroy();n
最後要做的就是在生成魔法球的時候正確的設置它的飛行速度變數:
if(sprite_index == spr_ysera_attack_side n|| sprite_index == spr_ysera_attack_frontn|| sprite_index == spr_ysera_attack_back){n if(image_index > 2 && m_fired == false){ n var magicBullet = instance_create(x, y, obj_ysera_magic_bullet); switch(m_playerDirection){ case PlayerDirection.UP: magicBullet.m_speedY = -10; break; case PlayerDirection.DOWN: magicBullet.m_speedY = 10; break; case PlayerDirection.LEFT: magicBullet.m_speedX = -10; break; case PlayerDirection.RIGHT: magicBullet.m_speedX = 10; break; } n m_fired = true;n }n}n
因為GMS的坐標系是這樣的:
所以按向上的時候,Y方向的速度值是負數;向下的時候是正數。
技能效果的消除
在技能效果的動畫播放完成一次以後將它銷毀,如果前面你都看過了就會發現這就像做選擇題一樣簡單:
對 obj_ysera_skill_effec 的[ 1 ]事件添加腳本[ 2 ]並在其中調用[ 3 ]函數。選擇1:
- 創建(Create)事件
- 更新(Step)事件
- 動畫播放完成(Animation End)事件
- 移動到房間以外(Outside Room)事件
選擇2:
- MagicBulletCreate
- MagicBulletAnimationEnd
- SkillEffectAnimationEnd
- SkillEffectCreate
選擇3:
- instance_create
- instance_find
- instance_destroy
- instance_copy
都是送分題哈~ 這節課就不講了。
之前有一期教程的留言中,有網友提到希望能夠為遊戲開發的新人講解如何通過拖拽的方式來完成遊戲的功能,雖然我不太提倡使用這些拖拽的功能,但還是可以演示一下的:
從功能上來講,這種拖拽的方式所實現的功能與之前調用腳本里的 instance_destroy 是完全一致的。
魔法球的兩個問題
再次運行遊戲驗證之前的功能是否已經實現:
可以看到魔法球往不同朝向的飛行、以及技能效果的銷毀都已經正常了。但還存在兩個明顯的問題,其一是當伊瑟拉向右側發射魔法球的時候,發射的點位與動畫不匹配。這是因為之前生成魔法球的代碼:
var magicBullet = instance_create(x, y, obj_ysera_magic_bullet);n
它是將魔法球初始化在了人物的 x、y 坐標,即人物自身的腳下。因此需要在這裡加一個偏移值才能和動畫對上,這個偏移值的大小可以打開攻擊動畫 spr_ysera_attack_side 來找到:
從這張圖中可以看到人物的雙腳之間的坐標值是 (100, 120),而魔法球在發出的前一瞬間的位置是 (35, 87)。那麼(-65, -33) 就是人物向左發射魔法球時的偏移值,向右就是 (65, -33)。用同樣的辦法可以得到向下發射的偏移值是(0, 7),向上發射時的偏移值是 (8, -89),但為了左右對稱起見,我們採用 (0, -89) 這個值。
另一個問題是魔法球飛行時的尾跡方向不對,這可以通過設置魔法球的 image_angle 參數來解決。
將這兩個改動反映到代碼里就是:
var magicBullet = instance_create(x, y, obj_ysera_magic_bullet);n var deltaX = 0;n var deltaY = 0;n switch(m_playerDirection)n {n case PlayerDirection.UP:n magicBullet.m_speedY = -10;n magicBullet.image_angle = 270;n deltaY = -89;n break;n case PlayerDirection.DOWN:n magicBullet.m_speedY = 10;n magicBullet.image_angle = 90;n deltaY = 7;n break;n case PlayerDirection.LEFT:n magicBullet.m_speedX = -10;n deltaX = -65;n deltaY = -33;n break;n case PlayerDirection.RIGHT:n magicBullet.m_speedX = 10;n magicBullet.image_angle = 180;n deltaX = 65;n deltaY = -33;n break;n } n magicBullet.x += deltaX;n magicBullet.y += deltaY;n m_fired = true;n
次回預告
接下來就是最後的完美運行測試了:
本次教程的內容就此完成了!
但對於目前來說,發出的魔法球和技能效果還僅僅是視覺上的表現而已,在接下來的教程中,我們將要逐步實現它們的功能!
附錄:教程資源鏈接
該系列教程的項目/代碼及原始美術素材全部更新至 GitHub 項目庫。
推薦閱讀:
※Indie Figure:Nicky Case 關乎交互設計的暢想
※Indie Focus#58:那些悄悄更新上架的遊戲
※「毒雞湯」遊戲諷刺了誰?我和它的開發者聊了聊
※Indie Focus #73:佳作頻出