Unity3D 学习笔记 - Garen Pick the Balls 捡球小游戏设计 (二) Macanim 动画状态机

注:本游戏开发环境为Unity3D 5.3.4 

 

本星期要求:

  1. 模仿 AnimationEvent 编写一个 StateEvents 类
  2. 用户可以创建一个指定时间、指定状态触发的事件类
  3. 事件可以采用反射机制,调用调用客户的方法;或使用订阅发布方法调用客户的方法。
  4. 在你的动画控制程序中使用 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 (); } }

 

posted on 2016-04-22 21:51  Wangsta  阅读(424)  评论(0编辑  收藏  举报

导航