1 2 3 4 5 6 7 8 9 10

实习项目学习记录(持续更新

配置表加载读取模块学习记录

整体流程概览

  • 编辑器阶段:将 XML 文件通过 XElement解析成VO数据对象,然后将自定义流传入对象的序列化方法,转为临时 .bytes二进制文件
  • 构建阶段:将 .bytes 文件打包成 AssetBundle(如 GameRes.unity3d/GameRes_VO.unity3d)
  • 运行时阶段
    1. 通过 XmlVOManager.GetBytes() 从 AB 包中读取配置二进制数据
    2. 通过 TConfig<T> 类来解析并管理 VO 数据

TConfig<VO>

  • TConfig<VO> 类是配置表加载读取模块的核心类,用于解析和加载配置数据。
    • TConfig<T> 类初次使用时,从 AB 包读取对应配置二进制数据
    • 初始化自定义流,并根据主键建立偏移表(使用 unsafe 指针操作)
    • 使用 Find(根据主键)或 Select(条件查询)获取 VO 对象
      • Select 需要给 VO 类字段添加 [TDBIndex] 特性

初始化流程

  1. 创建一个新的 TConfig<VO> 对象
  2. 通过 XmlVOManager.GetBytes() 获取 bytes[]
    • 本质是通过AB包加载TextAsset资源文件得到的二进制数据
  3. 创建自定义流 isstream,写入 bytes[],设置偏移等信息
  4. 解析并存储头部信息:
    • 主键名 (keyName),类型 (KeyType)
    • 字段索引表:Dict<string, FieldDesc>
      • FieldDesc:包含字段名、VO中偏移量、类型、顺序
    • 条件索引表:
      • _indexs 索引组集合
      • _dict_name_index:字段名 -> DBIndex
      • _dict_id_index:索引编号 -> DBIndex
  5. 创建 VO 对象缓存表;主键 ID -> VO 偏移存储表
  6. 调用 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 对象

  1. 查看 _dictItems 是否缓存
  2. 如未缓存,使用 _dict_K2Offset_Int 获取偏移值
  3. 移动自定义流指针到指定偏移
  4. 创建 VO 对象,调用 VO.fromBytes,解析并返回
  5. 缓存 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.csTCPInPacket.csTCPOutPacket.cs 主要功能是协议封装与缓存池管理:

  • TCPInPacketTCPOutPacket 分别处理接收数据和发送数据。

数据包结构

  • 发送包头结构: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 方法用于写入(递归调用)并组织数据。
具体处理:
  1. 使用固定长度命令头作为边界标志

    • 接收端先等待6字节的命令头(4字节长度 + 2字节命令ID)
    • 然后根据长度读取后续数据体
  2. 使用缓冲区逐步接收数据

    • 使用两个缓冲区byte[]分别保存命令头和数据体
    • 如果当前数据不够组成完整的命令头或者数据体,则保留数据到缓冲区,等待下一次调用继续拼接
  3. 递归调用处理剩余数据

    • 接收完数据后,如果还有未处理完的数据,就调整偏移量并递归调用WriteData,继续处理剩余数据。
  4. 触发接收完数据包的回调

上层

抽象层接口:InterfaceNet.cs 实现层类:TCPGame.cs 主要处理事件通知与命令分发逻辑。

TCPGame

  • InterfaceNet 的具体实现类。
  • 封装了大部分与服务器交互的逻辑:
    • Session 会话管理
    • 网络连接
    • 数据收发
    • 状态管理
    • 错误处理
  • SocketConnect 方法中通过 switch-case 分发消息枚举。

SocketConnectEventHandler

用于连接状态的回调处理:

  • SocketConnect:初始连接回调(所有类型的连接回调最终都会调用此方法)。
  • SocketFailed:连接失败(实现于 PlayZone)。
  • SocketSuccess:连接成功(实现于 PlayZone)。
  • SocketCommand:连接命令回调(实现于 PlayZone)。

大致使用流程

协议接收数据流程

  1. 在模块中注册协议回调函数(通过指令 ID)。
  2. 底层 Socket 通过异步方法(SocketAsyncResult)接收字节流,并传入 TCPClient 回调函数。
  3. 字节流通过回调传入 TCPInPacket,使用 WriteData 写入并组织数据(对象由 TCPClient 创建并复用)。
  4. TCPInPacket 递归处理字节流,最终完成一个完整数据包的封装。
  5. 封装完成后,触发 TCPClientTCPCmdPacketEvent 事件回调。
  6. 使用 MUSocketConnectEventArgs 对回调数据进行进一步封装,包含:
    • IP
    • 错误码
    • 命令ID
    • 字段(fields)
    • 原始字节流
    • 字节长度
  7. PlayZone 接收到回调,通过 GameSocketCommand 生成类似于如下的异步委托:
() => {center.Execute(e.CmdID, e);}
  1. 异步委托连同数据被加入 MainGame 的异步指令执行队列。
  2. 游戏主循环中从队列中取出并执行委托。
  3. 最终通过指令 ID 查找事件中心中注册的回调函数并执行。
  4. 在回调函数中,使用 DataHelperEx.BytesToObject 方法将字节流反序列化为对象。
    • 若为基础类型集合,可直接使用 e.fields 获取字符串并转换。

资源管理模块学习记录

运行时

  • MuAssetManager
    • 继承自MonoBehaviour
    • 资源管理模块的核心类,负责资源加载、缓存、管理、释放等操作。
    • 请求队列与请求ID字典
    • 资源缓存字典

生成对象大致流程

  1. 检查对象池是否存在可用对象
    • 存在则直接返回
  2. 检查资源缓存是否存在,存在则实例化后返回
    • 存在则实例化返回
  3. 检查是否有相同资源的加载请求
    • 存在则将回调添加到加载请求的回调中(多个地方请求同一个资源时,通过回调机制共享同一个资源对象)
    • 不存在将创建新资源加载请求加入队列(随后会触发DoAssetRequest协程方法加载资源)
    • 异步加载AB包和资源,加载完成时触发回调通知
  4. 资源对象使用时添加引用计数(回调前),回调完成时减引用计数(回调后),并根据缓存策略来释放资源

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. 触发回调 那一步;
    • 若不存在,则继续执行下一步。

2. 加载依赖包(仅适用于 GameObject 请求)

  • mSharedAssetBundles(共享依赖包缓存)中查找是否已有该依赖包:
    • 如果存在,则将对应的 SharedBundlerefCount 加一;
    • 如果不存在:
      • 创建一个 AB 包的异步加载请求,并使用 yield return 等待加载完成;
      • 加载完成后:
        • 若缓存中已有 SharedBundle,则增加其引用计数;
        • 否则,创建新的 SharedBundle 对象,封装该 AB 包及其共享包 ID,初始化 refCount 为 1,并添加进 mSharedAssetBundles
        • 同时,将所有共享资源记录到 SharedAssets<UnityEngine.Object, int> 计数字典中。

3. 加载资源本体

  • 首先尝试从 mCachedBundles 中查找目标 AB 包:

    • 若找到:

      • 增加 CachedBundlerefCount
      • 从 AB 包中发起资源加载请求,并 yield return 等待完成;
      • 加载完成后,将 refCount 减一,并在合适时机(默认 1 秒后)判断是否释放该 AB 包;
    • 若未找到缓存:

      • 根据资源路径创建新的 AB 包异步加载请求(若路径未命中,可能尝试类似 "Equip" → "Equip2" 的备选路径);
      • 加载完成后,将 AB 包及对应资源加入缓存,并设置延迟释放策略(默认 1 秒后释放)。
  • 最后,从请求记录字典和当前请求列表中移除该次请求的 ID。

4. 触发回调

  • 根据传入的请求类型触发不同的资源实例化方法:

    • 对于 GameObject 请求:调用 InstantiateGameObject(xxx)
    • 对于资源(如贴图、音频等):调用 InstantiateAsset(xxx)
  • 将实例化后的对象作为参数,调用请求中的回调函数。

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

posted @ 2025-06-12 18:27  mayoyi  阅读(25)  评论(0)    收藏  举报