《Unity3D动作游戏开发实战》笔记及扩展2.1

Posted 0qqppyvt

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《Unity3D动作游戏开发实战》笔记及扩展2.1相关的知识,希望对你有一定的参考价值。

2.1.1. 使用协程分解复杂逻辑
协程处理异步任务:当遇到一些需要异步处理的程序需求时,可以使用协程来实现
使用协程的优点:简单,易于实现
实例:使用协程代替有限状态机 (基于原书的代码做了些许修改以及增加了注释)

//一个表示村民的类
public class Villager : MonoBehaviour
{
    public float maxSatiation = 10f;        //最大饱食度
    public float maxFatigue = 10f;            //最大困倦值
    const float minSatiation = 0.2f;        //最小饱食度
    const float minFatigue = 0.2f;            //最小困倦值
    private float satiation;                //当前饱食度
    private float fatigue;                    //当前困倦值
    Coroutine currentCoroutine;                //当前的状态(协程)

    //OnEnable在脚本被激活时立即执行
    void OnEnable()
    {
        satiation = maxSatiation;            //初始化饱食度,设为最大值
        fatigue = maxFatigue;                //初始化困倦值,设为最大值
        StartCoroutine(Tick());                //开始“游戏循环”的协程
    }
    
    //模拟“游戏循环”,类似MonoBehaviour的Update方法
    IEnumerator Tick()
    {
        //每帧进行一次循环
        while(true)
        {
            DecrePerFrame(satiation);        //减少饱食度
            DecrePerFrame(fatigue);            //减少困倦值
    
            //如果饿死了且当前的状态为空,开始“吃”协程,并定“吃”为当前状态
            if(satiation < minSatiation && currentCoroutine == null)
            {
                currentCoroutine = StartCoroutine(Eat());
            }
            
            //如果困死了,不管现在在干啥,直接开始“睡”协程,并定“睡”为当前状态
            if(fatigue < minFatigue)
            {
                currentCoroutine = StartCoroutine(Sleep());
            }
            
            //停顿一帧
            yield return null;
        }
    }
    
    IEnumerator Eat()
    {
        //每帧吃一点,吃到饱为止
        while(satiation < maxSatiation)
        {
            IncrePerFrame(satiation);
            yield return null;
        }
        
        //吃饱了,当前状态改回空
        currentCoroutine = null;
    }

    IEnumerator Sleep()
    {
        //立即停止当前正在干的事(例如"吃")
        StopCoroutine(currentCoroutine);
        
        //如果没睡够,继续睡
        while(fatigue < maxFatigue)
        {
            IncrePerFrame(fatigue);
            yield return null;
        }
        
        //睡够了,当前状态改为空
        currentCoroutine = null;
    }
}

2.1.2. 自定义的插值公式
什么是插值(interpolation):在两个值/位置之间定义一个新的值/位置
插值通用公式:P01 = (1 - u) P0 + u P1;
u是什么:在线性内插中,u为一个0-1之间的浮点数,用于决定我们获得的插值更靠近P0还是P1. (线性外插的u是<0或者>1的,在游戏中并不常用)
常用的插值公式
线性插值:u = u
缓进:u = u * u
缓出:u = 1 - (1 - u) * (1 - u)
缓进出:u = ((u - 1) (u - 1) (u - 1) + 1) ((u - 1) (u - 1) * (u - 1) + 1)
Sin波长:u = u + range(0, 1) sin(u 2 * PI)
2.1.3. 消息模块的设计
消息/事件管理:游戏中往往有大量互相连接的元素,而他们十分需要消息系统的支持。
消息/事件的作用:在一个事情发生时,与之相关的结果被一并触发(例如,击杀一个敌人,我们的成就系统要记录我们多击杀了一个敌人,这就是消息/事件的用武之地)
消息模块的缓存:通常情况下,一个事件被触发后,会立即通知所有订阅的监听者,但这并不适用于所有的情况(例如玩家获得新武器,背包内的新武器会高光显示,但此时玩家还没打开背包,而等到打开时事件却早就通知过监听者了)
实例:消息模块的简易实现 (基于原书的代码简化成伪代码)

public class MessageManager
{
    Dictionary<string, Action<object[]>> messageDict;    //存储消息以及相关联的监听者的字典
    Dictionary<string object[]> dispatchCacheDict;        //缓存区
    
    public void Subscribe(string messageKey, Action<object[]> action)
    {
        if(messageKey in messageDict.Keys)
        {
            //Set action as a new subscriber of messageDict[messageKey];
            //如果已经存在这个消息,那么给他加一个订阅者(监听者)
        }
        else
        {
            //Add new messageKey and new action to messageDict
            //否则,加入新的消息
        }
    }
    
    public void Unsubscribe(string message)
    {
        messageDict.Remove(message);
    }

    public void Dispatch(string message, object[] args = null, bool addToCache = false)
    {
        if(addToCache)
        {
            //add message and args into cache
            //如果选择加入缓存,就将传入的所有事件加入缓存
        }
        else
        {
            //trigger all the co-related actions in this message
            //否则,触发所有与该信息关联的所有
        }
    }

    public void ProcessDispatchCache(string message)
    {
        //如果message存在于缓存中
        if(message in dispatchCacheDict.Keys)
        {
            //执行缓存中与该信息关联的所有事件,随后从缓存中移除message
            Dispatch(message, dispatchCacheDict[message]);
            dispatchCacheDicr.Remove(message);
        }
    }
}

2.1.4. 模块间的管理与协调
单例模式的管理
防止销毁后的调用:在单例中加入判断,若已被销毁,则避免调用
防止单例被重复创建:因为单例一般被标记为DontDestroyOnLoad,在场景切换时,单例会被再次创建,需加入判断避免重复创建单例
脚本执行优先级:在ProjectSetting里,寻找Script Execution Order,在其中对脚本优先级进行设置,能有效避免NullReferenceException(例如A脚本在Awake时想要获取B脚本的单例,但B的优先级在A后,那么A就不能成功获取到B,所以需要设置B的优先级高于A)

以上是关于《Unity3D动作游戏开发实战》笔记及扩展2.1的主要内容,如果未能解决你的问题,请参考以下文章

Android项目实战(三十九):Android集成Unity3D项目(图文详解)

Unity3D实战RPG黑暗之光:游戏分解及各系统的实现

求推荐一本unity3d的书.

Unity3D实战RPG黑暗之光Scene3:创建游戏运行场景及角色控制

我的书《Unity3D动作游戏开发实战》出版了

Unity3D游戏开发实战Unity3D实现休闲类游戏《2048》——算法源代码