Unity《ATD》塔防RPG类3D游戏架构设计(二)

《ATD》 游戏逻辑


先说明一下,全局游戏逻辑的全局并不是指变量的全局暴露,而是说负责游戏世界的整体逻辑。
全局游戏逻辑设计的话相对轻松一点:

  1. 首先为了更好管理个体游戏对象,引入了 对象工厂 来控制个体有对象的生命周期。
  2. 金钱管理器 负责玩家的金钱数据管理,例如击杀奖励,关卡结算奖励。
  3. 塔管理器 负责用规则限制塔的逻辑,例如建造一个塔的位置限制,建造塔的金钱消耗。
  4. 关卡管理器 负责生成每波怪物。

为了辅助这些逻辑,还额外引入了消息系统组件路径管理器怪物生成器三个脚本。

构造如下:

《ATD》游戏对象目录设置:

引入消息系统是为了让游戏逻辑可以监听个体对象之间的交互消息,从而做出一些符合游戏逻辑的行为。
例如,监听到基地个体对象死亡的消息,应判断游戏失败。

游戏逻辑比较多脚本都需要读入配置文件数据的功能,方便动态更新游戏。

此外,脚本应在Inspector面板应提供一些可调的逻辑参数,方便调试全局逻辑(例如金钱数调99999999)。

《ATD》 消息系统组件实现


观察者模式

观察者模式 是一个常见的设计模式,其定义对象间一种一对多的依赖关系,使得每当一个对象改变状态,则所有依赖它的对象都会得到通知并自动更新。有关该设计模式的更具体内容,本文就不多讲述。

在《ATD》里, 消息系统组件 接受任何 消息中心 转发的消息。
各个依赖 消息系统组件 的模块需要在 消息系统组件 订阅自己关心的消息类型(注册委托)。
任何地方都可以向 消息中心发出消息,消息中心 接着再转发该消息给各个 消息系统组件 ,接着 消息系统组件 根据消息类型,执行关心该类型的已注册委托对象。

实际上这个设计模式很重要,常常用于UI与逻辑的交互。
而在《ATD》里,被改造成一个新的消息机制,用于模型之间的交互和模型与全局逻辑之间的交互。

《ATD》的核心思想是:一切基于消息驱动。攻击是给敌人对象发送一个攻击类型的消息,上Buff是给自己这个个体对象发送一个Buff类型的消息...几乎每一个行为都是通过消息来驱动的。这在以后做高度定制的成就系统更是有潜在的帮助,毕竟成就系统也订阅其中几个感兴趣的类型消息即可获取想要的数据,而不会造成更多的耦合。

对象死亡解引用

在实际的实现中,发现个体对象死亡而引发的引用丢失问题非常多。
一个解决方法是:依赖个体对象引用的代码都需要使用一个 消息系统组件 从而对死亡类型的消息进行监听,当听到自己依赖的对象死亡时,则立即解除引用。这方法工作的很好。

《ATD》 对象工厂实现


工厂模式

工厂模式 是一个常见的设计模式:工厂往往是一个全局单例,用来管理对象的生命周期。

不过在《ATD》项目里, 对象工厂 的职责是:管理所有个体对象。

但需要生成个体对象时,必须使用 对象工厂 提供的生成对象接口。

至于销毁个体对象,一个要注意的问题是,游戏对象销毁和个体死亡是两种不同的概念:
一个个体对象受到伤害,血量低于0时,即可被判定为个体死亡,然而由于游戏效果需要保留尸体(例如用作死亡动画),所以此时游戏对象不应被销毁。除非直到该游戏对象的控制器组件认为该销毁游戏对象。

也就是说当个体组件死亡时,这个个体游戏对象不应存在于游戏逻辑中,而是相当于变成了一个游戏场景的摆设物。

所以 对象工厂 应该至少有两个存储容器:
一个存储表示所有个体对象,另一个存储表示个体存活的个体对象。

  • 个体对象被判定个体死亡时,对象工厂 应该注销该个体的存在。
  • 个体对象被判定为游戏对象销毁时, 对象工厂 应该销毁该游戏对象。

查询优化

前面说到 对象工厂 至少使用两个容器的原因,实际上还有另一个原因是游戏逻辑有很多需要查询游戏个体的操作。
而仅使用存储对象的容器是不够优的,因为很可能遍历到一些个体死亡而对象存在的个体对象,浪费效率。

实际上,《ATD》的 对象工厂 还专门用第三个容器来表示存活怪物对象,这是因为许多塔的行为树攻击行为都需要遍历所有怪物个体对象,而不需要遍历到其他个体对象。

额外:说到查询,就不得不提一下 世界查询器,它是一个全局单例类,职责是提供查询接口,例如:

  • 实现爆炸效果,需要查询某点方圆半径10米的所有对象,从而对查询的每个对象造成爆炸影响。
  • 指向性定位目标对象,查询某点发出一条射线碰到的第一个对象,并定位之。
  • 由于某个区域内发生警报,需要查询该区域内的所有对象来逐个通知。

实际上由于急于实现,《ATD》的对象工厂的实现包含了简单的世界查询器的功能。
在以后的扩展,最好这两者需要分离开,对象工厂只负责对象的生命周期,而世界查询器作为一个辅助工具,内维护各种数据结构以加速查询。

lazy delete

当一个个体对象向 对象工厂 请求摧毁该对象本身时, 对象工厂 并不立即Destroy该对象,而是将其SetActive(false),并添加到死亡对象列表。

对象工厂 接到一个新的个体对象构造请求时,若死亡列表有对象,从死亡对象列表中选一个个体对象进行属性的覆写,然后再将其SetActive(true);若死亡列表为空,才使用生成函数,真正生成一个新的个体对象。

这是个常见的操作,通过属性的覆写就能“生成”一个新的对象,可以极大的减少new/Destory对象的开销(特别是在这个塔防游戏里,个体对象的生成/死亡十分频繁)。

《ATD》 Buff系统组件实现


基本实现

Buff系统组件 是属于个体游戏对象的一种组件类,它负责容纳Buff对象,并计算这些Buff对象对个体属性造成的影响。

前篇说到,当Buff对象生成时,应造成个体属性的一次改变(Buff生效影响);当Buff对象销毁时,再造成个体属性的一次改变(Buff失效影响)。

对于一般的整形/浮点属性数值的影响,直接加减属性即可。
而对于布尔属性数值的影响,往往需要额外维护一个计数,当计数为0时视为false,当计数为1或以上则视为true。

计算顺序

一开始, Buff系统组件 每帧的计算函数,大概内容顺序:

  1. Update: 处理Buff消息后,根据消息添加Buff,然后计算一次生效影响。特别地,若已有Buff对象与待添加的Buff是同种ID,则对已有Buff对象进行生命期的叠加。
  2. Update: 减少各个Buff对象的生命期(减去一帧或者一帧的时间)。
  3. LateUpdate: 判断各个Buff对象的生命期是否结束,若结束则移除Buff对象,并计算一次失效影响。

这段逻辑看似正常,然而在某种特殊情况可能会造成不好的性能影响:光环类型的技能

这种技能会每帧向方圆范围一定距离的个体对象发送光环对应的Buff消息,而这种Buff生命期只有一帧。
倘若按照上图的顺序执行,一个一直停留在光环范围内的个体对象竟然在1s内重复生成和释放一个Buff对象60~80次(可以思考下为什么)。

于是,为了解决这个问题,换成了下图这种执行顺序,很好的解决了:

《ATD》 UI/HUD/特效/音乐


应为UI/HUD/特效/BGM各自编写一个 UI管理器/HUD管理器/特效管理器/音乐管理器
一是方便管理显示,二是更好的与游戏逻辑/游戏模型来交互。

然后也要为这些管理器引入 消息系统组件 用于辅助,从而接受一些重要的消息来改变显示效果。
举个例子,Buff特效管理器,通过监听游戏模型的Buff消息,来给对应的游戏模型生成Buff特效对象。

此时,项目整体架构关系如图:

是不是感觉有点像MVC视图?(笑

《ATD》 日志调试工具


当项目变得庞大起来时,各种Debug消息在Console喷涌而出。当你在Debug的时候,我保证你根本不会想看到一堆无关的Debug消息。

亲身体验过,运行团队项目,弹出一大堆别人写的Debug.log消息,才有了编写日志工具的想法。

一个日志调试工具是必不可少的:
在你想输出Debug信息时,你需要指定它的类型。
当你查看调试信息时,你可以在面板里勾选你关注的Debug信息类型。

《ATD》的日志工具:

结语


Unity《ATD》塔防RPG类3D游戏架构设计系列博文就到此结束,就两篇不多,懒得再写了。
博主本人一年半前才开始接触Unity,学术浅薄,有关知识仍不多,文章很多不成熟的解决方案或者错误,请多多指出。

不过博主的Unity之路也可能到此画上一个句号,因为博主对C++语言情有独钟,再加之偏向开发PC端主机端大型游戏的发展规划,因此以后就要踏上UE4的征途了(实际上正在学UE4)。

GitHub - ima-games/ATD: Unity RPG+塔防3D游戏

第一篇:Unity《ATD》塔防RPG类3D游戏架构设计(一) - KillerAery - 博客园

posted @ 2019-07-17 02:05  KillerAery  阅读(4007)  评论(0编辑  收藏  举报