尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

[简化版 GAMES 104] 现代游戏引擎 04:从0到1构建你的游戏世界

[简化版 GAMES 104] 现代游戏引擎 04:从0到1构建你的游戏世界 [简化版 GAMES 104] 现代游戏引擎 04从0到1构建你的游戏世界✨ 写在前面小明的困惑 第一章万物皆GO——游戏世界的统一抽象1.1 先拆个游戏世界看看里面有啥 1.2 核心方法论Everything is GO 第二章组件化大法好——告别继承的噩梦2.1 早期方案面向对象继承 2.2 致命缺陷钻石继承难题 2.3 现代方案组件化Component 2.4 代码层面怎么实现 2.5 商业引擎都是这么干的 2.6 继承 vs 组件化 大比拼 ⏱️ 第三章Tick循环——让世界活起来的秘密3.1 Tick是什么 3.2 直觉方案Per Object Tick 3.3 反直觉但更高效Per System Tick 3.4 流水线的魔力 3.5 两种Tick模式对比 第四章事件机制——解耦交互的优雅方案1 问题来了GO之间怎么交互 4.2 原始方案硬编码Hard Code 4.3 优雅方案Event事件机制 4.4 这就叫解耦 ✂️4.5 商业引擎怎么实现 第五章场景管理——N²灾难的破局之道5.1 新问题GO太多了怎么办 5.2 基础管理UID 位置 5.3 最朴素方案全遍历 ❌5.4 N平方的噩梦 5.5 分而治之画格子 5.6 进阶方案层级空间树 5.7 常见的空间数据结构 总结四大核心心法速速收藏 写在最后✨ 写在前面小明的困惑哈喽各位技术小伙伴们 今天咱们来聊一个超级硬核但又贼有意思的话题——如何构建一个真正的游戏世界。话说上节课我们聪明好学又蜜汁自信的小明同学已经掌握了游戏引擎的五层架构从最底层的硬件平台层到核心层、资源层、功能层再到最上层的工具层整个大厦的结构门儿清。️但是重点来了——知道大厦长啥样不等于知道大厦里的砖石水电是怎么一起work的啊就像你背熟了汽车的构造图不代表你会造发动机对吧所以今天这节课含金量直接拉满咱们就来扒一扒游戏世界到底是怎么从一堆代码变成能玩能嗨的虚拟世界的 第一章万物皆GO——游戏世界的统一抽象1.1 先拆个游戏世界看看里面有啥 小明玩了《战地2042》觉得好玩是好玩但bug多到离谱一拍大腿爷自己做一个 那首先得想想这个战争游戏世界里都有啥第一类最吸睛的动态物 Dynamic Game Objects 地上跑的坦克、天上飞的无人机、端着枪冲的NPC小兵、火炮、导弹……这些玩意儿能动能交互甚至你能直接上去操作是大家最容易注意到的明星选手。第二类默默奉献的静态物 高高的瞭望塔、飞机的机棚、各种房子……它们安安静静待在那儿虽然不能交互但构成了游戏的关键玩法元素属于幕后英雄。第三类无处不在的环境系统 地形系统无限绵延的大地是所有物体的托盘独立系统后面单独讲天空系统TODTime of Day日夜变换阴晴云雨云卷云舒看着简单做起来贼复杂植被系统最神奇的存在说它是静态吧风吹会摇、炸弹炸了会倒说它是动态吧又固定在那儿。而且量特别特别大属于环境界的卷王第四类你看不见但无处不在的隐形GO 这就有意思了游戏里很多东西你根本看不见但缺了它们真不行Trigger Box检测体走到夺奇点积分就涨就是它在干活空气墙玩家最恨的东西没有之一开飞机飞得正嗨突然提示你已飞出作战区域5秒后击落就是它搞的鬼游戏规则没错连玩法规则本身都可以抽象成一个物体1.2 核心方法论Everything is GO 划重点在现代游戏引擎中无论你是静态的、动态的、看得见的、看不见的甚至连游戏规则本身都会被统一抽象成一个东西——Game Object简称GO。注意啊这个GO和Go语言、围棋的Go都不是一回事别搞混了~ 所以构建游戏世界的本质说白了就是管理好这一大堆GO。就这么简单对就这么简单但简单的概念背后是一整套精妙的设计哲学。 第二章组件化大法好——告别继承的噩梦2.1 早期方案面向对象继承 咱们先从最朴素的想法说起。描述一个物体无非就是两类东西属性Property外形、位置、血量、油量、电量……就是数据嘛行为Behavior移动、巡逻、攻击……就是函数嘛于是乎早期的游戏引擎很自然地就用了面向对象类继承的思路。比如定义一个Drone无人机类classDrone{// 属性Vector3 position;// 位置floathealth;// 血量 0-100floatbattery;// 电量 0-1// 行为voidMove();// 移动voidPatrol();// 巡逻};想做个查打一体无人机简单继承一下加个攻击行为和弹药属性classCombatDrone:publicDrone{intammo;// 弹药量voidAttack();// 攻击};看上去很完美对不对非常直觉非常符合人类对世界的认知。早期游戏还真就是这么写的。2.2 致命缺陷钻石继承难题 但是随着游戏世界越来越复杂问题就来了——很多物体根本没有清晰的父子关系全是混血儿啊举个经典例子水陆两栖坦克。坦克派生于车辆船派生于船舶那水陆两栖坦克它爹到底是坦克还是船它爷爷到底是车辆还是船舶这就是经典的钻石继承问题不仅游戏引擎有现代编程语言里也是个老大难问题。2.3 现代方案组件化Component 怎么解有一个非常经典、也是现在引擎最常用的方法——组件化啥意思呢就是把对象拆分成一个个独立的组件就像乐高积木一样。你看那个玩具挖土机把铲子换成压路机的滚筒它就变成压路机了换成推土机的推板它就变成推土机了。同一个基础底盘配上不同组件就能变出各种花样~ 这思想其实大家天天见玩射击游戏自定义枪械的时候加个三倍镜就是狙击枪加个消音器就是微冲这不就是组件化嘛核心思想把物体的各种能力拆成独立、可插拔的组件一个GO可以自由增删组件来实现不同功能。咱们用无人机来重新理解一下Transform组件管空间位置、位移“我在哪儿”Model组件管模型外形“我长啥样”Motor组件管移动属性最快速度、加速度、惯性“我能怎么动”Health组件管血量“我有多抗揍”AI组件管行为逻辑巡逻、追踪“我要干啥”Animation组件管动画“我怎么动才好看”Physics组件管物理碰撞“我撞了会咋样”把这些组件拼到一起哎~ 一架无人机就出来了想做查打一体无人机简单把AI组件换成AttackAI再加个Combat战斗组件齐活✨2.4 代码层面怎么实现 说穿了也很简单定义一个ComponentBase基类统一所有组件的基础接口必须包含一个tick()函数后面会讲为啥Transform、Model、Animation等所有功能组件都继承自这个基类一个GO里面管理一组各种各样的组件大家协同工作// 组件基类classComponentBase{public:virtualvoidTick(floatdeltaTime)0;// 每个组件必须实现Tickvirtual~ComponentBase()default;};// 具体组件classTransformComponent:publicComponentBase{/* ... */};classModelComponent:publicComponentBase{/* ... */};classHealthComponent:publicComponentBase{/* ... */};// 游戏对象classGameObject{std::vectorComponentBase*components;// 一组组件public:voidTick(floatdeltaTime){for(auto*comp:components){comp-Tick(deltaTime);}}};2.5 商业引擎都是这么干的 你去玩Unity、Unreal这些商业引擎会发现它们全是这个路子Unity点开任何一个物体下面就是一长串组件还能自己加组件、写脚本UnrealActor就对应咱们说的GO概念里面也是各种组件注意区分Unreal里的UObject不是咱们说的GO哦UObject更像C#里的object是用来管理生命周期、内存释放、GC的基类是个底层句柄。真正对应GO概念的是Actor派生的那一系列东西别搞混了~2.6 继承 vs 组件化 大比拼 光说不练假把式咱们来个直观对比对比维度面向对象继承组件化架构直观程度⭐⭐⭐⭐⭐ 非常符合直觉⭐⭐⭐ 需要转变思维代码复用⭐⭐ 父类强耦合⭐⭐⭐⭐⭐ 组件独立复用扩展灵活性⭐⭐ 受限于继承树⭐⭐⭐⭐⭐ 自由组合拼装多类型混合 钻石继承灾难✅ 完美解决性能优化⭐⭐ 数据分散⭐⭐⭐⭐⭐ 便于批处理适用场景简单小游戏、类型清晰现代3A大作、复杂世界表1继承 vs 组件化全方位对比一句话总结游戏引擎的本质是生产力工具要让开发者好维护、好理解还要让艺术家和设计师用着顺手。组件化就是这么一个符合直觉、又极其灵活的设计是现代游戏引擎的核心理念。⏱️ 第三章Tick循环——让世界活起来的秘密3.1 Tick是什么 好了现在我们有了GO有了组件但是……这个世界是静止的啊坦克开不起来飞机飞不起来跟个模型展似的有啥意思怎么让世界动起来答案就是——Tick还记得上节课说的吗游戏引擎最核心的一个函数叫tick每隔1/30秒或者1/60秒就让整个世界往前走一步。就像真实世界里的普朗克时间一样——如果咱们生活在一个模拟世界里那每个普朗克时间就是上帝给咱们设的tick。3.2 直觉方案Per Object Tick 最直觉的想法是什么每个GO依次tick每个GO里的每个组件再依次tick。比如一辆坦克先Tick Motor组件速度10m/s1/30秒就是0.03秒往前挪30厘米再Tick Animation组件履带卷一下主炮转一下再Tick Combat组件看看要不要开火把整个游戏世界所有物体都Tick一遍世界不就动起来了吗完美这就是最直觉的Per Object Tick模式每个对象自己把自己的事儿干完。3.3 反直觉但更高效Per System Tick 但是现代游戏引擎一般不这么干。它们会把同类型的组件放一起按系统批量Tick先把所有Motor系统全部Tick一遍所有移动物体统一算位移再把所有Controller系统Tick一遍统一做物理碰撞检测再把所有Animation系统Tick一遍统一算动画……以此类推哎这听上去有点反直觉啊就好像说一个人你不让他先动头再动手再动脚你非得让所有人先一起动头再所有人一起动手再所有人一起动脚……这不是神经病吗为啥要这么设计答案就两个字效率3.4 流水线的魔力 给大家举个最简单的类比做汉堡包。假设汉堡店有5个工人最直觉的做法是什么每个人从头到尾做一个汉堡——烤面包、烤牛肉、洗蔬菜、抹黄油、组合起来。一个香甜可口的汉堡就出炉啦~但这样效率高吗显然不高现代工业最核心的概念叫什么Pipeline流水线啊最高效的做法是有人专门烤面包有人专门烤牛肉有人专门洗菜大家配合好到点啪的一下合成一个汉堡。这才是效率最高的。放到计算机上还有个更大的好处数据局部性。图灵机小知识还记得上节课讲的图灵机故事吗在图灵机上效率最高的处理方式就是把同样的数据尽可能放到一起然后一次批处理完。无论读写都在一起缓存命中率贼高效率贼好这就是为什么高级课会讲ECS、DOTS这些架构——它们就是把这个思路做到了极致。3.5 两种Tick模式对比 咱们用图来直观感受一下两种模式的区别图1两种Tick模式架构对比一句话记住造个流水线批处理干活效率就是高就这么简单~ 第四章事件机制——解耦交互的优雅方案1 问题来了GO之间怎么交互 好了现在世界能动了。但是……每个GO都在自顾自表演坦克自己开飞机自己飞互相之间半毛钱关系没有这叫啥游戏啊举个最简单的例子我跳上坦克朝远处敌人开了一炮要把敌人击倒。那问题来了——我开炮这事儿怎么让敌人那个GO知道自己被打了4.2 原始方案硬编码Hard Code 最朴素的想法是什么炮弹爆炸的时候遍历周围所有物体一个一个看你是人扣你血你是飞机扣你血你是坦克扣你血你是石头哦没事你继续待着。这就是所谓的Hard Code硬编码。大家别笑早期游戏引擎真就这么写的。但是世界一复杂这就崩了——每次加个新物体类型你就得去所有爆炸逻辑里改一遍维护起来简直是噩梦。4.3 优雅方案Event事件机制 那怎么解现代游戏引擎有一个非常优雅的机制——Event事件机制也叫消息机制。啥意思呢咱们别那么粗暴直接敲人家门说你被我打了。咱们换个方式**写邮件**每个人家门口都放个邮箱我不需要认识你我只需要知道方圆20米内的邻居给你们每家寄一封邮件。邮件内容是“不好意思我炸了扣你100点血。”这邮件叫blackmail敲诈信哈哈然后呢下个tick的时候你打开邮箱一看哦我被扣了100点血然后你的Health组件一查自己只有70点血那好吧只能死给你看了。就这么简单一个本来超级复杂的耦合问题通过一个事件机制瞬间变得清晰又干净。4.4 这就叫解耦 ✂️在系统架构里这叫解耦。本来GO和GO之间通讯需要知道所有其他GO的类型、每个GO里有哪些组件太复杂了。现在统一变成事件机制你只要发个事件给对应的GO让它自己处理就完事儿了。各个GO和组件之间的逻辑干干净净互不干扰。加新物体不用改爆炸逻辑只要给新物体加个事件处理函数就行。完美4.5 商业引擎怎么实现 Unity简单粗暴用字符串匹配。注册一个叫DAMAGE_EVENT的事件SendEvent就完事儿了。物体的Health组件收到事件回调函数激活自己扣血。Unreal复杂一些用C原生代码加反射机制。注册事件绑定回调函数。好处是蓝图里也能用可视化操作策划也能上手。引擎设计核心做游戏引擎最核心的就是做一个可扩展的消息系统。让开发者能在引擎之上不断定制和自己玩法相关的各种消息类型再定制各种组件来处理这些事件——这就是现代游戏引擎最核心的工作。 第五章场景管理——N²灾难的破局之道5.1 新问题GO太多了怎么办 等等先别急着高兴。还有个问题游戏世界里有那么多GO啊单机游戏里一般几百个动态GO多的几千甚至上万个。那每次发生个事件我怎么通知到这些GO每次爆炸我怎么找周围的物体咱们回到小明的战争游戏NPC大兵、飞机、坦克、大炮……这么多东西怎么管5.2 基础管理UID 位置 先讲两个最基本的概念UID唯一编号每个GO都有一个全局唯一ID就像门牌号一样用来标识定位物体。还记得上节课讲的GUID吗一个道理。Position空间位置每个GO都有个三维坐标告诉我们它在哪儿。有了这俩我们就能开始做场景管理了。5.3 最朴素方案全遍历 ❌最简单的管理是什么不管理小明很粗暴反正我也没几个兵炮弹爆炸的时候把场景里所有GO全部查一遍位置判断是不是在爆炸半径内是的话就发伤害消息。这写行不行做小游戏完全没问题跑得动。但是当场景里有几千上万个GO的时候这就是一场灾难5.4 N平方的噩梦 这就是游戏引擎里经典的N²挑战。什么意思每个物体都可能和其他物体发生互动如果每次都要和其他所有物体问一遍那就是N×(N-1)≈N²次运算。一万个物体的话N²是多少**一亿**这对计算机来说是巨大的负载尤其如果数据还分散在内存各个地方那效率低到没法看。5.5 分而治之画格子 那怎么破最简单的方法画格子Divide and Conquer分而治之。把整个世界画成均匀的格子每个格子里放哪些GO我都记着。爆炸的时候我只需要查爆炸所在的格子和周围几个格子就行了工作量瞬间降下来这方法听上去土但很多场景真的好用。如果场景不大、物体分布均匀简单画格子就够了又快又好写。5.6 进阶方案层级空间树 但是画格子有个问题如果场景物体分布不均匀呢实话告诉大家现代3A游戏里看着世界很广阔实际上玩家能走的地方非常受限。以前在国外做游戏的时候这叫Trench战壕——设计师给玩家挖了很多条战壕玩家只能在战壕里走来走去。战壕里的东西放得又密又细战壕外面稀稀拉拉啥也没有。这时候你均匀打格子是不是又慢又浪费于是就有了更聪明的方案层级结构空间树。啥意思呢就像地图一样整个世界很大分成国家国家分成省省分成城市城市分成街区街区再分成街道。有事件发生在北京海淀区某条大街那我只需要在北京海淀区那个小区域里找就行了犯不着把全中国都搜一遍。5.7 常见的空间数据结构 数据结构原理适用场景复杂度均匀网格世界均匀切分成格子小型场景、物体分布均匀O(1) 查找四叉树2D递归四等分空间密集区细分2D游戏、地面场景管理O(log N)八叉树Octree3D递归八等分3D空间3D空间、全空间管理O(log N)BSP二叉树沿平面分割空间常沿墙体室内场景、FPS游戏O(log N)BVH包围体层次树物体包围盒自底向上合并现代3A主流、射线检测、视锥裁剪O(log N)表2常见空间管理数据结构对比咱们用图来看看四叉树是怎么工作的图2四叉树空间细分示意图红色区域为物体密集区持续细分**怎么选**看你的游戏类型。做个2D超级马里奥那种平台游戏根本不需要复杂的场景管理。但做COD、战地这种3A射击游戏那必须花心思好好设计才能尽量节约计算资源。对小明的战争游戏来说用个四叉树或者BVH基本就够用了。 总结四大核心心法速速收藏好了讲到这里今天的核心内容就差不多了。动手能力强的小伙伴估计已经手痒痒想去写代码了吧咱们来总结一下今天这节课你只需要记住四件事就掌握了现代游戏引擎的核心逻辑万物皆GO游戏世界里几乎所有东西全部统一抽象成Game Object。无论是看得见的坦克飞机还是看不见的触发器、游戏规则全都是GO。组件是GO的原子每个GO由各种各样的组件组合而成用组件拼装替代类继承灵活扩展完美解决多类型混合的问题。Tick驱动世界运行⏱️每一帧Tick一次世界往前走一步。现代引擎采用按系统批量Tick的流水线模式追求极致性能。事件空间树事件系统实现跨GO交互完美解耦空间树结构管理海量游戏对象规避N平方性能灾难。就这四点记住了你就明白了现代游戏引擎的组合基础逻辑。是不是感觉自己离做出下一个3A大作又近了一步 写在最后其实游戏引擎这东西说难也难说简单也简单。很多核心思想说穿了都是些很朴素的道理——分而治之、流水线、解耦……这些计算机科学的经典思想换个场景换个包装就变成了游戏引擎里的核心设计。最重要的是什么是理解这些设计背后的为什么。为什么要用组件化不用继承为什么要按系统Tick不按对象Tick为什么要有事件系统想明白这些为什么你才是真的懂了而不是死记硬背几个概念。毕竟引擎不是用来炫技的它是个生产力工具。让大家方便、容易理解才是引擎设计最底层的需求。好啦今天的硬核分享就到这里如果觉得有帮助别忘了点赞 收藏⭐ 关注三连一波咱们下节课继续扒游戏引擎的那些事儿~有啥问题欢迎在评论区交流咱们一起学习一起进步— EOF —
返回列表