Unity3D 学习笔记 - Garen Pick the Balls 捡球小游戏设计 (二) Macanim 动画状态机
注:本游戏开发环境为Unity3D 5.3.4
本星期要求:
- 模仿 AnimationEvent 编写一个 StateEvents 类
- 用户可以创建一个指定时间、指定状态触发的事件类
- 事件可以采用反射机制,调用调用客户的方法;或使用订阅发布方法调用客户的方法。
- 在你的动画控制程序中使用 StateEvents 类
我采用的是上星期的Garen Pick the Balls小游戏,将Legacy动画部分用Mecanim重写。
要点:
1. 初次状态机开发,尚未实现Run和Attack同时进行(Blend Tree)
2. Run状态的实现
3. 位移旋转的新实现方法
4. StatesEvents的实现方法
5. C#反射机制
必须要承认的是,这次开发尚有许多不完善的地方。
通过这点也深刻认识到状态机的复杂性,在于如何将一个想实现的功能细分成许多小的原子状态,定义好混合状态,以及状态之间的迁移及其条件。
希望各位多多赐教。
1.首先,将所有素材的Animation Type改为Generic.
自己新建Animator Controller - GarenController拖到Garen上。
状态设计:Attack, Run, Idle三个
迁移设计:考虑到Attack带一个状态事件StateEvent, 当运行到Attack一半时会发出通知给Judge回收小球,因此不希望有其他动作来中断Attack.
Run和Attack,在发出攻击指令之后Run可以向Attack迁移。
Run和Idle,在用户发出和取消移动指令之后Run和Idle相互迁移。
状态机变量设计:
Trigger Attack_Trigger,
Bool Is_Run
2.Run状态的实现,位移旋转实现的新方法
与上次不同,这次实现位移和旋转的方法是:
//In Update() float vertical = Input.GetAxis ("Vertical"); float horizontal = Input.GetAxis ("Horizontal"); Vector3 translation = new Vector3 (-horizontal, 0, -vertical) * speed * Time.deltaTime;、
transform.position += translation; transform.rotation = Quaternion.LookRotation (Vector3.RotateTowards (transform.forward, translation, angularSpeed * Time.deltaTime, 0.0f));
而判断用户是否有发出运动指令的方法是判断 if (translation == Vector3.zero)
是, setbool("Is_Run", true)
否,setbool("Is_Run", false);
注意:Run状态自循环,且两个迁移无退出时间(uncheck Has exit time, Run可以在任何时间被中断)
3.StateEvents类的实现方法
吐槽一句:这里,和下面反射机制,完全是满足作业要求的技术训练,目前的Unity还是支持AnimationEvents的,比这里更加简便。
定义StateEvents类:
为了保证泛用性,采用动态传入方法的机制。
public class StateEvent : System.Object { private float t; private bool fired; object method_base; System.Reflection.MethodInfo method_to_call; object[] param; public StateEvent(float trigger_time, object method_base, object method_to_call, object[] param) { this.t = trigger_time; this.fired = false; this.method_base = method_base; this.method_to_call = method_to_call as System.Reflection.MethodInfo; this.param = param; } public bool triggerIt(float time) { if ((time > this.t) & !fired) { method_to_call.Invoke (method_base, param); fired = true; return true; } else if ((time <= this.t) & fired) { fired = false; return false; } else { return false; } } }
用户定义且添加一个事件的方法是(这里略去attackOnHit()的定义,和上星期的一样):
//Events Defined here. private System.Type garenControllerType = System.Type.GetType("GarenController"); private StateEvent attack_to_half; // Use this for initialization void Start () { //Init animator = GetComponent<Animator> (); attack_to_half = new StateEvent (0.5f, this, garenControllerType.GetMethod ("attackOnHit"), new object[]{}); }
这里attack_to_half里面就有了到什么时间,用什么参数Invoke哪个类里什么方法的信息。
Update():
void FixedUpdate () { //Determining if state has changed from compare. cur_ASI = animator.GetCurrentAnimatorStateInfo(0); if (pre_ASI.fullPathHash == cur_ASI.fullPathHash) { updateState (cur_ASI); } else { //update ASI to current. pre_ASI = cur_ASI; } }
updateState(AnimatorStateInfo):
判断状态的机制!是要用当前animatorStateInfo.fullPathHash来判断,这个量会包含Animation Layer信息。
SID_ATTACK1获得方法是Animator.StringToHash("Base Layer.Attack1"); 同样指明了Layer信息。
还是那句话,这是为了满足TriggerEvents硬造出来的机制,Unity开发者显然没有想到用户还需要自行去比较状态,因此代码有点Awkward.
void updateState(AnimatorStateInfo cur_ASI){ if (cur_ASI.fullPathHash == SID_ATTACK1) { attack_to_half.triggerIt (cur_ASI.normalizedTime); } }
4. C#反射机制:
做法是:定义反射
System.Reflection.MethodInfo method = System.Type.GetType("Your_Class").GetMethod("Method_To_Invoke");
//把method当皮球传来传去... 或者上面这句话本来就在其他地方
//需要呼叫这个方法时
method.Invoke (object method_base, object[] param);
method_base 是这个方法所在的类, param可以这样初始化param = new Object[] {p1, p2, ... , pn}; 对应方法的参数p1, p2, ... , pn.
所以,Garen击打之后发消息给Judge的整条信息通路是:
State进行到50%时,TriggerIt() 通过反射调用GarenController里的attackOnHit(), attackOnHit调用onHit(), onHit()通过订阅发布模式发消息给Judge.
GarenController.cs:
using UnityEngine; using System.Collections; using System.Collections.Generic; using Com.GarenPickingBalls; public class StateEvent : System.Object { private float t; private bool fired; object method_base; System.Reflection.MethodInfo method_to_call; object[] param; public StateEvent(float trigger_time, object method_base, object method_to_call, object[] param) { this.t = trigger_time; this.fired = false; this.method_base = method_base; this.method_to_call = method_to_call as System.Reflection.MethodInfo; this.param = param; } public bool triggerIt(float time) { if ((time > this.t) & !fired) { method_to_call.Invoke (method_base, param); fired = true; return true; } else if ((time <= this.t) & fired) { fired = false; return false; } else { return false; } } } public class GarenController : MonoBehaviour { private iJudgeInterface iJudge = Judge.GetInstance () as iJudgeInterface; private Animator animator; private float speed = 3f; private float angularSpeed = 10f; private int SID_ATTACK1 = Animator.StringToHash ("Base Layer.Attack1"); public static event onHitActionHandler onHit; private AnimatorStateInfo pre_ASI; private AnimatorStateInfo cur_ASI; //Events Defined here. private System.Type garenControllerType = System.Type.GetType("GarenController"); private StateEvent attack_to_half; // Use this for initialization void Start () { //Init animator = GetComponent<Animator> ();
attack_to_half = new StateEvent (0.5f, this, garenControllerType.GetMethod ("attackOnHit"), new object[]{}); pre_ASI = animator.GetCurrentAnimatorStateInfo (0); cur_ASI = animator.GetCurrentAnimatorStateInfo (0); initAllStates (); } // Update is called once per frame void FixedUpdate () { //Determining if state has changed from compare. cur_ASI = animator.GetCurrentAnimatorStateInfo(0); if (pre_ASI.fullPathHash == cur_ASI.fullPathHash) { updateState (cur_ASI); } else { //update ASI to current. pre_ASI = cur_ASI; } if (iJudge.getGameStatus () == 1) { if (cur_ASI.fullPathHash != SID_ATTACK1) { float vertical = Input.GetAxis ("Vertical"); float horizontal = Input.GetAxis ("Horizontal"); Vector3 translation = new Vector3 (-horizontal, 0, -vertical) * speed * Time.deltaTime; transform.position += translation; transform.rotation = Quaternion.LookRotation (Vector3.RotateTowards (transform.forward, translation, angularSpeed * Time.deltaTime, 0.0f)); if (translation == Vector3.zero) { resetRun (); } else { Run (); } } if (Input.GetMouseButtonDown (0)) { resetRun (); Attack (); } } else { resetRun (); } } void updateState(AnimatorStateInfo cur_ASI){ if (cur_ASI.fullPathHash == SID_ATTACK1) { attack_to_half.triggerIt (cur_ASI.normalizedTime); } } public void Run(){ animator.SetBool ("Is_Run", true); } public void Attack(){ animator.SetTrigger("Attack_Trigger"); } public void resetRun() { animator.SetBool("Is_Run", false); } void initAllStates() { resetRun (); } public void attackOnHit() { onHit (); } }