使用PlayMaker将逻辑和表现分开

发表于2015-07-07
评论2 6.4k浏览

想免费获取内部独家PPT资料库?观看行业大牛直播?点击加入腾讯游戏学院游戏程序行业精英群

711501594

              现在大部分游戏AI和很多的游戏逻辑,都会通过状态机实现。因此,当游戏角色需要换动作的时候就需要更换角色状态,如果一个动作有几个阶段,则可能需要更换好几次状态,于是经常出现大状态套小状态的情况,导致代码混乱,不好维护。

              在制作一个角色的时候,策划和美术会经常变动角色的动作,表现,特效等,这些改动不涉及游戏逻辑,但是有可能会影响到角色状态,比如一个二段击变成了三段击,或者需要在一个特效完了之后播放另一个特效。这些变动极大的影响了开发效率,使得程序不能专注于游戏的核心逻辑和功能。

              除了一些非常核心的动作游戏,对动作和逻辑依赖很高以外,大部分的游戏类型只是根据某些事件播放一段动画,允许动画的表现有很小的时间延迟,甚至有些游戏类型就是基于命令的,输入一个命令,执行一个动作。所以对于这些游戏,将逻辑和表现分开,将有利于提高开发效率,提高代码复用率和资源利用率。策划和美术可以很方便的自己调试动作和最终效果,不需要程序介入,甚至不需要程序干预就可以制作一个全新的角色。

              我们知道,Unity本身是基于组件设计的,一个空的GameObject,通过添加不同的组件就能实现不同的功能,但是它对状态的管理却不是很方便,仍然需要编写脚本来实现。PlayMaker是一套为Unity设计的可视化脚本编辑器,可以通过可视化操作,不需要写一行代码即可实现各种游戏的互动效果。PlayMaker本身具有很多优点,非常的轻量化,可视化编程,直观的连线编辑器,可以自由添加事件和变量,强大的Debug功能,高度的可扩展性等等。大名鼎鼎的《炉石传说》就使用了PlayMaker

http://www.hutonggames.com/images/UI_CompSmall.png

 

             

 

PlayMaker介绍

PlayMaker其实就是一个状态机 + 封装好的执行代码并且它作为Unity的组件可以直接加到GameObject上面。它只有4个基本元素:状态机(FSM)、动作(Action)、变量(Variables)、事件(Events)。只需要理解这4个元素,即可轻松驾驭。

l         状态机(Finite State Matchines

https://hutonggames.fogbugz.com/default.asp?pg=pgDownload&pgType=pgWikiAttachment&ixAttachment=483&sFileName=FsmDiagram.png

状态机用各种不同的状态来组织角色的行为,走路,等待,攻击,防御等等,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

https://hutonggames.fogbugz.com/default.asp?pg=pgDownload&pgType=pgWikiAttachment&ixAttachment=484&sFileName=ActionExample.png

当前激活的状态会执行一系列的动作,这些动作实际上就是封装好的一段代码,PlayMaker将一个个的功能都封装起来,便于策划自己去调用,程序要做的就是将策划需要的功能拆分,让他们自己去组合。PlayMakerAction可以使用参数,并且可以在编辑器中编辑。

 

l         变量(Variables

https://hutonggames.fogbugz.com/default.asp?pg=pgDownload&pgType=pgWikiAttachment&ixAttachment=486&sFileName=ActionVariable.png

PlayMaker中,一个Action定义的参数,可以像编辑其他UI的控件或者Unity的组件参数一样,填一个具体的值,也可以使用一个变量,而这个变量可以是另一个Action操作的结果,也可以是其他代码中传给PlayMaker的某个值。例如上面的图,左边是使用控件指定一个常量,而右边则是指定Time参数使用fadeInTime变量的值。

 

l         事件(Events

事件即是触发状态跳转的条件,所有的状态跳转都是依赖事件触发的。

 

上面是PlayMaker的基本介绍,有了这些东西,其实已经可以利用PlayMaker来制作一个角色的完整逻辑了,PlayMaker本身已经自带了很多的Action,包括2DToolKitNGUI等一些常用插件的Action还有逻辑运算,算术运算等Action。但是纯粹使用PlayMaker,对于程序来说有时候并不如写代码更方便,而对于策划又太复杂,需要一些程序员的逻辑思维,因此主体的逻辑依然由程序写代码完成,而表现上的不影响逻辑的东西则让策划自己去编辑。程序将各个事件都定好,然后发给PlayMaker,策划根据这些事件跳转状态去组织表现,这样就算策划编辑错了,也不会影响游戏逻辑。例如英雄砍了怪物一刀,即使策划编辑错了,使得英雄的挥刀动作没播放出来,但是怪物实际上还是会按照既定的逻辑掉血,不影响最终结果。

 

具体的PlayMaker的介绍和使用请看官方文档。

http://hutonggames.com/index.html                                                                      PlayMaker官方主页

https://hutonggames.fogbugz.com/default.asp?W1                            PlayerMaker Manual

Figure 1 PlayMaker自带的一些Action

 

 

C:UsersAdministratorDownloads_ (1).png

逻辑和表现的分层示意图如上图所示,核心逻辑层负责计算游戏的核心逻辑,包括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状态。1PlayMaker的状态图2Walk状态所执行的Action。第一个Action很好理解,就是播放Walk的动画,而第二个状态是我们自己添加的自定义Action他用刚才我们传进去的walkTime作为参数,作用是等待一段事件以后,发送FINISHED事件,从而使得角色的状态回到Idle这里之所以用自定义的Wait事件,而不是PlayMaker自带的Wait事件,是因为我们的核心层是逐帧迭代的,时间可能和Unity Tick时间不一致,所以使用自定义事件让时间和核心层的时间同步后面会讲如何自定义PlayMakerAction

 

当然上面的逻辑你也可以换一种方式实现,如下面的图3Walk状态等待idle事件,再返回Idle状态,这样就不需要VDWait CoreAction,自然也不需要传递参数。只要等待逻辑层再次发送idle事件即可。

 

注意,参数必须在发送事件之前设置,否则可能参数还未生效,Action就已经执行过了。

 

现在我们可以把上面的东西封装一下,便于使用。

C:UsersAdministratorDownloadsplaymakereventhandler (1).png

 

PlayMakerEventMonoBehaviour定义PlayMaker交互的接口函数SendPlayMakerEventSendPlayMakerEventToChild用于将事件发送到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

{

}

 

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

游戏学院公众号二维码
腾讯游戏学院
微信公众号

提供更专业的游戏知识学习平台