之前寫過一個系列《HTML5 2D平台游戲開發》,在此過程中發現有很多知識點沒有掌握,而且用純JavaScript來開發一個游戲效率極低,因為調試與地圖編輯都沒有可視化的工具,開發起來費時費力,加上業余時間有限,我決定暫且中止開發。為了彌補缺少的知識點,我打算先學習和借鑒一下Unity的開發思路,於是把原先的游戲素材移植了過來。首先還是先從人物的動作開始,Unity的動畫與之前開發時的思路有很大不同,Unity沒有“幀”這一概念,也就是說沒有辦法獲取到當前動畫播放到第幾幀,只能通過normalizedTime
來獲取動畫播放的百分比進度,一下子讓適應這種模式有些困難。先不考慮代碼實現細節,整理一下思路,人物實現三連擊的狀態機大致如下:
- Idle ⇢ attack_a ⇢ Idle
- Idle ⇢ attack_a ⇢ attack_b ⇢ Idle
- Idle ⇢ attack_a ⇢ attack_b ⇢ attack_c ⇢ Idle
在Idle狀態下按下攻擊鍵,過渡到attack_a,如果沒有下一步操作,attack_a動畫播放完畢后還原到Idle狀態。如果在attack_a狀態下再次按下攻擊鍵,則過渡到attack_b,如果在attack_b狀態下無操作,動畫播放完畢后還原到Idle,依此類推,多段連擊也是一樣的。為了表示攻擊的狀態,需要為Animator添加一個attack
參數:
attack等於0表示處於非攻擊狀態,attack等於1表示處於attack_a,2和3分別表示處於attack_b、attack_c。
接下來一步一步分析各個狀態間的過渡。
Idle ⇢ attack_a
Idle狀態可以隨時通過按下攻擊鍵打斷並過渡到attack_a,故沒有 Has Exit Time
,其它項也都置為0。
attack_a ⇢ Idle
最初我是這樣考慮的,給這個過渡設置 Has Exit Time
,如果沒有任何操作,讓其還原到Idle,於是有了
但最終運行時攻擊總是會卡在最后一幀一段時間,只有把Exit Time設置為0.1才流暢。也許是動畫播放速度設置太快的緣故,但我總覺得通過Exit Time的方式來實現不太好。在查閱一番資料后,我覺得給動作添加Behaviour是比較好的方式。選中attack_a,為其添加一個名為SetNormalizeTime
的Behaviour:
public class SetNormalizedTime : StateMachineBehaviour { private string targetParameter = "Normalized Time"; // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { animator.SetFloat(targetParameter, 0); } // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { animator.SetFloat(targetParameter, stateInfo.normalizedTime); } // OnStateExit is called when a transition ends and the state machine finishes evaluating this state override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { animator.SetFloat(targetParameter, 0); } }
每次進入該狀態,先將Normalized Time
重置為0,表示動畫從頭開始,然后在OnStateUpdate
中更新它,最后退出時再置為0。同時也別忘了在Animator中添加一個名為Normalized Time
的參數(float類型):
現在可以把attack_a ⇢ Idle的 Has Exit Time
去掉了,同時添加一個 Conditions
:
表示當動畫播放完畢時(NormalizeTime > 1.0f) 過渡到 Idle。同樣的,也給attack_b ⇢ Idle 和 attack_c ⇢ Idle 附加上這個 Behaviour。
代碼實現
常規操作:
private Animator anim; private Rigidbody2D myRigidbody; private AnimatorStateInfo stateInfo; public int hitCount = 0; //0:表示idle狀態。 1:表示當前正在進行attack_a。 2:attack_b。 3:attack_c。 void Start () { anim = GetComponent<Animator>(); //獲取動畫組件 myRigidbody = GetComponent<Rigidbody2D>(); //獲取剛體組件 } void Update() { stateInfo = anim.GetCurrentAnimatorStateInfo(0); HandleInput(); }
下面實現HandleInput方法:
void HandleInput() { //若動畫為三種狀態之一並且已經播放完畢 if ((stateInfo.IsName("attack_a") || stateInfo.IsName("attack_b") || stateInfo.IsName("attack_c")) && stateInfo.normalizedTime > 1.0f) { hitCount = 0; //將hitCount重置為0,即Idle狀態 anim.SetInteger("attack", hitCount); attack = false; } //按下鍵盤J鍵攻擊 if (Input.GetKeyDown(KeyCode.J)) { HandleAttack(); } }
(這里踩了一個坑,實現這部分邏輯時我是遠程操作完成的,發送的指令實際上有一定的延遲,這樣就導致按住鍵盤J鍵不放可以連續觸發攻擊,也就是連發。讓我誤以為GetKeyDown是連續觸發的,實際上GetKeyDown只觸發一次。)
HandleAttack的實現:
void HandleAttack() { //若處於Idle狀態,則直接打斷並過渡到attack_a(攻擊階段一) if (stateInfo.IsName("Idle") && hitCount == 0) { hitCount = 1; anim.SetInteger("attack", hitCount); } //如果當前動畫處於attack_a(攻擊階段一)並且該動畫播放進度小於80%,此時按下攻擊鍵可過渡到攻擊階段二 else if(stateInfo.IsName("attack_a") && hitCount == 1 && stateInfo.normalizedTime < 0.8f) { hitCount = 2; } //同上 else if(stateInfo.IsName("attack_b") && hitCount == 2 && stateInfo.normalizedTime < 0.8f) { hitCount = 3; } }
這里要注意,比如在觸發第二段攻擊時需要滿足條件 normalizedTime < 0.8f ,但此時按下攻擊鍵是不會馬上播放第二段攻擊動畫的,如果馬上播放就顯得動作非常不協調了,應該等到第一階段的攻擊動畫播放到一定階段才播放第二段攻擊動畫。所以需要給關鍵幀添加一個方法,告訴動畫系統在這一幀要執行某個指令。
void GoToNextAttackAction() { anim.SetInteger("attack", hitCount); }
給第7幀添加一個事件,指向 GoToNextAttackAction 這個方法,動畫將在第7幀的時候被打斷並進入下一個攻擊動畫。如果hitCount沒有改變,SetInteger("attack",hitCount) 不會影響當前正在播放的動畫,動畫會持續播放完畢(至第9幀)。
P.S. 雖然費了一些周折,但還是把效果實現出來了,Unity的開發效率比JS高出太多,大概100倍左右吧,這是在開發過程中的感覺。不知道離游戲成品還有多遙遠,但我還是會繼續學習。