实习项目学习记录(持续更新
配置表加载读取模块学习记录
整体流程概览
- 编辑器阶段:将 XML 文件通过
XElement
解析成VO数据对象,然后将自定义流传入对象的序列化方法,转为临时.bytes
二进制文件 - 构建阶段:将
.bytes
文件打包成 AssetBundle(如 GameRes.unity3d/GameRes_VO.unity3d) - 运行时阶段:
- 通过
XmlVOManager.GetBytes()
从 AB 包中读取配置二进制数据 - 通过
TConfig<T>
类来解析并管理 VO 数据
- 通过
TConfig<VO>
TConfig<VO>
类是配置表加载读取模块的核心类,用于解析和加载配置数据。TConfig<T>
类初次使用时,从 AB 包读取对应配置二进制数据- 初始化自定义流,并根据主键建立偏移表(使用 unsafe 指针操作)
- 使用
Find
(根据主键)或Select
(条件查询)获取 VO 对象Select
需要给 VO 类字段添加[TDBIndex]
特性
初始化流程
- 创建一个新的
TConfig<VO>
对象 - 通过
XmlVOManager.GetBytes()
获取 bytes[]- 本质是通过AB包加载
TextAsset
资源文件得到的二进制数据
- 本质是通过AB包加载
- 创建自定义流 isstream,写入 bytes[],设置偏移等信息
- 解析并存储头部信息:
- 主键名 (keyName),类型 (KeyType)
- 字段索引表:
Dict<string, FieldDesc>
FieldDesc
:包含字段名、VO中偏移量、类型、顺序
- 条件索引表:
_indexs
索引组集合_dict_name_index
:字段名 -> DBIndex_dict_id_index
:索引编号 -> DBIndex
- 创建 VO 对象缓存表;主键 ID -> VO 偏移存储表
- 调用
LoadPKS
,将流指针移动到 PK 区,归档 ID->偏移 存入字典
实现部分
由各种分布类实现
TConfig_Search.cs
TConfig_Define.cs
TConfig_Serialize.cs
TConfig_StrTable.cs
TConfig_Attribute.cs
二进制数据结构
TConfig<VO>
实例包含自定义流 _input
其中二进制数据结构如下:
- Header (头部信息)
记录了整个配置文件的结构信息等元数据- 主键字段名和类型
- 字段顺序和偏移地址
- 索引数据起始偏移
- 字符串池起始偏移
- VO数据区起始偏移
- 数据项数量
- 是否支持迭代器等等.....
- String Table (字符串池)
- 字符串常量池,减少重复字符串的存储开销
- PK Index Section (主键索引区)
- 用于VO数据的懒加载
- 示例:|{1001(主键id): 3000(vo数据偏移), 1002: 3020, 1003: 3040}|
- VO Data Section (对象数据区)
- 每个VO按字段顺序存储
- 字符串类型使用2+2=4字节表示为长度+位于字符串池的偏移量
- Index Section (字段索引区)
- 含有索引编号,字段数量,字段名等信息
- 字段索引映射区:
- 单字段:50 -> 3000
- 多字段:(50, 0) -> 3000
用法原理
Find
直接通过主键 ID对应的偏移量来获取 VO 对象
- 查看
_dictItems
是否缓存 - 如未缓存,使用
_dict_K2Offset_Int
获取偏移值 - 移动自定义流指针到指定偏移
- 创建 VO 对象,调用 VO.fromBytes,解析并返回
- 缓存 VO 对象,返回
Select
- 首先通过传入的字段名或者id从
TConfigHeader
的_dict_name_index
或_dict_id_index
得到对应的DBIndex
- 调用
DBIndex
实例的Select
方法查询主键id- 如果已预加载
preload = true
,则使用字典获取主键id - 如果未预加载,或者查询不到,则从二进制数据中进行二分查找主键id
- 如果已预加载
- 最后得到主键id,调用
Find
返回VO对象 - 最后根据
loadIndexData
来判断是否需要缓存
SelectMulti
大致与Select流程类似
不同的是:
- 返回所有匹配项
- 未预加载时线性扫描获取所有匹配项VO偏移地址
网络模块学习记录
架构层级
底层
主要负责Socket通信:
- 使用
System.Net.Sockets
实现 TCP 客户端。 - 根据设备支持动态选择 IPv4 / IPv6。
- 异步发送与接收数据(使用
BeginSend
/BeginReceive
方法)。
中层
相关代码文件:TCPClient.cs
、TCPInPacket.cs
、TCPOutPacket.cs
主要功能是协议封装与缓存池管理:
TCPInPacket
与TCPOutPacket
分别处理接收数据和发送数据。
数据包结构
- 发送包头结构:4字节长度 + 2字节命令 + 1字节校验码 + 4字节时间戳。
- 校验码:使用 CRC32 算法实现。
- 接收包头结构: 6 字节(4字节长度 + 2字节命令ID)。
- 包体:业务逻辑数据。
数据来源与处理流程:
- 接收包数据来源于 Socket 缓冲区,发送包数据由程序主动构造(使用对象池)。
- 解析顺序:数据包头 → 数据包体 → 构造数据体 → 最后封装数据包头。
对象池(TCPOutPackPool
, TCPInPackPool
)
TCPOutPackPool
用于复用发送包对象TCPInPackPool
并未使用在项目中,位于工具类。- 使用简单的
Stack<T>
实现对象池,不具备接口统一、时间戳管理、动态扩缩容等高级功能。
TCPOutPacket
- 对发送数据进行封装,构造完整数据包。
- 包头固定长度为 11 字节(4+2+1+4)。
- 非线程安全。
封装发送接口(SendData
)
实现位置:TCPClient.cs
- 接收一个
TCPOutPacket
。 - 检测缓存是否已有待发数据,若有则跳过发送。
- 清除当前命令相关的其他命令并注册强联网命令。
- 使用异步
BeginSend
方式发送数据包。 - 记录发送字节数,并更新发送时间计时器。
TCPInPacket
- 用于接收并处理从 Socket 获取的数据包。
- 线程安全。
- 包头结构为 6 字节(4字节长度 + 2字节命令ID)。
- 数据写入流程:
- 底层 Socket 接收的字节流通过回调传入。
WriteData
方法用于写入(递归调用)并组织数据。
具体处理:
-
使用固定长度命令头作为边界标志
- 接收端先等待6字节的命令头(4字节长度 + 2字节命令ID)
- 然后根据长度读取后续数据体
-
使用缓冲区逐步接收数据
- 使用两个缓冲区
byte[]
分别保存命令头和数据体 - 如果当前数据不够组成完整的命令头或者数据体,则保留数据到缓冲区,等待下一次调用继续拼接
- 使用两个缓冲区
-
递归调用处理剩余数据
- 接收完数据后,如果还有未处理完的数据,就调整偏移量并递归调用
WriteData
,继续处理剩余数据。
- 接收完数据后,如果还有未处理完的数据,就调整偏移量并递归调用
-
触发接收完数据包的回调
上层
抽象层接口:InterfaceNet.cs
实现层类:TCPGame.cs
主要处理事件通知与命令分发逻辑。
TCPGame
- 是
InterfaceNet
的具体实现类。 - 封装了大部分与服务器交互的逻辑:
- Session 会话管理
- 网络连接
- 数据收发
- 状态管理
- 错误处理
- 在
SocketConnect
方法中通过switch-case
分发消息枚举。
SocketConnectEventHandler
用于连接状态的回调处理:
SocketConnect
:初始连接回调(所有类型的连接回调最终都会调用此方法)。SocketFailed
:连接失败(实现于PlayZone
)。SocketSuccess
:连接成功(实现于PlayZone
)。SocketCommand
:连接命令回调(实现于PlayZone
)。
大致使用流程
协议接收数据流程
- 在模块中注册协议回调函数(通过指令 ID)。
- 底层 Socket 通过异步方法(
SocketAsyncResult
)接收字节流,并传入TCPClient
回调函数。 - 字节流通过回调传入
TCPInPacket
,使用WriteData
写入并组织数据(对象由TCPClient
创建并复用)。 TCPInPacket
递归处理字节流,最终完成一个完整数据包的封装。- 封装完成后,触发
TCPClient
的TCPCmdPacketEvent
事件回调。 - 使用
MUSocketConnectEventArgs
对回调数据进行进一步封装,包含:- IP
- 错误码
- 命令ID
- 字段(fields)
- 原始字节流
- 字节长度
PlayZone
接收到回调,通过GameSocketCommand
生成类似于如下的异步委托:
() => {center.Execute(e.CmdID, e);}
- 异步委托连同数据被加入
MainGame
的异步指令执行队列。 - 游戏主循环中从队列中取出并执行委托。
- 最终通过指令 ID 查找事件中心中注册的回调函数并执行。
- 在回调函数中,使用
DataHelperEx.BytesToObject
方法将字节流反序列化为对象。- 若为基础类型集合,可直接使用
e.fields
获取字符串并转换。
- 若为基础类型集合,可直接使用
资源管理模块学习记录
运行时
MuAssetManager
- 继承自MonoBehaviour
- 资源管理模块的核心类,负责资源加载、缓存、管理、释放等操作。
- 请求队列与请求ID字典
- 资源缓存字典
生成对象大致流程
- 检查对象池是否存在可用对象
- 存在则直接返回
- 检查资源缓存是否存在,存在则实例化后返回
- 存在则实例化返回
- 检查是否有相同资源的加载请求
- 存在则将回调添加到加载请求的回调中(多个地方请求同一个资源时,通过回调机制共享同一个资源对象)
- 不存在将创建新资源加载请求加入队列(随后会触发
DoAssetRequest
协程方法加载资源) - 异步加载AB包和资源,加载完成时触发回调通知
- 资源对象使用时添加引用计数(回调前),回调完成时减引用计数(回调后),并根据缓存策略来释放资源
ObjectPool
用于管理 GameObject
的对象池系统,提升复用效率,减少频繁创建和销毁带来的性能开销。
通过失效帧控制机制,有效规避了多次复用带来的组件状态冲突问题。
核心结构
-
CachedObject
携带缓存的资源对象,用于实例化。 -
激活字典(Active Dictionary)
类型:Dictionary<int, GameObject>
存储当前已激活的对象,Key 为GameObject.GetInstanceID()
。 -
失效列表(Inactive List)
类型:List<KeyValuePair<GameObject, int>>
用于记录被回收的对象及其失效帧数(Time.frameCount
)。
该机制用于避免在同一帧内对同一对象的重复回收与复用,防止组件状态(MonoBehaviour 生命周期、协程)、物理行为或动画状态出错。注意:失效列表的遍历从尾部开始,以优化删除操作的性能(尾部删除开销较低)。
-
卸载策略
- UnLoadTime:记录对象池被完全卸载的超时时间。
- 卸载策略枚举:支持自定义的卸载策略(如按时间、按帧数、条件判断等)。
对象池创建与卸载流程
对象池由 MuAssetManager
统一管理其创建初始化与卸载:
-
对象创建
- 当对象池需要新实例时,且字典查询没有对应的对象池会用
CachedObject
实例化新对象池并缓存。
- 当对象池需要新实例时,且字典查询没有对应的对象池会用
-
对象卸载
- 当对象池中无激活对象,且最后一次使用时间已超过
UnLoadTime
,由MuAssetManager
自动进行资源卸载。
- 当对象池中无激活对象,且最后一次使用时间已超过
对象初始化
对象在创建或复用时会执行初始化逻辑:
- 设置对象为初始状态(位置、旋转、激活状态等)
- 挂载
ManagedObject
生命周期管理组件 - 为
ManagedObject
绑定生命周期回调函数,包括:- OnActive:对象激活时调用
- OnInactive:对象失效时调用
- OnDestroy:对象被销毁时调用
这样可以确保每个对象的生命周期事件在合适的节点触发,避免因状态残留或逻辑错乱造成的 Bug。
ManagedObject
- 管理对象池中生命周期的组件
- 继承自MonoBehaviour
- 包含生命周期回调函数
- 保存对象上所有材质状态,复用时恢复初始外观
- 记录字段初始值,再复用时恢复
DoAssetRequest
资源资源加载请求队列数量大于0时调用。
资源加载的核心在于 DoAssetRequest
协程方法,是资源管理模块中实际执行资源加载的协程函数。
该协程的最大数量不超过MaxConCurrency = 3
。
该方法接收一个资源请求基类作为参数(可能是资源类型或 GameObject 类型),并依次执行以下步骤:
- 简单描述如下:
- 检查资源是否缓存
- 加载依赖包
- 加载AB包和资源本体
- 缓存加载结果
- 触发实例化和加载完成的回调
- 管理引用计数和资源释放
1. 检查缓存
- 首先在
mCachedResources
(资源缓存字典)中查找目标资源:- 若存在缓存,直接返回该资源,跳过依赖包、AB包、资源本体的加载,直接到下面
4. 触发回调
那一步; - 若不存在,则继续执行下一步。
- 若存在缓存,直接返回该资源,跳过依赖包、AB包、资源本体的加载,直接到下面
2. 加载依赖包(仅适用于 GameObject 请求)
- 在
mSharedAssetBundles
(共享依赖包缓存)中查找是否已有该依赖包:- 如果存在,则将对应的
SharedBundle
的refCount
加一; - 如果不存在:
- 创建一个 AB 包的异步加载请求,并使用
yield return
等待加载完成; - 加载完成后:
- 若缓存中已有
SharedBundle
,则增加其引用计数; - 否则,创建新的
SharedBundle
对象,封装该 AB 包及其共享包 ID,初始化refCount
为 1,并添加进mSharedAssetBundles
; - 同时,将所有共享资源记录到
SharedAssets<UnityEngine.Object, int>
计数字典中。
- 若缓存中已有
- 创建一个 AB 包的异步加载请求,并使用
- 如果存在,则将对应的
3. 加载资源本体
-
首先尝试从
mCachedBundles
中查找目标 AB 包:-
若找到:
- 增加
CachedBundle
的refCount
; - 从 AB 包中发起资源加载请求,并
yield return
等待完成; - 加载完成后,将
refCount
减一,并在合适时机(默认 1 秒后)判断是否释放该 AB 包;
- 增加
-
若未找到缓存:
- 根据资源路径创建新的 AB 包异步加载请求(若路径未命中,可能尝试类似 "Equip" → "Equip2" 的备选路径);
- 加载完成后,将 AB 包及对应资源加入缓存,并设置延迟释放策略(默认 1 秒后释放)。
-
-
最后,从请求记录字典和当前请求列表中移除该次请求的 ID。
4. 触发回调
-
根据传入的请求类型触发不同的资源实例化方法:
- 对于 GameObject 请求:调用
InstantiateGameObject(xxx)
; - 对于资源(如贴图、音频等):调用
InstantiateAsset(xxx)
;
- 对于 GameObject 请求:调用
-
将实例化后的对象作为参数,调用请求中的回调函数。
5. 引用计数与资源释放
该部分位于try-catch中的finally
块
- 对所有依赖包执行引用计数减一操作;
- 若某个包的引用计数为 0,则调用
Unload(false)
卸载该 AB 包(不卸载已实例化对象)。
A*Fast
初始化
首先传入一个 byte[,] Grid 原始数组
这个数据是一个矩形,且满足长宽为2的次幂
内部使用一个 Node[] CalcGrid 计算数组来存储计算时的数组
Node的位置是将原始数组的二维坐标转换成 Node[] CalcGrid 的一维坐标
计算公式为 result = Y << YOffset + X
Node
YOffset
主要表现为X的最大取值的Bit偏移
Y在一个二进制数据中的偏移,例如YOffset为2:0100 表示 一个(0,1)
根据传入的Grid原始数组的GridX(列数)进行计算
结果为以2为底,GridX的对数,例如GridX为8,则YOffset为3