欢迎来到我的博客小站。  交流请加我微信好友: studyjava。  也欢迎关注同名公众号:Java学习之道

学Unity的猫之Animator动画播放(十三)

  |   0 评论   |   0 浏览

13.1 皮皮猫打字机游戏

皮皮:“铲屎官,你为什么打字速度这么快?”

我:“一个字,练。” 皮皮:“教教我,怎么连打字速度。”

我:“来,我给你做一个打字练习游戏吧。”

我:“当你可以一分钟连击180次的时候,你就可以出山了。”

皮皮:“作为猫族,不能被速度打败。”

游戏画面如下:模块设计如下

本工程使用的Unity版本为2020.1.14f1c1 (64-bit),工程已上传到GitHub,感兴趣的同学可以下载下来学习。GitHub地址:https://github.com/linxinfa/Unity-TypeWriting-Game

13.2 场景制作

13.2.1 入口场景

EntryScene.unity一个标题文本(Text组件),一个开始按钮(Button组件),一个难度选择勾选(ToggleGroupToggle组件)。 背景图使用SpriteRender组件在3D摄像机中渲染。难度等级的勾选使用了ToggleGroup组件,用来给Toggle分组。子节点中的Toggle需要指明相同的ToggleGroup实现单选的效果。

13.2.2 游戏场景

GameScene.unity一个血条(Slider组件),一个得分(Text组件),一个字母盘(GridLayoutGroupText组件)、一个连击(Text组件),一个角色(SpriteRendererAnimator组件)、一个背景图(SpriteRenderer组件)。 其中字母盘只做一个字母,游戏中进行动态克隆。再做游戏结束面板,提供一个返回和重来的按钮。

13.2.3 场景切换

点击菜单File - Build Settings...将场景添加到Scenes In Build中。代码中,通过SceneManager.LoadScene切换场景,如下

UnityEngine.SceneManagement.SceneManager.LoadScene(1);

13.3 游戏管理器

游戏管理器GameMgr,它需要包括数据和逻辑,如下。

13.3.1 数据定义

public int hardLevel { get; set; }
 
 
 
 
 
 public int score { get; set; }
 
 
 
 
 private const int MAX_BLOOD = 1500;
 
 
 
 
 public int blood
 {
     get { return m_blood; }
     set
     {
         m_blood = value;
         if (m_blood <= 0)
         {
             gameOver = true;
    
         }
     }
 }
 private int m_blood = 0;
 
 
 
 
 public int comboCnt { get; set; }
 
 
 
 public float comboTimer { get; set; }
 
 
 
 
 public bool gameOver { get; private set; }
 
 
 
 
 public List<KeyCode> keyList { get { return m_keyList; } }
 private List<KeyCode> m_keyList = new List<KeyCode>();

13.3.2 生成字母盘

生成字母盘(16个字母),要求每个字母都不重复,生成的字母存到m_keyList中。

private void GenKeys()
    {
        for (int i = 0; i < 16; ++i)
        {
            m_keyList.Add(GenOneKey());
        }
    }

    
    
    
    
    private KeyCode GenOneKey()
    {
        var key = (KeyCode)UnityEngine.Random.Range((int)KeyCode.A, (int)KeyCode.Z);
        for(int i=0,cnt=m_keyList.Count;i<cnt;++i)
        {
            if(m_keyList[i] == key)
            {
                
                return GenOneKey();
            }
        }
        return key;
    }
13.3.3 按键判断

我们需要先判断按键类型,封装一个接口GetKeyDownCode

皮皮:“微信搜索公众号 [爱上游戏开发],回复 “资料”,免费领取 200G 学习资料!”

public KeyCode GetKeyDownCode()
    {
        if (Input.anyKeyDown)
        {
            foreach (KeyCode keyCode in Enum.GetValues(typeof(KeyCode)))
            {
                if (Input.GetKeyDown(keyCode))
                {
                    return keyCode;
                }
            }
        }
        return KeyCode.None;
    }

然后判断按下的按键是否在字母盘中,返回对应的索引,如果不在字母盘中,则返回-1。

private int IsKeyBingo(KeyCode key)
    {
        for (int i = 0, cnt = m_keyList.Count; i < cnt; ++i)
        {
            if (m_keyList[i] == key)
                return i;
        }
        return -1;
    }

按键正确的时候,执行连击计算,加血加分,生成新的字母,抛事件更新ui

private void OnKeyBingo(int bingoIndex)
    {
        
        ++comboCnt;

        if (comboCnt >= 3)
        {
            
            blood += 150;
            if (blood > MAX_BLOOD)
                blood = MAX_BLOOD;
            score += 20;
        }
        else
        {
            
            blood += 50;
            score += 10;
        }
  
        var oldKey = m_keyList[bingoIndex];
        var newKey = GenOneKey();
        m_keyList[bingoIndex] = newKey;

        
    }

按键错误的时候,连击中断,扣血,抛事件更新ui

private void OnKeyError()
    {
        
        comboCnt = 0;
        
        blood -= 30;
        
    }

13.3.4 连击定时器

如下,其中Time.deltaTime是一帧的间隔时间。每帧调用UpdateComboTimer,对comboTimer进行帧间隔时间递减,通过comboTimer判断是否超过时间限制,超过则中断连击。

public void UpdateComboTimer()
    {
        if (comboTimer > 0)
        {
            comboTimer -= Time.deltaTime;
            
            if (comboTimer <= 0)
            {
                comboCnt = 0;
                
            }
        }
    }

13.4 动画控制器Animator

Unity可以用两种方式控制动画 :

1 Animation,这种方式简单,直接 Play(“Idle”)或者CorssFade(“Idle”)就可以播放动画;

2 AnimatorUnity5.x之后推荐使用这种方式,因为里面可以加上混合动画,让动画切换更加平滑。

13.4.1 添加Animator

点击菜单Window - Animation - Animation,可以打开Animation窗口,快捷键是Ctrl+6选中某个物体后,可以为该物体添加或编辑动画,比如选中一个空物体,由于没有动画,会出现一个Create按钮。点击Create按钮,会弹出窗口设置文件保存路劲。创建成功后,物体上会出现一个Animator组件。

并且我们可以在目录中看到生成了两个文件。.controller文件是一个动画状态机,在Unity中双击它会打开Animator窗口,即可看到里面的内容,我们可以在这个窗口中组织各个动画文件。.anim是动画文件,在Unity中双击它会打开Animation窗口,我们可以在这个窗口中制作动画。

13.4.2 Animator状态机

每个Animator Controller都会自带三个状态:Any State, EntryExit

13.4.2.1 Any State状态

表示任意状态的特殊状态。例如我们如果希望角色在任何状态下都有可能切换到死亡状态,那么Any State就可以帮我们做到。当你发现某个状态可以从任何状态以相同的条件跳转到时,那么你就可以用Any State来简化过渡关系。

13.4.2.2 Entry状态

表示状态机的入口状态。当我们为某个GameObject添加上Animator组件时,这个组件就会开始发挥它的作用。 如果Animator Controller控制多个Animation的播放,那么默认情况下Animator组件会播放哪个动画呢? 由Entry来决定的。 但是Entry本身并不包含动画,而是指向某个带有动画的状态,并设置其为默认状态。被设置为默认状态的状态会显示为 橘黄色。当然,你可以随时在任意一个状态上通过 鼠标右键->Set as Layer Default State更改默认状态。

记住, EntryAnimator组件被激活后 =无条件= 跳转到默认状态,并且每个Layer有且仅有一个默认状态。

13.4.2.3 Exit状态

表示状态机的出口状态,以红色标识。如果你的动画控制器只有一层,那么这个状态可能并没有什么卵用。但是当你需要从子状态机中返回到上一层(Layer)时,把状态指向Exit就可以了。

13.4.3 动画状态的属性

我们可以选中某个自定义状态,并在Inspector窗口下观察它具有的属性

| 属性名 | 描述 |
| - | - |
| Motion | 状态对应的动画。每个状态的基本属性,直接选择已定义好的动画(Animation Clip)即可 |
| Speed | 动画播放的速度。默认值为1,表示速度为原动画的1.0倍。 |
| Mutiplier | 勾选右侧的Parameter后可用,即在计算Speed的时考虑 区域1 中定义的某个参数。若选择的参数为smooth, 则动画播放速度的计算公式为 smooth * speed * fps(animation clip中指定) |
| Mirror | 仅适用于humanoid animation(人型机动画) |
| Cycle Offset | 周期偏移,取值范围为0-1.0,用于控制动画起始的偏移量。把它和正弦函数的offset进行对比就能够理解了,只会影响起始动画的播放位置。 |
| Foot IK | 仅适用于humanoid animation(人型机动画) |
| Write Default | 最好保持默认,感兴趣可以参考官方手册 |
| Transitions | 该状态向其他状态发起的过渡列表,包含了Solo和Mute两个参数,在预览状态机的效果时起作用 |
| Add Behaviour | 用于向状态添加“行为 |

###13.4.4 状态间的过渡关系(Transitions)

状态间的过渡关系,直观上说它们就是连接不同状态的有向箭头。

要创建一个从状态A状态B的过渡,直接在状态A上 鼠标右键 - Make Transition并把出现的箭头拖拽到状态B上点击鼠标左边即可。

13.4.5 添加状态控制参数

参数有FloatIntBoolTriggerFloatInt用来控制一个动画状态的参数,比如速度方向等可以用数值量化的东西,Bool用来控制动画状态的转变,比如从走路转变到跑步,Trigger本质上也是bool类型,但它默认为false,且当程序设置为true后,它会自动变回false

如下这里创建一个Int类型的参数AnimState

13.4.6 编辑切换状态的条件

点击连线,在Inspecter窗口中可以进行设置,在Conditions栏下可以添加条件,如下图表示当参数AnimState0时会执行这个动画Any StateNew Animation2的过渡

必须在Parameters面板中添加了参数才可以在这里查看到,其次添加的条件为&& ”与” 关系,即必须同时满足。

## 13.5 猫娘动画状态机

13.5.1 模型资源下载

Assets Store上下载猫娘模型,资源地址:https://assetstore.unity.com/packages/2d/characters/fancydoll-c000-little-cat-girl-112776

13.5.2 动画循环设置

模型自带了一些动画idle(站立)、walk(走路)、run(跑)需要循环播放,勾选Loop Timeget_hit(受击)、die(阵亡)不需要循环播放,不勾选Loop Time

13.5.3 状态过渡设置

我们需要通过Animator将这些动画进行合理的组织,如下添加变量Action,过渡条件根据Action的值进行判断。过渡条件如下

| 状态1 | 状态2 | 条件 |
| - | - | - |
| Any State | idle | Action == 1 |
| Any State | get_hit | Action == 4 |
| get_hit | idle | 无 |
| Any State | die | Action == 5 |

13.6 角色动画控制器

角色动画控制器CharacterAniCtrler,它需要包括数据和逻辑,如下。运行中的状态过渡

13.6.1 数据定义

public enum CharacterAniId
 {
     Idle = 1,
     Walk = 2,
     Run = 3,
 
 
     Hit = 4,
     Death = 5,
 }
 
    
    
    
 private Animator m_animator;
 
 
    
    
    private Queue<int> m_animQueue = new Queue<int>();

13.6.2 直接播放动画接口

private void PlayAniImmediately(string name)
    {
        if (IsDeath) return;
        m_animator.CrossFade(name, 0.1f, 0);
    }

如立即播放走路动画

public void PlayWalk()
    {
        PlayAniImmediately("walk");
    }

13.6.3 设置变量值

m_animator.SetInteger("Action", 4);

封装成接口

private const string STR_ACTION = "Action";
 
    
    
    
    
    
    public void PlayAnimation(int actionID)
    {
        if (IsDeath) return;
        if (m_animator == null)
            return;
        if (!m_animator.isInitialized || m_animator.IsInTransition(0))
        {
            
            m_animQueue.Enqueue(actionID);
            return;
        }
        m_animator.SetInteger(STR_ACTION, actionID);
    }

13.6.4 根据状态队列设置状态

提供一个LateUpdate接口每帧调用,设置了Action值需要在下一帧的时候重置为0,然后从队列中取下一个状态进行处理。

public void LateUpdate()
    {
        if (m_animator == null)
        {
            return;
        }
        if (!m_animator.isInitialized || m_animator.IsInTransition(0))
        {
            return;
        }
        if (null == mClips)
            mClips = m_animator.GetCurrentAnimatorClipInfo(0);
        if (null == mClips || mClips.Length == 0)
            return;

        int actionID = m_animator.GetInteger(STR_ACTION);
        if (actionID > 0)
        {
            
            m_animator.SetInteger(STR_ACTION, 0);
        }

        
        PlayRemainAction();
    }

    
    
    
    void PlayRemainAction()
    {
        if (m_animQueue.Count > 0)
        {
            PlayAnimation(m_animQueue.Dequeue());
        }
    }

13.7 入口场景脚本

入口场景脚本EntryScene.cs挂在Canvas上,设置Start Game BtnTgl Group代码如下

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class EntryScene : MonoBehaviour
{
    public Button startGameBtn;
    public ToggleGroup tglGroup;

    void Start()
    {
        startGameBtn.onClick.AddListener(() =>
        {
            
            foreach (var item in tglGroup.ActiveToggles())
            {
                GameMgr.Instance.hardLevel = int.Parse(item.name);
                break;
            }

            
            SceneManager.LoadScene(1);
        });
    }
}

13.8 游戏场景脚本

游戏场景脚本GameScene.cs挂在Canvas上,设置公开的成员对象。主要根据各种事件更新ui

皮皮:“微信搜索公众号 [爱上游戏开发],回复 “资料”,免费领取 200G 学习资料!”

using System;
using UnityEngine;
using UnityEngine.UI;

public class GameScene : MonoBehaviour
{

    public Animator anitor;
    public Text comboText;
    public Text scoreText;
    public Slider bloodSlider;
    public Image bloodImage;
    public GameOverDlg gameOverDlg;
    public KeyGrid keyGrid;

    private CharacterAniCtrler m_aniCtrler;


    private void Awake()
    {
        
        EventDispatcher.Instance.Regist(EventNameDef.EVENT_KEY_BINGO_INDEX, OnEventKeyBingoIndex);
        EventDispatcher.Instance.Regist(EventNameDef.EVENT_COMBO, OnEventCombo);
        EventDispatcher.Instance.Regist(EventNameDef.EVENT_PLAY_ANI, OnEventPlayAni);
        EventDispatcher.Instance.Regist(EventNameDef.EVENT_UPDATE_SCORE, OnEventUpdateScore);
        EventDispatcher.Instance.Regist(EventNameDef.EVENT_RESTART_GAME, OnEventRestartGame);
        EventDispatcher.Instance.Regist(EventNameDef.EVENT_GAMEOVER, OnEventGameOver);

        m_aniCtrler = new CharacterAniCtrler();
        m_aniCtrler.Init(anitor);

        
        StartGame();
    }

    private void OnDestroy()
    {
        
        EventDispatcher.Instance.UnRegist(EventNameDef.EVENT_KEY_BINGO_INDEX, OnEventKeyBingoIndex);
        EventDispatcher.Instance.UnRegist(EventNameDef.EVENT_COMBO, OnEventCombo);
        EventDispatcher.Instance.UnRegist(EventNameDef.EVENT_PLAY_ANI, OnEventPlayAni);
        EventDispatcher.Instance.UnRegist(EventNameDef.EVENT_UPDATE_SCORE, OnEventUpdateScore);
        EventDispatcher.Instance.UnRegist(EventNameDef.EVENT_RESTART_GAME, OnEventRestartGame);
        EventDispatcher.Instance.UnRegist(EventNameDef.EVENT_GAMEOVER, OnEventGameOver);
    }

    
    
    
    private void StartGame()
    {
        GameMgr.Instance.Init();
        TextEffect.Init();
        
        bloodSlider.maxValue = GameMgr.Instance.blood;
        bloodSlider.value = GameMgr.Instance.blood;
        bloodImage.enabled = true;
        
        keyGrid.CreateKeyList(GameMgr.Instance.keyList);

        comboText.gameObject.SetActive(false);
        scoreText.text = "0";
        gameOverDlg.Hide();
    }

   

    void Update()
    {
        if (GameMgr.Instance.gameOver) return;
        
        GameMgr.Instance.UpdateComboTimer();

        
        bloodSlider.value = GameMgr.Instance.blood;
        GameMgr.Instance.blood -= GameMgr.Instance.hardLevel;

        
        var keyCode = GameMgr.Instance.GetKeyDownCode();
        if (KeyCode.None == keyCode) return;
        GameMgr.Instance.OnKey(keyCode);
    }

    private void LateUpdate()
    {
        
        m_aniCtrler.LateUpdate();
    }

    
    
    
    
    private void OnEventKeyBingoIndex(params object[] args)
    {
        int index = (int)args[0];
        KeyCode oldKey = (KeyCode)args[1];
        KeyCode newKey = (KeyCode)args[2];
        keyGrid.UpdateKeyByIndex(index, oldKey, newKey);
    }

    
    
    
    
    private void OnEventCombo(params object[] args)
    {
        var combo = (int)args[0];
        comboText.text = "连击" + combo;
        comboText.gameObject.SetActive(combo >= 3);
    }

    
    
    
    
    private void OnEventPlayAni(params object[] args)
    {
        var ani = (string)args[0];
        switch (ani)
        {
            case "idle": m_aniCtrler.PlayAnimation((int)CharacterAniId.Idle); break;
            case "walk": GameMgr.Instance.comboTimer = 0.5f; m_aniCtrler.PlayWalk(); break;
            case "run": GameMgr.Instance.comboTimer = 0.5f; m_aniCtrler.PlayRun(); break;
            case "hit": m_aniCtrler.PlayAnimation((int)CharacterAniId.Hit); break;
            case "die": m_aniCtrler.PlayDieImmediately(); break;
        }
    }

    
    
    
    
    private void OnEventUpdateScore(params object[] args)
    {
        var score = (int)args[0];
        scoreText.text = score.ToString();
    }

    
    
    
    
    private void OnEventGameOver(params object[] args)
    {
        bloodImage.enabled = false;
        gameOverDlg.Show(GameMgr.Instance.score);
    }

    
    
    
    
    private void OnEventRestartGame(params object[] args)
    {
        m_aniCtrler.PlayReviveImmediately();
        StartGame();
    }
}

其中事件定义如下

public class EventNameDef 
{
    
    
    
    public const string EVENT_KEY_BINGO_INDEX = "EVENT_KEY_BINGO_INDEX";
    
    
    
    public const string EVENT_COMBO = "EVENT_COMBO";
    
    
    
    public const string EVENT_PLAY_ANI = "EVENT_PLAY_ANI";
    
    
    
    public const string EVENT_GAMEOVER = "EVENT_GAMEOVER";
    
    
    
    public const string EVENT_UPDATE_SCORE = "EVENT_UPDATE_SCORE";
    
    
    
    public const string EVENT_RESTART_GAME = "EVENT_RESTART_GAME";
}

13.9 文字特效动画

制作一个TextEffect.prefab预设,添加动画如下。由于游戏中需要重复显示这个特效,所以采用对象池方式。 特效动画结束时回收到对象池中,这样可以反复利用。为了监听动画结束,在动画的最后一帧添加帧事件。创建一个TextEffect.cs脚本,挂到预设上,提供一个OnAnimationEnd共有方法

public void OnAnimationEnd()

这样就可以设置帧事件的响应函数了TextEffect.cs脚本如下

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;




public class TextEffect : MonoBehaviour
{

    
    
    
    public static void Init()
    {
        if(null != s_root)
        {
            Destroy(s_root.gameObject);
            s_root = null;
        }
        
        s_objPool.Clear();
        var canvas = GameObject.Find("Canvas");
        if (null != canvas)
        {
            var rootObj = new GameObject("EffectRoot");
            s_root = rootObj.transform;
            s_root.SetParent(canvas.transform, false);
        }
    }


    
    
    
    
    
    public static void Show(string text, Vector3 pos)
    {
        if (null == s_prefab)
        {
            s_prefab = Resources.Load<GameObject>("TextEffect");
        }

        TextEffect bhv = null;
        if (s_objPool.Count > 0)
        {
            
            bhv = s_objPool.Dequeue();
        }
        else
        {
            var obj = Instantiate(s_prefab);
            obj.transform.SetParent(s_root, false);
            bhv = obj.GetComponent<TextEffect>();
        }
        bhv.gameObject.SetActive(true);
        bhv.transform.position = pos;
        bhv.keyText.text = text;
    }

    
    
    
    public void OnAnimationEnd()
    {
        gameObject.SetActive(false);
        
        s_objPool.Enqueue(this);
    }

    private static GameObject s_prefab;
    
    
    
    private static Queue<TextEffect> s_objPool = new Queue<TextEffect>();
    
    
    
    private static Transform s_root;

    
    
    
    public Text keyText;
}

完成。 如果有什么疑问,欢迎留言或私信。


标题:学Unity的猫之Animator动画播放(十三)
作者:shirlnGame
地址:https://www.mmzsblog.cn/articles/2021/01/12/1610457729174.html
-----------------------------
如未加特殊说明,此网站文章均为原创。
网站转载须在文章起始位置标注作者及原文连接,否则保留追究法律责任的权利。
公众号转载请联系网站首页的微信号申请白名单!

个人微信公众号 ↓↓↓                 

微信搜一搜爱上游戏开发