找回密码
 立即注册
查看: 130|回复: 5

[简易教程] Unity 游戏框架搭建的过程是什么?

[复制链接]
发表于 2024-7-15 17:34 | 显示全部楼层 |阅读模式
Unity 游戏框架搭建的过程是什么?
发表于 2024-7-15 17:35 | 显示全部楼层
在Unity框架设计中与游戏服务器对接的网络框架也是非常重要的一个模块,本文給大家分享如何来基于Unity来设计一个网络框架, 主要的讲解以下几个点:


对啦!这里有个unity学习交流小组里面聚集了一帮热爱学习unity的零基础小白,也有一些正在从事unity开发的技术大佬,欢迎你来交流学习。
TCP半包粘包, 长连接与短连接, IO阻塞

TCP 是可靠的网络传送协议,网络传输底层每发送一个TCP数据包,就要等对方确认,收到确认消息以后才能发下一个TCP数据包。当我们在应用层发送一个应用层的数据包的时候,TCP网络底层可能会把这个应用层的数据包分成若干”TCP数据包”,通过网络底层发出去。那么这里就会有一个问题,应用层发的数据包有可能被拆散成几个”TCP数据包”发出去,我们在另外一端接收的时候,可能要把这些拆散的包组合起来。这个就是”半包”。底层有可能把两个应用层的数据包分到一个”TCP数据包”发过去,接收到后,一部分是属于上一个应用层的数据包,一部分属于下一个应用层的数据包,这个叫做”粘包”。


Tcp Socket与UDP Socket 的技术方案
了解完网络的一些基本概念以后,接下来看下使用TCP/UDP Socket用哪些技术方案。Unity其实是作为网络的客户端,而客户端只要去连接服务端与服务器通讯就可以了,不用像服户端同时处理N个客户端的数据传送。所以客户端Socket非常的简单。Unity客户端的Socket我们用哪个插件呢?其实这种完全不需要用什么插件,直接使用OS为我们提供的Socket的相关API就可以了。


UDP Socket的使用, UDP Socket不是面向连接的,只是调用底层的网络协议,直接把数据包发往特定地址+端口。所以直接是SendTo(网络地址+端口), RecvFrom(网络地址+端口)
TCP/UDP Socket已经足够简单,Unity开发者在选取技术的时候直接使用即可。
Unity的序列化与反序列化技术方案

网络发送的都是数据字节流,Unity与服务端通讯要把要发送的数据对象变成二进制字节流,然后通过网络传送出去,收到字节流以后,又要重建回数据对象,完成数据发送。数据对象变成二进制字节流这个过程叫序列化,把二进制字节流转回数据对象叫反序列化。
序列化/反序列化目前主要有两个打的方向:一个是二进制序列化,一个是文本序列化;


二进制序列化/反序列化: 我们把一些基本的数据类型用bit来进行编码,如 int, float, string bool等。写好这些基本数据类型的编码和解码的代码, 然后编写要数据对象的协议,然后开发一个编译器,他可以根据这个协议文件,基于基本数据类型的编码/解码函数,根据协议,结合基本数据类型的编码解码,按照对象的结构,拆分成若干基本数据类型,自动生成编码/解码函数代码。
文本序列化的解决方案: json, xml等;
二进制序列化/反序列化解决方案: protobuf;
TCP的封包与拆包
上面讲解了,应用层的数据包有可能被拆分成若干”TCP数据包”,还有可能两个应用层的数据包连在一起在一个”TCP数据包”中。为了收数据的时候能完整正确的收到应用层的数据包,我们必须要把两个应用层的数据报之间做好分隔标记,当我们收到数据以后就可以根据分隔标记来决定哪些数据是哪个包的。目前有两种做法:


基于http的短连接技术方案

短连接,前面个讲过的,用完就断开连接,下次要用的时候再重新的连接。短连接是使用TCP Socket策略,而这种策略最常用的就是http(基于TCP Socket+http超文本传输协议)。


Unity 网络框架设计与实现原理

铺垫了这么多,我们直接来上Unity网络框架的架构设计结构图,供大家参考。


这里接收数据的时候,采用多线程来做,收到数据后,进行拆包与反序列化,得到数据对象后,放事件队列,游戏主线程从事件队列里面获取网络事件,然后进行处理。发送数据出去的时候,我们在游戏主线程调用异步的网络Send函数,避免IO阻塞。注意好这些点,就可以设计出一个很好的网络框架。
本节分享就到这里了,关注我,学习更多的Unity框架设计的技巧。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2024-7-15 17:36 | 显示全部楼层
下面这个导图列出了列出了游戏客户端框架中会用到的(但不是全部)底层通用系统,这些系统跟具体的游戏功能无关,但它们支撑这一款游戏的客户端的运转。如果要搭建一个客户端框架,需要把这些问题都考虑到并处理好。


更多关于游戏研发的信息与技术图谱,可以打开并关注这个Github开源项目:
GitHub - gonglei007/GameDevMind: 网络手游开发知识地图,投身游戏开发技术可能需要了解的方方面面。​github.com/gonglei007/GameDevMind​github.com/gonglei007/GameDevMind​github.com/gonglei007/GameDevMind

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2024-7-15 17:36 | 显示全部楼层
这个问题也可以换一个问法:游戏框架搭建时需要提供什么能力?
首先需要明确一点的时,游戏引擎本身已经算是一个框架,只是Unity相对而言做的还不够好,因此大多数中、大型项目会在Unity基础上再搭建一个框架;
回到问题,游戏框架需要提供一些通用的功能模块,让项目开发期间能无痛的接入各种开发工作流;

  • 日志系统;应当提供一个线程安全的、可分级的、可分模块的、性能足够高效的、时序准确的日志系统;同时也应该辅助提供配套的日志检索、日志监控等周边系统,如基础的错误报警,日志过滤等等;
  • 配置表管理系统;应当提供一个高效优雅的从策划配置表(如Excel, CSV,Json等)到游戏实时数据的转换流程,该转换流程中应当包括基本的规则检测以及错误报警配置功能;并且此系统应该能流畅的接入其他各类工作流工具,如技能编辑器、剧情编辑器等等;
  • 资源管理;应当提供一个资源加载、卸载、更新的资源管理系统;该系统应当提供协程异步、多线程异步、分帧、优先级拆分等多种用于避免资源加载影响主线程的能力;除了基础的本地资源处理,也应当提供网络资源处理的能力,用于游戏资源热更新等;此外,也应当增加游戏资源引用关系生成功能,增加无效资源检测,资源数据统计(比如贴图大小/模型面片数等),资源处理耗时统计(包含运行时解析以及打包时压缩转码等处理)等周边功能,方便后期调优;
  • 热更新系统;应当接入或实现一个热更新框架,如ToLua/XLua/ILRuntime/InjectFix/HybridCLR等,用于实现游戏的热更或者游戏的热修复;同时也应当制定热更新的各个流程(比如Lua的hotfix生成或编写,hotfix的加载等);
  • 脚本层框架(如果使用了lua);需要封装性能可靠的lua/C#交互层,同时在lua层应当也能流畅的使用框架中的各项必需功能;
  • 技能系统;应当提供较为完备的 技能效果/冷却/持续时长/技能逻辑/技能交互(技能免疫,叠加等)等的处理,必要的话,可以提供一个技能编辑器;可参考隔壁UE的GAS
  • 时间系统;应当提供一个所有模块可公用的同一且稳定的时间获取、计算系统,这样才能确保各系统获取的时间是一致的;
  • 多语言管理系统;如果可以的话,应当提供i18n管理系统,以支持多语言切换;
  • 输入系统;应当提供硬件输入到逻辑行为的映射能力(比如按下W键/滑杆前推的行为都是Moveforward),便于游戏开发者支持多种硬件的输入,这个一般引擎已经支持,但可能需要提供更加完善的输入绑定能力;可参考隔壁UE的EnhancedInput
  • 消息系统(事件系统);这个无需多提,基本必定需要的;
  • 基础数据结构和算法;应当提供一个高效的常用数据结构的封装(也可用C#自己提供的);比如对象池、状态机等;
  • 网络系统;这个有两层含义,往全面的方向做可以把服务端一起做了(这块不提);往小而美的方向做可以仅提供对常用协议(比如Http, TCP,  UDP, KCP)的封装,需要注意的是,也应当提供Wifi和4G/5G网络的无缝切换功能;
  • RPC系统;这个一般和服务端关联较为紧密,基于网络系统之上,一般用于客户端和服务端间的通信;
搭建一个框架基本上就是把一个游戏应该解决的除了业务逻辑外的问题都提供较好的解决方案; 更加完善的框架还应当提供较为完善的周边工具链辅助项目开发;
发表于 2024-7-15 17:37 | 显示全部楼层
简述

本文假定读者至少懂得或精通C#、C++、Lua、Python等常用语言。当然最好有行业的背景知识,Unity、Unreal引擎之类的起码要了解一些。
众所周知,在我们的手机里、电脑里装着的游戏,都称之为端游。只是业内大部分人都把手机端或同质的产品,称为移动端,而端游一词则是泛指曾经辉煌一时的那些PC游戏(网络)。还有一类是即将湮灭于历史长河的,那些正走在穷途末路旧时代的页游。
在我们常见的网络游戏的游戏架构中,常常分为客户端/服务器,一般而言,客户端主要是做一些非核心的逻辑,表现上的逻辑,当然并非全部。服务器呢,则是更多的用来为玩家提供一些增删改查的能力。有没有其他的能力?当然有,后面说。
那么,既然我们要全栈,至少得了解那么几种语言对不对?
在服务器上,C++这门语言,是传统上很多游戏大厂偏好的,当然,不排除某些喜欢python这类的。那么,作为一门发展了如此之久的上古神器,自然而然的就成为了很多老鸟架构师的首选。至于选择java、node.js的也是挺多的,C#相对就比较少了。
那么,在客户端呢?
我们先来说说web的历史,在wasm之类的玩意出来之前web不能跑C++之类语言的产物,现在早可以了,完。
所以C++也就更加成了客户端首选的语言。
但是大部分游戏玩法程序员接触到都是些什么C#、lua之类的啊?那是因为大家比较幸运,不需要经历过野指针乱飘、内存泄漏的时代,像C#这种语言,想写内存泄漏的代码还真的要有一些水平。写这些语言,终究也要穿到底层,跑在大部分Gameplay程序员接触不到的所谓C++底层上,可以说,目前市面上的基于端的游戏,绝大多数都是C++写的底层,业务层为了敏捷开发快速迭代照顾客户,都选择了门槛偏低的C#、lua之类的语言作为开发语言。然而有些时候不得不因为一些奇奇怪怪的原因给开发者开一些后门,供开发者获得一些更深层的控制权,比如C#的P/Invoke、封送,其实都是一些极其强大的能力,不过,这是双面刃,用得好威力无穷,用不好打包走人。
言归正传,我们从游戏的Gameplay开始。
阶段一 Gameplay设计

本阶段内容涵盖客户端服务器。
Gameplay,即是游戏玩法,可以说是大部分游戏公司任职的程序员而言所接触到最下层的逻辑了。而Gameplay之中,个人认为,最有技术含量的就是战斗系统,包括视野同步、技能、Buff等等。如果说还有什么比这个更没有技术含量,那恐怕就是大家都认为的UI程序员了。然而,UI程序要设计好一个大型的UI系统,也是需要了解不少东西的,至少是比如MVC、MVVM之类的要懂得。
我们来看看一个宏观上的思维导图。



整体来看

从大部分项目来看,我们自顶向下切分成UI、GameplaySystem,以及Core。UI主要是界面的prefab到对应的视图层,为了合理的处理界面间的遮挡关系,在这里我们可以适当的放一些逻辑控制,如果没有特别的癖好以及什么其他的原因,不要选择一些复杂化的设计去解决问题,如果只是为了设计而设计,那么后面会在这些完全不了解的设计原理上耗费一些没有意义的时间。GameplaySystem的划分主要是为了在一定程度上解耦,还有避免逻辑系统的循环依赖,这会使得代码很难看。Core这一层,主要是针对于引擎比较熟悉的人,比如图程、主程等等,可能会在这里去做一些框架、以及所谓底层之类的,这里有一种失败的做法,那就是有些人喜欢对引擎进行二次封装,限制使用一些功能,如果水平、经验跟时间都够,自然是无所谓的,但是如果一旦其一欠缺,后面会弄成个四不像。
UI的设计,我们往往可以基于如下的思想进行一些设计的总结。管理容器、加载(考虑cache,加载完界面之后上下文无关的初始化)、卸载、打开界面(事件广播)、关闭界面(事件广播),等等。然后还有一个需要关心的就是UI排序问题,即谁前谁后。



UI管理系统的基本构思



UI类跟资源关系

我们一般会有一个抽象基类Abstract Widget,继承自MonoBehaviour或某些interface,然后在WidgetSystem中进行注册跟管理,比如加载,我们可能往往会先去在Cache中找一下,如果已经存在,我们可能直接就Instantiate。往往是一个一个独立的界面会作为一个Prefab去拼凑,从而完成解耦以及各种各样的控制,而XXWidget,往往会对应这个UIPrefab,要么直接挂载上去,要么通过WidgetSystem加载完这个Prefab之后,把其Instance作为这个XXWidget的初始化附加对象,通过代码上组合的方式进行逻辑捆绑,至于序列化数据,要么就是老方法拖拖拖,要么就是在Initialize中使用Find解决问题。至于UI的Bundle策略,往往我们会把这些Bundle都打在一起。至于图集之类的乱七八糟的问题,不在讨论范畴。在WidgetSystem中,我们往往都会通过Open/Close去控制UI的开关,而通知UI的打开和更新往往会使用事件机制完成。
UI往往使人厌烦的一点就是:不断重做。所以UI系统一定要设计的足够好,能够让业务逻辑层完全不关心UI是什么,更不应该设计双向逻辑,一般业务逻辑层需要让UI更新或者是做些什么事情,应该使用事件机制。一旦在业务逻辑上出现UI相关的东西,很可能会让你的项目后期痛苦不已。所以业内常用的做法是将UI逻辑放在Lua层。
下面讲讲Gameplay。但是在这之前我们先来想通一件事。
每一个Gameplay系统有没有什么共性?
能不能将这些共性抽象出来?



系统共性

如果在Gameplay路线上,有过思考,或者接受过一些比较成熟的设计框架的洗礼,我们可以发现一点,配置、每个系统的独立实体、处理这些实体跟角色关系的处理类、以及系统范畴里面管理这些处理器的管理器、创建这些实体的工厂类等等。
它们从概念上而言,是一样的东西,既然一样,我们如何细化然后归纳起来呢?
战斗系统/技能系统/Buff系统,见补充参考:
青狐秀水:浅谈技能系统这里的关系组成,简化版本往往都是下图所示的关系。



阉割版的系统关系图

在配置输出的结果上,我们常见的做法一般是是使用csv、json、xml、protocol buffers等等,但对于Unity来说,其实还有一种选择就是ScriptableObjet,而这些配置如果是基于上述格式,那不让策划哭死,所以往往策划同学们原始配置会在excel上完成。如果仅仅使用C#,这是一个在上述格式来说的几乎是最优解,如果解析过程性能或者内存有着极高的要求,不怕麻烦,也可以自己用C/C++写一遍解析或者反序列化,丢给C#。



将导出文件数据格式规范化,变换到项目约定的格式

然后就是热更流程,热更基本上是每一个游戏都要去考虑的事情了,在Unity中我们往往会采用AssetBundle去做更新的选择,但是在Bundle的粒度上的选择可能会成为一个话题,这个只能说按需。只需要注意一点即可,一个Bundle一个头一个handle,同时处理太多的Bundle散文件会频繁的穿r0去执行open,这时候可能会是一个瓶颈,太少则可能会在更新的流量上造成额外消耗,至于更新完之后合包的想法,最好别。AssetBundle是一个用于做资产更新的东西,基本上非独立游戏都会使用。随便搜搜Google就有更详细的资料了,就不科普了。
这里可以考虑一下自己魔改Addressable,因为这个系统设计的基本上把我们需要的东西都给考虑进去了。
更新的时候需要考虑版本兼容的问题。
然后就是Gameplay跟远端通讯的问题,下面是我刚入行的时候选型去解决远端本地兼容(即服务端有没有都不影响自测)的办法,安利一下。



通讯的方式

将每一个系统都抽象出一个网络请求以及回调管理的一层,这一层请求就由某个开关做路由选择,可以选择本地回环模式(测试),也可以走远程请求。这个看起来是维护两份代码,但是对于调整系统或自测某些情况的用例而言是可以极大速度的减少了测试时间的(程序自测),尤其是当你觉得服务器的人不靠谱的时候。
接下来说说更新业务部分的设计。



最简单的通讯结构

应对与不同的客户端,客户端在登录的时候,如果我们在不得不维护一些久版本的前提下,我们往往会询问某个服务器,获取我们当前这个版本对应的服务器的地址,进行通讯,然后再去获取不同的版本的CDN的地址,来取得相应的AssetBundle的URL的信息。当然,这里的做法极多,而且在游戏开发的情况下,往往都会废弃旧版本的维护,直接要求强制更新。
那么我们在服务端上一般是怎样选型的呢?



简化版的客户端-服务器

一般而言,我们不会直接对客户端暴露业务服的ip,所有的业务通过维护了用户部分状态信息的Session管理进行封装转发,这样有助于提高安全性,隔离不安全的业务,同时可以实现流量控制等等。
在World里面,姑且认为里面就是我们客户端写着的逻辑业务增加了各种安全条件、消费处理的啰嗦版本,好吧,这个其实意思就是,你只要语言过关,你写得了客户端自然也可以写服务端…反之亦然,所以以前写端游的时候,我们客户端服务端的同一个业务系统一般都是一个人写的,这样其实更省心,反正没听过哪个同事自己对着镜子吵架的,而且谁会给你找一个来给你分担工作量啊…
此外,自然就是怎么处理客户端发送的请求的部分。


为了解决延迟问题,我们一般的方法会在数据库层加一个redis之类的数据库做中间层。数据库层就是给你的游戏写持久化数据的,比如你的VIP等级、女朋友们之类的,那么重要。
写到这…
发现好像能够脱离业务说的也就这样了,一个从客户端UI、Gameplay到服务端的基本流程其实讲完了。至于客户端另外的哪些什么乱七八糟的SDKs,一般都是两类,一个是登录+支付为主的发行时要求的SDK,一个是用于统计用户行为的SDK,在合适的地方收集用户操作行为用于做行为分析修正数据曲线的。这些就算了,基本上都是看看文档就能明白的。下面补一张图。



三方SDK背后的泪水

上面的东西其实写一两年代码基本上就可以总结出来了,可能别人还有更好的代码设计但就是不说吧。
写了好几年的东西了,忘记了发出来,后面的有空继续,包含引擎、渲染、物理、声音、加解密、系统等等。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2024-7-15 17:37 | 显示全部楼层
0、本质上所有的框架都是为了抽象与复用。
1、以最常见的从AssetBundle加载prefab为例,你需要根据平台得到路径、查看缓存是否有AssetBundle和asset,没的话判断是远程下载还是本地加载。等你拿到AssetBundle,获取到asset,还要实例化。这这个在框架里面可能就简单的封装成一个简单的Load(string path, Type assetType)了,这个函数抽象了不同平台、不同情况下加载资源的复杂的细节处理,更加符合使用者的习惯。而且它是可以复用的。
2、一般我理解的框架都是支持依赖注入的,也就是说它内部维护了一套机制。你想要什么功能需要按照定好的函数声明,注入你自己的处理函数。这个也是框架和库的区别。
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-21 11:28 , Processed in 0.111077 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表