3D沙盒游戏开发日志6——系统化的人物功能模块

日志

终于放假了,这两天会把前面做的东西没写的日志都补上(大概3-5篇),然后恢复正常三天一更

前言

之前写了几个功能像人物的移动及视角控制、攻击、建筑物放置,接下来还会有背包,以及人物的各种行为如吃东西,
捡起东西丢东西等包括与环境物体和怪物的交互,并且攻击等行为都会和背包里的装备情况联系起来。但当我去做这些
的时候尤其是人物的各种动作,我意识到有必要写一个系统化的人物控制器来协调人物各个模块之间的关系了,之前我
们只有建筑物放置和攻击会调用移动模块,关系很简单也不用担心耦合问题。但接下来人物的动作会有很多,必须要
有一个像中枢一样的系统来协调人物的所有行为。

Action系统

解决冲突

在协调冲突前我们需要先意识到冲突具体是什么
一方面是实际行为与动画的不同步,这是相当困难的一点,Unity提供了功能强大的animator状态机来处理人物的动画但同时也就意味着动画本身成为了一套独立于代码外的系统(依靠你在animator中建立的转换关系和条件工作),那么你就需要时刻注意当你代码逻辑切换时(从一个动作切换到另一个)是否也同步了动画的切换
另一方面是代码逻辑的不完善导致的冲突,也就是说当一个行为打断另一个行为时你如何保证之前行为正确的退出和下一个行为正确的开始(当然也要保证动画的合理切换)

新的方案

之前我们在LocomotionController中写的moveInterruption和moveDoneCallback都是在处理这样的动作切换,以及在animator中制定复杂的转换条件。但当行为从两三个增加到十几个就不能再一个一个处理冲突。最好的方法是让所有行为遵循同一套规则,让逻辑和动画的接口尽可能的少,让动画的转换以及层级关系尽可能简单
回想我们最简单的ai逻辑状态机,对于每个状态都会有类似于begin,tick,end等函数,然后靠一个单独的模块来完成各状态间的切换,各状态之间是互不可知的也就是不耦合的,全部依赖于这一系统,那么我就想把这样子的系统也放到人物上来,我把它叫做ActionController。

ActionController管理哪些事情

人物的所有行为和动作,包括移动、攻击、背包切换、与其它生物和物体的交互甚至什么技能之类的,换句话说只要它需要播放动画就需要受到ActionController的唯一管理,播放动画的所有行为之间都是互斥的(角色不可能同时播放两段动画)它需要处理互斥行为间的切换(冲突二),ActionController同时也就成了代码与动画交互的唯一接口(冲突一)

具体实现

PlayerAction

首先我们需要对所有接受ActionController的行为抽象一个基类PlayerAction

public abstract class PlayerAction : MonoBehaviour
{
    //动作名称
    public abstract string actionName{ get; }
    //优先级  higher number means higher priority
    public abstract int priority{ get; }
    //动作顺利完成的标志
    public bool finish;
    //actiontrigger注册表
    public Dictionary<string, ActionTrigger> triggers = new Dictionary<string, ActionTrigger>();
    //动作启动函数
    public abstract void Begin(params object[] target);
    //被打断
    public abstract void Interrupted();
    //注册ActionTrigger
    protected void RegisterTrigger(string name)
    {
        triggers[name] = new ActionTrigger(name);
    }
    //重置所有ActionTrigger
    protected virtual void ResetActionTrigger()
    {
        foreach (var key in triggers.Keys)
        {
            triggers[key].Reset();
        }
    }
}

所有变量和函数都注释的比较清楚了,但还有一些问题需要说明

继承MonoBehaviour

可以看到PlayerAction继承了MonoBehaviour,也就是说行为都是挂载在角色身上的脚本正如我们之前写的LocomotionController以及AttackController那样,其实最理想情况下每个行为可以是一个协程函数,这样真正给人一种无耦合的感觉,每个行为都在协程体内做自己独立的事情像一个黑盒子,但有些行为确实需要持久保存的变量以及update函数如移动等,而且需要访问别的脚本中的数据如背包装备情况。每一个Action都会预先挂载在人物上但不执行时将处于Unenable状态,由ActionController进行Enable

关于ActionTrigger

一开始这个体系中并没有ActionTrigger,但我在写一些实际的行为中发现几乎所有的行为在与动画交互时都需要这样的流程
行为脚本中开始播放动画(通过SetBool或者SetTrigger)->脚本等待动画播放到某一帧触发实际效果->等待动画播放结束进行退出行为
在以往一般的解决思路是利用动画的AnimationEvent来实现播放到某一帧触发具体的事件,在这里也是一样,但如果每个行为我都设置自己单独的监听函数就会有两个问题。
1.太过繁琐,很多无用的代码,每个动作中都要写额外两三个函数,每个动画帧也要特别选取不同的事件
2.不遵循所有行为与动画的交互全部通过ActionController这一规则,Animation直接将消息发给了行为
ActionTrigger就是为了解决这两个问题,至于为什么要叫这个名字,因为它是动画推动行为进入到下一个阶段,并且在推动结束后需要复位(下一次行为重新等待)就像animator中的trigger一样

//ActionTrigger是动画帧事件与动作交互的唯一方式
//动画帧通过调用ActionController的SetActionTrigger(name)来触发ActionTrigger
//当前正在进行的动作会检测对应的trigger并进入到下一阶段(产生实质性的效果)
public class ActionTrigger
{
    private bool value = false;
    private string triggerName;
    public ActionTrigger(string name)
    {
        triggerName = name;
    }
    public static implicit operator bool(ActionTrigger at)
    {
        return at.value;
    }
    public void Set()
    {
        value = true;
    }
    public void Reset()
    {
        value = false;
    }
}

ActionTrigger由行为在Awake中注册RegisterTrigger,然后动画调用ActionController中的唯一函数SetActionTrigger进行设置Set(由名字通过当前行为的注册哈希表找到对应的ActionTrigger),由行为在结束或被打断时Reset所有Trigger

public void SetActionTrigger(string triggerName)
{
    if(triggerName == null || !currentAction || currentAction.finish || !currentAction.triggers.ContainsKey(triggerName))
    {
        Debug.LogError("action trigger set error" + triggerName + " " + currentAction);
        return;
    }
    currentAction.triggers[triggerName].Set();
}

ActionController

public class ActionController : MonoBehaviour
{
    private Animator animator;
    private PlayerAction currentAction;

    void FixedUpdate()
    {
        if(currentAction == null) return;
        //Debug.Log("CurrentAction: " + currentAction.actionName);
        if(currentAction.finish)
        {
            Debug.Log("Action Finish: " + currentAction.actionName);
            currentAction.enabled = false;
            currentAction = null;
        } 
    }
    public bool DoAction<T>(bool interruptSame, params object[] target) where T : PlayerAction
    {
        T action = GetComponent<T>();
        if(currentAction != null)
        {
            //相同动作看参数,不同动作相同优先级或更高优先级可以打断
            if((currentAction.actionName != action.actionName && currentAction.priority <= action.priority)
            || (currentAction.actionName == action.actionName && interruptSame))
            {
                currentAction.Interrupted();
                Debug.Log("ActionInterrupted: " + currentAction.actionName + " by " + action.actionName);
            }
            else return false;
        }
        currentAction = action;
        currentAction.Begin(target);
        currentAction.enabled = true;
        Debug.Log("DoAction: " + action.actionName);
        return true;
    }
    public void StopAction()
    {
        if(currentAction == null) return;
        currentAction.Interrupted();
        currentAction = null;
    }
    public void SetActionTrigger(string triggerName)
    {
        if(triggerName == null || !currentAction || currentAction.finish || !currentAction.triggers.ContainsKey(triggerName))
        {
            Debug.LogError("action trigger set error" + triggerName + " " + currentAction);
            return;
        }
        currentAction.triggers[triggerName].Set();
    }
}

ActionController保存一个当前正在进行的动作和animator用于动画控制。
通过在fixedupdate中检测当前动作的finish状态来确定动作是否已结束。核心功能是DoAction处理动作的切换,由输入等行为进行调用DoAction,返回bool表示是否成功开始执行

总结

ActionController是一个比较抽象的系统,但它非常好的完成了对于十几种动作的控制,让代码无论是结构还是调试都大大得到优化,这个系统也是我自己根据状态机所制定一个人物系统,以后如果遇到问题的话也会继续完善的。
接下来的几篇日志我将改写之前的LocomotionController、AttackController和ConstructController,并引入其它新的动作如CraftController等,通过这些例子可以更好的看出该系统是如何工作的,但这个ActionController系统确实是整个玩家人物的核心

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值