Unity Gameplay工具集(Unity Gameplay Tool Set)

Unity可扩展、灵活的战斗框架实现3 GameplayTag 由于我们希望实现的目的是所有的技能、效果等都是可配置的,而目前来说一个效果的配置是存储在scriptableObject中,多个效果实例在代码层面上,都是一个类型,仅仅是读取的配置项不同。而为了区分它们,以便对同一效果进行区分对待,比如有的技能多次命中会刷新时间eg:大部分的眩晕技能),有的技能多次命中则叠加层数(eg:小鱼人的敏捷偷取),这里就抽象出一层标签。一个parent,可以为空,用于配置自己的父级,一个方法用于确认标签是否是另一个标签的衍生层级。我们来看代码,代码非常简单。 阅读详情


Gameplay Tool Set

概述

本文就数种重要的Gameplay框架及插件,简述它们的原理,介绍这些Gameplay框架的适用场合,并进行对比。
本文假设读者有一定的游戏开发经验、Unity开发经验。
本文会写得比较随性和啰嗦。

从Gameplay这词说起

Wikipedia:
Gameplay is the pattern defined through the game rules.

Gameplay,游戏性、玩法、游戏规则。

第一次听到Gameplay这英文单词,是大学毕业后到老东家上海育碧上班第一天。“之后你的职位是Gameplay programmer”,HR大叔对我说。这对一个刚刚毕业的、目光狭窄的、笔试靠写Shader进公司的、认为游戏等同于Rendering的、当时的我,是一种打击。我甚至内心开始形成鄙视链开始鄙视Gameplay,还幼稚地在公司电脑屏幕贴了一张小纸条安慰鼓励自己:

“Gameplay programmer in office, Rendering programmer at home.”

即在公司写写Gameplay、回家后研究Shader。好傻好可爱。

现在回头看,有点后悔当时没多花时间去参透一下前公司的Gameplay框架、应用代码。因为离开前东家后也断续地进行Gameplay开发,但都有种蛮荒时代没有火种摸石头过河地开发的感觉,缺少经验和积累。

关于Gameplay


Mario & Luigi RPG

Hearthstone

Overwatch


做游戏还是玩游戏,Gameplay都是最最最重要的因素之一。
玩家开始玩一款游戏的原因是多样的,表现、心流、炫耀、交友,但其中最有可能的是:好玩。
玩家停止玩一款游戏的原因也是多样的,难度、重复、劳累、孤独,但其中最有可能的是:乏味。

为了让我们的游戏不乏味,我们必须持续添加内容、更新规则,让玩家持续地感受到新意和有趣。
但项目组的人员是有限的、工作时间哪怕加班也是有限的、玩家的耐心也是有限的,如何能让项目组在有限资源的情况下,更好更快地进行游戏Gameplay迭代更新,是Gameplay框架的一大责任。

(另,可能一般不会太关注到的点是,我们也不能过度更改我们的游戏。一个游戏当前玩家是已经认可之前版本玩法设定的、受之前版本重重过滤后留下的玩家,如果玩家手上的版本本来是个RAC,我们下一个版本把它改成RTS,那玩家肯定都流失了。比如笔者之前负责过的一款游戏,个人觉得其2.0版本因为对战斗外体验更改过大,是造成2.0版本上线后数据滑落的重要原因之一。)

Gameplay框架


开始实现各种各样Gameplay时,我们常会编写符合需求,却相对更hardcode的Gameplay代码。
这做法有一定好处,其在时间紧急的情况下,能在初期就立刻见成效。
随着时间推演,Gameplay需求越来越多、越来越复杂、越来越和自己之前所想不一样的时候,这些之前hardcode的代码就越来越难以维护。
此时我们需要重构,需要针对这些各种各样的Gameplay需求,进行归纳总结。
(换句话说,上述这种更hardcode的Gameplay代码还有一个好处:其的确能让我们更早地了解细节,更早地知道自己为何重构、如何重构,甚至给重构提供集成测试用例。)

世界万物都可被归纳、被总结。
我们不能拒绝归纳总结,否则解决一个问题后、再出现类似问题我们又得从零开始苦思冥想。归纳总结可以帮助人去理解并记住结论,让人有可能举一反三。
但过分的归纳总结是抽象、甚至可能是无用的、不严谨的。不存在万金油。(“ToE”也尚未被证实。:P)

框架也是。
框架是必须的,为了更好地提供服务解决某一类问题,我们搭建底层框架。
但从我们写框架的第一行代码开始,给它带来功能的同时,也给它带来了限制。
即,没有万能的框架、只有适用的框架。

在游戏行业中,根据前人的实践、思考,已归纳总结出不错的几种重要Gameplay框架。
本文将讨论几种Gameplay框架,讨论它们是什么、它们之间的联系和区别、它们各自的适用场合。它们是:

并非说以上框架能满足一切Gameplay,但它们组合在一起,相信已能满足颇多需求。
这些框架是实用的。本文之所以会提到这几个框架,并非生硬地把它们堆砌在一起。恰恰相反,而是因为作者本人在游戏开发中遇到了实际问题,思考后发现,“这不是恰好可以用这种Gameplay框架来解决这个问题吗?”,通过实验和实践,才体会到这些框架的实用价值。


实体组件系统(Entity-Component-System)


Unity的GameObject/Component是很好的Entity-Component System例子

之所以把实体组件系统(Entity-Component-System,以下简称ECS)放在最前面,是因为它是最最最重要的、同时也是我们最熟悉的、可能也是我们最容易忽略的。

ECS不复杂,本人亦曾2度写过ECS,分别是Flash游戏《弹道轨迹(TNT)》和一个开发中的Unity帧同步游戏。如果我必须做出N选一,我会放弃其他所有Gameplay框架而选择保留ECS。
另,从《Game Engine Architecture》将ECS这个话题收编于其Runtime Gameplay Foundation Systems一章,重点着墨介绍,也能证明其与Gameplay的密切关系。

Is-A转为Has-A

ECS最核心的功能很简单:将传统继承的is-a换成了has-a,将Component保存于Entity的一个容器中,Entity提供API进行Component的查找访问。
因为针对任何一个事物进行有限的功能拆分必然是不完整的,选取任意一个维度将其作为基类,都是不那么严谨的。所以,将这些功能有限拆分后,与其不精确地必须选取一个作为基类,倒不如把它们公平地作为组件,公平地处于Entity里。
ECS能让我们更好地分解复杂的问题、整理复杂的关系。

狭义的ECS只包括上述这个功能,但一般,广义的ECS也会被修改成拥有以下几项重要功能。

生存期

ECS还可以提供API,进行Entity、Component的生存期管理,以及生存期相关事件的回调。
生存期以Unity的术语为例,一般指的是:

  • 创建(Awake)
  • 有效(OnEnable)
  • 启动(Start)
  • 轮转(Update)
  • 无效(OnDisable)
  • 销毁(OnDestroy)

实现生存期的重难点在于:

  • 如何确保“同时”创建的Entity的所有Start都发生在Awake之后。比如可以使用ms_gameObjectsWillStart列表实现。
  • 如何确保创建销毁不会影响轮转阶段。每一次Tick()都会对组件列表进行遍历调用Update()。用户在Update()内调用创建或销毁后,如果ECS立刻将其从列表中添加或移除,这将可能影响遍历逻辑。所以ECS会在Tick的开始阶段或末尾阶段才真正将Entity、Component添加或移除到最终列表里。比如可以使用ms_gameObjectsWillStart列表和ms_gameObjectsWillDestroy队列实现。
  • 如何确保高效的轮转。比如通过接口(Unity通过反射检测Update()等函数)让用户有权限规定某些自定义的Component是否接受Update。

通信

Entity之间可以通信、Component之间也可以通信。通信的方式可以是多样的,包括:

  • 事件(GameObject.SendMessage())
  • 搜索并直接依赖(GameObject.Find()、GameObject.GetComponent())
  • 也有一些做法,是将数据(黑板)也作为通信方式(GetProperty()、SetProperty()),但Unity并无此设计

父子从属关系

Entity之间可以有父子从属关系,从而进一步拆分功能。

比如人是一个Entity,它有Human这个Component;如果游戏需要重点关心心脏及其跳动次数,让Human提供GetHeartPumpCount()已不太合适,则可把心脏也作为一个Entity,作为人Entity的子Entity,同时心脏Entity有Heart这个Component,提供Heart.GetPumpCount()接口。

但Unity的实现中,并不将此功能归于GameObject,而是归于Transform。这样子有其好处,即进行Transform世界空间坐标运算时,仅仅关心Transform这个组件本身就好了。但坏处是,为了表达父子层级关系,必须引入Transform、居然就被迫引入Position、Rotaiton、Scale这些可能没用的信息了。

重要属性

有一些重要的、通用的属性,也直接定义在Entity中,比如唯一ID。
Unity的GameObject,还有供(物理、渲染)引擎内部使用的Layer属性,供Gameplay使用的Tag属性。

从上面的例子可以看出,ECS的功能是如此基础和重要,所以才说是Gameplay的必备要素。


节点可视化编程(Node-based Visual Scripting)


  • 状态机(Finite State Machine)
  • 行为树(Behavior Tree)
  • 事件驱动可视化编程(Event Driven Visual Scripting)
  • 非线性编辑(Non-linear editing)

上面提到的Gameplay框架及插件都有共同的一点:它们都可以以Node-based Visual Scripting的形式存在。

Visual Scripting

可能有人对Visual Scripting反感,直觉觉得它们的性能是低效的。Visual Scripting的Editor的UI复杂程度,是造成这种偏见的主要原因,但Editor的复杂度和它的Runtime运行性能完全不相关。理论上,一个语言的Front-end也可实现成Visual Scripting。比如,在《Game Programming Patterns》的Bytecode一章,如果为游戏开发一门语言,作者的确建议使用Visual Scripting作为Bytecode的一环,而并非使用文本文件,因为Visual Scripting中用户的每一个操作都是分开的,其机制忽略用户的每一个非法操作,但文本编程不同,用户是可以输入所有代码了之后才交给编译器编译,这将大幅提升实现编译器错误检测、错误提示的难度。

Node-based

至于Node-based,其思想就是封装和组合。
我们可以合理地考虑重用性,将功能拆分为非常通用、非常细小的Node,作为一个又一个Node。但这样有可能会造成Node过多,造成浏览、编写时的麻烦。
我们可以针对比较重要的一段逻辑进行归纳,将本由多个Node才能实现的重要逻辑,重新以1个Node的形式呈现。
这事实上是个何时进行重构的问题,也是个提取共性、保留异性的思想。

Blackboard

各个Node是相对独立解耦的,但各个Node有是有可能需要数据交互的。往往通过在主体中添加一个Blackboard(黑板)和SharedValue,来让这些Node进行数据交互。


利用Blackboard实现找寻Target、移动到Target、并进行Attack的行为树


以上图行为树作为Blackboard的例子。它实现的需求是

  1. 找寻玩家控制的Actor(FindLocalUserActor节点)
  2. 移动到该Actor到足够近(ActorMoveToTargetUntilDistance节点)
  3. 攻击(FunActorVKey节点)

留意到,Blackboard定义了TargetTransform的一个ShanredValue。
我们再观察FindLocalUserActor节点和ActorMoveToTargetUntilDistance节点:


FindLocalUserActor节点定义了 Transform这个SharedValue。 FindLocalUserActor将找寻到的Transform通过 Transform这个SharedValue设置给Blackboard的 TargetTransform

ActorMoveToTargetUntilDistance节点定义了 TargetTransform这个SharedValue(原谅命名和Blackboard的 TargetTransform同名了,请读者留意),它的值在这棵行为树里绑定的Value是Blackboard中的 TargetTransform

从而,FindLocalUserActor节点找到的目标Transform,成功地通过Blackboard的TargetTransform,传递给了ActorMoveToTargetUntilDistance的TargetTransform,成功地通过Blackboard让两个相对解耦的节点又能合作起来。
Blackboard和SharedValue往往通过Dictionary来实现。各个节点仅仅保存了SharedValue的Key的字符串,取值的时候,都是携带这个Key去Blackboard中查Dictionary对应Key的Value。

总而言之,通过Node-based Visual Scripting,可以让程序、策划更加好地分工。

  • 程序通过实现代码实现各种通用的Node、封装各种常用的Node,
  • 策划通过这些Node,通过Visual Scrpting,在将这些Node“有机”地组合起来,即能实现各种不同的逻辑。

虽然都是Node-based Visual Scripting,不同的Gameplay框架,有不同的具体机制和限制。下面将逐一介绍。


状态机(Finite State Machine)


PlayMaker

状态机也是我们非常熟悉的概念。在Unity中,我们常通过Mecanim或PlayMaker接触到状态机。
《Game Programming Patterns》的《State》一章,非常直观地概况了状态机的用处。
其将以下响应玩家输入事件的混乱代码:

void Heroine::handleInput(Input input)
{
  if (input == PRESS_B)
  {
    if (!isJumping_ && !isDucking_)
    {
      // Jump...
    }
  }
  else if (input == PRESS_DOWN)
  {
    if (!isJumping_)
    {
      isDucking_ = true;
      setGraphics(IMAGE_DUCK);
    }
    else
    {
      isJumping_ = false;
      setGraphics(IMAGE_DIVE);
    }
  }
  else if (input == RELEASE_DOWN)
  {
    if (isDucking_)
    {
      // Stand...
    }
  }
}

重构为:



这么简单直观的“一幅图”。

状态机之所以能将其问题简化,是因为它框架符合需求地提供了(但也限定死了)以下基础功能:

  • 一个状态机内部的各个状态是互斥的,一个状态机一个时刻只处于一个特定状态
    (比如上图的“STANDING”、“JUMPING”等方框)
    (当然如果你坚持hardcode,你也可以把isJumping_、isDucking_这些独立的变量变为一个枚举变量State来表达互斥,这的确能大幅优化上面代码的繁乱程度)
  • 可以将不同的事件发送给状态机
    (比如上图的“PRESS↓”、“RELEASE ↓”等事件)
  • 如状态A能跳转到状态B,则它们俩间会有一个从A指向B的Transition,该Transition指定由什么事件触发,从而触发状态跳转
    (比如上图“JUMPING”状态到“DIVING”状态之间有一个Transition,其指定由“PRESS↓”事件触发)
    • 当状态机接受到新事件时,如该事件是局部事件,则只有当前所在状态有该事件对应的Transition时,才进行跳转
      (比如上图,假设状态机当前处于“JUMPING”状态,因其只包含一个响应“PRESS↓”事件的Transition,所以当状态机接受到“PRESS B”局部事件时,将不会进行跳转;当状态机接受到“PRESS↓”局部事件时,才会跳转到“DIVING”状态)
    • 全局事件不管当前处于什么状态,都可以立刻进行状态跳转
      (即类似于Mecanim中AnyState相连的Transition、或PlayMaker的Global Transition)
  • A状态可以设置成能跳转到A状态自己,也可以设置成不可以
  • 状态有Enter()、Update()、Exit()三个阶段函数。
    (比如上图“JUMPING”状态跳转到“DRIVING”状态的过程中,将会依次调用到“JUMPING”这个状态对象的Exit()、“ DRIVING”这个状态对象的Enter();如果会停留在“DRIVING”这个状态对象的话,将一直调用它的Update())
  • 状态由用户自定义的脚本组成,分别都可以实现自己的Enter()、Update()、Exit()逻辑。脚本默认为串行执行,有些状态机也可以并行执行脚本。
  • 状态机提供Tick()函数以驱动当前状态的当前脚本的Update()函数
  • 状态机是张图
  • 可以有多个状态机同时并行运行

从状态机的特点触发,它适用于简单的、需要全局事件跳转的、有状态的逻辑。
但状态机不适用于复杂的逻辑,否则状态机即变成盘丝洞。

使用状态机的具体举例有:技能的逻辑或表现、Buff的逻辑或表现、有明显步骤的动画表现(炉石传说主要用PlayMaker做表现动画)。
通过多个状态机并行执行,可以把多种互不相关的状态结合起来实现一个复杂的角色动作逻辑。
比如一个角色按身体姿态分有moveLayer={stand|run|crouch},按动作分有actionLayer={idle|shoot|melee},按状态分有statusLayer={normal|weak|speedup}。
我们可以使用1个状态机去表达上述所有情况,这个状态机将包括:

  • s0={stand&idle&normal},
  • s1={run&idle&normal},
  • s2={crouch&idle&normal},
  • s3={stand&shoot&normal}
  • s4={run&shoot&normal}
  • ...等最大可能4*3*3=36种状态及其切换。

我们也可将这3个相关性本就较小的状态用3个并行执行的状态机去表达,此时,我们只需要考虑4+3+3=10种状态切换就好。
注意到,要成功这样做,需要依赖于底层服务提供者(如控制move的组件、控制action的组件、控制status的组件)本就能互不相关地被设置。

行为树(Behavior Tree)


Behavior Designer

行为树是诞生于游戏行业的一种重要的执行模型。

行为树的使用示例恰好在前面的Blackboard一节有提到,故不赘述。

行为树因为是树状,所以比状态机能够更好地应付复杂的执行流程。通过不断地拆分子问题、重用行为树,来帮助设计者更轻松地、更少出现错误地解决复杂问题。
虽然行为树也能和状态机一样响应外界事件、也能被外界事件中断某棵子树而跳到另一棵子树。但行为树常不这样做,常用于受外界事件突发事件影响较少的场合,而是通过行为树内部不断拉去游戏世界的信息,进行自发的流程控制。

所以,行为树常用于AI设计、流程相对比较固定的关卡逻辑。

其内部实现机制可概括为:

  • 行为树类似分层状态机(Hierarchical Finite State Machine, HFSM),注意和上面提到的多个并行状态机并不同。
  • 以树状的形式存在,状态被改叫为Task
  • 其每个Task可返回Success、Running、Failure的执行结果给父节点
  • 组合节点(Composite)是一种Task,其有1个或多个孩子Task。根据孩子Task返回的执行结果,不同的组合节点有不同的响应逻辑,从而不同地决定下一个节点是哪一个孩子并返回Running状态,或者不再执行孩子而返回Success或Failure节点
  • 修饰节点(Decorator)是一种Task,组合节点差不多,但其只能有1个孩子Task
  • 行为节点(Action)是一种Task,它对游戏世界信息进行读写操作,其必然是行为树的叶子节点,因为它并不能包含孩子节点。
  • 判断节点(Conditional)是一种Task,它和行为节点差不多,但我们口头约定好,判断节点只对游戏世界信息进行读操作来判断其执行结果、而不要对游戏世界信息进行写操作
  • 行为树提供Tick()函数,从而驱动当前待执行的或正在Running的节点的Update()函数。可以通过一个执行栈的列表来记录当前正在执行哪些节点。具体为:
    • 从Root点开始递归深度逐一遍历,
      • 将刚刚遍历到的新节点(包括Root自己)Push到执行栈栈顶;
      • 调用该节点的Update();
      • 先假设该节点的Update()只返回Success或Failue状态,即代表其已经执行完毕,即可将其返回状态保存在自身、Pop出栈、并交由父节点对其孩子们进行状态判断,决定需否执行下一个子节点,如没,则父节点本身返回状态并Pop出栈
      • 如果期间没有并行节点、所有节点都返回Success或Failue状态,则这1个Tick()内都可以执行整棵行为树
    • 如果一个执行栈执行过程中出现节点返回Running状态,则这次Tick()不再执行这个执行栈。而是下一次Tick()再执行这个执行栈的栈顶元素
    • 如果遇到并行组合节点,则该并行组合节点为所有孩子节点都new一个新的执行栈来供孩子节点分别使用,从而实现并行执行。这个并行组合节点执行完毕时,可以销毁被它new出来的这些执行栈们
    • 所有执行栈可以保存在一个执行栈列表中,在Tick()内就这个执行栈列表进行遍历执行

事件驱动可视化编程(Event Driven Visual Scripting)


Flow Canvas

在前一个项目《独立防线》中,我们采用行为树作为关卡逻辑编辑。
在打算实现新项目关卡逻辑的时候,却发现有太多全局事件跳转,导致行为树出现各种interrupt节点,从这颗子树跳到另一棵毫不相干的子树,很是突兀和麻烦。才意识到之所以行为树能用于独立防线的关卡逻辑,是因为它的关卡逻辑需求是相对比较线性的,都是按照现行剧本去挨个发生的。
这时我们也正常但不合理地联想到状态机也能响应全局事件,但由于状态机一次全局事件只能被一个状态捕获,所以是和我们的需求不一致的。

于是参考兄弟项目组的经验,我们将目光转移到了Starcraft2的Galaxy Editor的关卡编辑器上:


Starcraft2 Galaxy Editor - Trigger

从视频和上图可以看出,一个“Trigger”包括了

  • Event
  • Local Variables
  • Conditions
  • Action

这个Trigger机制非常棒!某某Event在世界里发生了,策划配置好这个Event对应的Trigger们都会进行一系列Condition的判断,如果判断通过,则执行对应的一系列Action,过程中Trigger自己的局部状态通过Local Variables去记录,供Condition和Action读写。

重点是在Local Variables和Conditions。从视频中你会发现,策划不已经是在编写逻辑了吗?只不过编写逻辑是通过UI来进行而已。
但问题是,类似于Galaxy Editor中的Conditions的操作、UI,都显得比较繁琐不直观(比如上图中的一长串配置英文:“Number of Living units in (Any units in (Entire map) owned by player1 matching Excluded: Structure, Missile, Dead, Hidden, with at mostAny Amount) == 0”)。

这时,我立刻联想到了Unreal4唯一押宝的Gameplay框架:Blueprints(前Unreal3 Kismet)。


Unreal4 Blueprints Visual Scripting

了解Blueprints后,发现Blueprints和Galaxy Editor的Trigger事实上都是属于Event-Driven。而且因为Blueprints是基于Visual Scripting的概念出发的,所以对于Variable、Condition的实现会显得更加灵活和强大。

然后,恰好,在Unity Assets Store里,有不错的一些EDVS插件,包括uScript、FlowCanvas等。考虑到我们的关卡逻辑需要进行AssetBundle更新,所以将EDVS翻译成C#脚本的uScript并不适合,最后再通过各种使用和性能评估,我们选定了FlowCanvas。

EDVS的特点是:

  • 基于Event触发,事件产生了之后push才触发逻辑。这点和状态机一样,比行为树轮询pull检查的性能较好
  • 默认一个Event发生后,对应的Flow都是同步执行完的。和状态机、行为树不同,默认未定义“状态”、“运行中”这些概念。你也可以实现自己的有“执行中”状态的节点,但需要自己定义同样的事件在这个状态下再发一次给你的这个节点,你的节点是什么行为
  • 提供更加类似于编程语言的变量和流程控制,比状态机行为树的粒度能做到更细

我们当前正将EDVS应用于关卡逻辑配置上。

非线性编辑(Non-linear editing)


In-house Character Action Editor: FunAction editor


什么是“ 非线性编辑(Non-linear editing,以下简称NLE)”?我们先通过图片搜索来找个直观感受。


Image search of Non-linear editing


NLE事实上就是老百姓口中的视频编辑,或者也可称为时间线(Timeline)编辑。
注意到“非线性”这个字眼和时间线本身比较“线性”这个感觉,比较矛盾。这是因为历史原因导致的。在上个世纪90年代,线性编辑(Linear video editing)是主要的视频编辑方式,其弊端是,进行视频编辑的时候,源视频必须线性地进行访问(想象一下录像带),给编辑带来了极大不便。所以,非线性编辑的最大特点是视频编辑时,可以对源视频进行非破坏性的随机访问。
所以,非线性编辑器和线性编辑器的差别并非我们当前游戏开发的重点——因为我们现在对外存、内存的访问都是非破坏性、可随机访问的。非线性编辑和线性编辑,都属于时间线编辑。

在游戏中,NLE主要用在实时过场动画(Cut-scene)的制作。
其核心概况是:

  • 多对象共存于时间线上,受NLE操作。NLE就好像导演,去控制摄影师、演员们、特效师们、音效师们什么时候该做什么事
  • 和Unity的Animation有相似性,都是基于时间线进行“某些东西”的编辑,但Animation中每一帧所编辑的东西非常固定:对象的属性或一些简单参数的事件,这远远不能满足于Cut-scene制作
  • NLE在时间线的基础上,允许开发自定义各种行为节点,及对行为节点进行参数配置
  • 节点在时间线上有明确的开始点、结束点,即形象地以“条状”表达一段持续的“事件”。这样将[开始帧,结束帧)的帧范围(Frame Span)封装成一段范围事件的好处是:
    • 明确区分1个Track内的多个帧范围事件对象拼接组成,以帧范围事件对象为单位,单独配置、操作、执行。举例为:
      • 给帧范围单独设置角色动画,即可以不修改原有动画文件的情况下,单独配置角色所播动画的范围、播放速度
      • 给帧范围传入一组路径点数据,作为对象(角色、Camera等)的运动轨迹
    • 方便地单独调节一段事件的长短
    • 方便地修改交换A事件和B事件的发生次序

NLE还可以用在角色动作编辑上。
一般游戏类型的角色动作,我们完全可以使用上面提到的状态机或行为树来配置实现动作。


Street Fighter 4: Hit and Hurt boxes

Street Fighter: frame by frame hurt boxes

但在类似于FTG、ACT这些游戏类型,角色的动作精度要求极高,高到必须按帧进行单独配置(如上图Ryu的蓝色受击框是逐帧进行配置的)。所以我们也会把NLE的概念用于进行这种帧级别精度要求的角色配置上。

本章开篇图为本人参考多款NLE编辑器所制作出来的FunAction动作编辑器。
有Unity Flux插件经验的人会感觉其与Flux长得非常像,的确Editor方面FunAction是参考Flux的,但两者除了长得像之外,内在思路却完全不一样。
FunAction的概况如下:

  • 最重要的,Action提供Tick()函数,从而一帧一帧地驱动执行
  • 任意角色模型可和任意Action运行时动态绑定。但一旦绑定,规定了1个角色对象有且只有1个Action,1个Action认定只操作1个角色对象
    • 事实上这对传统NLE多对象共存于时间线上来说,是一种退化。但这种退化是满足角色动作编辑的需求的,是合理的。未来如果有时间,在不能给编辑器带来额外操作复杂度的前提下,是可以实现成允许多对象同时编辑的,即一个既可编辑cutscene、也可编辑角色动作的NLE编辑器
  • Action有多个Motion(动作,如idle、attack、hurt等),每个Motion有多个Track(轨道),每个Track和且只和一种BaseEvent的子类(事件类型,如PlayAniamation)绑定,Track可以出现其绑定的事件类型的任意个事件对象。BaseEvent可以让用户重载Enter()、Update(currentFrame)、Exit()等函数,从而实现各种千变万化的功能。
  • BaseEvent的子类除了DurationEvent(样子为长条状)外,还有子类InstantEvent(箭头状)。DurationEvent类似于传统NLE的时间轴对象,有明确的StartFrame、EndFrame;InstantEvent类似,但规定StartFrame和EndFrame必须相同。这是因为在动作游戏中,有许多事件的持续帧数是只有1帧(比如攻击检测等)、或持续帧数是不用限定无法限定的(比如播放特效、播放音效等)
  • Action提供SetMotion()函数,从而切换动作
  • 可自定义序列化、反序列化方式。默认为Protobuf-net,效率比Unity的各种XML、各种JSON序列化方式好多个数量级。开发使用的方式非常简单,以PlayAnimation为例,如下图

  • 每个自定义的Event都可方便地再自定义Inspector的逻辑和画法。示例如下图(留意到PlayAnimation的Inspector自定义实现了自动寻找动画属性的逻辑)

  • 每个自定义的Event都可方便地再自定义在Editor场景绘制额外元素。示例如下图,为ActorHurtBody的受击Capsule(可从AABB/Capsule/OBB间选择),和ActorHitTest的攻击OBB


第三方Gameplay插件



上面这些Gameplay框架的Runtime实现都并非困难。但实现起来,往往大量开发时间消耗在:

  • 提供功能齐全、人性化的Editor和Inspector
  • 实现性能高效、人性化的序列化反序列化

一个好的游戏设计思路,是能让开发者可以重复造轮子、而不是让开发者必须重复造轮子。
让开发者必须重复造轮子是简单粗暴欠妥的,让开发者既能选择重复造轮子、也能选择采用已有第三方插件,反而需要更多对基础框架扩展性的思考。

在Unity Asset Store里有好一些比较不错的Gameplay框架具体实现插件。它们是:

开发者不能有因使用第三方插件而感到“技术性羞耻自卑”的心态。
相反,开发者应该发挥开发的能力去评估一款第三方插件是否优秀,评估的角度包括:

  • 是否满足基本需求
  • 是否开源(这很重要,因为代码即文档、文档不透彻更新不及时、二次修改的可能)
  • 运行性能、反序列化性能
  • 版本迭代、作者、社区是否活跃
  • UI、操作、体验

如果决定应用第三方插件,我们不应该轻易修改它,而是优先去扩展它。
在Unity里,第三方插件(及其他项目无关的通用基础功能),建议都摆放在“Standard Assets”目录里,因其与其他文件夹的脚本是处于不同的两个dll,从而防止普通开发者错误地把具体项目业务逻辑感染进通用逻辑里。
这样子,我们可以通过继承、或者partial、或者extend、或者static function等途径进行第三方插件的扩展。
对于一些重要不紧急的插件修改,可以通过社区和作者进行交流,让其进行修改。比如本人就多次对FlowCanvas/NodeCanvas/BehaviorDesigner的作者交流讨论、提出多项建议(如1、2等),最后被采纳。
如果有必要,我们决定修改第三方插件,我们需要承担从此不能再轻易更新这些插件的后果。
如果我们已大幅修改第三方插件,此时我们可以反问自己:“这第三方插件是否已经太不满足需求了?我们是否应该开始重新造更适合我们的轮子了?”


结语

通过上述Gameplay框架的有机合理组合,能够实现丰富的Gameplay逻辑。

Gameplay框架工具也远不只这些,地形编辑器、Starcraft2的Unit编辑器、技能编辑器,是更进一步、更具体细分的Gameplay编辑器。
也能就上述Gameplay框架进行特例化修改,比如主要用于对话设计的Dialog tree是状态机的一种重要特例化应用。
Utility AI是一种不错的AI思路。相比更“Rule-based”的FSM/BehaviorTree,Utility AI和GOAP相似,更有“Plan-based”的感觉。


Utility AI的Apex实现


如上图,程序写好评分的Node后,策略填填不同Node的分数(Score),就一个不同性格的AI就出来了。你是喜欢近战的路霸,就把“Proximity To Nearest Enemy”的Score调高,你是喜欢直线攻击的76,就把“Line Of Sight To Cloeset”的Score调高。

应注意,没必要为了用工具而用工具,要看需求有否用到。但也要考虑,需求是易变的、市场是易变的、方向是易变的、玩家是不耐心的。要为Gameplay的通用性、扩展性做好准备。

【UE4】GamePlay框架简介(蓝图) 虚幻4中的GamePlay框架简介 阅读详情

相关推荐

Unity构建优化利器:Build Report Tool深度解析与实战指南

在Unity游戏开发中,构建流程的耗时与最终包体大小是影响开发效率和产品发布质量的关键指标。其核心原理在于Unity编辑器将项目资源与代码编译、处理并打包成目标平台可执行文件的过程。这一过程的价值在于,高效的构建能显著提升团队迭代速度,而精简的包体则直接影响用户下载意愿与存储成本。常见的应用场景包括日常开发测试、版本发布前的性能优化以及持续集成流水线中的自动化监控。针对构建耗时的黑盒化与包体虚胖等痛点,Unity Build Report Tool作为一款诊断分析插件,通过提供详尽的构建步骤耗时分析、资源大

weixin_30820077的博客 410

Unity GamePlay浅谈

总的来说Gameplay就是使得游戏好玩的游戏交互。我们Unity 的游戏玩法设计主要就是依据他来进行开发的,涉及以下几个方面:场景设计,角色控制,物理模拟,游戏逻辑,UI设计,音效音乐,网络。

weixin_57287213的博客 1739

Unity团队协作开发实战:从Git分支策略到项目架构设计

在游戏开发领域,版本控制是团队协作的基石,它通过管理代码和资源的变更历史,确保多人并行开发的秩序与效率。其核心原理在于分支管理策略,如GitHub Flow,通过主分支的稳定性与功能分支的灵活性相结合,实现持续集成与快速迭代。这一实践的技术价值在于大幅降低合并冲突风险,提升开发流程的可控性。在Unity引擎的应用场景中,结合文本序列化、程序集定义等特性,能够构建出高效、可维护的协作框架。本文以Unity开源项目为例,深入剖析了包括Git工作流、资源标准化管理在内的实战经验,为中小团队提供了即学即用的协作方案

weixin_30236595的博客 514

【Unity】 GamePlay开发:通用的检查点/成就/条件触发系统

适用于任何游戏的组合事件触发系统

qq_42885747的博客 900

Unity游戏多语言本地化:5分钟搭建自动翻译工作流

本地化(Localization)是游戏开发中实现多语言支持的核心技术,其核心原理是将文本内容与程序逻辑分离,通过键值对(Key-Value)系统进行管理。这项技术的核心价值在于实现文本与代码的解耦,支持运行时动态切换语言,从而显著降低多语言版本的维护成本。在游戏开发、应用软件、Web前端等需要面向全球用户的应用场景中,本地化是提升产品可访问性和市场覆盖度的关键技术。本文聚焦于Unity引擎环境,详细介绍了如何利用其官方Localization Package,结合Google Cloud Translat

weixin_33717117的博客 325

Unity转UE开发必知:C++脚本编译机制与Live Coding热重载实战

在游戏开发中,脚本编译是连接代码逻辑与运行时表现的核心环节。传统C++采用AOT(提前编译)机制,需将源码完整编译为机器码才能执行,这确保了高性能但牺牲了迭代速度。Unity的C#采用JIT(即时编译)或AOT编译,实现了近乎实时的代码重载,支撑了快速原型开发。而Unreal Engine(UE)基于C++的模块化架构,编译时需链接引擎本身,导致传统的“修改-停止-编译-重启”流程,严重打断开发心流。为此,UE引入了Live Coding技术,它允许在编辑器运行时动态重载C++代码,将编译单元从整个模块细化

weixin_30700977的博客 347

AlgoNote · 0564 寻找最近的回文数:五个候选一张表,可直接运行的避坑题解

| 项目 | 内容 | | --- | --- | | 题号 | 0564 寻找最近的回文数 | | 难度 | 困难 | | 标签 | 数学 + 字符串 | | 一句话题意 | 给定数字串 n,找与它差值绝对值最小的回文数(n 本身不算),差值相同取较小的 | | 为什么输入是字符串 | n 为纯数字串、无前导 0,1 ≤ n.length ≤ 18,对应整数范围 [1, 10^18-1];直接做

gitblog_00352的博客 622

虚幻引擎C++开发:从标准语法到引擎核心机制的思维转换与实践指南

C++作为高性能游戏开发的核心语言,其内存管理、对象生命周期和模板机制是构建复杂系统的基础。在游戏引擎领域,这些基础原理被扩展为与编辑器深度集成、支持数据驱动和跨平台运行的特定框架。虚幻引擎(Unreal Engine)的C++开发便是这一技术价值的典型体现,它通过宏系统(如UCLASS、UPROPERTY)、反射机制和独特的对象模型(UObject),重塑了代码的组织与交互方式,极大地提升了开发效率和协作能力。这种“引擎化”的C++广泛应用于3A游戏、实时可视化及虚拟制片等高性能图形应用场景。本文聚焦于*

weixin_34293911的博客 494

Unity脚本未包含在C#工程?系统化解决方案与自动化集成指南

在Unity3D开发中,C#工程文件(.csproj)是IDE进行代码智能感知、导航和重构的基础。其生成机制依赖于Unity编辑器对Assets目录的监控与资产数据库(AssetDatabase)的状态同步。当通过非标准方式(如外部工具、命令行)创建脚本,或遇到文件系统监控延迟、脚本编译错误阻塞时,常导致新脚本未被包含,引发“断链”问题,严重影响开发效率和团队协作。理解Unity的工程文件生成时机(如项目打开、脚本结构变化、手动触发)和程序集定义(.asmdef)的引用逻辑,是解决此类问题的关键。通过系统化

weixin_33726313的博客 314

unity-gameplay-framework:一个简化Unity开发的游戏框架

Unity3D游戏框架 介绍 在Unity场景中产生的所有对象都是Behaviour 。 对于本地游戏,我们使用MonoBehaviour ;对于网络游戏,Unity向我们提供NetworkBehaviour 。 除了那些之外,Unity支持我们的抽象并不多。 我得出的结论是,强大的游戏支持应该是引擎附带的一项功能,因为我自己没有这样做,所以放弃了项目。 我使用了我真正喜欢的,因此希望通过添加从UE4( )中学到的一些东西来改善Unity的游戏层。 但是,现有的Behaviour代码库位于Unity Engine的核心,因此无法删除。 格萨尔 术语 描述 播放器 玩家是真实的人。 请注意,AI(机器人)不是玩家。 参加者 当玩家和AI请求加入游戏时,他们可以成为参与者。 参与者正在参与游戏,并且可以与世界互动。 他们会对比赛的结果产生巨大的影响。 例如,不能与世界互动的玩家观众不是参与

unity-gameplay-ability-system:在Unity中实施能力系统的统一框架

Unity游戏能力系统 Unity的游戏能力系统(简称GAS)是一个Unity框架,用于创建包含任何能力(例如DotA,Skyrim)的游戏。 该框架有助于以统一的方式简化组件之间的交互。 该项目的灵感主要来自虚幻游戏能力系统,并使用类似的术语,但实现特定于Unity。 这里有三个主要的子项目: 该演示包括一个简单的游戏,您可以在该角色上四处奔跑,并通过按鼠标左键来施放Ice Blast功能,并通过按鼠标右键来施放Blood Sacrifice能力。 如果前方有敌人,那么Ice Blast技能会向前方发射弹丸,并伤害敌人(消耗法力值并将其置于冷却状态)。 使用Blood Sacrifice根据剩余百分比恢复法力值,但会消耗生命值(并具有冷却时间)。 有关新项目中的安装步骤,请参考 。 先决条件 Unity 2021.1.1f1(Unity 2020.3也应该起作用) 安装 克隆存储库(

Unity游戏规则引擎实战:数据驱动架构构建生存游戏食物链系统

在游戏开发中,数据驱动架构是一种将游戏逻辑与代码分离的核心设计模式,它通过将规则、属性和行为定义为可配置的数据(如JSON或ScriptableObject),实现游戏系统的灵活配置与动态调整。其技术原理在于构建一个规则引擎,该引擎负责解析和执行这些外部定义的数据,从而将传统的硬编码逻辑转化为可热更新、易维护的模块化系统。这种架构的价值在于大幅提升了开发效率,使策划人员能够独立调整游戏平衡性,同时降低了代码耦合度,便于团队协作与迭代。在应用场景上,数据驱动架构特别适用于需要复杂逻辑和频繁调整的系统,如RPG

chuange6363的博客 448

【Unity Behavior Designer】AI行为树插件 Behavior Designer.zip

Unity Behavior Designer-内含Behavior Designer Movement及Behavior Designer本体 Behavior Designer在Unity商店中为付费插件,所以在此就暂不公开免费,但以极低价售出。还请见谅! Behavior Designer 是专为每个人设计的行为树实现 - 程序员、艺术家、设计师。 Behavior Designer 提供具有强大的 API 的直观可视化编辑器,帮您轻松创建新任务。它还包含数百个任务,PlayMaker 集成和广泛的第三方集成让你无需编写任何代码就能创建复杂 AI! 从设计伊始,Behavior Designer 就旨在尽可能高效,做到初始化后零配置。因此,它在包括移动设备的所有平台上运行都很出色。Behavior Designer 专注于行为树,这意味着每次更新都会有更好的行为树实现。 功能: • 一款直观的可视化编辑器 • 强大的 API 可视化运行时调试器 • 进行任务之间通信的变量 • 条件终止 • 内置在事件系统中 • 使用现有代码,进行反射任务 • 数百项任务

Claude Code + MCP:Unity空项目秒变可玩游戏的工程化实践

Unity开发中,'从零开始实现可玩原型'长期受限于工程语境缺失与手动操作冗余。其本质是自然语言意图与引擎API语义之间的翻译断层。MCP(Model Context Protocol)作为轻量级结构化协议,将LLM输出约束为Unity可安全消费的JSON指令;Claude Code则扮演上下文感知的决策仲裁者,动态解析项目状态、校验依赖、规避Script Execution Order等底层陷阱。该组合不替代设计思维,而是将'让角色动起来''添加金币拾取'等模糊需求,转化为可审计、可重试、可集成CI/CD

weixin_27945229的博客 193

保姆级教程:在NXPS32144环境下安装EB TRESOS 29.0.0并配置MCAL开发环境

本文提供了一份详细的保姆级教程,指导如何在NXP S32K144环境下安装EB TRESOS 29.0.0并配置MCAL开发环境。内容涵盖从前期资源准备、软件安装步骤、核心的MCAL关联配置,到首次创建AUTOSAR工程并进行模块配置的全流程,旨在帮助汽车电子开发者搭建稳定、可用的底层软件开发地基。

wind6的博客 955

通过CRC32爆破修改图片的宽高 ctf-misc图片隐写

** 一.在处理图片隐写题的时候,可能会修改图片原有的宽高,导致图片里面的内容无法正常显示,那么怎么判断它的宽高是否被修改了呢? ** 1.把图片拖进010editor中会发现左下角提示CRC不匹配,这是因为修改了高宽,却没有修改CRC,导致读取报错。 2.在linux中CRC32错误的图片是无法打开的,也可以利用pngcheck检查图片,会提示CRC32校验错误 ** 二.既然知道了CRC校验码错误,那么如何修改呢? 首先我们要知道CRC32是有哪些字节构成的 ** PNG文件中总是以固定的八个字节开

weixin_44145452的博客 1万+

Unity开发效率提升:构建可复用的API工具库与编辑器扩展框架

在Unity游戏开发中,性能优化和编辑器扩展是提升开发效率的核心技术。性能优化涉及对引擎底层机制的深入理解,通过减少Draw Call、优化内存管理、避免GC分配等手段提升运行时效率;编辑器扩展则通过自定义Inspector、自动化工具和构建管线集成,将重复性工作自动化。这些技术的价值在于让开发者从繁琐的底层操作中解放出来,专注于游戏逻辑和创意实现。在实际应用场景中,无论是解决Addressables资源打包后的材质丢失问题,还是适配抖音小游戏等特定平台,一套设计良好的工具包都能提供稳定支持。本文聚焦于如何

chuange6363的博客 414

STM32F103中GPIO_Remap1_CAN1与GPIO_Remap2_CAN1的配置差异与应用场景解析

本文深入解析了STM32F103中GPIO_Remap1_CAN1与GPIO_Remap2_CAN1的配置差异与应用场景。通过剖析AFIO_MAPR寄存器,明确了两者在引脚映射(PB8/PB9 vs PD0/PD1)和适用封装上的区别,并提供了详细的配置步骤、常见陷阱排查及基于项目需求的选型指南,帮助开发者高效完成CAN引脚重映射。

u6v7w8x的博客 394

Unity、Unreal、Godot三大游戏引擎核心技术对比与选型实战

游戏引擎是现代游戏开发的核心框架,它封装了渲染、物理、音频等底层系统,为开发者提供高效的内容创作工具链。其核心原理在于通过抽象硬件接口,管理资源生命周期,并执行游戏循环,驱动逻辑与画面的更新。引擎的技术价值在于大幅降低了交互式实时图形应用的开发门槛,使团队能聚焦于创意实现而非底层细节。在应用场景上,从移动端休闲游戏到3A级主机大作,再到影视虚拟制片和工业仿真,引擎技术已渗透至数字创作的各个领域。本文聚焦于Unity、Unreal Engine和Godot三大主流引擎,通过对比分析其渲染管线、物理模拟、脚本系

weixin_30700099的博客 338

Winform从入门到精通(7)——ComboBox(史上最全)

本文介绍ComboBox控件的详细用法,包括所有属性、事件,绝对史上最全!

qq_34059233的博客 2万+

[QMT量化交易小白入门]-八十九、深度学习训练多维特征进行金融价格行情预测

本专栏主要是介绍QMT的基础用法,常见函数,写策略的方法,也会分享一些量化交易的思路,大概会写100篇左右。QMT的相关资料较少,在使用过程中不断的摸索,遇到了一些问题,记录下来和大家一起沟通,共同进步。

木头大左的博客 218

Unity游戏开发:基于ScriptableObject的数据驱动背包系统设计与实现

在游戏开发中,数据驱动设计是一种将游戏逻辑与具体数据分离的架构思想,其核心原理是通过外部数据资产(而非硬编码)来定义游戏行为,从而实现高度的可配置性和可维护性。这种模式的技术价值在于,它允许策划人员独立调整游戏内容,而无需程序员修改代码,极大地提升了开发效率和迭代速度。在Unity引擎中,ScriptableObject是实现数据驱动的关键技术组件,它允许开发者创建可在编辑器中配置、运行时高效引用的数据资产。这一特性使其非常适合应用于需要管理大量可变数据的场景,例如角色属性、对话树以及本文聚焦的背包系统。一

weixin_30824599的博客 360

5、卡尔ibration中Denavit - Hartenberg与Hayati约定的转换

本文详细介绍了机器人运动学中Denavit-Hartenberg(DH)参数与Hayati参数的定义、适用场景及其相互转换方法。重点分析了两种参数在描述关节坐标系变换时的差异,特别是在处理平行或接近平行关节轴时的稳定性问题,并给出了从DH到Hayati及反向转换的具体数学公式与流程图。通过参数转换,可在不改变运动学模型的前提下提升建模精度与计算鲁棒性,适用于机器人标定与运动学分析等应用场景。

h0i1j2k3l的博客 121

Luban游戏配置解决方案:跨平台数据管理与代码生成终极指南

Luban是一个强大、易用、优雅且稳定的游戏配置解决方案,专为游戏开发团队设计。无论您是独立开发者还是大型游戏工作室,Luban都能提供从数据定义到代码生成的全流程自动化工具链,显著提升游戏配置开发效率。 ## 🎯 为什么选择Luban游戏配置工具? 在游戏开发中,配置管理是连接策划与程序的关键桥梁。传统的手动配置方式不仅效率低下,还容易出错。Luban通过标准化的游戏配置开发工作流,彻底改

gitblog_00989的博客 871

华为OD最新机试真题-池化资源共享

摘要:该问题要求管理n台设备的资源池,处理资源申请和释放操作。申请操作需返回满足条件且剩余资源最少的设备ID,释放操作需将指定申请的资源归还。采用线段树数据结构维护各设备当前最大可用资源值,支持快速查询和更新。线段树实现了单点修改、区间最大值查询以及查找最左满足条件的节点功能。算法时间复杂度为O((n+m)logn),空间复杂度O(n),适用于大规模操作。提供了C++、Java和Python三种实现方案。

面试高手的博客 324

STM32CubeIDE开发环境搭建与GPIO控制实战

嵌入式开发中,MCU开发环境构建是软硬件协同的起点,其核心在于工具链选型、外设抽象层级与调试可靠性。HAL库作为ST官方标准硬件抽象层,通过统一接口屏蔽芯片差异,显著提升工程可移植性;而STM32CubeIDE则以图形化配置+自动生成代码的方式,实现‘配置即代码’的确定性开发范式。该组合在教学验证、原型迭代和中小规模量产中具备高性价比技术价值,广泛应用于智能传感、工业HMI及IoT终端等场景。本文聚焦STM32CubeIDE 1.15.0环境部署、MyST账户与固件包管理、ST-LINK驱动校验,以及基于H

weixin_33531560的博客 79
上一篇: Unity3D控制动画播放
下一篇: Unity Shader-渲染队列,ZTest,ZWrite,Early-Z
puppet_master
puppet_master 领域专家: C/C++技术领域 领域专家: C/C++技术领域
博客等级 码龄12年 2695粉丝 161原创
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值