从"会用编辑器"到"能改引擎、能扛性能"
嗨皮的个人技术笔记,覆盖三个方向:Cocos Creator 3.8.x 客户端、
Godot 4.6 客户端与 Go 游戏服务器。
内容包括引擎帧循环、资源与内存、Shader 与后处理、DrawCall 与性能优化、服务器架构与并发、
网络协议、数据层与写回、战斗同步与 pprof 剖析。所有示例均为可迁移到工程中的真实代码形态,非 "Hello World" 级别。
01学习路径与知识地图
Cocos Creator 3.8 是一个"TS 脚本层 + C++ 原生内核 + GFX 图形抽象层"的三层引擎。想真正吃透它,必须同时理解这三层各自的职责边界。
1.1 三层架构与职责边界
| 层次 | 位置 | 职责 | 你会在什么时候碰到它 |
|---|---|---|---|
| 脚本层(TS) | engine/cocos/ | 组件系统、UI、动画、资源管理、调度器,暴露 cc 命名空间 | 日常 90% 的业务开发 |
| 原生内核(C++ / JSB) | native/cocos/ | 场景剔除、渲染管线执行、物理、模型/材质底层、内存与线程 | 排查性能瓶颈、原生热更、改引擎 |
| 图形抽象层 GFX | native/cocos/renderer/gfx-* | 把 WebGL2 / Metal / Vulkan / GLES 统一成一套 Device 接口 | 写 Shader、做后处理、查 GPU 侧耗时 |
关键认知:JS 层只负责"提交数据",真正的渲染发生在 C++ 侧。所以"减少每帧的 JS 计算"与"减少 GPU 提交"是两件独立的事,优化前必须先定位瓶颈在哪一侧。
1.2 四阶段学习路径
阶段一 · 打地基(1~2 周)
组件生命周期、节点树与变换、事件系统、资源加载与释放、Prefab 与场景切换。产出:一个跑得起来、切场景不泄漏的小项目骨架。
阶段二 · 看得见的技术(2~4 周)
Surface Shader 体系、材质参数与宏、渲染管线与 RenderTexture 后处理、相机与 Layer、光照与阴影配置。
阶段三 · 扛得住的性能(1~2 月)
DrawCall 与合批、GC 与对象池、纹理内存与压缩、骨骼与粒子开销、分帧加载、性能剖析工具链。
阶段四 · 工程化与交付(持续)
运行时架构(服务 / 系统 / UI 栈)、Asset Bundle 拆分、热更新与灰度、多平台构建与首包优化、编辑器扩展提效。
1.3 一条硬性工作习惯
做任何优化前,先建立可复现的测量:固定测试机型、固定场景、固定采样窗口,记录 帧时间 / DrawCall / 三角面 / 纹理内存 / JS 堆。没有基线的优化等于玄学。下面这段代码就是基线采集器的最小实现。
02引擎架构与帧循环
不理解帧循环,你就无法回答"我的代码到底在这一帧的哪个时刻执行"——而 80% 的时序类 Bug 都源于此。
2.1 一帧里发生了什么
| 阶段 | 回调 / 事件 | 典型用途 |
|---|---|---|
| 输入与平台事件 | 触摸 / 键盘派发、EventTarget | 输入采样、手势识别 |
| 帧开始 | Director.EVENT_BEFORE_UPDATE | 帧级前置逻辑(如网络消息出队) |
| 组件启动 | 新增组件的 start() | 依赖其他节点的初始化 |
| 主更新 | 组件 update(dt) + 调度器 | 游戏逻辑 |
| 后更新 | 组件 lateUpdate(dt) | 相机跟随(必须在被跟随物移动之后) |
| 帧结束 | Director.EVENT_AFTER_UPDATE | 帧级收尾、性能采样 |
| 物理步进 | 固定步长模拟(可多次子步) | RigidBody 位姿计算 |
| 动画 / 粒子 / 骨骼 | Animation、ParticleSystem | 蒙皮与粒子模拟 |
| 渲染 | EVENT_BEFORE_DRAW → 剔除 → 合批 → 提交 GFX | 最后写相机 / RenderTexture 的时机 |
| 帧末 | EVENT_AFTER_DRAW | 帧截图、读回像素 |
update 里的 dt 是真实帧间隔,在低端机上可能是 100ms+。任何"每帧移动固定距离"的逻辑都必须乘 dt;
物理相关的推进必须走固定步长(见第 09 章),否则不同帧率下手感与碰撞结果都不一致。
2.2 全局系统的挂载点:常驻节点
跨场景常驻的全局系统,正确姿势是"一个常驻根节点 + 一个驱动器组件",而不是在每个场景里放一份相同组件。
03组件生命周期与执行顺序
生命周期不是"背下来"的知识点,而是你判断"这段代码能不能拿到有效数据"的依据。
3.1 完整生命周期与判定规则
| 回调 | 触发时机 | 次数 | 关键约束 |
|---|---|---|---|
onLoad | 节点首次激活时(含所在子树激活) | 一次 | 节点 active=false 时不会触发;适合取自身引用与初始化自身状态 |
onEnable | 组件 enabled 且节点激活 | 每次激活 | 事件监听在此注册,保证与 onDisable 严格配对 |
start | 首次 update 前 | 一次 | 访问其它节点的可靠位置;若在 onLoad 中节点被销毁则不会执行 |
update | 每帧 | N | 务必乘 dt;不要在这里 new 对象 |
lateUpdate | 全部 update 之后 | N | 相机跟随、IK、UI 跟随世界坐标 |
onDisable | 组件或节点失活 | 每次 | 与 onEnable 一一对应解绑 |
onDestroy | node.destroy() 生效时 | 一次 | 不等于内存立即释放,JS 对象等待 GC;资源需 decRef |
onLoad 里访问"另一个还没 onLoad 的节点"→ 数据为空,改到 start;
② 在 onEnable 注册监听却在 onDestroy 解绑 → 反复开关界面导致监听器累积(表现为点击一次触发 N 次);
③ 节点 active=false 时指望 onLoad 执行 → 永远不执行,被引用对象保持 null。
3.2 调度器:比 update 更精确的工具
组件内置 schedule / unschedule / unscheduleAllCallbacks 基于引擎调度器,适合低频任务;每帧都要跑的逻辑留在 update 里更省(调度器有额外的遍历开销)。
04资源管理与内存
Creator 的资源释放基于依赖图 + 引用计数。理解这两件事,就能解释 90% 的"资源没释放 / 资源被误释放"问题。
4.1 三个必须记住的概念
- 依赖图(dependUtil):每个资源知道自己依赖谁。加载一个 Prefab 会连带加载其贴图、材质、图集。
- 引用计数(addRef / decRef):
load出来的资源 refCount ≥ 0;只有releaseUnusedAssets()或releaseAsset()才会真正卸载。 - Bundle 生命周期:Bundle 本身持有其内部资源引用,
assetManager.removeBundle(bundle)会连带释放里面的资源。
releaseAsset(prefab) → 贴图变白 / 材质丢失;正确顺序是 销毁所有实例节点 → 确认无引用 → decRef → releaseUnusedAssets。
② 静态变量 / 数组缓存了 SpriteFrame 却忘了清 → 计数永不归零。
③ 场景中直接拖拽引用的资源由场景持有计数,不需要你手动 addRef,误调 decRef 会导致场景内资源被卸载。
4.2 自定义下载管线:加密资源配置
AssetManager 的下行管线是"预处理 → 下载 → 解析",可以针对扩展名注册自定义下载器,实现配置解密 / 自定义协议。
4.3 内存账本:纹理到底占多少
纹理内存 ≈ 宽 × 高 × 每像素字节 × (开启 mipmap 时约 1.33)。一张 2048×2048 的 RGBA8 贴图 = 16MB,含 mipmap ≈ 21MB。压缩纹理能直接降到 1/4 ~ 1/8:
| 格式 | bits/px | 2048² 占用 | 适用平台 |
|---|---|---|---|
| RGBA8 | 32 | 16 MB | 全平台(兜底) |
| ASTC 6x6 | 3.56 | ≈1.8 MB | iOS A8+ / 高端 Android / 小游戏(推荐) |
| ETC2 RGBA | 8 | 4 MB | Android GLES3.0+ |
| PVRTC 4bpp | 4 | 2 MB | 旧 iOS(需 2 的幂尺寸) |
05渲染管线与 Surface Shader
3.8 的材质体系核心是 Surface Shader 框架:引擎把 PBR 光照/阴影/雾的公共代码封装好,你只通过宏与钩子函数注入自己的逻辑。这是 3.x 与 2.x 写 Shader 最大的区别。
5.1 Effect 文件结构
一个 .effect = CCEffect(渲染状态与属性声明)+ 若干 CCProgram(GLSL 代码块)。
5.2 Surface Shader 的注入点
Surface 框架通过"定义宏 → 实现同名函数"的方式让你介入渲染流程,而不用重写整套光照:
| 宏 | 需实现的函数 | 用途 |
|---|---|---|
CC_SURFACES_VERTEX_MODIFY_WORLD_POS | SurfacesVertexModifyWorldPos(inout vec3 worldPos) | 顶点动画:风摆、呼吸、顶点位移 |
CC_SURFACES_VERTEX_MODIFY_SHARED_DATA | SurfacesVertexModifySharedData(inout SurfacesVertexData data) | 改法线/切线/UV 等共享数据 |
CC_SURFACES_FRAGMENT_MODIFY_BASE_COLOR_AND_TRANSPARENCY | SurfacesFragmentModifyBaseColorAndTransparency(inout vec4 baseColor) | 改颜色/透明度:溶解、闪烁、命中白闪 |
CC_SURFACES_LIGHTING_MODIFY_FINAL_COLOR | SurfacesLightingModifyFinalColor(inout vec4 color) | 最终颜色后处理:描边压暗、风格化 |
v_worldPos、v_uv)在不同小版本可能调整。
核对路径:internal/effects/surfaces/includes/*.chunk;也可在材质面板用 Shader 调试工具查看最终生成代码。
meshRenderer.material.setProperty() 会作用于所有使用同一材质资产的物体。
需要逐物体差异化时,必须用 getMaterialInstance(i)(或克隆材质)。代价:每个实例都是一次 SetPass,会打断合批——差异化与合批是一对天然矛盾,按物体数量权衡。
5.3 顶点动画:风摆(适合草地、旗帜、头发)
5.4 Shell 描边法
思路:额外画一遍只渲染背面的外扩壳体,并关闭深度写入冲突,得到均匀外描边。优点是简单稳定,缺点是对硬边模型会断裂(需要在法线上做平滑处理)。
06RenderTexture 后处理实战
不依赖自定义渲染管线,也能做出可靠的后处理:主相机渲染到 RenderTexture → 用全屏面片采样 RT 做处理 → 输出到屏幕。这条链路在任何小版本都稳定可用。
6.1 链路搭建
- 主相机
camera.targetTexture = rt,其 cullingMask 排除 UI 层; - 另建一个"呈现相机"(或直接用 UI 相机)渲染一张铺满屏幕的
Sprite; - 该 Sprite 使用自定义材质,采样主 RT 并输出处理结果;
- 多阶段效果(Bloom)用多个 RT 串联:亮度提取 → 降采样模糊 H → 降采样模糊 V → 合成。
Layers 严格分层是最省心的做法。
6.2 高斯模糊:分离卷积
2D 高斯可分解为"横向 + 纵向"两次一维卷积,复杂度从 O(n²) 降到 O(2n)。9 tap 用 5 次采样即可(利用线性采样的对称性)。
6.3 一次完整的 Bloom 合成
07UI 合批与 DrawCall 治理
UI 合批的本质:把渲染顺序相邻、且渲染状态相同的 UI 元素合并成一次提交。所以治理手段只有两招——让状态相同,让顺序相邻。
7.1 打断合批的元凶
| 因素 | 原因 | 规避方式 |
|---|---|---|
| 不同贴图 / 图集 | 每次换贴图 = 一次 SetPass | 按"界面模块"规划图集,保证同一界面内元素同图集 |
| 不同材质实例 | 运行时 getMaterialInstance 后各不相同 | UI 上尽量不逐元素改材质;需要时用顶点色 |
| Mask | 需要 stencil 写入 + 清理,至少多 2 次 DrawCall,且必然打断 | 用九宫格切图代替矩形遮罩;必要时把 Mask 内的元素集中成块 |
| Graphics | 动态几何无法与静态精灵合并 | 能画图解决的一律用切图 |
| Sprite 与 Label 交叉 | 位图字体与图集不同源 | 把同类元素在节点树里排到连续区间 |
| 多个 Canvas / 不同 priority | 会被拆成多个渲染根 | 减少 Canvas 数量,用同一渲染根下的层级排序 |
7.2 写一个"合批体检"工具
与其靠肉眼猜,不如遍历 UI 树统计理论批次数并定位第一个打断点。下面这个脚本可以直接放在编辑器扩展里一键跑。
7.3 Label 的隐藏成本
- CacheMode.NONE:每次文本变化重建网格,适合频繁变化的短文本。
- CacheMode.BITMAP:把字形渲到一张动态图集,文字内容变化时才更新,可参与合批(要占用动态图集内存)。
- CacheMode.CHAR:按字符缓存,适合大量重复字符(如数字),内存最省,但每个字符一个顶点块。
实践:静态文案用 BITMAP 并规划在同一图集;跳动的伤害数字用 CHAR;聊天类长文本用 NONE 并控制刷新频率。
08性能优化体系
优化的通用流程:定指标 → 分侧定位(CPU JS / CPU 提交 / GPU / 内存 / IO)→ 单点优化 → 回归验证。跳过度量直接改代码,是最贵的做法。
8.1 预算表
| 指标 | 中端机目标 | 低端机目标 | 超标时的典型原因 |
|---|---|---|---|
| 帧时间 p95 | < 20ms | < 33ms | GC 尖刺、资源同步加载、批次重建 |
| DrawCall | < 150 | < 80 | 图集规划差、材质实例过多、Mask 泛滥 |
| 三角面 | < 30 万 | < 12 万 | 模型未减面、阴影 Pass 翻倍 |
| 纹理内存 | < 350MB | < 200MB | 未压缩大图、图集冗余、RT 未释放 |
| JS 堆峰值 | < 120MB | < 80MB | 每帧分配、缓存不清理、闭包引用 |
8.2 分侧定位法
CPU(JS 侧)
关掉渲染(把相机 targetTexture 设空/隐藏场景)后帧时间仍高 → 脚本侧问题。查:每帧的 new、数组遍历、JSON 解析、字符串拼接。
CPU(提交侧)
DrawCall 高 → 合批问题。减少 SetPass 的收益通常大于减少三角面。
GPU
降分辨率后帧率明显上升 → 填充率瓶颈。查:全屏后处理、大面积半透明、Overdraw、复杂光照。
内存 / IO
帧率周期性掉 → GC 或资源加载。用分帧加载 + 预加载 + 对象池解决。
8.3 对象池
频繁创建销毁(子弹、飘字、粒子、列表项)是 GC 尖刺的头号来源。NodePool 配合"预热"能把尖刺摊平到加载阶段。
removeFromParent 后仍可能被 getComponent 误引用。
务必设上限,并在切场景/退出玩法时 clear()。
8.4 热路径编码纪律
8.5 其它高收益手段
- 纹理压缩 + 图集规划:内存与带宽双降,收益最大的一档。
- 阴影降级:平面阴影(Planar)比 ShadowMap 便宜一个数量级;或只给主角投影、场景用烘焙/假阴影贴片。
- 骨骼:同屏多角色时用
useBakedAnimation,并把jointTextureLayout调到可容纳的最大骨骼数(减少纹理采样)。 - LOD 与剔除:远处换低模 / 关阴影 / 停动画;区域型大世界按"区块"整体激活。
- 光照探针 / 反射探针:用烘焙探针替代实时光照,动静分离。
- 分帧加载:首屏只加载必需资源,其余在 Loading 动画期间分帧补齐(见 3.2)。
09物理与碰撞
物理的两个核心问题:确定性(固定步长)与视觉平滑(渲染插值)。绝大多数"手感抖、穿透、不同帧率不一致"都出在这。
9.1 固定步长与子步
9.2 渲染插值:消除固定步长带来的抖动
物理以 60Hz 步进、渲染可能 30/90Hz,直接把物理位姿赋给节点会产生"时快时慢"的抖动。正确做法是保存上一步与本步的变换,按剩余时间比例插值。
9.3 碰撞/触发事件与射线的正确用法
10动画系统
3.8 有两套并行的动画能力:传统 Animation 组件(片段 + 状态切换)与 Marionette 动画图(状态机 + 混合 + 层级)。小项目用前者,复杂角色用后者。
10.1 Animation 组件:够用且可控
10.2 性能考量
| 手段 | 收益 | 代价 |
|---|---|---|
useBakedAnimation(骨骼烘焙) | CPU 蒙皮开销大幅下降 | 内存上升、无法运行时改骨骼 |
| 合理的 jointTextureLayout | 减少骨骼矩阵纹理采样次数 | 纹理内存增加 |
| 动画精度(压缩位置/旋转/缩放) | 包体与内存下降 | 可能有细微抖动 |
| 远处角色降频 / 停播 | CPU 与 DrawCall 双降 | 需要 LOD 逻辑配合 |
11运行时架构设计
游戏项目的复杂度来自于"状态多、时序杂、耦合重"。架构的目标只有一个:让变化局部化。
11.1 四层结构
| 层 | 内容 | 规则 |
|---|---|---|
| 数据层 | 配置表(CSV/JSON)、存档、协议数据 | 只读快照 + 版本号;不直接持有 Node |
| 服务层 | ResService / NetService / AudioService / SaveService | 无 MonoBehaviour 依赖、可单测、由 SystemManager 驱动 |
| 系统层 | 玩法系统(战斗、任务、背包…) | 只依赖服务层与事件总线,不直接引用 UI |
| 表现层 | 场景实体、UI 界面、特效 | 只订阅事件、调用系统接口,反向不依赖 |
11.2 类型安全的事件总线
全局 director.on 用多了会变成"字符串地狱"。用一个带类型映射的轻量总线,能在编译期挡掉 80% 的拼错与参数错。
11.3 栈式 UI 管理
11.4 配置表驱动
12分包与热更新
两件事要分清:Web/小游戏靠 Bundle 远程加载(资源可以随时换),原生靠 manifest 热更(可以把脚本和资源一起换)。
12.1 Bundle 的四种形态
| 形态 | 位置 | 能否热更 | 用途 |
|---|---|---|---|
| 内置 main | 首包 | 否 | 主场景与启动必需资源 |
| 内置 resources | 首包 | 否 | 小而通用的公共资源(谨慎滥用) |
| 普通 Bundle(文件夹) | 可配置为远程 | 是 | 按玩法/活动拆分 |
| 小游戏分包 / Zip | 平台机制 | 部分 | 绕开主包体积限制 |
removeBundle)。
12.2 Web 端远程加载
12.3 原生热更新(JSB)
13多平台构建与适配
同一份代码要跑在 Web、微信小游戏、Android/iOS 上,差异必须收敛到一个适配层,而不是散落各处的 if (isWechat)。
13.1 平台适配层
13.2 各平台关键约束
| 平台 | 核心限制 | 应对策略 |
|---|---|---|
| 微信小游戏 | 主包 4MB、总包有上限;无真多线程;GC 不可控 | Bundle 分包 + 远程资源;ASTC 压缩;wx.triggerGC() 在 Loading 后主动调用;引擎插件化 |
| Web | 首包下载体积决定留存;iOS Safari 内存限制严 | 引擎模块裁剪、gzip/brotli、CDN 分片预加载、纹理压缩 |
| Android / iOS | 包体审核、热更合规、机型碎片化 | manifest 热更 + 灰度;画质分档;GPU 驱动黑名单降级 |
13.3 首包瘦身清单
- 引擎模块裁剪:不用的物理后端(cannon/ammo/bullet)、不用的 3D 模块、音频后端,全部裁掉。
- 启动场景只依赖最少资源:Logo 与 Loading 用内置小图,主资源全部走远程 Bundle。
- 纹理压缩 + 图集:这是体积与内存收益最大的一档(见 4.3)。
- 开启
md5Cache:允许 CDN 长期强缓存,配合版本号更新。 - 删除
resources里"图省事"塞进去的资源——它会无条件打进首包。
assets/ 与 src/ 的实际大小分布,比任何"感觉"都准。
超过 500KB 的单个资源都应该被单独拎出来审视。
14编辑器扩展与调试
把重复劳动(合批体检、图集检查、命名规范、构建后处理)做成编辑器扩展,是团队效率的分水岭。
14.1 扩展的三进程模型
| 进程 | 能做什么 | 不能做什么 |
|---|---|---|
| 主进程(main.js) | 文件读写、消息中转、菜单、面板生命周期 | 拿不到场景节点 |
| 渲染进程(panel) | UI 面板(HTML/Vue) | 不能直接改场景 |
| 场景脚本(scene) | 在场景进程里访问节点树、执行编辑器逻辑 | 不能操作文件系统 UI |
14.2 构建钩子
构建插件可在打包流程中插入自定义处理:资源加密、生成版本号、产出校验报告、自动上传 CDN。
14.3 调试技巧集合
- 帧调试:用
debug.setDisplayStats(true)看 FPS/DC/三角面;复杂问题用原生端抓帧工具。 - Shader 验证:先在片元里直接输出中间量(法线、UV、世界坐标)作为颜色,确认数据正确后再接光照。
- 内存快照:Web 端用 DevTools Memory 抓 Heap Snapshot,重点看
Texture2D/SpriteFrame的 retained size。 - 日志分级:统一封装
log,用DEBUG常量在发布包里把log编成空函数,避免字符串拼接开销。
15可交互演示
下面两个演示用纯 JS 复现了引擎内部的两个核心机制:DrawCall 合批与对象池。它们能让你直观感受到"排序"与"复用"带来的量级差异。
new 一个小对象其实很快,池的主要收益在于避免 GC 造成的帧尖刺而不是"分配慢"。
在 Cocos 场景里差异会被放大数倍:节点创建涉及 instantiate 的递归克隆、组件 onLoad/onEnable 调用、渲染数据初始化与批次重建——
这些才是对象池真正的价值所在。
16路线图与上线自查
16.1 上线前自查清单
| 维度 | 检查项 | 判定标准 |
|---|---|---|
| 性能 | 帧时间 p95、DrawCall、三角面 | 达到 8.1 预算表的中端机目标 |
| 内存 | 纹理内存、JS 堆峰值、切场景后回落 | 连续切换 20 次场景,内存不持续上涨 |
| 资源 | 无未压缩大图、resources 无冗余、Bundle 拆分合理 | 构建产物各文件均已被审视 |
| 兼容 | 低端机 / 不同分辨率 / 刘海屏安全区 | 至少 3 档机型实测通过 |
| 网络 | 弱网、断线重连、请求超时与重试 | 断网 30s 后能自动恢复且不卡死 UI |
| 热更 | 版本号、回滚方案、灰度开关 | 可随时把远程资源切回内置版本 |
| 异常 | 全局错误捕获与上报 | JS 异常有堆栈上报,不白屏 |
16.2 值得反复琢磨的 10 个问题
- 一帧里
update和lateUpdate分别在什么时刻执行?相机跟随该放哪个? - 为什么节点
active=false时onLoad不执行?这会导致哪些空引用? - 资源引用计数是谁在加、谁在减?"场景中拖拽引用"要不要手动 decRef?
- DrawCall 的三要素是什么(状态、顺序、遮挡)?Mask 为什么必然打断?
- 纹理内存怎么算?2048² RGBA8 与 ASTC 6x6 差多少倍?
- 固定步长物理为什么需要渲染插值?alpha 怎么算?
- 共享材质与材质实例的区别?逐物体改参数会带来什么副作用?
- Surface Shader 的注入点有哪些?顶点动画该改哪个函数?
- 对象池的收益到底来自哪里?池无限大有什么问题?
- 热更新脚本与资源,为什么前者必须重启才生效?
17参考与源码定位
官方文档
Cocos Creator 3.8 用户手册(docs.cocos.com/creator/3.8/manual/zh/):组件、资源、Shader、编辑器扩展的权威说明。
引擎源码
cocos/:TS 脚本层;native/:C++ 内核与 GFX;cocos/rendering/:渲染管线;写 Shader 与查瓶颈前必读。
社区与问答
Cocos 中文社区(forum.cocos.org):渲染/热更/平台相关的高质量讨论集中地;GitHub Issues 用于确认已知缺陷。
延伸知识
《Real-Time Rendering》用于补齐图形学;GPU 厂商的移动端优化指南(Arm Mali、Adreno、PowerVR)用于理解带宽与填充率瓶颈。
18资源传送门 · Cocos 官方与权威资源
以下链接均为 Cocos Creator 官方站点,已逐条核验可用性。文档类链接锁定在 3.8 版本,避免跳到 4.x 手册造成 API 对不上。全部在新标签页打开。 本页只收录本技术方向的官方资源;其它方向的传送门,请在顶部 Tab 切换后查看各自分类下的「资源传送门」章节。
官方文档 · 锁定 3.8 版本
引擎源码与示例工程
社区
平台与底层标准
/creator/3.8/manual/zh/ 首页导航进入即可定位。
01引擎架构与节点模型
Godot 4 是"场景树 + Server 服务层"架构:上层是 Node 组成的树,底层是
RenderingServer / PhysicsServer2D/3D / AudioServer。
理解这两层,才能回答"这个操作到底发生在主线程还是渲染线程"。
G1.1 四层对象模型
| 类型 | 内存管理 | 典型用途 | 释放方式 |
|---|---|---|---|
Object | 手动(或由所属容器管理) | 所有引擎对象的基类 | free() |
RefCounted | 引用计数 | Resource、纯数据类 | 引用归零自动释放 |
Node | 树结构手动管理 | 场景中的一切实体 | queue_free()(安全,帧末销毁) |
Resource | 引用计数 + 全局缓存 | 贴图、材质、场景、脚本数据 | 无引用时自动回收 |
Node 不参与引用计数。一个节点被移出树后如果没人 queue_free(),它就永远泄漏(在调试器的 Monitor → Object → Nodes 里能看到数量持续上涨)。
而 Resource 相反:只要还有引用就不释放,常见于"全局字典缓存了 Texture 导致切场景不降内存"。
G1.2 进程模式:比"暂停"更精细的控制
Godot 用 Node.process_mode 控制子树如何响应暂停,共四档。这是一套内建机制——
不需要在每个回调里手写 if paused: return,也不会出现"某个节点忘了判断导致暂停后还在动"的疏漏。
| 模式 | 行为 | 典型用途 |
|---|---|---|
INHERIT(默认) | 继承父节点 | 绝大多数节点 |
PAUSABLE | get_tree().paused = true 时停止 | 玩法逻辑、怪物 AI |
WHEN_PAUSED | 只有暂停时才运行 | 暂停菜单本身 |
ALWAYS | 始终运行 | 背景音乐、UI 动画、计时器 |
DISABLED | 永不运行 | 被系统托管的节点 |
G1.3 场景树组织:组合优于继承
Godot 的范式是一个节点挂一个脚本 + 用子节点组合能力。把上千个小节点组织成一棵树是正常且被鼓励的, 关键是形成稳定的"职责边界",而不是把所有逻辑塞进一个巨型脚本。
| 组织手段 | 作用 | 典型用法 |
|---|---|---|
| 场景实例化 | 把一棵子树封装成可复用单元 | Enemy.tscn、Bullet.tscn,运行时 instantiate |
场景唯一名 %Name | 跨层级稳定引用,重构时不失效 | @onready var hp_bar := %HealthBar |
分组 add_to_group | 按角色批量查找,不依赖树结构 | get_tree().call_group("enemy", "take_damage", 10) |
| Autoload 单例 | 跨场景的全局服务 | EventBus、GameState、SaveManager |
_ready() 依赖另一个(或依赖项目配置),
而它的注册顺序更靠前,就会读到空值。解决办法:把被依赖的排到前面,或把初始化逻辑延迟一帧
(await get_tree().process_frame / call_deferred)。
02GDScript 工程实践
GDScript 4 已经是非常"正经"的语言:静态类型、lambda、first-class 信号、协程。写得好的 GDScript 与写得差的性能差距可达数倍。
G2.1 必须写全静态类型
Godot 4 的动态类型调用走 Variant 反射,静态类型则直接绑定。热路径上加类型标注是最廉价的性能优化。
G2.2 信号与 Callable:解耦的标准手段
_exit_tree() 里断开,是最省心的纪律。
G2.3 协程 await:把异步写成同步
G2.4 选型:GDScript 还是 C#
| 维度 | GDScript | C#(.NET) |
|---|---|---|
| 迭代速度 | 极快,引擎内建编辑器 | 需外部 IDE + 编译 |
| 性能 | 热路径需注意类型,整体够用 | 数值密集逻辑快 2~10 倍 |
| Web 导出 | 支持(.wasm) | 4.x 的 Web + C# 仍受限,需实测 |
| 生态 | Asset Library 绝大多数资源 | NuGet 生态 |
GDExtension 或 C# 项目引用互通。混编时务必把边界接口设计得足够窄。
03生命周期与帧循环
Godot 的初始化顺序有一条必须记住的规则:_enter_tree 自顶向下,_ready 自底向上。这是绝大多数"子节点还没准备好"类 Bug 的根源。
G3.1 完整顺序
| 回调 | 时机 | 顺序 | 该做什么 |
|---|---|---|---|
_init | 对象构造 | — | 只初始化自身字段,此时不在树中 |
_enter_tree | 进入场景树 | 父 → 子 | 注册全局监听 |
_ready | 自身与全部子节点就绪 | 子 → 父 | 取节点引用、读配置、初始化依赖 |
_process(delta) | 每渲染帧 | 树序 | 输入响应、UI、与渲染相关的逻辑 |
_physics_process(delta) | 每物理帧(默认 60Hz) | 树序 | 移动、碰撞、物理查询 |
_exit_tree | 离开树(销毁前) | — | 断开信号、归还资源 |
_ready 执行时,所有子节点的 _ready 已经跑完,所以在父节点里访问子节点是安全的;
反过来,子节点 _ready 里访问父节点的初始化结果则可能拿到默认值(父的 _ready 还没执行)。
G3.2 两个 delta 与固定步长
G3.3 物理插值:消除固定步长的抖动
物理 60Hz、渲染可能 144Hz 或 30Hz,直接把物理位姿给渲染节点会抖。Godot 4.4+ 提供内建插值,老版本可以手动做:
physics/common/physics_interpolation,引擎会自动处理;
开启后 不要再手动改 global_position(会与插值打架),改用 reset_physics_interpolation() 做瞬移。
G3.4 回调顺序控制与调试
默认按树序(深度优先)执行。需要插队时用 process_priority:数值越小越先执行(默认 0,可为负)。
| 手段 | 作用 | 注意 |
|---|---|---|
process_priority | 调整同层回调的执行先后 | 只影响 process / physics_process,不影响 _ready 顺序 |
set_process(false) | 彻底关闭回调 | 比在回调里 if 判断更省 |
_input / _unhandled_input | 输入优先于 GUI,后者在 GUI 未消费时才触发 | 处理完记得 set_input_as_handled() |
_notification(what) | 接收引擎通知(如 NOTIFICATION_PAUSED) | 比信号更底层,用于生命周期细节 |
print_tree_pretty() 能快速定位"节点到底挂在哪一层、顺序对不对"。
04场景与资源管线
Godot 里"场景"就是一个可实例化的节点树(PackedScene),"资源"是引用计数的数据(Resource)。把这两件事分开看,加载与释放就不会乱。
G4.1 三种加载方式与选择
| 方式 | 特点 | 适用 |
|---|---|---|
preload("res://...") | 编译期解析,脚本加载时即载入 | 必定要用的小资源;路径写错会直接报错 |
load("res://...") | 运行时同步加载,会卡当前帧 | 切换场景、Loading 中可接受卡顿处 |
load_threaded_request | 后台线程加载,需轮询状态 | 大场景 / 大贴图 / 需要进度条 |
G4.2 ResourceUID:重命名的安全网
Godot 4 给每个资源分配 uid://,编辑器里移动/重命名文件时引用会自动跟随。
手写路径时优先用 uid:// 可以避免"改个名字全场报错"。
G4.3 释放与内存
- Resource:引用归零自动释放;可用
ResourceLoader.has_cached(path)判断是否仍在缓存。 - Node:必须
queue_free();批量删除建议先remove_child()再释放,避免在遍历中修改树。 - 切场景:旧场景由引擎自动释放,但 Autoload 中的引用、全局字典、信号连接 会让它残留——这就是"切几次场景内存不降"的主因。
Resource 若在 Autoload 里被字典长期持有,等于常驻内存。
大资源(贴图、音频流)建议用路径字符串 + 按需加载,而不是直接持有对象。
05渲染管线与 GDShader
Godot 4 提供三套渲染方法(Forward+ / Mobile / Compatibility),着色器语言是自研的 GDShader: 它把 GLSL 的样板代码(attribute/uniform/varying 声明)大幅简化,代价是能力受限于引擎提供的内置变量。
G5.1 三种渲染方法怎么选
| 方法 | 后端 | 特点 | 适用 |
|---|---|---|---|
| Forward+ | Vulkan / D3D12 | 集群前向渲染,光源数量几乎无上限 | 桌面端、画面优先的 3D 项目 |
| Mobile | Vulkan / Metal | 简化前向,光源数量受限但更省 | 移动端 3D |
| Compatibility | OpenGL 3 / GLES3 | 兼容老设备;2D 项目首选,非常快 | 2D 游戏、低端机、Web |
G5.2 2D 着色器(canvas_item)
.tres 赋给多个节点的材质是共享的。任何"逐实例改 shader 参数"的场景都必须先
duplicate();代价是这些实例无法再被 2D 批渲染合并(见 G7.1 的批次规则)。
G5.3 3D 着色器(spatial)
G5.4 屏幕空间效果
Godot 做后处理有两条路:① SubViewport + TextureRect(把整个场景渲到纹理再当贴图处理,灵活但有额外开销);② 全屏 CanvasLayer + 屏幕读取着色器(hint_screen_texture)。
hint_screen_texture 前,需要在该节点之上放置 BackBufferCopy(当效果需要读取"已绘制内容"时);
若只做覆盖式效果且不需要旧画面,可省去以省一次拷贝。
06物理、输入与 UI 系统
Godot 的角色控制器、层掩码、Control 布局都是"约定优先"的设计:按它的套路写非常省事,逆着来则处处别扭。
G6.1 物理体选择
| 类型 | 用途 | 移动方式 |
|---|---|---|
CharacterBody2D/3D | 玩家、NPC( kinematic 风格) | move_and_slide() / move_and_collide() |
RigidBody2D/3D | 受力的箱子、掉落物 | 施力 / 冲量,引擎积分 |
StaticBody2D/3D | 地形、墙壁 | 不动 |
Area2D/3D | 触发器、伤害区域、拾取 | 只检测重叠,不产生碰撞响应 |
G6.2 碰撞层与掩码
Godot 用 32 层(2D/3D 各一套):collision_layer 表示"我属于哪些层",collision_mask 表示"我要检测哪些层"。
G6.3 UI:Control 的布局心智模型
- 锚点(Anchor)决定控件相对父容器边的对齐;偏移(Offset)是相对锚点的像素距离。
- 容器(HBox/VBox/Grid/Margin/Center…)接管子节点的位置,此时手动改 position 无效——要改的是
size_flags与custom_minimum_size。 - Size Flags:fill(填满)/ expand(抢占剩余空间)/ shrink_center 等组合决定拉伸行为。
CanvasLayer用于分层:每个 layer 有独立layer序号,HUD、弹窗、加载遮罩各占一层,互不干扰。
Theme 资源统一维护,个别控件用
add_theme_*_override 微调。不要在检查器里逐控件改样式,后期换皮会变成灾难。
07性能、调试与导出
Godot 的性能工具链很完整:内置 Profiler、Monitor、Performance 单例。先量化再优化,这条原则在 Godot 同样适用。
G7.1 2D 批渲染
Godot 会自动合并相邻且同材质同纹理的 CanvasItem。破坏批次的常见因素:
- 不同纹理/材质交替出现(把同类元素在节点树里排到连续区间)
- 使用
ClipChildren、mask类效果(会打断批次) - 逐实例 duplicate 材质(见 G5.2)
- 大量独立
Label(文本网格各自独立;可用位图字体或减少刷新)
在 调试 → 可见碰撞/导航 之外,用 项目设置 → debug/settings/stdout/verbose_stdout 或渲染信息面板可查看当前 draw call 数。
G7.2 对象池与节点复用
acquire 后显式重置位置/血量/可见性;
更稳妥的做法是给池对象定义统一的 reset() 约定。
G7.3 自定义性能面板
RENDERING_INFO_TOTAL_DRAW_CALLS_IN_FRAME 在 _process 里读到的是上一帧的值(渲染发生在 process 之后)。
需要精确值时放到 _physics_process 末尾或监听渲染完成信号。
G7.4 导出与自动化
| 平台 | 关键配置 | 注意事项 |
|---|---|---|
| Windows / Linux / macOS | 导出模板 + 图标 + 代码签名(macOS) | PCK 可外挂,便于热更资源 |
| Android | 安装 SDK + 导出模板;选择 arm64 | Gradle 构建;纹理用 ETC2 / ASTC |
| iOS | 导出模板 + Xcode 签名 | 需要 macOS 主机;ATS 与隐私描述 |
| Web | Compatibility 渲染方法 | 需要 COOP/COEP 响应头才支持多线程与部分特性 |
--headless --script 让这件事几乎零成本。
08资源传送门 · Godot 官方与权威资源
以下链接均为 Godot 官方站点,已逐条核验可用性,全部在新标签页打开。 Godot 文档站左下角可切换版本(4.x / 4.5 / 4.4 / 4.3),本页统一指向 4.x 中文文档。
官方文档
源码、社区与下载
01服务器架构与进程模型
游戏服务器的第一性问题不是"用什么框架",而是状态放在哪、请求怎么路由、进程挂了怎么办。 这三点想清楚,架构选型自然收敛。
S1.1 进程角色划分
| 进程 | 职责 | 是否持有状态 | 扩缩容 |
|---|---|---|---|
| Gate 网关 | 维护客户端连接、加解密、限流、协议转发 | 无业务状态(只有连接) | 水平扩展最容易 |
| Game 逻辑服 | 玩家数据、背包、任务、社交 | 有状态(玩家在线数据) | 按 uid 分片 |
| Battle 战斗服 | 房间、帧同步、结算 | 有状态且短生命周期 | 按房间数弹性伸缩 |
| Match 匹配服 | 匹配队列、房间分配 | 轻状态 | 可多实例 + Redis |
S1.2 玩家路由
有状态服务必须保证同一个玩家的请求总是落到同一个进程。三种做法:
- 一致性哈希(uid → 节点):扩缩容时迁移量最小,但需要处理虚拟节点与数据迁移
- 路由表 + 中心存储(uid → nodeID 存 Redis):灵活、便于手动调度,但每次转发多一次查询(可本地缓存)
- 网关固定绑定:玩家登录后由网关记录后端节点,适合"网关即调度者"的形态
S1.3 分层与目录结构
02网络层与协议设计
Go 写长连接服务器最舒服的地方是"一个连接两个 goroutine":读协程解包、写协程发包, 业务处理交给玩家自己的邮箱。难点全在细节:粘包、写并发、超时、慢客户端。
S2.1 帧格式与粘包拆包(裸 TCP 场景)
先界定适用范围:本节讲的是裸 TCP 自研协议 —— TCP 是字节流、没有消息边界,所以必须自己定帧、自己切帧。
如果你用 WebSocket(H5、微信小游戏只能用这个),消息边界由协议帧 + 库保证,ReadMessage() 返回的永远是一条完整消息,
业务层不需要自己做拆包。什么时候该用哪种、以及"丢包"到底发生在哪一层,见 S2.2。
send 队列)。
② 没有读超时:客户端掉线但 TCP 未断开(拔网线),连接会永久占用 —— 务必 SetReadDeadline + 心跳。
③ 写队列无上限:慢客户端会耗尽内存 —— 队列满即断开,或按业务分级丢弃。
注:以上针对裸 TCP 自研协议。WebSocket 场景下 ① 与"消息边界"由库接管(单写者 + 帧解析), 但 ② 读超时/心跳 与 ③ 写队列上限 依然必须你自己做。
S2.2 消息路由
init() 自动注册很方便,但会让"哪些协议被注册"变得不可见,且容易出现循环 import。
更可控的做法是在 app 里显式调用 Register,配合启动日志打印全部路由。
03并发模型与状态管理
游戏服务器最常见的并发错误不是死锁,而是同一个玩家的数据被两个 goroutine 同时改。 最省心的解法不是加锁,而是让一个玩家永远只由一个 goroutine 处理。
S3.1 玩家邮箱(串行化)
Exec 里做同步 DB 调用)。
S3.2 共享状态的正确姿势
| 场景 | 方案 | 说明 |
|---|---|---|
| 玩家私有数据 | 邮箱 / 单协程 | 首选,零锁 |
| 全局只读配置 | atomic.Value | 热更新时整体替换,读写都无锁 |
| 计数器 | atomic.Int64 | 比 Mutex 快一个量级 |
| 读多写少的容器 | sync.RWMutex | 注意不要在持锁时做 IO |
| 高频临时对象 | sync.Pool | 降低 GC 压力,注意取用后必须 Reset |
| 并发任务编排 | errgroup + context | 任一失败则整体取消 |
time.After 在循环里创建(定时器到期前不释放,循环里应改用 time.NewTicker);
③ HTTP 响应体未 Close 导致连接协程残留。排查手段:/debug/pprof/goroutine?debug=2 看堆栈分布。
04数据层与缓存策略
玩家数据的基本原则:内存里有一份权威副本,DB 只在必要时刻同步。 每次操作都写库会把数据库压垮,不写又会在宕机时丢档 —— 折中方案是"脏标记 + 写回"。
S4.1 写回(Write-Behind)
Flush;
④ 关键道具(充值、交易)应同步落库 + 事务,不能走异步写回。
S4.2 并发覆盖与乐观锁
S4.3 排行榜与冷热分离
| 数据 | 存储 | 要点 |
|---|---|---|
| 实时排行榜 | Redis ZSet | 只存 uid + 分数,展示时批量回查昵称头像(注意缓存穿透) |
| 周期结算榜 | Redis 滚动域 + 定时落 MySQL | 按周期(日/周)分 key,结算后归档并清理,避免 key 无限增长 |
| 玩家主数据 | MySQL(binlog/定时备份) | 大字段(背包 JSON)单独拆表,减少主表膨胀 |
| 离线消息 | Redis List + 上限 | 设最大长度,超限丢弃最旧,避免离线玩家堆积 |
SetMaxOpenConns 要略小于数据库实际承受上限,SetMaxIdleConns 与并发量匹配,
SetConnMaxLifetime 建议小于负载均衡/数据库的空闲断连时间(如 30 分钟),否则会偶发 connection reset。
05优雅重启、热加载与可观测性
游戏服务器最怕的不是出 Bug,而是重启一次丢一批档、断一批连接。 优雅关闭、配置热加载、pprof 三件事做完,运维就从"救火"变成"看板"。
S5.1 优雅关闭的四步
kill -9)时任何优雅逻辑都不会执行 —— 编排系统要留出足够的 graceful period,别用 -9 直接干掉。
S5.2 配置热加载
S5.3 可观测性
| 能力 | 手段 | 注意 |
|---|---|---|
| 日志 | 结构化日志 + 角色标签 + 按天滚动 + 保留天数清理 | 热路径禁止拼接大字符串;用占位符延迟格式化 |
| 指标 | 在线数、消息量、耗时分位(p95/p99)、错误码计数 | 分位数比平均值更能反映"卡" |
| 剖析 | net/http/pprof | 只监听内网地址,公网暴露会泄露堆栈与内存信息 |
| 追踪 | 请求 TraceID 贯穿 gate → game → db | 跨进程时通过协议头传递 |
06战斗服与帧同步
同步方案的选择决定了战斗手感、反作弊难度与服务器成本。 没有"更好的方案",只有"更适合玩法"的方案。
S6.1 三种同步模型
| 模型 | 传输量 | 延迟敏感 | 反作弊 | 典型玩法 |
|---|---|---|---|---|
| 帧同步(Lockstep) | 极小(只传输入) | 极高 | 难(客户端权威) | MOBA、格斗、RTS |
| 状态同步 | 较大(传实体状态) | 中 | 易(服务器权威) | MMO、FPS、ARPG |
| 混合同步(帧同步 + 关键帧校验) | 中 | 高 | 中 | 带判定的休闲竞技 |
| 回合/半回合 | 最小 | 低 | 易 | 卡牌、自走棋 |
S6.2 帧同步的房间循环
S6.3 匹配与房间生命周期
- 匹配服按"分段 + 等待时长"放宽条件:前 10 秒严格按分差,之后逐步放宽,避免高分玩家永远匹配不到人。
- 房间创建后战斗服自报负载,匹配服按负载挑选,避免"某台机器堆满房间"。
- 房间结束要释放全部资源:停止 tick 协程、关闭输入通道、清理快照、归还对象池。
- 战斗服应该是可随时被杀的:状态在 Redis / 快照中,重连能从最近快照恢复。
07性能剖析与稳定性
Go 的性能问题 90% 集中在两处:GC 压力(分配太多)和协程泄漏。 靠猜是猜不出来的,pprof 是唯一答案。
S7.1 pprof 接入与常用剖面
S7.2 降低 GC 压力
| 手段 | 做法 | 收益 |
|---|---|---|
| 对象池 | sync.Pool 复用消息包、缓冲区 | 显著减少年轻代分配 |
| 预分配 | slice make([]T, 0, n)、map 指定容量 | 避免扩容拷贝 |
| 避免字符串拼接 | 热路径用 strings.Builder 或字节缓冲 | 减少临时对象 |
| 值语义 | 小结构体传值而非指针 | 减少堆分配与指针扫描 |
| Ballast | 启动时分配一大块内存抬高 GC 触发阈值 | 降低 GC 频率(代价是常驻内存上升) |
| GOGC 调优 | debug.SetGCPercent | 内存换 CPU 的直接杠杆 |
S7.3 稳定性清单
- panic 兜底:每个玩家协程、每个 handler 都要
defer recover,崩溃不能扩散到进程。 - 慢查询告警:DB / Redis 调用超过阈值打日志,很多"卡顿"其实是某条 SQL。
- 队列长度监控:玩家邮箱、写队列的长度是"系统是否消化得过来"的先行指标。
- 限流:按连接与按命令字双重限流,防止单个客户端打爆服务器。
- 压测:上线前用机器人模拟峰值在线 + 消息量,观察 p95 与 goroutine 数量是否平稳。
08资源传送门 · Go 官方与生态
以下链接均为 Go 官方站点,已逐条核验可用性,全部在新标签页打开。
官方文档
源码、博客与并发原语
net/http、sync 的源码就有答案。