Appearance
Unity-天命6源码
求职用的,主要扒游戏管理和UI部分。想要可以找我要,我有翻译韩语完的。
游戏中最好不要使用魔法值,所有的值都应该用常量定义出来,放到Scripts/GameDefine下面。
游戏从Logo场景开始,一个简单的动画控制,播放对应商店的Logo Sprite,Logo播放完毕的时候加载Certification场景。
这个系统的UI用的是NGUI,每个面板用不同的摄像机然后叠加culling。把组件都挂到Animation下面,便于控制动画和效果
从Logo场景进入,Logo场景很简单,就是一个logo,判断了一个莫名其妙的宏,加载动画,然后进入Certification场景
Certification场景目前来的功能是验证AB包+验证身份登录(? UI部分重要的结构
C
UStartPackage_Launcher: UStartPackage
Certification下
加载脚本的顺序是:
- UStartPackage_Launcher : UStartPackage
- 这里面塞了三个全局prefab管理器
- GlobalFontManager 字体
- BGM Module 背景音乐
- UI Root Dialog 全局的UI提示弹窗
- 单例触发ULocalPlayer
- ULoginCtrl
- ULauncherCtrl
- 表驱动
- 网络
- 搞定两个UIUStartPackage 设置UI缩放和分辨率,读取预制体,加载三个模块的预制体,中间重置本地客户端的旧数据 如果PlayerPrefs中没有保存数据,则从旧文件中读取信息并保存到PlayerPrefs(我好像没怎么弄懂这东西的数据持久化是怎么管理的 Lancher完事了,会调用PostAwake自定义生命周期函数
ULocalPlayer 是“全局玩家与系统服务的聚合单例”,涵盖资源、网络、设置、前后台行为等。没有全看完,ULocalPlayer.Instance.LocalPushMgr.CancelLocalPush();里面包含了一个服务通知推送管理模块,底层推送使用的是NMSDK 聚合单例使用partial关键字分段编写,降低合作编写难度和耦合。 LobbyMenuData是大厅(战斗外)的核心数据单例,战斗时的核心数据单例使用的是另一个上下文。
此时已经初始化好了UI缩放适配,音乐,游戏设置,AB包的资源,全局字体管理器。
接下来使用的是,ULoginCtrl,
仅针对非战斗部分 首先是基础架构和业务使用分开 基础架构看 网络 数据
跨场景管理
换大厅的时候使用单例里GetLobbySkinSenceName获取皮肤按需加载不同场景,因为不同大厅皮肤里的灯光效果等等,都是不同的 换场景的时候统一抽象中转出GameLoading场景,这个场景下会清理资源AB包,等等,最后进去要进的场景,UI上也能统一加载
感觉只看不太够,得自己古法把这些坑踩一遍才有从头构造的能力。
复习一遍登录的全过程
1. 启动与生命周期:从冷启动到进入大厅
游戏从 Logo 场景开始,播放完 Logo 动画后加载 Certification 场景。Certification 里先走 UStartPackage_Launcher.Awake():设置帧率、写入基础 PlayerPrefs、注册并加载客户端 Config,取消本地推送;随后调用父类 UStartPackage.Awake(),由父类统一做 UI 缩放计算、实例化 startPrefabs,并通知 BGM 模块和全局 Dialog 执行 PostAwake。如果实例化出的 prefab 带有 GlobalFontManager,这里也会顺手设置默认语言。
UStartPackage_Launcher 自己的 Awake 收尾时会初始化游戏系统设置,包括画质、帧率、BGM 音量和音效音量,然后通过 SendMessage("PostAwake") 通知同一个场景里的业务脚本。这里主要进入 ULoginCtrl.PostAwake():禁用系统软键、激活日志查看器、初始化 ULocalPlayer、加载 Launcher 阶段 CSV、播放登录 BGM,并先把状态切到 SDKSignIn。之后启动 StartingDestinyConfig 协程,资源包模式下会先从 CDN 拉最新 DestinyConfig,然后初始化 Netmarble SDK,最后进入 Launcher 流程。
ULoginCtrl 虽然继承自 LoginController,但父类的 SetState() 基本是空实现,实际 UI 状态切换都在 ULoginCtrl.SetState() 里。Launcher 阶段会 SetState(LoginState.Launcher),显示启动弹窗,然后调用 ULauncherCtrl.OnStartLauncher(OnSuccessLauncher)。真正的版本检查、维护检查、补丁检查/下载、表格加载和最终 HandShake 都在 ULauncherCtrl.ProcessLauncher() 中完成。Launcher 完成后回调 OnSuccessLauncher(),关闭系统弹窗,并切换到 LocalLogin 场景。
2. 资源管理:AB 包、版本检查与缓存机制
静态资源通过 AssetBundle(AB 包)进行管理,启动时进行版本检查,对比本地与 CDN 上的版本差异,按需下载补丁并更新本地缓存。
3. 配置与表驱动:CSV、本地化与加载策略
游戏里大量静态配置都不是直接写死在代码里,而是通过 CSV 表驱动。也就是说,代码里只保存一个表 ID,然后通过 UGameTableManager 查到对应的配置对象,再根据配置决定 UI 文本、角色数值、资源路径、功能开关、关卡信息、技能参数等。
3.1 UGameTableManager:全游戏表集合
UGameTableManager 可以理解为客户端侧的“表管理总入口”。它继承自 Singleton<UGameTableManager>,所以业务代码通常通过:
csharp
UGameTableManager.Instance.GetUnitTable(unitId)来查询表数据。
它里面主要有三类内容:
- 泛型表声明
csharp
protected CSVDataTable<UTable_Unit> unitTable = new CSVDataTable<UTable_Unit>();
protected CSVDataTable<UTable_Item> itemTable = new CSVDataTable<UTable_Item>();
protected CSVDataTable<UTable_Define> defineTable = new CSVDataTable<UTable_Define>();
protected CSVDataTable<UTable_GameStrings> gameStringTable = new CSVDataTable<UTable_GameStrings>();CSVDataTable<T> 表示“一张 CSV 表”,泛型 T 表示“每一行解析出来是什么数据类”。比如 unit.csv 的每行会解析成 UTable_Unit,item.csv 的每行会解析成 UTable_Item。
- 路径注册
UGameTableManager.Awake() 里会给每张表注册资源路径:
csharp
unitTable.SetFullPathName(TablePath + "unit");
itemTable.SetFullPathName(TablePath + "item");
defineTable.SetFullPathName(TablePath + "define");
gameImgTable.SetFullPathName(TablePath + "resource_game_image");这里的 TablePath 是:
csharp
protected readonly string TablePath = "CSVTable/";所以 unitTable 实际对应的是 CSVTable/unit 这个资源路径。
- Getter 查询函数
表加载完以后,业务侧通过 getter 访问:
csharp
public UTable_Unit GetUnitTable(int ID) { return unitTable.GetData(ID); }
public UTable_Item GetItemTable(int ID) { return itemTable.GetData(ID); }
public int GetDefineTableValue(int ID) { ... }
public string GetGameStringTableText(int ID) { ... }这就是表驱动在业务层的使用方式:业务代码不关心 CSV 怎么读,只拿 ID 查配置。
3.2 CSVData:所有表行数据的基类
CSVData 是大部分 UTable_xxx 的基类,提供每张表都通用的字段:
csharp
public int id;
public string regionKey;id 是表 ID,也是运行时查表的 key。regionKey 是地区标记,用于区分默认数据和地区覆盖数据。
它的核心解析函数是:
csharp
public virtual void InitializeValue(string[] elements, ref int fieldIdx)
{
id = int.Parse(elements[fieldIdx++]);
regionKey = elements[fieldIdx++].ToLower();
}这表示默认情况下,CSV 每行的前两列是 id 和 regionKey。子类会先调用 base.InitializeValue(...),再继续解析自己的业务字段。
比如 UTable_Unit 会继续解析角色星级、觉醒状态、势力、战斗类型、图标 ID、文本 ID 等:
csharp
public override void InitializeValue(string[] elements, ref int fieldIdx)
{
base.InitializeValue(elements, ref fieldIdx);
grade = int.Parse(elements[fieldIdx++]);
ultimate = int.Parse(elements[fieldIdx++]);
unit_group = (UGameDefine.ERealmType)int.Parse(elements[fieldIdx++]);
battle_type = (UGameDefine.EUnitBattleStyleType)int.Parse(elements[fieldIdx++]);
...
}CSVData 里几个函数的职责:
GetId():返回id,给CSVDataTable<T>存字典时使用。GetTableID():也是返回id,给解析器统一获取表 ID。GetRegionKey():返回地区 key。IsIgnoreRegionKey():默认返回false,表示这张表参与地区覆盖。有些表不需要地区覆盖,会在子类里 override 成true。AfterParse():解析完成后的钩子,子类可以在这里做二次处理。BooleanParse():兼容0/1和true/false两种布尔写法。CollectionAssetData():给资源收集用,表里如果配置了 prefab、贴图、音频路径,可以通过它把依赖资源收集出来。PreLoad():给表数据预加载资源时扩展用。
所以 CSVData 不是一张具体表,而是“表行数据”的统一协议和默认行为。
3.3 CSVDataTable<T>一张表的运行时容器
CSVDataTable<T> 内部最重要的是两个字典:
csharp
private Dictionary<int, System.Object> table = new Dictionary<int, System.Object>();
private Dictionary<string, RegionDataTable> regionDataTable = new Dictionary<string, RegionDataTable>();table 是当前生效的数据,按 id 查询。regionDataTable 是按地区暂存的覆盖数据,后面由 UpdateRegionData() 合并进去。
查询时就是:
csharp
public T GetData(int id)
{
return CSVDataTable<T>.GetData(id, table);
}如果 id <= 0 或者字典里找不到,会返回 default(T),也就是引用类型里的 null。
3.4 Load 和 LoadFromFile 的区别
这两个函数都会清空旧数据,然后读取 TextAsset,最后调用:
csharp
CSVParser<T>.ParseToData(text.text, ref table, ref regionDataTable);区别在于资源来源不同。
Load() 使用项目自己的资源委托层:
csharp
TextAsset text = UResourceDelegator.Instance.LoadResource<UnityEngine.TextAsset>(fullPathName);UResourceDelegator 会根据当前工程模式去资源管理系统里找资源。在 AssetBundle 模式下,它会走 AssetBundle 相关的加载流程;在编辑器或非 AB 模式下,则可能走模拟加载。所以主游戏表通常使用 Load()。
LoadFromFile(string filename) 使用 Unity 原生 Resources.Load:
csharp
TextAsset text = Resources.Load(filename, typeof(TextAsset)) as TextAsset;这要求资源位于 Resources 目录下。启动器表就在:
txt
Assets/Launcher/Resources/Table/所以 LoadLauncherCSV() 会使用:
csharp
appStringTable.LoadFromFile(launcherTablePath + "app_string_" + language.ToLower());
languageFontSizeTable.LoadFromFile(launcherTablePath + "language_font_size");
cdnthemaTable.LoadFromFile(launcherTablePath + "cdn_theme");
cdnThemaSequenceTable.LoadFromFile(launcherTablePath + "cdn_theme_sequence");一句话总结:
txt
Load() = 走 UResourceDelegator,适合主游戏表,AB 模式下可从 AB/资源管理器取
LoadFromFile() = 走 Resources.Load,适合 Launcher 阶段内置在 Resources 下的小表3.5 启动器表和完整表加载
UGameTableManager.Instance.LoadLauncherCSV() 只加载启动器阶段必须用到的少量表,比如登录/补丁界面的本地化字符串、字体大小、CDN 主题信息。它不是完整加载所有游戏表。
完整加载在 LoadFullCSV() 或 AssetBundle 流程的 LoadTableToAssetBundle() 里。
LoadTableToAssetBundle() 的流程是:
csharp
for (int nIndex = 0; nIndex < allTablePaths.Count; ++nIndex)
{
AssetBundleUtil.FLoadAssetOperation operation =
AssetBundleUtil.UAssetBundleManager.LoadAssetOp(allTablePaths[nIndex], typeof(UnityEngine.TextAsset), false);
if (operation != null)
{
yield return StartCoroutine(operation);
}
}
LoadFullCSV();也就是先把所有表资源通过 AB 管理器加载/缓存好,再调用 LoadFullCSV() 把 CSV 内容解析进内存字典。
3.6 CSVParser:CSV 行如何变成 UTable 对象
CSVParser<T>.ParseToData() 会把 CSV 文本按行切开:
csharp
string[] lines = inSource.Split('\n');然后从第 3 行开始解析:
csharp
for (int nIndex = 2; nIndex < lines.Length; ++nIndex)这说明 CSV 前两行大概率是表头、类型说明或注释,真正的数据从第 3 行开始。
每行按分号切字段:
csharp
string[] elements = lines[nIndex].Split(';');然后创建泛型对象:
csharp
T obj = new T();
obj.InitializeValue(elements, ref fieldIdx);
obj.AfterParse();这里没有使用反射自动映射字段,而是每个 UTable_xxx 自己手写 InitializeValue()。这种方式比较原始,但性能和可控性更好,也容易处理枚举、数组、特殊字段。
最后根据 regionKey 判断数据放哪里:
- 默认地区数据进入主表
table - 非默认地区数据进入
regionDataTable
3.7 UpdateRegionData:地区覆盖机制
UpdateRegionData() 是这套表驱动里很关键的一层,它负责把当前地区的配置覆盖到默认配置上。
解析时,如果某行的 regionKey 不是默认地区,解析器不会直接把它加入主表,而是放进:
csharp
regionDataTable[regionKey]等确定当前地区后,UGameTableManager.UpdateRegionData() 会获取当前地区:
csharp
string netmarbleRegionKey = UGameStatices.GetRegion().ToLower();然后对所有表调用:
csharp
unitTable.UpdateRegionData(netmarbleRegionKey);
itemTable.UpdateRegionData(netmarbleRegionKey);
defineTable.UpdateRegionData(netmarbleRegionKey);
...单张表的覆盖逻辑是:
csharp
if (table.ContainsKey(dicEnum.Current.Key))
table.Remove(dicEnum.Current.Key);
table.Add(dicEnum.Current.Key, dicEnum.Current.Value);也就是说,如果当前地区有同 ID 的配置,就删除默认数据,换成地区数据。
例子:
txt
id=100; region=default; value=10
id=100; region=japan; value=12如果当前地区是 japan,那么最终:
csharp
GetData(100)拿到的是 value=12。如果当前地区没有覆盖行,就继续使用 default 数据。
这个设计的好处是:大部分地区共用默认配置,只有需要差异化的 ID 才单独写地区覆盖行。
3.8 表驱动整体链路
完整链路可以概括为:
txt
CSV 文件
-> UGameTableManager 注册表路径
-> Load / LoadFromFile 读取 TextAsset
-> CSVParser<T>.ParseToData
-> 每行生成 UTable_xxx 对象
-> 默认数据进入 table
-> 地区数据进入 regionDataTable
-> UpdateRegionData 按当前地区覆盖
-> 业务代码通过 UGameTableManager getter 按 ID 查询所以这个游戏的表驱动核心不是“动态脚本”或者“规则引擎”,而是很朴素的静态配置系统:策划通过 CSV 配数据,客户端启动时解析成强类型对象,业务代码通过 ID 查询使用。
4. 账号与网络:渠道登录、握手校验与数据拉取
支持多渠道 SDK 登录,登录后与游戏服进行握手校验,校验通过后拉取用户数据,确保账号安全与数据一致性。
5. UI 系统:NGUI 分层、摄像机与 Culling 组织
UI 采用 NGUI 分层设计,合理组织页面、弹窗、全局提示与加载中界面;通过摄像机 Depth 与 Culling Mask 控制渲染层级与可见性。
6. 全局服务:音频、字体、推送与单例管理
全局服务包括音频播放、字体管理、推送通知、日志记录、设备适配等,通常通过全局单例模式统一管理,确保跨场景一致性。
7. 场景与流程编排:场景跳转与状态控制
由流程控制器决定下一个场景,如从登录跳转至大厅、从大厅进入剧情或教程,支持场景恢复与分支跳转逻辑。
8. 本地持久化:PlayerPrefs、数据迁移与缓存
使用 PlayerPrefs 或本地文件持久化客户端设置、登录痕迹与缓存数据;支持旧数据迁移机制,确保版本升级后数据兼容。
9. 网络模块:请求发送、数据拉取与分层设计
网络模块负责请求发送与响应处理,登录后自动拉取用户数据;采用分层设计,分离网络层、业务层与 UI 层,提升可维护性。
10. 表驱动与本地持久化:配置组织与数据落盘
静态配置的表驱动细节见第 3 节。这里需要区分“表数据”和“本地持久化数据”:CSV 表在客户端侧基本是只读配置,主要负责加载、解析、地区覆盖和查询;真正会保存到本地的更多是客户端设置、玩家偏好、登录痕迹、教程状态、队伍编辑缓存等运行时数据。
11. 场景管理:启动链路、登录跳转与恢复逻辑
管理从启动到登录、登录到大厅、教程/剧情恢复等完整链路;通过状态机或流程控制器决定场景跳转时机与恢复策略。
12. UI 架构:NGUI 分层、摄像机与弹窗组织
UI 架构基于 NGUI 实现分层渲染,通过摄像机 Depth 与 Culling Mask 控制层级;统一管理页面、弹窗、全局提示与加载界面。
13. MVC 业务写法:控制器、视图与数据交互
采用 MVC 模式拆分业务逻辑,控制器处理按钮事件与状态刷新,视图负责 UI 表现,数据层读取表配置与网络回包,实现 UI 与逻辑解耦。
14. 资源管理:AB 包、版本检查与启动器设计
资源管理以 AB 包为核心,启动器负责版本检查与补丁下载;优先完成补丁更新再进入登录流程,确保资源一致性。
15. 全局单例与基础设施:大总管类的职责划分
全局单例如 ULocalPlayer、UGameStatics、UGameTableManager 等承担全局能力,包括玩家状态、游戏配置、表数据管理等,作为基础设施支撑各模块运行。