U3D动作游戏开发读书笔记–2.1一些通用的预备知识

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动作游戏开发读书笔记--2.1一些通用的预备知识

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

U3D动作游戏开发读书笔记--2.1一些通用的预备知识

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

U3D动作游戏开发读书笔记--2.1一些通用的预备知识

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

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

2.1.2 自定义的插值公式

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

  1. Leap差值

    U3D动作游戏开发读书笔记--2.1一些通用的预备知识

  2. SmoothDamp差值函数差值

    Mathf.SmoothDamp 是 Unity 引擎中一个非常实用的数学函数,主要用于平滑地从一个值过渡到另一个目标值,并可以模拟真实世界中的物理运动效果,比如弹簧、惯性等。

U3D动作游戏开发读书笔记--2.1一些通用的预备知识

​ 工作原理

Mathf.SmoothDamp 的核心原理是根据当前值和目标值之间的差异,动态调整速度,使运动看起来更加自然。 它会自动计算需要的加速度和减速度,创造出类似弹簧的平滑效果。

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

U3D动作游戏开发读书笔记--2.1一些通用的预备知识

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

U3D动作游戏开发读书笔记--2.1一些通用的预备知识

与帧数相关的插值类型:

  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一些通用的预备知识

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动作游戏开发读书笔记--2.1一些通用的预备知识

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

U3D动作游戏开发读书笔记--2.1一些通用的预备知识

发表评论

评论已关闭。

相关文章