使用PlayMaker将逻辑和表现分开
现在大部分游戏AI和很多的游戏逻辑,都会通过状态机实现。因此,当游戏角色需要换动作的时候就需要更换角色状态,如果一个动作有几个阶段,则可能需要更换好几次状态,于是经常出现大状态套小状态的情况,导致代码混乱,不好维护。
在制作一个角色的时候,策划和美术会经常变动角色的动作,表现,特效等,这些改动不涉及游戏逻辑,但是有可能会影响到角色状态,比如一个二段击变成了三段击,或者需要在一个特效完了之后播放另一个特效。这些变动极大的影响了开发效率,使得程序不能专注于游戏的核心逻辑和功能。
除了一些非常核心的动作游戏,对动作和逻辑依赖很高以外,大部分的游戏类型只是根据某些事件播放一段动画,允许动画的表现有很小的时间延迟,甚至有些游戏类型就是基于命令的,输入一个命令,执行一个动作。所以对于这些游戏,将逻辑和表现分开,将有利于提高开发效率,提高代码复用率和资源利用率。策划和美术可以很方便的自己调试动作和最终效果,不需要程序介入,甚至不需要程序干预就可以制作一个全新的角色。
我们知道,Unity本身是基于组件设计的,一个空的GameObject,通过添加不同的组件就能实现不同的功能,但是它对状态的管理却不是很方便,仍然需要编写脚本来实现。PlayMaker是一套为Unity设计的可视化脚本编辑器,可以通过可视化操作,不需要写一行代码即可实现各种游戏的互动效果。PlayMaker本身具有很多优点,非常的轻量化,可视化编程,直观的连线编辑器,可以自由添加事件和变量,强大的Debug功能,高度的可扩展性等等。大名鼎鼎的《炉石传说》就使用了PlayMaker。
PlayMaker介绍
PlayMaker其实就是一个“状态机 + 封装好的执行代码集”,并且它作为Unity的组件可以直接加到GameObject上面。它只有4个基本元素:状态机(FSM)、动作(Action)、变量(Variables)、事件(Events)。只需要理解这4个元素,即可轻松驾驭。
l 状态机(Finite State Matchines)
状态机用各种不同的状态来组织角色的行为,走路,等待,攻击,防御等等,PlayMaker使用事件来驱动状态的切换。
1. 开始事件(Start Event)
开始时间是PlayMaker状态机的第一个事件,当FSM组件Enable的时候,开始事件会调用,它所指向的状态即为状态机的第一个状态。
2. 状态(State)
每一个状态都会执行若干个行为,同一时间只有一个状态会被激活,并且只有处于激活的状态会执行动作(Action)和接收事件(Events)。
3. 跳转事件(Transition Event)
事件会触发当前状态跳转到下一个状态,事件可以既可以来自Unity的脚本或者组件,也可以来自PlayMaker自己的Action。
4. 跳转(Transition)
这条连线表示当某个事件触发时状态会跳转到哪个状态,通过改变连线就可以改变状态机之间的跳转逻辑。并且在Debug时,也可以实时看到状态间的跳转。PlayMaker支持断点调试,如果在某个状态上打上断点,则当跳转到该状态时,游戏会暂停。
5. 全局跳转(Global Transition)
当设定的全局跳转事件被触发时,不管当前状态是哪个状态,状态机都会直接跳转到全局事件指向的状态,上面5指向的Hit事件,就是只要角色被击中,就会进入FallDown状态。全局跳转事件有利于减少过多的跳转事件,简化状态机的跳转逻辑。
l 动作(Action)
当前激活的状态会执行一系列的动作,这些动作实际上就是封装好的一段代码,PlayMaker将一个个的功能都封装起来,便于策划自己去调用,程序要做的就是将策划需要的功能拆分,让他们自己去组合。PlayMaker的Action可以使用参数,并且可以在编辑器中编辑。
l 变量(Variables)
在PlayMaker中,一个Action定义的参数,可以像编辑其他UI的控件或者Unity的组件参数一样,填一个具体的值,也可以使用一个变量,而这个变量可以是另一个Action操作的结果,也可以是其他代码中传给PlayMaker的某个值。例如上面的图,左边是使用控件指定一个常量,而右边则是指定Time参数使用fadeInTime变量的值。
l 事件(Events)
事件即是触发状态跳转的条件,所有的状态跳转都是依赖事件触发的。
上面是PlayMaker的基本介绍,有了这些东西,其实已经可以利用PlayMaker来制作一个角色的完整逻辑了,PlayMaker本身已经自带了很多的Action,包括2DToolKit和NGUI等一些常用插件的Action,还有逻辑运算,算术运算等Action。但是纯粹使用PlayMaker,对于程序来说有时候并不如写代码更方便,而对于策划又太复杂,需要一些程序员的逻辑思维,因此主体的逻辑依然由程序写代码完成,而表现上的不影响逻辑的东西则让策划自己去编辑。程序将各个事件都定好,然后发给PlayMaker,策划根据这些事件跳转状态去组织表现,这样就算策划编辑错了,也不会影响游戏逻辑。例如英雄砍了怪物一刀,即使策划编辑错了,使得英雄的挥刀动作没播放出来,但是怪物实际上还是会按照既定的逻辑掉血,不影响最终结果。
具体的PlayMaker的介绍和使用请看官方文档。
http://hutonggames.com/index.html PlayMaker官方主页
https://hutonggames.fogbugz.com/default.asp?W1 PlayerMaker Manual
Figure 1 PlayMaker自带的一些Action
逻辑和表现的分层示意图如上图所示,核心逻辑层负责计算游戏的核心逻辑,包括AI和处理用户的输入,所有处理后的结果通过事件通知给表现层,然后表现层再给出具体的表现,如改变位置,播放动画,或者生成一个特效。我们的目的是为了让策划可以更好的控制表现,于是加入了PlayMaker,表现层再把事件传递给PlayMaker,于是大部分的表现就可以通过PlayMaker来编辑实现了。
将事件发送给PlayMaker
要将事件发送给PlayMaker,需要使用PlayMaker的接口,将事件发送给特定的PlayMaker组件。
public void Event(string fsmEventName);
例:
PlayMakerFSM pm = gameObject.GetComponent<PlayMakerFSM>();
pm.Fsm.Event(“Walk”);
要将事件发送给场景中所有的PlayMaker组件,可以用
PlayMakerFSM.BroadcastEvent(“Walk”);
当PlayMaker收到walk的事件,就会从Idle状态跳转到Walk状态。
将参数传递给PlayMaker
要将参数传递给PlayMaker,直接通过组件的接口,获取PlayMaker中定义的变量,然后直接赋值即可。
string name = "walkTime"; float paramValue = 3.0f; PlayMakerFSM pm = gameObject.GetComponent<PlayMakerFSM>(); FsmVariables vs = pm.Fsm.Variables; if (vs==null) { return; }
NamedVariable v = vs.GetVariable(name); if (v != null) { Type paramType = v.GetType(); if (paramType == typeof(FsmFloat)) { FsmFloat tv = (FsmFloat)v; tv.Value = paramValue; } }
|
图1 状态图
图2 Walk状态执行的Action
你可能会像上面那样发送一个walk事件,使得你的角色在播放3秒走路的动画之后回到Idle状态。图1为PlayMaker的状态图,图2为Walk状态所执行的Action。第一个Action很好理解,就是播放Walk的动画,而第二个状态是我们自己添加的自定义Action,他用刚才我们传进去的walkTime作为参数,作用是等待一段事件以后,发送FINISHED事件,从而使得角色的状态回到Idle。这里之所以用自定义的Wait事件,而不是PlayMaker自带的Wait事件,是因为我们的核心层是逐帧迭代的,时间可能和Unity Tick的时间不一致,所以使用自定义事件让时间和核心层的时间同步。后面会讲如何自定义PlayMaker的Action。
当然上面的逻辑你也可以换一种方式实现,如下面的图3,在Walk状态等待idle事件,再返回Idle状态,这样就不需要VDWait Core的Action,自然也不需要传递参数。只要等待逻辑层再次发送idle事件即可。
注意,参数必须在发送事件之前设置,否则可能参数还未生效,Action就已经执行过了。
现在我们可以把上面讲的东西封装一下,便于使用。
PlayMakerEventMonoBehaviour定义和PlayMaker交互的接口函数,SendPlayMakerEvent和SendPlayMakerEventToChild用于将事件发送到PlayMaker,两个SetPlayMakerParam函数用于在发送事件之前设置参数。
EntityInfoHandler作为所有角色的父类,继承自PlayMakerEventMonoBehaviour,封装和声明发送PlayMaker事件的函数,并且负责自动注册要发送给PlayMaker的事件。成员Entity是角色实例的ID,用于唯一标识该角色的实体,在注册事件的时候通过EntityID,来区分事件是否是发给自己的。HandlerPlayMakerEvent是处理核心逻辑层事件的函数,它负责将核心逻辑层的事件转发给PlayMaker,在创建一个实例并且分配到ID之后,即调用RegisterPlayMakerEvent()来将HandlerPlayMakerEvent注册到事件监听器上。而该类的静态成员sGameEventToPlayMakerEventMap则是一个事件映射表,他保存了哪些子类型需要注册什么事件,用于转发给PlayMaker。
为了可以自动注册事件,我们添加了一个属性——PlayMakerEventAttribute,该属性包含一个GameEventType的参数类型,用来声明一个类需要注册什么事件。RegisterPlayMakerEvent函数就是搜索所有子类声明的属性,然后将该属性声明的GameEventType注册到事件监听器上。
至此,PlayMaker和我们自己的脚本就关联起来了,核心层的事件可以传递到PlayMaker,并且可以带有参数的传递,至于核心层和表现层的交互,以及用户输入到核心层的交互和封装,不是本文讨论的内容,相信大家已经有很多成熟的方法让逻辑和表现分开。下面贴上以上各个类的主要代码。
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)] public class PlayMakerEventAttribute { public PlayMakerEventAttribute (GameEventType gameEvtType) { mEventType = gameEvtType; }
public GameEventType EventType { get { return mEventType; } }
protected GameEventType mEventType; }
|
public class PlayMakerEventMonoBehavior: MonoBehaviour { public void SendPlayMakerEvent(string evtString) { PlayMakerFSM[] pm = gameObject.GetComponents<PlayMakerFSM>(); for (int i = 0; i < pm.Length; i++) { pm[i].Fsm.Event(evtString); } }
static public void SendPlayMakerGlobalEvent(string evtString) { PlayMakerFSM.BroadcastEvent(evtString); }
public void SetPlayMakerParam(string paramName, object paramValue) { PlayMakerFSM[] pm = gameObject.GetComponents<PlayMakerFSM>(); for (int i = 0; i < pm.Length; i++) { if (pm[i] == null) { continue; } PlayMakerParamAttribute.SetParamToPlaymakerFsm(pm[i].Fsm, paramName, paramValue); } }
public void SetPlayMakerParam(GameEventBase evt) { Type evtType = evt.GetType();
FieldInfo[] fields = evtType.GetFields(); for (int i = 0; i < fields.Length; i++) { FieldInfo finfo = fields[i]; object[] paramAttrs = finfo.GetCustomAttributes(typeof(PlayMakerParamDataAttribute), false); if (paramAttrs.Length > 0) { PlayMakerParamDataAttribute attr = (PlayMakerParamDataAttribute)paramAttrs[0]; if (attr.IsGlobal) { PlayMakerParamAttribute.SetGlobalParam(attr.ParamName, finfo.GetValue(evt)); } else { SetPlayMakerParam(attr.ParamName, finfo.GetValue(evt)); } } } } }
|
public class EntityInfoHandler : PlayMakerEventMonoBehavior {
protected uint m_EntityId = 0;
//通过sendmessage 设置过来的 public virtual void SetEntity(SetEntityParam param) { m_EntityId = param.EntityId; RegisterPlayMakerEvent(); RegisterEventCallback(); }
public virtual void OnDestroy() { UnRegisterEventCallback(); UnRegisterPlayMakerEvent(); }
void HandlePlayMakerEvent(GameEventBase e) { SetPlayMakerParam(e); SendPlayMakerEvent(e.eventType.ToString()); }
public uint GetEntityID() { return m_EntityId; }
#region Static Event Register protected struct GameEventInfo { public GameEventType EventType; public MethodInfo MethodInfo; }
protected static Dictionary<Type, GameEventType[]> sGameEventToPlayMakerEventMap = new Dictionary<Type, GameEventType[]>(); protected static Dictionary<Type, GameEventInfo[]> sGameEventInfoMap = new Dictionary<Type, GameEventInfo[]>();
protected Dictionary<string, EventCallback> mEventCallback = new Dictionary<string, EventCallback>();
protected void RegisterPlayMakerEvent() { Type t = this.GetType();
GameEventType[] eventTypes = null; if (!sGameEventToPlayMakerEventMap.TryGetValue(t, out eventTypes)) { PlayMakerEventAttribute[] attrs = (PlayMakerEventAttribute[])t.GetCustomAttributes( typeof(PlayMakerEventAttribute), true);
eventTypes = new GameEventType[attrs.Length]; for (int i = 0; i < attrs.Length; ++i) { eventTypes[i] = attrs[i].EventType; }
sGameEventToPlayMakerEventMap.Add(t, eventTypes); }
if (eventTypes != null && eventTypes.Length != 0) { VD.Instance().Processor.RegisterEvents.RegisterEntityEvent(m_EntityId, eventTypes, HandlePlayMakerEvent); } }
protected void UnRegisterPlayMakerEvent() { if (VD.Instance() == null || VD.Instance().Processor == null) { return; }
Type t = this.GetType(); GameEventType[] eventTypes = null; if (sGameEventToPlayMakerEventMap.TryGetValue(t, out eventTypes)) { if(eventTypes.Length != 0) { VD.Instance().Processor.RegisterEvents.UnRegisterEntityEvent(m_EntityId, eventTypes, HandlePlayMakerEvent); } } }
#endregion }
|
[GameEventToPlayMakerEvent(GameEventType.ActorIdleStart)] [GameEventToPlayMakerEvent(GameEventType.ActorMoveStart)] public class HeroEventHandler : EntityInfoHandler { } |