三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

基于GameFramework的SLG游戏项目结构与资源管理规范实践

基于GameFramework的SLG游戏项目结构与资源管理规范实践

1. 项目概述:为什么SLG游戏需要一个“框架”?

如果你正在或打算开发一款SLG(Simulation Game,模拟策略游戏),无论是《文明》那样的4X大作,还是《部落冲突》那样的城建对战,你大概率已经体会过项目中期那种“剪不断,理还乱”的痛苦。SLG游戏天生就复杂:庞大的资源系统(金币、木材、粮食、士兵),多层次的建筑与科技树,异步的玩家交互,以及海量的UI界面和本地化需求。当你的Assets文件夹里塞满了ScriptsPrefabsScenes,而团队成员还在为“这个资源加载该用Resources.Load还是AssetBundle”争论不休时,项目就已经走在失控的边缘了。

混乱的开发,往往始于混乱的项目结构。一个没有规范约束的Unity项目,就像一座没有城市规划的都市,初期可能生机勃勃,但随着规模膨胀,交通堵塞(依赖混乱)、建筑危楼(代码耦合)、资源浪费(冗余资产)等问题会接踵而至,最终导致开发效率断崖式下跌,甚至项目重构或失败。

这正是我们需要一个成熟框架的原因。它不是一个限制创造力的枷锁,而是一套经过实战检验的“城市规划方案”和“市政管理条例”。Unity GameFramework (GF)正是这样一套由国内开发者社区广泛认可的开源游戏框架。它并非要取代Unity,而是为Unity项目补上工业化生产中最关键的一环:规范与流程。GF提供了一套从资源管理、配置表、UI、实体、场景、声音到网络请求的完整模块化解决方案,并强制你按照其约定的方式去组织代码和资源。

今天,我们就聚焦于SLG游戏开发中最基础、也最致命的两环:项目结构资源管理规范。我将带你从零开始,基于GameFramework,搭建一个清晰、可扩展、便于团队协作的SLG项目骨架。这不是一篇简单的“Hello World”教程,而是一位踩过无数坑的同行,为你绘制的一张避坑地图和施工蓝图。

2. 核心需求解析:SLG游戏对框架的独特要求

在套用任何框架前,我们必须先理解SLG游戏的独特性,这样才能明白GF的哪些特性是为我们量身定制的。

2.1 海量配置数据驱动

SLG游戏的核心玩法——建筑升级条件、兵种属性、科技效果、任务奖励——几乎全部由数值配置表驱动。这意味着我们需要一个强大、易用且支持热更的配置表加载方案。手动写ScriptableObject或解析JSON文件在小型项目中可行,但在拥有成百上千张配置表的SLG中,维护和更新将是噩梦。GF内置的配置表(Data Table)模块,支持从Excel等格式生成强类型的二进制或JSON数据文件,并提供了便捷的加载、读取接口,完美契合此需求。

2.2 复杂的UI层级与状态管理

一个典型的SLG主界面可能同时包含资源栏、建筑队列、活动入口、邮件提示、聊天窗口等数十个UI元素。如何管理它们的打开、关闭、层级关系、数据刷新?GF的UI模块提供了基于UI组(UIGroup)的层级管理,和基于界面逻辑脚本(UIFormLogic)的标准化开发流程,让复杂的UI栈变得井然有序。

2.3 资源加载的多样性与性能

SLG的资源类型极其繁杂:图标、模型、特效、音频、配置表、本地化文本。有些资源(如UI图标)需要常驻内存,有些(如过场动画)用完即可卸载。GF的资源管理模块是其核心,它抽象了资源加载过程,统一了ResourcesAssetBundle的加载接口,并提供了依赖引用计数、自动释放等高级功能,让你无需关心底层是哪种加载方式,只需关注“加载”和“释放”这两个业务逻辑。

2.4 实体与对象池的频繁使用

游戏中的士兵、建筑、特效等都是“实体”。SLG中经常有成百上千的单位同时存在,频繁创建和销毁GameObject会造成严重的GC(垃圾回收)压力。GF的实体模块对象池模块深度集成,为游戏对象的生命周期管理提供了标准化方案,能极大提升运行时性能。

2.5 可扩展的模块化架构

SLG玩法迭代快,经常需要增加新系统(如联盟战、赛季玩法)。一个高内聚、低耦合的架构至关重要。GF本身采用模块化设计,我们基于GF构建的项目也应如此,确保新功能可以像插件一样方便地接入,而不影响旧有代码。

理解了这些需求,我们就能有的放矢地利用GameFramework来搭建我们的项目。

3. 项目整体结构设计与思路拆解

一个基于GameFramework的SLG项目,其结构应该是“框架层 + 游戏逻辑层”的清晰划分。我们的目标是将所有游戏特有的代码和资源,都组织在框架约定好的位置,形成一种“约定大于配置”的开发习惯。

3.1 目录结构规划

我们不从零开始创造结构,而是遵循并扩展GF推荐的最佳实践。以下是一个典型的SLG项目Assets目录结构:

Assets/ ├── GameFramework/ # GameFramework 框架源码(可通过Package Manager安装) ├── GameMain/ # 游戏逻辑主目录(这是我们工作的核心区域) │ ├── Scripts/ # 所有游戏逻辑C#脚本 │ │ ├── Base/ # 基础类、枚举、常量定义 │ │ ├── Definition/ # 数据结构定义(如玩家数据、建筑数据) │ │ ├── Runtime/ # 运行时逻辑(按模块划分) │ │ │ ├── UI/ # UI相关逻辑(继承自UILogicBase) │ │ │ ├── Entity/ # 实体相关逻辑(继承自EntityLogic) │ │ │ ├── Procedure/ # 流程控制(继承自ProcedureBase) │ │ │ ├── DataNode/ # 数据结点存取逻辑 │ │ │ ├── Event/ # 游戏内事件定义与派发 │ │ │ └── Manager/ # 自定义管理器(如战斗管理器、联盟管理器) │ │ ├── Editor/ # 编辑器扩展脚本 │ │ └── Libraries/ # 第三方插件或自研库(非GF) │ ├── Resources/ # 必须通过Resources.Load加载的资源(尽量少) │ ├── Configs/ # 原始配置表(如Excel文件) │ ├── Localization/ # 本地化文本文件 │ ├── Fonts/ # 字体文件 │ ├── Materials/ # 材质球 │ ├── Models/ # 模型文件(FBX等) │ ├── Prefabs/ # 预制体(按UI、Entity、Effect等子文件夹分类) │ ├── Scenes/ # 场景文件(如Launch, Main, Battle) │ ├── Sounds/ # 音效和背景音乐 │ ├── Textures/ # 纹理图片(按UI、Icon、Map等分类) │ ├── UI/ # UI相关资源(UGUI的Atlas、Sprite等) │ └── Shaders/ # 自定义Shader ├── Packages/ # Unity Package Manager 管理的包 └── ProjectSettings/ # Unity项目设置(通常不入库)

为什么这样设计?

  1. 隔离框架与业务GameFramework是稳定的底层,GameMain是易变的业务层。两者分离,便于框架升级和业务代码管理。
  2. 按功能而非类型组织脚本:传统的Scripts下直接放所有MonoBehaviour的做法会导致后期难以查找。按Base,Definition,Runtime/UI等分类,使代码职责更清晰。Runtime下的文件夹对应GF的核心模块,天然契合。
  3. 资源分类存储:将资源按类型(Textures, Sounds, Prefabs)而非用途存放,是Unity项目管理的常见误区。我们这里采用了一种混合策略:在GameMain下先按大类型分,再在内部(如Prefabs)按用途分子文件夹。这平衡了资源管理员的便利性和程序加载的直观性。更关键的是,这个结构是为配合GF的资源分组功能准备的。
  4. 明确Resources目录用途:GF虽然主要使用AssetBundle,但启动器、初始化配置等极少数资源仍需通过Resources加载。将此目录单独列出并严格控制内容,避免滥用。

3.2 与GameFramework的对接点

我们的GameMain目录需要与GF的几个核心组件建立联系:

  • 启动场景:通常是一个极简场景,只包含GameEntry预制体(GF入口)。该场景路径可放在Assets/Scenes/Launch.unity,但更常见的做法是放在GameMain/Scenes/Launch.unity
  • 流程(Procedure):游戏状态机。我们需要在GameMain/Scripts/Runtime/Procedure/下创建自己的流程类,如ProcedureLaunch,ProcedurePreload,ProcedureMain等,并在GameEntry中注册它们。
  • UI与实体:我们自定义的UILogicBaseEntityLogic子类,必须放在GameMain/Scripts/Runtime/UI/.../Entity/下,并在对应的UIFormEntity预制体上挂载。
  • 资源配置(AssetBundle):这是重中之重。我们需要创建一个资源收集和构建的配置文件,告诉GF如何将GameMain下的资源打包成AssetBundle。

4. 资源管理规范深度解析与实操要点

资源管理是GF框架的精华所在,也是SLG项目规范的基石。理解并正确运用它,能解决80%的性能和协作问题。

4.1 资源分组策略:告别“一锅烩”

GF允许你将资源划分到不同的“组”(Group)中,每个组可以独立打包、加载和卸载。对于SLG游戏,合理的分组策略如下:

  1. 基础组(Base):包含游戏启动所必须的资源,如初始化UI、游戏字体、基础配置表。此组在游戏启动时加载,常驻内存。
  2. 核心UI组(UICommon):包含所有通用UI的预制体、图集和音效,如弹窗、按钮、通用图标。常驻或高频使用。
  3. 场景组(Scene_[Name]):按场景划分。例如Scene_MainCity包含主城的所有模型、纹理、场景光照贴图等。进入主城时加载,离开时卸载。
  4. 功能模块组(Module_[Name]):按游戏功能划分。例如Module_Hero包含所有英雄相关的模型、技能特效、UI;Module_Building包含所有建筑模型和升级特效。这种分组最适合SLG,因为玩家可能长时间停留在主城,但只在需要时才查看英雄或建筑界面,实现资源的按需加载。
  5. 本地化组(Localization_[Language]):按语言分包。玩家只需下载当前语言的资源包。

实操步骤:配置资源收集规则在Unity编辑器中,通过Tools/Game Framework/Resource Editor打开资源编辑器。

  1. Assets窗格中,浏览到你的GameMain目录。
  2. 将需要打包的资源或文件夹拖入中间的“资源列表”。
  3. 在右侧,为这组资源设置关键属性:
    • 组名(Group Name):如UICommon
    • 变体(Variant):可用于区分同一资源的不同版本(如高清/标清),SLG中不常用。
    • 文件系统(File System):通常选择Read-Write,表示打包进AssetBundle。
    • 加载类型(Load Type):对于小图集或配置,选LoadFromMemory;对于大纹理或模型,选LoadFromFile以减少内存占用。
  4. 为所有资源分组后,点击Save保存配置(如GameMain/Configs/ResourceRule.xml)。

4.2 资源加载与释放的黄金法则

GF提供了GameEntry.GetComponent<ResourceComponent>().LoadAsset()等接口。在SLG开发中,必须严格遵守以下法则:

  • 谁加载,谁释放:这是一个基本原则。在UI界面OnOpen时加载资源,在OnClose时释放。在实体OnShow时加载模型,在OnHide或回收至对象池时释放。
  • 使用引用计数:GF的资源组件内置了引用计数。同一个资源被多个地方请求时,只有所有引用都释放后,资源才会被真正卸载。这避免了重复加载和过早卸载。
  • 避免同步加载:尤其在移动端,同步加载LoadAsset会阻塞主线程,导致卡顿。务必使用异步加载LoadAssetAsync,并提供回调函数或配合async/await(需GF支持或自行封装)。
  • 预加载关键资源:在进入核心玩法(如主城)前的加载界面,使用PreloadResources接口预加载该场景/模块所需的关键资源组,提升体验流畅度。

代码示例:在UI界面中异步加载图标

// 在某个UILogicBase的子类中 private AssetOperationHandle m_IconHandle; // 保存操作句柄,用于释放 private IEnumerator LoadHeroIconAsync(int heroId) { // 构建资源路径,假设图标资源按模块分组在Module_Hero中 string assetName = AssetUtility.GetHeroIconAsset(heroId); // 自定义工具类,返回如"Assets/GameMain/Textures/UI/Hero/hero_001.png" // 异步加载 m_IconHandle = GameEntry.GetComponent<ResourceComponent>().LoadAssetAsync<Texture>(assetName); yield return m_IconHandle; if (m_IconHandle.IsValid && m_IconHandle.AssetObject != null) { m_IconImage.texture = m_IconHandle.AssetObject as Texture; } else { // 加载失败处理 Debug.LogError($"Load hero icon failed: {assetName}"); } } // 在界面关闭时释放资源 protected internal override void OnClose(bool isShutdown, object userData) { base.OnClose(isShutdown, userData); if (m_IconHandle != null) { m_IconHandle.Release(); m_IconHandle = null; } }

4.3 配置表管理:SLG的数据引擎

GF的配置表模块能将Excel等结构化数据,转换为游戏内高效访问的二进制文件。流程如下:

  1. 设计Excel:在GameMain/Configs/下创建Excel,如Hero.xlsx,定义好字段(如ID, Name, HP, Attack)。
  2. 生成数据文件:使用GF提供的工具(或自定义编辑器脚本),将Excel导出为.bytes二进制文件或.json文件,并自动生成对应的强类型C#数据类(如DRHero)。
  3. 加载与读取:在游戏初始化时,加载配置表文件。之后,便可以通过ID快速读取数据。
// 预加载所有配置表 GameEntry.GetComponent<DataTableComponent>().LoadAllDataTables(); // 读取数据 DRHero heroData = GameEntry.GetComponent<DataTableComponent>().GetDataRow<DRHero>(1001); if (heroData != null) { Debug.Log($"Hero Name: {heroData.Name}, HP: {heroData.HP}"); }

对于SLG,配置表可能非常庞大。建议按模块分组加载,而非一次性加载全部。

5. 核心模块的规范实现与代码组织

有了清晰的结构和资源管理策略,接下来我们需要规范核心模块的代码编写。

5.1 UI模块:界面开发的标准化流程

  1. 创建UI预制体:在GameMain/Prefabs/UI/下创建界面预制体,例如UIMainCityForm.prefab
  2. 绑定UI逻辑脚本
    • GameMain/Scripts/Runtime/UI/下创建UIMainCityForm.cs
    • 该类必须继承自UILogicBase(这是GF的UI逻辑基类)。
    • 在预制体根节点上添加UIForm组件,并将UI Form Logic设置为UIMainCityForm
  3. 自动化绑定UI控件:GF支持通过[SerializeField]和代码生成工具来绑定UI控件,避免手写Transform.Find。你可以使用GF自带的工具或像GameFramework中常见的UIComponentBinder这类自定义编辑器脚本,自动将预制体中的Button,Image,Text等控件引用赋值到逻辑脚本的字段中。
  4. 界面生命周期与数据通信
    • OnInit: 界面初始化,获取组件引用。
    • OnOpen: 界面打开,接收并处理传入的参数(userData)。
    • OnClose: 界面关闭,执行资源释放等清理操作。
    • 界面间的通信应尽量通过事件(Event)数据结点(Data Node),而非直接引用,以降低耦合度。

5.2 实体与对象池:管理游戏中的动态对象

  1. 创建实体预制体:在GameMain/Prefabs/Entity/下创建,如EntitySoldier.prefab
  2. 绑定实体逻辑脚本
    • GameMain/Scripts/Runtime/Entity/下创建EntitySoldierLogic.cs
    • 继承EntityLogic
    • 在预制体上添加Entity组件,并绑定该逻辑脚本。
  3. 使用对象池:不要直接InstantiateDestroy实体。
// 从对象池获取一个士兵实体 int entityId = GameEntry.GetComponent<EntityComponent>().ShowEntity<EntitySoldierLogic>( "EntitySoldier", // 实体资源名 "SoldierGroup", // 实体所属的组(在Entity Editor中配置) priority: 0, // 加载优先级 userData: soldierData // 初始化数据 ); // 隐藏(回收)实体 GameEntry.GetComponent<EntityComponent>().HideEntity(entityId);
  1. 实体生命周期OnShow(显示/创建时)、OnUpdate(每帧更新)、OnHide(隐藏/回收时)。在OnShow中根据userData初始化实体状态,在OnHide中重置状态并释放资源。

5.3 流程(Procedure):游戏状态机

流程控制游戏的整体状态流转,如启动->检查资源->更新资源->登录->主城。

  1. 创建流程脚本:在GameMain/Scripts/Runtime/Procedure/下创建,如ProcedureLaunch.cs
  2. 继承与重写:继承ProcedureBase,重写OnEnter,OnUpdate,OnLeave等方法。
  3. 状态切换:在流程内部,通过ChangeState方法切换到下一个流程。
// 在 ProcedureLaunch 的 OnUpdate 中 protected override void OnUpdate(ProcedureOwner procedureOwner, float elapseSeconds, float realElapseSeconds) { base.OnUpdate(procedureOwner, elapseSeconds, realElapseSeconds); // 假设启动动画播放完毕 if (/* 条件满足 */) { // 切换到预加载流程 ChangeState<ProcedurePreload>(procedureOwner); } }
  1. 注册流程:在游戏入口脚本(或自定义的GameEntry扩展脚本)中,将所有流程添加到流程组件。
// 在 GameEntry 的某个初始化方法中 ProcedureComponent procedureComponent = GetComponent<ProcedureComponent>(); procedureComponent.Initialize(new Type[] { typeof(ProcedureLaunch), typeof(ProcedurePreload), typeof(ProcedureMain), // ... 其他流程 }); procedureComponent.StartProcedure<ProcedureLaunch>(); // 从启动流程开始

6. 构建、打包与持续集成规范

规范不仅体现在开发期,也贯穿于构建和发布流程。

6.1 资源打包(AssetBundle Build)

  1. 使用GF工具:通过Tools/Game Framework/Build Tools打开构建工具。
  2. 配置构建参数
    • Build Target: 选择目标平台(Android, iOS, Windows等)。
    • Internal Resource Version: 内部资源版本号,每次打包递增,用于资源热更判定。
    • Compression: 压缩格式,LengthFast(LZ4)在加载速度和包体大小间取得较好平衡。
    • Additional Flags: 根据平台需要设置。
  3. 执行构建:点击Build,GF会根据之前Resource Editor中配置的规则,将GameMain下的资源打包成AssetBundle,输出到AssetBundles/[Platform]目录。同时会生成一个Version.txt文件,记录所有资源的版本、哈希值和大小,这是资源热更的基础。

6.2 代码编译与程序集定义(Assembly Definition)

随着项目扩大,编译时间会变长。可以使用.asmdef文件将代码分割成多个程序集,实现增量编译。

  1. GameMain/Scripts/Runtime下的每个子文件夹(如UI,Entity)创建.asmdef文件。
  2. 合理设置程序集之间的引用关系。例如,GameMain.Base程序集(定义基础数据)可以被所有其他程序集引用,但GameMain.UI程序集不应引用GameMain.Entity
  3. 这不仅能加快编译速度,还能强制实施模块间的依赖关系,防止循环引用,让架构更清晰。

6.3 版本管理与热更流程

  1. 版本号:统一游戏版本号格式,如1.0.0.123(主版本.次版本.修订版.构建号)。构建号每次出包自动递增。
  2. 资源热更
    • 游戏启动时,对比本地的Version.txt与服务器上的最新Version.txt
    • 通过GF的ResourceComponent.CheckVersionList()UpdateResources()接口,下载有变动的AssetBundle文件。
    • 这个过程可以放在ProcedureCheckResources流程中自动完成。
  3. 代码热更:对于Unity项目,代码热更通常依赖Lua等脚本语言或HybridCLR等C#热更方案。GF本身不提供代码热更,但可以与这些方案集成。如果使用HybridCLR,需要将热更代码放在独立的程序集中,并通过GF的资源系统下载和加载这些DLL。

7. 团队协作与开发流程规范

再好的框架和结构,没有团队协作规范,也会形同虚设。

7.1 版本控制(Git)规范

  1. .gitignore:必须正确配置。忽略Library/,Temp/,Obj/,Logs/,AssetBundles/等文件夹,以及用户特定的项目设置文件。
  2. 提交信息:使用清晰的提交信息格式,如[UI] 新增主城界面框架[Fix] 修复士兵移动卡顿bug
  3. 分支策略:推荐使用Git Flow或简化版。main分支用于发布,develop分支用于集成最新开发内容,每个新功能或修复从develop拉出feature/xxx分支,开发完成后合并回develop
  4. 处理Unity Meta文件:必须将.meta文件纳入版本控制。它们记录了资源(包括文件夹)的GUID和导入设置,是保证资源引用正确的关键。团队成员在移动或重命名资源时,务必在Unity编辑器内操作,以保证.meta文件同步移动。

7.2 代码审查与命名规范

  1. 命名约定
    • 类名、方法名、属性名:使用PascalCase,如UIMainCityForm,LoadHeroData
    • 私有字段:使用_camelCase,如_currentHeroId
    • 局部变量、参数:使用camelCase
    • 常量:使用UPPER_SNAKE_CASE,如MAX_HERO_COUNT
  2. 代码风格:统一使用.editorconfig文件或团队约定的IDE格式化设置,确保缩进、空格、换行一致。
  3. 审查要点:在合并请求(Merge Request)中,重点审查资源加载/释放是否成对、事件监听是否及时移除、公共API设计是否合理、是否有性能隐患(如每帧FindGetComponent)。

7.3 文档与知识沉淀

  1. 项目README:在仓库根目录维护README.md,说明项目结构、框架使用指南、构建步骤、常见问题。
  2. 模块文档:在每个核心模块的脚本文件夹内,放置简单的README.md,说明该模块的职责、主要接口和示例。
  3. 配置表文档:维护一个在线文档(如Confluence)或一个总览Excel,描述每张配置表的用途、字段含义和关联关系。
  4. 会议纪要与决策记录:重要的技术决策(如为什么选择某种资源分组策略)应记录下来,避免日后遗忘或产生争议。

8. 常见问题、排查技巧与避坑指南

在实际开发中,你一定会遇到各种问题。以下是一些高频问题的解决方案和避坑经验。

8.1 资源加载失败

  • 问题LoadAssetAsync回调中,AssetObjectnull,或报错“Asset not found”。
  • 排查
    1. 检查资源路径:确保传入LoadAssetAsync的路径与资源收集规则中配置的完全一致,包括大小写。使用AssetUtility工具类统一生成路径。
    2. 检查资源是否被打包:打开构建后的AssetBundle文件查看工具(如AssetStudio),确认你的资源是否在预期的Bundle中。
    3. 检查依赖:如果资源A依赖资源B(如材质依赖纹理),确保它们被打包在同一个Bundle,或者B所在的Bundle已被提前加载。GF的资源组件会自动处理依赖加载,但前提是依赖关系在打包时被正确记录。
    4. 检查平台:确保你加载的AssetBundle是为当前运行平台构建的。

8.2 UI界面打开异常或控件绑定失效

  • 问题:打开UI时报空引用,或控件显示不正常。
  • 排查
    1. 检查UI逻辑脚本绑定:在Unity编辑器中,确认预制体上的UIForm组件是否正确引用了你的UILogicBase子类脚本。
    2. 检查控件自动绑定:如果使用自动绑定工具,检查生成的代码字段名是否与预制体中的游戏对象名匹配。对象名修改后,需要重新生成绑定代码。
    3. 检查UI组(UIGroup)深度:确保你的UI界面被打开到了正确的UIGroup(如Background,Normal,Popup,Tips),并且该组的深度设置合理,避免被遮挡。
    4. 检查打开参数:在OnOpen中正确处理userData参数,做好空值判断和类型转换。

8.3 对象池对象状态残留

  • 问题:从对象池中取出的实体,还保持着上次被回收时的状态(如血量为0、特效播放中)。
  • 解决:务必在实体的OnHideOnRecycle方法中,重置所有可变状态。例如,停止所有协程、取消所有动画、重置血量到初始值、隐藏所有子特效等。对象池只是复用GameObject,不会自动帮你重置脚本上的数据。

8.4 流程切换卡住或循环

  • 问题:游戏卡在某个流程(如加载界面)不动,或流程间循环切换。
  • 排查
    1. 检查切换条件:确保流程切换的条件逻辑正确,避免在OnUpdate中每帧都触发ChangeState
    2. 使用调试日志:在每个流程的OnEnterOnLeave开始处添加日志,清晰跟踪流程流转路径。
    3. 检查异步操作:如果流程中启动了异步加载(如资源更新),确保在异步操作完成后再进行状态切换,可以使用协程或async/await配合一个状态标志位来管理。

8.5 打包后日志丢失或不完整

  • 问题:在Editor中运行正常,但打包后Debug.Log信息看不到,或者GameFramework的日志不输出。
  • 解决
    1. 确认日志辅助器:GF使用LogHelper来记录日志。确保在打包时,使用的LogHelper实现是适用于目标平台的(如DefaultLogHelper在开发包中可用,发布包中可能需关闭)。
    2. 配置Build Settings:在File -> Build Settings -> Player Settings...中,找到Scripting Define Symbols,为开发包添加ENABLE_LOG符号。在GF的设置中,也可以控制日志输出级别。
    3. 使用文件日志:对于线上问题排查,可以集成GF的FileLogHelper,将日志写入到设备持久化路径下的文件中,方便拉取分析。

8.6 内存泄漏排查

  • 现象:游戏运行一段时间后,内存持续增长,甚至导致崩溃。
  • 工具:使用Unity Profiler的Memory模块,定期抓取快照对比。
  • 常见原因
    1. 资源未释放:检查所有AssetOperationHandleGameObject实例是否在适当的时候被释放或销毁。特别注意被静态对象或全局事件持有的引用。
    2. 事件监听未移除:在OnEnableOnOpen中订阅的事件,必须在对应的OnDisableOnClose中取消订阅。否则,该对象将无法被垃圾回收。
    3. 协程未停止:由StartCoroutine启动的协程,如果其所在的MonoBehaviour被禁用或销毁,而协程内含有无限循环或等待一个永远不会发生的事件,可能导致隐式引用。使用StopCoroutine或在OnDestroy中妥善处理。

从零开始用GameFramework搭建一个规范的SLG项目,初期确实会比随意创建脚本和文件夹花费更多时间。你会经历配置资源规则的繁琐,适应模块化编程的约束,学习框架特有的API。但当你和团队度过这个磨合期,你会发现,所有的前期投入都在后期得到了超额回报:新人能快速上手,模块间耦合度低,资源加载井然有序,线上问题易于定位和修复。这套规范就像为你的游戏项目注入了“秩序”基因,让它即使在面对SLG这种复杂需求时,也能保持健康的生长态势,稳步走向成功。

← 返回列表