发布

U3D动作游戏开发读书笔记

WAP站长网 2025-9-9 23:30
0 3

2.1 一些通用的预备知识:

2.1.1 使用协程分解复杂逻辑

试想一下如何实现一个简单的NPC人物行为,例如是村民。村民饿了会去吃饭,困倦了会去睡觉。上来上一个状态机?其实用不着这么复杂,可以使用协程来实现。

namespace LearnBook.Chapter2 { /// <summary> /// 村民 /// 使用协程模拟村民的行为 不用使用复杂的状态机 /// 适合一些简单的 硬编码实现的 NPC行为 /// </summary> public class Villager : MonoBehaviour { #region 状态常量 const float FATIGUE_DEFAULT_VALUE = 5F; const float SATIATION_DEFAULT_VALUE = 5F; private const float FATIGUE_MIN_VALUE = 0.2F; const float SATIATION_MIN_VALUE = 0.2F; #endregion private float mSatiation; //饱食度 private float mFatigue; //疲劳度 private Coroutine mActionCoroutine; //NPC的行为协程 private void OnEnable() { //初始化状态:既不累也不 mSatiation = SATIATION_DEFAULT_VALUE; mFatigue = FATIGUE_DEFAULT_VALUE; //启动行为协程 模拟村民的行为 StartCoroutine(Tick()); } /// <summary> /// 模拟村民的行为的协程 /// </summary> /// <returns></returns> IEnumerator Tick() { while (true) { //更新饱食度和疲劳度 随着时间推移下降 mSatiation = Mathf.Max(0,mSatiation - Time.deltaTime); mFatigue = Mathf.Max(0,mFatigue - Time.deltaTime); if (mSatiation <= SATIATION_MIN_VALUE && mActionCoroutine == null) { mActionCoroutine = StartCoroutine(EatFood()); } if (mFatigue <= FATIGUE_MIN_VALUE ) { mActionCoroutine = StartCoroutine(Sleep()); } //暂停下一帧 执行循环 yield return null; } } /// <summary> /// 模拟村民吃食物的行为 /// </summary> /// <returns></returns> IEnumerator EatFood() { mSatiation = SATIATION_DEFAULT_VALUE; mActionCoroutine = null; yield return null; } /// <summary> /// 模拟村民睡觉的行为 /// </summary> /// <returns></returns> IEnumerator Sleep() { StopCoroutine(mActionCoroutine); mFatigue = FATIGUE_DEFAULT_VALUE; mActionCoroutine = null; yield return null; } } } 

首先,设置一些常量数值:

U3D动作游戏开发读书笔记

然后再开始时候开启一个协程,协程内容每一帧执行一次,消耗精力数值和饱腹度,当消耗到最小的数值时便会触发执行对应的状态;

U3D动作游戏开发读书笔记

对应的状态也十分简单:在此帧率给自己状态进行充值。

U3D动作游戏开发读书笔记

这里睡觉的优先级高,假如正在吃饭 发现也需要睡觉 则停止吃饭转而睡觉。

实际上一些非常重要的角色或者关卡逻辑,如触发剧情走向的村民,结合协程进行硬编码处理非常高效。但是作为硬编码,可以理解为写死的逻辑,仅仅是方便对一些逻辑简化处理。

2.1.2 自定义的插值公式

只需要给一个速率值相关的插值函数(帧数无关插值可以理解为没有一个时间限制,只需要提供一个速率值即可):

  1. Leap差值

    U3D动作游戏开发读书笔记

  2. SmoothDamp差值函数差值

    Mathf.SmoothDamp 是 Unity 引擎中一个非常实用的数学函数,主要用于

    平滑地从一个值过渡到另一个目标值

    ,并可以模拟真实世界中的物理运动效果,比如弹簧、惯性等。

U3D动作游戏开发读书笔记

​ 工作原理

Mathf.SmoothDamp 的核心原理是

根据当前值和目标值之间的差异,动态调整速度

,使运动看起来更加自然。 它会自动计算需要的加速度和减速度,创造出类似弹簧的平滑效果。

​ 特别要注意的是 currentVelocity 参数,它是一个引用参数,函数会在每次调用时更新它的值。这意味着你需 要在函数外部定义并维护这个变量,不能每次调用都创建新的变量。

U3D动作游戏开发读书笔记

书中所说的两种差值的效果:

U3D动作游戏开发读书笔记

与帧数相关的插值类型:

  1. Quicken类型

    t = t * t 

    Quicken类型非常简单实用,而且不会造成太多开销。可以修改为t^n进行细调,其中,t是一个0~1之间的值.

  2. EaseInOut插值类型

    t = (t - 1f) * (t - 1f) * (t - 1f) + 1f; t = t * t; 

书中展示的插值效果:

U3D动作游戏开发读书笔记

2.1.3消息模块的设计

来实现一个简单的消息模块,功能支持订阅、取消订阅,消息的分发和缓存分发。

namespace LearnBook.Chapter2 { /// <summary> /// 消息管理类 简单实现 /// 支持消息订阅 缓存分发消息 /// </summary> public class MessageManager { static MessageManager mInstance; public static MessageManager Instance { get { return mInstance ?? (mInstance = new MessageManager()); } } /// <summary> /// 消息字典 存储消息 和 订阅者(回调函数:无返回值 有参数的方法) /// </summary> private Dictionary<string,Action<object[]>> mMessageDic = new Dictionary<string, Action<object[]>>(32); /// <summary> /// 缓存消息 存储消息 和 参数 /// </summary> private Dictionary<string,object[]> mDispatchCacheDic = new Dictionary<string, object[]>(32); private MessageManager() { } /// <summary> /// 订阅消息 /// </summary> /// <param name="msg">消息名称</param> /// <param name="action">回调函数</param> public void Subscribe(string msg,Action<object[]> action) { if (mMessageDic.ContainsKey(msg)) { mMessageDic[msg] += action; } else { mMessageDic.Add(msg, action); } } /// <summary> /// 取消订阅消息 /// </summary> /// <param name="msg">消息名称</param> public void Unsubscribe(string msg) { if (mMessageDic.ContainsKey(msg)) { mMessageDic[msg] = null; } else { Debug.LogError("未订阅该消息"); } } /// <summary> /// 分发消息 /// </summary> /// <param name="msg">消息名称</param> /// <param name="args">参数</param> /// <param name="addToCache">是否添加到缓存</param> public void Dispatch(string msg, object[] args = null,bool addToCache = false) { if (addToCache) { mDispatchCacheDic[msg] = args; } else { Action<object[]> action; if(mMessageDic.TryGetValue(msg,out action)) { action?.Invoke(args); } else { Debug.LogError("未订阅该消息"); } } } /// <summary> /// 处理缓存消息 /// </summary> /// <param name="msg">消息名称</param> public void ProcessDispatchCache(string msg) { object[] value = null; if(mDispatchCacheDic.TryGetValue(msg,out value)) { Dispatch(msg,value); } } } } 

要点:

  • 作为一个功能类型的管理脚本,自然设置为单例。

  • 脚本中有两个字典,分别存储消息订阅方法引用(委托:回调函数容器)和存储消息缓存参数。

  • 支持延迟分发(提前缓存调用函数的参数。

    支持延迟分发是为了处理一些时序上的情景,假设玩家在游戏中获得新装备后,系统则会发送消息通知背包面板去显示第二个页签上的红点提示,但此时背包面板尚未创建,当玩家打开背包时消息早就发送过了。而延迟消息可以先把消息推送到缓存中,由需要拉取延迟消息的类自行调用拉取函数即可。

2.1.4模块间的管理与协调

简单的实现一个MonoBehaviour单例。

MonoBehaviour单例会在运行时创建一个Game-Object对象并置于DontDestroyOnLoad场景中,另外MonoBehaviour单例需

注意销毁问题

amespace LearnBook.Chapter2 { /// <summary> /// 简单实现Mono单例 /// </summary> public class MonoBehaviourSingleton : MonoBehaviour { private static bool mIsDestroying; private static MonoBehaviourSingleton mInstance; public static MonoBehaviourSingleton Instance { get { if (mIsDestroying) { return null; } if (mInstance == null) { mInstance = new GameObject("[MonoBehaviourSingleton]") .AddComponent<MonoBehaviourSingleton>(); DontDestroyOnLoad(mInstance.gameObject); } return mInstance; } } private void OnDestroy() { mIsDestroying = true; } } } 

要点:使用mIsDestroying变量来检查是否被销毁,防止对已经销毁的单例进行重新创建。因为在单例销毁时不能保证外部是否完全没有调用情况,如果在销毁后外部有新的调用则重新生成一个单例,会影响掉我们期望此单例销毁的状态。

U3D动作游戏开发读书笔记

对于脚本之间有明确的依赖关系时,我们可以手动的更改脚本的编译优先级。

U3D动作游戏开发读书笔记