二.架构设计、构建游戏世界
1.面向对象的游戏世界
1.可交互的动态物
2.默默的静态物
3.地形系统,后面会详细介绍,地形生成、隧道、植物、道路、贴花
4.天空系统,天气变化、大气、TOD(Time of the Day日夜变换系统)
5.动靜态物体统称为游戏对象Game Object (GO),大地和天空独立出去。
6.按照面向对象的类,去设计
每个GO的信息分为两类,属性、行为。
还有派生、继承。
7.问题:
随着游戏世界越来越复杂,有些东西它并没有那么清晰的父子关系。
2.组件化
由早期的面向对象,转变为“组件化”。
组合优于继承。
Unity、UE,他们都会去提供组件的概念,如果大家打开Unity点开任何一个物体,都会发现下面出现各个组件,而且这个组件你可以增加,定义,甚至自定义化它的小脚本。
2-1.现代游戏引擎的设计概念
第一,游戏世界里面几乎把所有的东西抽象成了游戏对象Game Object (GO)这样一个东西。
第二,每个GO用各种各样多功能的组件把它组合起来,所以各种组件又是游戏对象的原子;
3.让游戏世界动起来
1.tick的设计
(1)一般不是按照每个对象进行 Tick 的,我们会把一个个系统里面的全部组件进行Tick
(2)像工业流水线;
汉堡店5个工人,每个人先去分别烤面包,烤牛肉,洗蔬菜,然后放上黄油在一起,最后五个各式各样的汉堡就做出来了,但这样的生产效率显然是不高的。
现代工业核心的概念叫流水线,最高效的做法是有人专门去烤面包,有人专门去烤牛肉,有人专门去洗菜,大家配合好,然后到了时间,一起合成一个汉堡,这样的效率是最高的。
(3)senseCube使用的aframe框架也是分component、system,整个应用tick来处理每一个system的tick,处理该system下的所有组件;
(4)实体-组件-系统(ECS)架构。
4.事件与事件机制
1.把各个游戏对象之间的通讯,统一的变成了事件机制。你只要发出一个事件给对应的游戏对象,让它来处理就可以了。各个对象和对象内部的组件逻辑就解耦和了,非常的干净。
2.unity和UE都是这样的机制
(1)Unity
unity中简单注册一个事件,然后发送事件,最后销毁事件。如果物体的核心组件收到了这个事件后,有一个回调函数会激活,然后做相应的处理。这一切只需要一个字符串符合就可以了。
gameObject.SendMessage("eventName", data);
(2)UE,过于复杂
用C++源码去处理比较复杂的反射机制。当你要注册这样的一个事件,当这个事件发生的时候,每个组件哪个函数被毁掉,基本道理都是大同小异的。
DECLARE_EVENT 前缀
DECLARE_EVENT_OneParam
DECLARE_EVENT_TwoParam
DECLARE_EVENT_ThreeParam
(3)总之要设计成一个可扩展的消息系统,让游戏开发者可以在我们的引擎之上不断的定制玩法和相关的各种各样的消息类型,然后他们可以定制各种各样自己想要的组件去对这些消息,
5.场景管理--分而治之
1.背景、问题:
游戏引擎经常讲的N平方的挑战,什么意思?
每一个物体都可能和其他的物体发生互动,如果每一次我都要和其余的所有对象去问一遍,那就是nx(n-1)≈n²。
如果我有1万个物体的话,n²是一个亿,这样会对计算机造成巨大的负载,尤其如果这些数据分散在内存的各个地方的话,效率是非常低的。
2.分而治之法
简单最直觉的做法的方法就是对世界画格子,分而治之法(Divide and conquer)。
3.均分
把场景均匀的平分,但是当场景分布不均匀的时候,这样又不够高效;
4.用层级结构来管理场景
(1)思想非常简单,就像世界地图一样,当我们把世界分成国家,国家分成行省,行省分成城市,城市分成街区,区再分成街道;如果有任意一件事件发生,比如北京海淀区的某一条大街发生的事情,我只需要在北京海淀区里面去找就可以了。
1.四叉树
(1)典型的四叉树
根据地图上的对象分布以空间的四叉树不断的进行划分。如果这个地方的人数足够少,只有一个到两个,那就不用再划分了。如果人数很多,我就不停的划分,这样就形成了一个树状结构。
(2)JS轻量级四叉树实现:github.com/timohausman…
2.二叉树:
(1)最简单的二叉树的流派,我们把世界一刀一刀地切分掉,那把二叉树的划分往三维做得更精致一点,那么就变成了经典的八叉树。
(2)八叉树是三维空间的“二叉树”,2^3=8,二分查找效率高。
3.八叉树,BVH(bounding Volume Hierachies)层次包围盒
(1)现在游戏引擎比较流行的是BVH,也就是我们常用的Bounding Box,每一个物体有一个小bonding box,我们把它从小往大慢慢的做一些计算;
(2)比如说最常见的视锥剔除,用BVH快速的把很多东西全部扔掉,当我们打出一条子弹,一个弹道的时候,其实我们也需要用这样的技术去帮我们去快速定位。
(3)一般游戏用四叉树或者BVH就可以满足;
6.时序一致性
(1)背景:
在实际游戏中,当我们去Tick一个组件的时候,很多时候是要分散到多CPU上去执行的,很多的Tick是并行执行的,所以并行执行的时序是非常重要的。
(2)悖论问题
举个例子,在大学跟女朋友分手的时候,当你兴冲冲的写了一封分手信,打算投送到你女朋友寝室的时候,突然发现你的女朋友也急匆匆穿好衣服从寝室出来,手上拿着另外一封分手信。
你们两是并行的。你那时候是不是马上在怀疑那到底是你甩了你女朋友还是女朋友甩了你?这件事情就讲不清楚了,对不对?
(3)引入“邮局”的概念;事件总线
你要给你女朋友写信,你先把信寄到邮局去,然后邮局在统一时间把信发到每一个人,每个人第二天能收到自己的信。
然后你第二天收到这个信后再决定下一步的工作,比如你女朋友看到你的这封分手信很生气,打算写一封信来骂你,她也只能当天晚上写完然后寄到邮局,第二天早上你才能收到。
这样的话,我们就能确保它的时序是严格一致的,所以在游戏引擎中真实的消息传递比大家想象的要复杂的多.
(4)更完善的tick
实际在游戏引擎中Tick实现还有更复杂的,比如Pre Tick和Post Tick这两个函数, 就是为了解决各种各样的持续性的问题。
(5)循环依赖
真实的游戏引擎开发的时候,会经常遇到它们彼此之间好像有那么一点点循环依赖。
很多游戏如果写得不够好的时候,会经常发现它有大概一帧到两帧的延迟,这种延迟很多时候就是因为时序问题导致的。