如何讓動作在遊戲中更加流暢(1)
先放一張最終效果
一.設計銜接,而非全靠淡入淡出
最初在設計中我們盡量使得動作與動作之間的過度是自然的,事與願違,製作中想要應付不同的變化,即便是再相似的動作之間也會有生硬的切換,動作遊戲中大多有兩種做法來保證這種銜接的美感和流暢度。
1.在製作中就去設計銜接
最簡單的銜接設計就是根據能量的增強和衰減來做的,如果一股能量從右到左非常的大,那麼衰減的時間和空間也就會更大,就連擊舉例來講,combo_2的動作開啟位置和方向所產生的能量在視覺上和combo_1的衰減能量相互重疊交叉(這裡的重疊交叉可能是玩家自行感受,具體也需要看不同的角色,比如一個靈巧的角色和一個魁梧的角色來比較的話,質量越大,慣性越強,能量的衰減幅度和空間就會越大,玩家會根據不同的角色形象先入為主的去定義這種視覺感受),動作生硬感就容易產生了。
-
2.放棄曲線,跳幀設計
除開比較細膩的歐美主機類遊戲外,大多數日韓風和卡通風格遊戲都是採用這樣的方式,具體的用法也分了好多種,比如鬼泣的跳幀和卡通類遊戲的區別。
----------------------------------------------------------------------------------------------
二.G系統和Motion
在我們設計完動作的銜接後,還需要考慮如何能將角色的速度節奏完全帶進遊戲中。
1.為什麼要用Motion?
很多步伐、身體軀幹肢體的動作是需要重心來調整出最佳的狀態,節奏的快慢,往往只能被感受到,在角色下創建Root節點,用Motion代替線性運動可以最大提升動作的真實性和打擊感,更真實自然。
加入我自己的理解,RootMotion是根據物體在水平面的一個正交投影,會計算出物體在水平面上X、Y的位移,並且是每一幀都在計算,結果便是可以完全配合出動作師調整出來的位移。移位和動作的匹配能最大發揮出來。
2.為了實現X Z的水平位移和動作的匹配,需要在角色上增加一個Root點,這裡我用一個幫助節點替代,位置在角色的腳下,沒有使用角色的BoneRoot原因最重要的就是其帶有旋轉屬性和Y方向的一些位移。
在確定好位移後,需要將置心的X方向的關鍵幀記錄下來,隨後復刻到Root上並把X軸方向的屬性全部歸0,最後使用Root還原這種X的位移,達到既保留了角色置心的旋轉動畫,又可以將帶有X Z位移屬性的Root位移映射給Motion。
----------------------------------
三.動作中的Events判斷和狀態機
----------------------------------------------
1.如GIF圖一樣,攻擊1已經開始進入收招狀態,如果這個時候玩家點擊了combo鍵,就會有3種情況:
a.玩家在剛開始攻擊的時候按了combo
b.玩家在combo_1攻擊階段剛完成後按下了combo
c.玩家在角色已經攻擊完成收招階段按下了combo
2.再根據上面3種情況在製作階段需要準備的便是:
d.合理的硬直POSE和保留時間
e.留出符合整個攻擊節奏的收招時間
f.Exit Time時間和Event的觸發什麼地方比較好
3.在什麼地方設置這個comboEvent?
我們需要提前在動作幀上做一個區域判斷comboEvent。
最後還需要在類中加入2個event方法來判斷
void combo1Event1() { isCanCombo2 = true; }void combo1Event2() { isCanCombo2 = false; }
做完了Event的觸發,下一步先看看玩家對於觸發的簡單邏輯,先兩連擊為例
我的看法是,不同的時間滿足不同的觸發,再分類設計Exit Time的時間,以達到玩家在攻擊或者是移動的時候都能很舒服的切換出去,而在每一個動作之間,手動調整融合值,除了最開始我們講的設計銜接外,活用Transition Offset和 Transition Duration來完成動作的銜接。
4.設置Mecanim狀態機
為了能講得更清楚,還是以兩連擊為例。
先放出一個完整的狀態機:
有了動作的事件觸發,就可以讓玩家在合適的時間滿足連擊條件
定義一個整數變數ActionID.
讓其所有combo切換到idle的條件為0,idle切換到combo_1為1,combo_1切換到combo_2ID為2,然後編寫動作連擊腳本來進行控制:
將clip動作的名稱轉化為對當前狀態的判斷
public Animator animator; public AnimatorStateInfo animSta; private const string IdleState = "idle"; private const string RunState = "run"; private const string Combo1State = "combo_1"; private const string Combo2State = "combo_2"; private const string Combo3State = "combo_3"; private const string Combo4State = "combo_4"; private const string Combo5State = "combo_5"; private const string Combo6State = "combo_6"; private int HitCount;
我在Update中監測了水平X Y的數值變化,檢驗玩家的意向,如果有數值變化,則表示玩家有位移的意願。
在玩家有位移意願的情況下,如果有HitCount並且當前動畫狀態的向量時間>1.0 讓其直接到Run狀態
void Update () { animSta = animator.GetCurrentAnimatorStateInfo(0); h = Input.GetAxis("Horizontal"); v = Input.GetAxis("Vertical"); if (Joystick.h != 0 || Joystick.v != 0) { h = Joystick.h; v = Joystick.v; } if(Mathf.Abs(h) !=0 || Mathf.Abs(v) !=0 && HitCount >0 && animSta.normalizedTime>1.0f) { animator.SetBool("Run", true); animator.SetInteger("ActionID", -1); Run(); } else { animator.SetBool("Run", false); } if ( Mathf.Abs(h) != 0 || Mathf.Abs(v) != 0) { animator.SetBool("Run", true); Run(); } else { animator.SetBool("Run", false); }
根據玩家點擊combo的次數和當前攻擊的狀態 判斷HitCount 注意的是這裡進入下一個狀態後我關閉了當前狀態Event中的 isCanCombo 。 因為不做這一步,在第二輪攻擊的時候,攻擊的節奏不受開關控制,會徹底亂。
if (!animSta.IsName(IdleState) && animSta.normalizedTime>0.90f && HitCount >0) { this.animator.SetInteger("ActionID", 0); HitCount = 0; } if(animSta.IsName(RunState)&&animSta.normalizedTime >0.90f && HitCount > 0) { this.animator.SetInteger("ActionID", -1); } if (animSta.IsName(Combo1State) && HitCount == 2) { this.animator.SetInteger("ActionID", 2); isCanCombo2 = false; } if (animSta.IsName(Combo2State) && HitCount == 3) { this.animator.SetInteger("ActionID", 3); isCanCombo3 = false; } if (animSta.IsName(Combo3State)&& HitCount == 4) { this.animator.SetInteger("ActionID", 4); isCanCombo4 = false; } if (animSta.IsName(Combo4State) && HitCount == 5) { this.animator.SetInteger("ActionID", 5); isCanCombo5 = false; } if (animSta.IsName(Combo5State) && HitCount == 6) { this.animator.SetInteger("ActionID", 6); isCanCombo6 = false; } if (animSta.IsName(Combo6State) && HitCount == 1) { this.animator.SetInteger("ActionID", 1); }
在combo方法中返回HitCount值
void Combo() { animSta = animator.GetCurrentAnimatorStateInfo(0); if (animSta.IsName(IdleState)) { HitCount = 1; this.animator.SetInteger("ActionID", 1); }else if (animSta.IsName(RunState)) { HitCount = 1; this.animator.SetInteger("ActionID", -2); } else if (animSta.IsName(Combo1State)&&isCanCombo2) { HitCount = 2; } else if (animSta.IsName(Combo2State)&&isCanCombo3) { HitCount = 3; } else if (animSta.IsName(Combo3State)&&isCanCombo4) { HitCount = 4; } else if (animSta.IsName(Combo4State)&&isCanCombo5) { HitCount = 5; } else if (animSta.IsName(Combo5State)&isCanCombo6) { HitCount = 6; } else if (animSta.IsName(Combo6State)&&isCanCombo6) { HitCount = 1; } }
總結
個人覺得通過這種方式,戰鬥中動作的流暢會得到很大提升,匹配次時代60幀動畫的速率,理論上可以達到很好的視覺效果,後面會學習運用Blend Tree、Layers等和Mecanim配合,試看能否加強動作效果。
推薦閱讀: