学习Unity极具扩展性的战斗系统——Buff机制

[收藏]用Unity制作一个极具扩展性的顶视角射击游戏战斗系统 - 知乎 ->原文地址

oop:不同类要做同一件事情用接口,同一个类做同一件事情不同效果用委托。

函数式编程的思维方式 MBF

代码调用位置

在Unity中渲染层使用Update()和Time.deltaTime,逻辑层使用FixedUpdate()和Time.fixedDeltaTime。

Model、Obj和Info

Model 静态数据。通常来自策划设计的数据
Obj 在Unity的框架下,可以理解成GameObject,比如子弹的预制体。
Model是用来初始化GameObject的初始数据!
Info 逻辑数据,在Unity中它以class形式存在。

Model和Obj的区别

如果把“玩家角色”、“npc”、“怪物”、“宠物”分成4个不同的类(哪怕派生自角色类),这个抽象都是错误的!因为角色的Object运作在游戏逻辑世界里,是等价的!

20241201函数编程学习笔记 - 图1

所以Model和Obj是两个完全不同的东西,区分好哪些数据是Model里的,哪些数据是Obj里的,是一个非常非常重要的游戏设计工作!因为这会定义整个游戏的元素。

比如技能等级,通常来说是一个SkillObj的数据,而非SkillModel的数据,如果每一级技能是一个SkillModel,那说明这个游戏中不存在“技能升级系统”只有“技能替换系统”。

角色的属性,就是最典型的Obj数据,而非Model数据,因为这是玩家自己玩出来的结果,而不是策划预设好的。

Info的作用

Info是一些逻辑业务所依赖的数据,比如我们创建一个buff,就需要一个AddBuffInfo,来告诉系统我要创建的是哪个buff,多少层等等,哪个buff就是BuffModel中的数据,也就是一个Buff的模板;而“多少层”,其实是一个从BuffModel变成BuffObj才有的数据,但是初始化的时候应该给多少层呢?这是添加buff的系统最关心的问题,因此我们才有了AddBuffInfo作为“中间件”来完成这个过程。

伤害信息DamageInfo

伤害处理流程图

20241201函数编程学习笔记 - 图2

伤害信息的组成数据元素

  • attacker:伤害来源
  • defender:受伤的角色
  • tags:伤害生效范围类型标签。比如:直接伤害、间接伤害、反弹伤害。不要和伤害数值类型(火伤、冰伤等)混淆了。
  • damage:伤害数据结构
    • 伤害数值类型:元素伤害、暗影伤害等
    • 伤害数值
  • damageDegree:伤害的角度。也就是伤害打向defender的入射角度,通常这个角度来源是取自子弹的飞行方向或者aoe中心点指向defender的矢量角度
  • criticalRate:本次攻击的最终暴击率
  • hitRate:命中率
  • addBuffs:击中后附带的buff信息。生效在BuffOnHit、BuffBeHurt、BuffOnKill、BuffBeKilled中。

定义一次攻击到底是个什么样的事情,具体的事情——我们通常会认为“一次攻击”还不好理解吗?但实际上确实不好理解,因为“一次攻击”只是一个概括,什么都是也什么都不是,在我们实际开发工作中,定义好DamageInfo和DamageManager(也就是伤害流程),才是完整的游戏伤害系统设计。

角色 Character——视觉依赖于逻辑

整个CharacterObj看起来是“空”的,因为没有图像。通常我们可能会对Unity的设计有一个误会,认为GameObject就是存在于游戏中的一个模型、或者一个被渲染出来的东西、或者说是一个元素,反正是因为有了这么一个元素,然后给他绑了一些关于这个东西的属性——比如一个角色,他应该是一个小人,然后给这个小人绑上属性、buff等等的东西。但实际上不是这么回事儿,因为GameObject,在Unity的逻辑下,他是Scene下的一个数据,Unity的IDE,把这个数据渲染到了他的编辑器上,这并不代表这个数据就是“有形的”,所以空的GameObject丢在Scene,如果有Render控件,他才会被渲染,这个逻辑其实是对的、是符合数据驱动的、也是符合视觉依赖于逻辑——这条游戏开发铁律的。

Unity中的角色Obj结构

20241201函数编程学习笔记 - 图3

  • CharacterObj:根节点
    • ViewContainer:角色模型节点
    • PieChart:角色其它信息节点,比如血条等

时间轴Timeline

Timeline是一个时间轴,在这个时间轴上由若干个节点,每个节点都有一个时间点,即从timeline开始运作后多久;然后是其执行的事件——时间点和事件形成的数据组成了数组就是Timeline的基本逻辑。

Timeline的数据结构

20241201函数编程学习笔记 - 图4

技能Skill

释放技能的流程图

技能是绝大多数游戏中角色的核心元素之一,尤其是对于ARPG下任何分类的游戏都是如此。我们通常见过的绝大多数的技能框架,都会错误地把技能的效果抽象为技能的属性,所以最后技能看起来是一个非常庞大的系统。但实际上,真正的技能系统,无非就是一个简单的“发射器”,他做的一切就是:

20241201函数编程学习笔记 - 图5

比如在游戏中有一个火球魔法,我们释放技能,首先就是检查我们的资源是否足够释放,或者说是否学会了这个火球魔法,总之一系列检查是否能放;通过之后,就扣除对应的资源,并且创建了一个Timeline,这个Timeline做了几件事情:

  • 0秒时角色开始播放吟唱动作(低头念咒)
  • 1.5秒时角色开始播放施法动作(手一伸,指向目标)
  • 1.5秒时创建一个子弹(火球)飞向目标

技能的数据结构

20241201函数编程学习笔记 - 图6

Buff

Buff是整个技能体系的核心存在元素。在任何游戏的任何系统中,buff都是一个非常核心的元素,它更像是一个标记。如果把Buff单纯的理解为一个角色属性的变化器,或者一个简单的Modifer,这样的抽象都是错误的。

Buff的数据结构

20241201函数编程学习笔记 - 图7

Buff的“回调点”

回调点名称 说明 代码定义
OnOccur 在BuffObj被创建、或者已经存在的BuffObj层数发生变化(但结果并不小于等于0)的时候会触发 public delegate void BuffOnOccur(BuffObj buff, int modStack);
OnRemoved 在一个buff因为生命周期结束,或者层数<=0的时候,他要被移除掉之前会触发 public delegate bool BuffOnRemoved(BuffObj buff, GameObject dispeller);
OnTick 这是最常见的buff效果“每一跳”的回调点,我们通常所说的“间歇性效果” public delegate void BuffOnTick(BuffObj buff)
OnCast 这是在角色释放技能的时候发生的回调 public delegate TimelineObj BuffOnCast(BuffObj buff, SkillObj skill, TimelineObj skillEffect);
OnHit 这是技能在“命中”时候执行的一个回调点,此处的“命中”并不是最终结果的命中的意思,他更像是“碰撞到了”的意思,也就是当有一个DamageInfo的时候就有一次“命中”了 public delegate void BuffOnHit(BuffObj buff, ref DamageInfo damageInfo, GameObject target);
OnBeHurt 与OnHit相呼应的是,这是受到攻击时候触发的逻辑,如果说OnHit是主动攻击的效果,那么BeHurt就是被动挨打时候的效果 public delegate void BuffOnBeHurt(BuffObj buff, ref DamageInfo damageInfo, GameObject attacker);
OnKill 在确定会击败对手的时候执行的回调 public delegate void BuffOnKill(BuffObj buff, DamageInfo damageInfo, GameObject target);
OnBeKilled 在确定会被击败之后执行的回调 public delegate void BuffOnBeKilled(BuffObj buff, DamageInfo damageInfo, GameObject attacker);

子弹Bullet

子弹的数据结构

20241201函数编程学习笔记 - 图8

Area Of Effect(AoE)

AoE通常的理解是游戏中的范围性技能,这个理解出现于《魔兽世界》,玩家把类似暴风雪之类能对一个范围内所有敌人造成伤害的技能称为AoE。在实际的游戏开发中,AoE更像是一个范围捕捉器,他的作用是捕捉一个范围内所有符合条件的内容,并执行对应的回调函数,起到一个“批管理作用”。