文章目录
-
-
-
- 13.1 皮皮猫打字机游戏
- 13.2 场景制作
-
- 13.2.1 入口场景
- 13.2.2 游戏场景
- 13.2.3 场景切换
- 13.3 游戏管理器
-
- 13.3.1 数据定义
- 13.3.2 生成字母盘
- 13.3.3 按键判断
- 13.3.4 连击定时器
- 13.4 动画控制器Animator
-
- 13.4.1 添加Animator
- 13.4.2 Animator状态机
-
- 13.4.2.1 Any State状态
- 13.4.2.2 Entry状态
- 13.4.2.3 Exit状态
- 13.4.3 动画状态的属性
- 13.4.4 状态间的过渡关系(Transitions)
- 13.4.5 添加状态控制参数
- 13.4.6 编辑切换状态的条件
- 13.5 猫娘动画状态机
-
- 13.5.1 模型资源下载
- 13.5.2 动画循环设置
- 13.5.3 状态过渡设置
- 13.6 角色动画控制器
-
- 13.6.1 数据定义
- 13.6.2 直接播放动画接口
- 13.6.3 设置变量值
- 13.6.4 根据状态队列设置状态
- 13.7 入口场景脚本
- 13.8 游戏场景脚本
- 13.9 文字特效动画
-
-
简介:我是一名
Unity
游戏开发工程师,皮皮是我养的猫,会讲人话,它接到了喵星的特殊任务:学习编程,学习Unity
游戏开发。于是,发生了一系列有趣的故事。
文章图片
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
切换场景,如下// 进入GameScene场景
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;
// TODO 抛出事件
}
}
}
private int m_blood = 0;
///
/// 连击数量
///
public int comboCnt { get;
set;
}
///
/// 连击定时器
///
public float comboTimer { get;
set;
}
///
/// 游戏结束
///
public bool gameOver { get;
private set;
}
///
/// 按键列表
///
public List keyList { get { return m_keyList;
} }
private List m_keyList = new List();
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
13.3.3 按键判断 我们需要先判断按键类型,封装一个接口
GetKeyDownCode
。///
/// 获取按键类型
///
///
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;
// TODO 抛事件,更新ui
}
按键错误的时候,连击中断,扣血,抛事件更新
ui
。///
/// 按键错误
///
private void OnKeyError()
{
// 连击中断
comboCnt = 0;
// 扣血
blood -= 30;
// TODO 抛事件,更新ui
}
13.3.4 连击定时器 如下,其中
Time.deltaTime
是一帧的间隔时间。每帧调用UpdateComboTimer
,对comboTimer
进行帧间隔时间递减,通过comboTimer
判断是否超过时间限制,超过则中断连击。///
/// 连击定时器
///
public void UpdateComboTimer()
{
if (comboTimer > 0)
{
comboTimer -= Time.deltaTime;
// 超过时间限制,连击断开
if (comboTimer <= 0)
{
comboCnt = 0;
// TODO 抛事件更新连击ui
}
}
}
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 | 用于向状态添加“行为 |
文章图片
要创建一个从
状态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 状态过渡设置 【Unity3D|《学Unity的猫》——第十三章(Unity使用Animator控制动画播放,皮皮猫打字机游戏)】我们需要通过
Animator
将这些动画进行合理的组织,如下文章图片
添加变量
Action
,过渡条件根据Action
的值进行判断。文章图片
过渡条件如下
状态1 | 状态2 | 条件 |
---|---|---|
Any State | idle | Action == 1 |
Any State | get_hit | Action == 4 |
get_hit | idle | 无 |
Any State | die | Action == 5 |
CharacterAniCtrler
,它需要包括数据和逻辑,如下。文章图片
运行中的状态过渡
文章图片
13.6.1 数据定义
///
/// 状态定义,默认为Idle状态。
///
public enum CharacterAniId
{
Idle = 1,
Walk = 2,
Run = 3,
Hit = 4,
Death = 5,
}
///
/// 角色动画Animator组件
///
private Animator m_animator;
///
/// 动画队列
///
private Queue m_animQueue = new Queue();
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 设置变量值
// 设置Action变量值为4
m_animator.SetInteger("Action", 4);
封装成接口
private const string STR_ACTION = "Action";
///
/// 播放不同动作ID
///
///
///
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)
{
//将Action复位
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;
}// 进入Game场景
SceneManager.LoadScene(1);
});
}
}
13.8 游戏场景脚本 游戏场景脚本
GameScene.cs
挂在Canvas
上,设置公开的成员对象。文章图片
主要根据各种事件更新
ui
。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 = https://www.it610.com/article/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();
// 更新血量ui
bloodSlider.value = https://www.it610.com/article/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("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();
}
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 s_objPool = new Queue();
///
/// 根节点
///
private static Transform s_root;
///
/// 文字组件
///
public Text keyText;
}
完成。
如果有什么疑问,欢迎留言或私信。
《学Unity的猫》——第十四章:Unity实现文件上传下载,支持续传,猫后爪的秘密
推荐阅读
- Unity3D|【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)
- Unity3D|《学Unity的猫》——第十六集(Unity动画使用混合树BlendTree实现动画过渡控制)
- Unity3D|【游戏开发实战】Unity UGUI实现循环复用列表,显示巨量列表信息,含Demo工程源码
- Unity3D|(完结)Unity游戏开发——新发教你做游戏(七)(Animator控制角色动画播放)
- CSS|CSS实现水滴动效
- Unity常见的优化性能设置
- 游戏|元宇宙密室逃脱游戏攻略来啦!
- 每天记录学习的新知识 : ObjectAnimator 基础和用法
- Unity3d|Unity中的层级以及渲染顺序