学Unity的猫之Animator动画播放(十三)
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
组件),一个难度选择勾选(ToggleGroup
和Toggle
组件)。 背景图使用SpriteRender
组件在3D
摄像机中渲染。难度等级的勾选使用了
ToggleGroup
组件,用来给Toggle
分组。子节点中的
Toggle
需要指明相同的ToggleGroup
。实现单选的效果。
13.2.2 游戏场景
GameScene.unity
一个血条(
Slider
组件),一个得分(Text
组件),一个字母盘(GridLayoutGroup
、Text
组件)、一个连击(Text
组件),一个角色(SpriteRenderer
、Animator
组件)、一个背景图(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 Animator
,Unity5.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
, Entry
和 Exit
。
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
更改默认状态。
记住, Entry
在Animator
组件被激活后 =无条件= 跳转到默认状态,并且每个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 添加状态控制参数
参数有Float
,Int
,Bool
,Trigger
。Float
、Int
用来控制一个动画状态的参数,比如速度方向等可以用数值量化的东西,Bool
用来控制动画状态的转变,比如从走路转变到跑步,Trigger
本质上也是bool
类型,但它默认为false
,且当程序设置为true
后,它会自动变回false
。
如下这里创建一个Int
类型的参数AnimState
13.4.6 编辑切换状态的条件
点击连线,在Inspecter
窗口中可以进行设置,在Conditions
栏下可以添加条件,如下图表示当参数AnimState
为0
时会执行这个动画Any State
到New 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 Time
。get_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 Btn
和Tgl 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
-----------------------------
如未加特殊说明,此网站文章均为原创。
网站转载须在文章起始位置标注作者及原文连接,否则保留追究法律责任的权利。
公众号转载请联系网站首页的微信号申请白名单!
