前段时间在群里刷到一个大一新生晒作业截图,一行#include <stdio.h>配printf("hello world! 我是大一新生,c语言环境部署成功啦!\n"),评论区一片"经典开局""梦开始的地方"。我当时顺手回了句:等哪天你想让这行字出现在手机屏幕上,还能被手指点一下、跳个动画,你就该认识 Cocos Creator 了。这话不是抬杠——printf和游戏引擎本质上都在解决同一个问题:怎么把一行字送到用户眼前,只是前者送到终端窗口,后者送到一块会动、会响、会响应触摸的屏幕。而每一个 Cocos Creator 项目的起点,几乎都是从 Hello World 这个最朴素的场景开始的。
这篇文章想聊的就是这件事:Cocos Creator 的 Hello World,到底该怎么做,才算真正跑通了。我会从环境搭建、场景结构、脚本编写一路写到打包 APK 装到真机上,把中间那些文档里写得含糊、但实操时一定会绊你一脚的地方都摊开讲。适合的读者有三类:完全零基础想入门游戏开发的新人、从 C/C++ 或前端转过来想快速上手引擎的老手、以及只想把手上这个小 Demo 塞进手机里看看效果的折腾党。全程以 Cocos Creator 3.8 这类 3.x 主流版本为参考,2.x 的用户也能看懂思路,具体菜单差异我会标出来。
1. Hello World 在游戏引擎里到底在验证什么
1.1 从 printf 到场景树,两种"你好世界"的本质区别
写 C 语言的时候,printf背后是一整条链路:源码被编译器处理成机器码,运行时把字符串塞进标准输出缓冲区,再由终端渲染出来。你只需要管一行调用,因为"往哪打印""谁来显示"这些问题,操作系统和终端已经替你兜底了。游戏引擎没有这么贴心——它要面对的是分辨率千奇百怪的屏幕、几十种 GPU、随时可能被切到后台的应用生命周期,所以它必须自己接管一切。
Cocos Creator 的"接管方式"叫场景树(Scene Graph)。你的所有东西,文字、图片、按钮、角色,都是树上的一个节点(Node)。节点本身是个空壳,只负责位置、旋转、缩放这些变换信息;真正决定它"长什么样""干什么事"的,是挂在它身上的组件(Component)。文字之所以显示出来,是因为节点上挂了一个 Label 组件;能点,是因为挂了 Button 组件。
所以引擎里的 Hello World,验证的从来不是"能不能打印一行字",而是三件事:你能不能把资源正确组织成一棵场景树、能不能让引擎在正确的时机执行你的脚本、能不能把这套东西完整地搬到目标平台上还长得一样。第三件事才是最要命的,因为它直接关系到后面打包 APK 的那一整套流程。
1.2 一个最小 Cocos Creator 项目里必须有哪些东西
一个能跑起来的最小项目,拆到骨头就是四样东西。
第一是场景文件(.scene),它是一个 JSON 格式的序列化文件,记录了场景树里每个节点的层级、每个组件的属性值。你在编辑器里拖拖拽拽,本质上就是在改这个文件里的字段。
第二是画布(Canvas),可以理解成"屏幕这块画布的数学抽象"。它携带一个 UI 相机,负责把 2D 内容投射到屏幕上,同时承担分辨率适配的职责。
第三是脚本组件,用 TypeScript 写,通过装饰器告诉引擎"这个类是一个组件""这个字段需要在编辑器里暴露出来让我填"。
第四是构建配置,也就是把项目编译到某个平台(Web、Android、iOS、微信小游戏……)时用的那套参数。很多新手翻车的点就在这里——前面三样都做对了,结果打包出来是个黑屏,原因仅仅是构建时忘了把场景加进"参与构建的场景列表"。
把这四样理清楚,"Hello World 该怎么做"就变成了"这四样怎么组装",而不是"我该抄哪段代码"。
1.3 引擎版本和语言选型上的取舍
挑版本这件事,我踩过坑。3.x 早期的一些小版本,编辑器崩溃、构建报错、原生编译工具链不匹配的问题穿插出现,你在社区搜到的解决方案往往对应另一个小版本,照着改反而更乱。所以我的建议很直接:用 3.x 系列里较新的 LTS 版本,别当小白鼠去追最新的 beta,也别守着已经停止维护的 2.x 舍不得走。
语言层面就更没什么可纠结的了。3.x 全面转向 TypeScript,官方文档、示例工程、社区问答基本都围绕 TS 展开。你可能看到过一些老项目还在用 JavaScript,语法上确实更"平易近人",但缺失类型提示之后,你在写node.getComponent('xxx')这种字符串式调用时会非常痛苦——参数写错了编辑器不会报错,要等到运行时才炸给你看。
提示:如果你是第一次接触 TypeScript,不用专门去啃一本语法书。Cocos Creator 项目里用到的 TS 特性非常有限,核心就是类型标注、类、装饰器和箭头函数这四样,边写边查完全够用。
2. 环境搭建:把编辑器跑起来这一步别偷懒
2.1 Dashboard 与编辑器的安装选项怎么选
Cocos Creator 现在的分发方式是"Dashboard 管版本,编辑器管项目"。你先去官网下载 Dashboard 这个启动器,装好之后在里面登录账号(离线也能用,但登录后会方便些),然后在"编辑器"标签里挑选并下载具体版本。这个设计的好处是你可以同时装 3.8.0 和 3.8.6,遇到兼容问题时切换着用。
安装路径有个硬性建议:全英文,不带空格,层级尽量浅。原因在于后面构建原生平台时,命令行工具会对路径做转义处理,中文路径或者带空格的目录经常在这些环节出问题。我自己习惯装在D:\Cocos\Editor\3.8.x这种结构下,几年下来没在这上面栽过跟头。
系统盘空间要留够。一个编辑器本体几个 G,加上后续导入的资源和构建缓存,一个中等项目吃掉十几 G 稀松平常。文件解压到一半提示空间不足、只能推倒重来的体验相当糟糕。
2.2 新建项目时的模板选择与目录结构解读
打开 Dashboard 点"新建项目",会看到一串模板。列表里通常包含 Empty (2D)、Empty (3D) 以及若干示例工程。我的建议是选 Empty (2D)——越干净的脚手架,越容易看清引擎本身在做什么。那些功能齐全的示例工程对一个连场景树都没搞明白的人来说,就是一堆噪音。
建好之后看到项目根目录,先认识这几个文件夹,后面的排错全靠它们:
| 目录 | 作用 | 能不能手动改 |
|---|---|---|
assets | 所有你创建的资源和脚本 | 可以,这是你的主战场 |
library | 资源导入后生成的引擎内部格式 | 不能,删了会自动重建 |
local | 本机相关的编辑器配置 | 一般不动 |
profiles | 项目级的编辑器偏好设置 | 一般不动 |
settings | 项目的构建、物理等全局配置 | 通过编辑器界面改 |
temp | 临时文件 | 可以删,构建前删掉有时能解决诡异问题 |
build | 构建产物输出目录 | 构建后自动生成 |
新手最容易犯的错是"我想清理一下项目,把 library 和 temp 都删了吧"。temp 删掉没问题,library 删掉之后编辑器需要重新导入全部资源,大项目可能要等十几分钟,而且如果资源本身有损坏,重建过程一样会卡住。
2.3 编辑器核心面板速览
编辑器打开后,界面被切成几块,刚开始会觉得眼花,其实每块职责很明确。
**层级管理器(Hierarchy)**在左上,显示当前场景的节点树,你可以拖拽调整父子关系。**资源管理器(Assets)**在左下,显示assets目录下的文件,场景、脚本、图片、字体都在这儿。场景编辑器占据中央,是可视化的摆放区域,能直接拖动节点、缩放视图。**属性检查器(Inspector)**在右侧,选中某个节点或资源后,这里显示它所有可调的属性。
还有一个藏在编辑器和浏览器里的控制台(Console),显示日志和报错。它的重要性怎么强调都不过分——脚本编译错误、资源加载失败、运行时空指针,全都要靠它定位。我养成的习惯是,只要编辑器行为不对劲,第一反应就是切到控制台看有没有红色报错,八成问题当场就能定位,比到处瞎点快得多。
3. 场景搭建:用组件思维点亮第一行字
3.1 节点、组件、坐标系的对应关系
动手之前,先把三个概念钉死。
节点是场景树的骨架,本身不渲染任何东西,只有变换属性:位置(Position)、旋转(Rotation)、缩放(Scale)。组件是挂在节点上的功能单元,同一个节点可以挂多个组件,比如一个节点既可以是 Label(显示文字),又可以有 Button(响应点击)。坐标系则是 UI 世界里的一切参照系——Cocos Creator 的 UI 采用左下角为原点的直角坐标系,X 向右为正,Y 向上为正,单位是像素,但实际经过画布缩放后是"设计像素"。
理解这套坐标系非常重要。你把一个 Label 节点的位置设成(0, 0),它显示在画布中心,不是因为(0,0)是中心,而是因为 UI 节点默认以父节点的锚点为参照,而 Canvas 的锚点在正中央。一旦你把这个 Label 拖到另一个节点下面当子节点,坐标系参照物就变了,位置(0,0)的含义也跟着变。很多"我明明设置了坐标怎么跑偏了"的问题,根源都在这儿。
3.2 用 Label 组件显示 Hello World 的完整操作
现在开始实际动手。假设你已经创建好一个 2D 空项目,接下来的步骤我按顺序写清楚:
- 在资源管理器里找到
assets目录,右键选择"创建 -> 场景",命名为HelloWorld,双击打开。 - 在层级管理器的空白区域右键,选择"创建 -> 2D 对象 -> Label"。如果当前场景里还没有 Canvas,编辑器会自动帮你生成一个 Canvas 节点和一个 UI 相机,把新 Label 放进去。
- 选中这个 Label 节点,在右侧属性检查器里找到
String属性,把默认的 "Label" 改成你要显示的文字。 - 调整
Font Size到 40 左右,Line Height通常跟着字号走,设成 50 上下看着舒服。 - 用场景编辑器顶部的移动工具,把 Label 拖到画面中间;或者在属性检查器的 Position 里直接填
(0, 0)。
注意:不同小版本的三级菜单命名略有出入,比如有的版本把 2D 对象归在"UI 组件"下面,找不到就两个菜单都翻一下,别以为功能被砍了。
到这里,静态的 Hello World 就出现了。你按编辑器顶部的预览按钮,能用浏览器或者内置预览窗口看到它。但它是死的——不会动、不会响应、打包出去也只是张图片。接下来得让脚本接管它。
3.3 分辨率适配参数的推导过程
在往下走之前,必须先解决一个隐雷:适配。你可能已经注意到,Canvas 节点的属性检查器里有Fit Height和Fit Width两个勾选项,还有一组设计分辨率。这两个选项直接决定你的 UI 在不同手机屏上会不会被裁掉。
原理不复杂。假设设计分辨率是W0 × H0,目标屏幕是W1 × H1:
- 勾选Fit Height时,引擎保证设计高度完整显示,缩放比
scale = H1 / H0,此时画面能容纳的逻辑宽度是W1 / scale。 - 勾选Fit Width时,保证设计宽度完整显示,
scale = W1 / W0,画面能容纳的逻辑高度是H1 / scale。
拿一个真实场景算一遍。假设设计分辨率设成960 × 640(横屏思路),跑到一台竖屏手机1080 × 2340上:
Fit Height 情况下,scale = 2340 / 640 ≈ 3.656,可见逻辑宽度= 1080 / 3.656 ≈ 295,远小于设计宽度 960,左右两侧被裁掉一大截。Fit Width 情况更别扭:scale = 1080 / 960 = 1.125,可见逻辑高度= 2340 / 1.125 = 2080,比设计高度 640 大了三倍多,内容缩在中间一条,上下全是空白。
所以结论很清楚:竖屏项目别拿横屏比例的设计分辨率硬套。换成720 × 1280再算一次,Fit Width 下scale = 1080 / 720 = 1.5,可见逻辑高度= 2340 / 1.5 = 1560,比设计高度多出 280 像素。这多出来的部分就是给"异形屏"预留的缓冲区,你只要用 Widget 组件把关键 UI 贴边对齐,就能做到大部分机型都好看。
| 设计分辨率 | 适配模式 | 手机 1080×2340 上的可见逻辑尺寸 | 结论 |
|---|---|---|---|
| 960×640 | Fit Height | 约 295×640 | 左右严重裁切,不可用 |
| 960×640 | Fit Width | 960×2080 | 上下大片空白,浪费 |
| 720×1280 | Fit Width | 720×1560 | 有缓冲,推荐 |
4. 脚本上手:让 Hello World 动起来
4.1 新建脚本与装饰器机制
在资源管理器的assets目录上右键,选择"创建 -> 脚本(TypeScript)",命名成HelloWorld。编辑器默认会用配置好的代码编辑器打开它,没有配置的话会弹窗让你选,选 VS Code 就行。
打开后你会看到一段骨架代码,核心是这么几行:
import { _decorator, Component, Node } from 'cc'; const { ccclass, property } = _decorator; @ccclass('HelloWorld') export class HelloWorld extends Component { start() { } }这里有两个装饰器值得说清楚。@ccclass('HelloWorld')是把类注册到引擎的组件系统里,括号里的名字必须全局唯一,这也是为什么你在编辑器"添加组件"的菜单里能找到它的原因。@property则是把类的某个字段暴露到属性检查器,让你能在编辑器里拖资源进去。
提示:
@ccclass的参数名和文件名不一致时,编辑器会以参数名为准注册组件,但脚本类型识别可能出问题。养成习惯,让类名和文件名保持一致,比如Player.ts里写@ccclass('Player')。
4.2 完整代码与逐行拆解
下面是我平时用来做"打字机效果"的 Hello World,比静态显示多了一点意思,也顺便验证了update的存在:
import { _decorator, Component, Label } from 'cc'; const { ccclass, property } = _decorator; @ccclass('HelloWorld') export class HelloWorld extends Component { @property({ type: Label, tooltip: '要显示文字的 Label 组件' }) public label: Label = null!; private _fullText: string = 'Hello World! 我是用 Cocos Creator 打出来的第一行字'; private _typedCount: number = 0; private _accumulator: number = 0; private readonly _charInterval: number = 0.08; start() { if (!this.label) { console.warn('HelloWorld: 还没有把 Label 组件拖到属性面板上'); return; } this.label.string = ''; this._typedCount = 0; this._accumulator = 0; } update(deltaTime: number) { if (!this.label) return; if (this._typedCount >= this._fullText.length) return; this._accumulator += deltaTime; if (this._accumulator >= this._charInterval) { this._accumulator -= this._charInterval; this._typedCount++; this.label.string = this._fullText.substring(0, this._typedCount); } } }逐段解释一下我的取舍。
@property({ type: Label })显式指定了类型。虽然在 TS 里写上public label: Label之后引擎一般也能推断出来,但显式声明更稳,而且在编辑器里鼠标悬停能看到 tooltip,团队协作时很有用。null!这个写法是 TS 的非空断言,意思是"我知道编辑器里我会把它填上,编译期别报警"。
update(deltaTime)每帧被调用一次,参数是"距离上一帧过去了多少秒"。这里我没有用setInterval或者setTimeout,因为那些 API 不受引擎的暂停、时间缩放影响。当应用被切到后台或者你做了慢动作特效时,用deltaTime累加才是正确姿势。
还有个容易忽略的点:我判断阈值时用的是>=加"减去一个间隔"而不是"清零"。因为deltaTime是浮动的,某一帧可能从 0.05 跳到 0.12,如果直接清零,累积误差会越来越大,久了会出现"打字忽快忽慢"的观感。减去间隔能保留多余的那部分时间,让节奏更稳。
注意:
substring处理中文没问题,但如果你在文字里塞了 emoji,按 UTF-16 码元切割会把表情劈成两半,显示成乱码方块。真要做表情打字机,得按码点数组切。
4.3 把脚本挂上去并调参
回到编辑器,选中你的 Label 节点,在属性检查器底部点"添加组件",搜索框里输入HelloWorld,选中即可。挂上之后,属性面板上会出现一个Label字段,旁边写着None。
这时把层级管理器里的 Label 节点,直接拖到那个字段的框里,松手,字段就绑好了。这一步的操作意图是"建立引用关系"——脚本运行时需要通过这个引用去改label.string,没有引用就只能报空指针。
然后点预览按钮。你应该看到文字从空白开始,一个字一个字往外蹦,最后停在完整的那句话上。
实测下来有几个调参心得:_charInterval设成 0.08 比较接近真人打字速度,想更快就降到 0.04;Label 的Overflow属性如果设成NONE,长文本会自动换行,但换行会导致字符串长度变化,打字过程中文字会跳行,看着很晃,这时候可以提前把Overflow设成CLAMP并给节点足够的宽度。
5. 打包 APK:从编辑器走到真机
5.1 构建原生平台的前置依赖
到这一步,Web 环境已经跑通了,接下来是重头戏:打包 APK。原生构建需要三样外部工具:JDK、Android SDK、NDK。它们不是 Cocos 提供的,得你自己准备好。
版本搭配这件事我必须敲黑板:三者的版本必须严格对齐官方文档给那一组。JDK 和 Gradle 版本不匹配、NDK 版本和引擎的原生代码不兼容,都会在你按下构建按钮之后用一大段看不懂的报错给你当头一棒。以 3.8 时期为例,主流搭配是 JDK 17 配 NDK r23c,具体还是以你所用版本官方文档里"原生平台开发环境搭建"那一页为准,别自己凭感觉升到最新。
配置路径的位置在:项目 -> 项目设置 -> 原生开发环境。把 SDK、NDK、JDK 三个路径分别填进去,路径同样遵守"全英文、无空格"的铁律。填完后编辑器一般会自动校验,路径不对会当场标红,这是好事,能省下后面瞎猜的时间。
5.2 构建面板参数逐项解释
点编辑器工具栏的"构建发布"按钮,或者菜单里的对应入口,会弹出构建面板。平台下拉选 Android 之后,字段不少,我挑必须搞懂的讲。
应用包名(Bundle Identifier / Application ID),格式类似com.你的标识.helloworld。规则是每段只能用字母数字下划线,且不能以数字开头。这个名字一旦发布出去就改不了了,所以别用com.example.test这种随手写的。
目标 API 级别(Target API Level),跟着应用市场的最新要求走就行,选太低会在上架时被拒。
ABI 架构,这个必看。arm64-v8a是现在绝大多数真机的主力架构,必须勾。armeabi-v7a是给老设备的兼容选项,勾上会让 APK 体积变大,只在确实需要覆盖老机型时开。x86_64基本只有模拟器用,打正式包不用勾。
屏幕方向,我们的 Demo 是竖屏布局,选 Portrait。
签名,调试阶段勾一个"使用调试密钥"就行,编辑器会自动生成 debug keystore。真机上随便装。等你真要发布的时候,得自己生成:
keytool -genkeypair -v -keystore helloworld.keystore -alias helloworld -keyalg RSA -keysize 2048 -validity 36500-validity 36500是 100 年有效期,听着夸张,但这是业界通行做法——Android 要求签名密钥的有效期必须长于应用的预期生命期,不然到期后无法更新。
还有一个极其容易漏的字段:参与构建的场景列表。面板上会有一个列表让你勾选要把哪些.scene打进包里。如果你没把HelloWorld.scene勾上、或者没有把它设成"主场景",构建出来的 APK 装上去就是纯黑屏——程序正常启动,但没有场景可加载。
5.3 构建、生成与真机安装
Cocos Creator 的 Android 打包是两步走的:先点构建,把资源编译、代码转译、原生工程生成到build/android目录下;再点生成,调用 Gradle 把原生工程编译成 APK。
第二步是新手最容易卡住的地方,因为 Gradle 要下载一大堆依赖。如果你的网络环境访问官方仓库比较慢,最有效的办法是在原生工程的build.gradle里把仓库地址换成国内的 Maven 镜像。操作方式是构建完成后,去build/android/proj目录下找到 gradle 相关配置文件,把google()、mavenCentral()前面加上镜像源地址。改完再点"生成"就行,通常能从"卡半小时"变成"几分钟搞定"。
编译成功后,构建面板的日志末尾会打印出 APK 的绝对路径,别去猜目录结构,直接拉到最后一行复制路径最省事。然后连上手机(开启开发者模式和 USB 调试),命令行执行:
adb install -r 路径/你的应用名.apk-r表示覆盖安装,重复安装时不用先卸载。装好后点开图标,如果一切正常,你会看到那行文字从空白开始一个字一个字往外蹦——这种"我的代码在真机上跑起来了"的感觉,和当年在终端里打下第一行hello world是一模一样的。
看日志的办法是adb logcat,但输出太杂,可以用标签过滤:
adb logcat -s CocosWindows 下用findstr替代grep。把console.log和报错都打出来,真机上出问题时这是唯一的线索来源。
6. 常见问题与排查技巧实录
6.1 高频问题速查表
下面这张表是我这几年攒下来的,基本覆盖了新手在 Hello World 阶段会撞见的问题:
| 现象 | 大概率原因 | 排查动作 |
|---|---|---|
| 构建时报找不到 SDK/NDK | 路径含中文或空格,或版本不匹配 | 换纯英文浅层级路径,核对官方版本表 |
| Gradle 长时间卡在 Downloading | 依赖仓库访问慢 | 配置国内 Maven 镜像源 |
| APK 装上去闪退 | ABI 不匹配或原生库缺失 | 检查 arm64-v8a 是否勾选,看 logcat 的 native crash |
| 装上去是黑屏但不是闪退 | 构建时没勾场景 | 检查"参与构建的场景"列表和主场景设置 |
| 中文显示成方块 | 目标机缺字体 | 把 ttf/otf 字体文件放进项目并绑定给 Label |
| 编辑器里属性显示 None 且拖不进去 | 脚本编译报错 | 看控制台红色报错,先修语法 |
| 预览时脚本完全没响应 | 组件没挂或引用没绑 | 检查节点上有没有该组件,字段是否为空 |
| 文字位置和编辑器里不一致 | 父节点锚点或 Widget 影响 | 展开节点树看父级变换,检查 Widget 对齐 |
6.2 几个新手最容易忽略的细节
第一个坑:脚本保存了但编辑器没同步。Cocos Creator 的脚本是随编辑器进程一起编译的,你在外部编辑器里改完代码,必须切回引擎窗口,等它完成一次资源刷新和编译。有时候它卡住了,你写的代码根本没生效,然后你对着"为什么改了没反应"发呆半小时。遇到这种情况,先看窗口标题栏有没有转圈,或者干脆重启编辑器。
第二个坑:Label 节点的尺寸是零。这在使用 Button 的时候特别致命——Button 的点击判定依赖节点的 UITransform 尺寸,如果尺寸是 0×0,你怎么点都没反应。Label 组件在Overflow设为NONE时,节点尺寸会自动撑到文字大小;一旦设成CLAMP或SHRINK,尺寸就由你手动指定了,得自己改成合适的值。
第三个坑:UI 相机可见性。3.x 里相机有一个Visibility属性,决定它渲染哪些层的内容。UI 默认在UI_2D层。如果你不小心把相机的可见层改动了,或者新建了一个自定义相机忘了勾 UI_2D,就会出现"节点在,属性都对,但屏幕上什么都没有"的诡异现象。
第四个坑:中文项目的路径问题。我把这条单拎出来说,是因为它太隐蔽了。项目的完整路径里只要有任何一层目录是中文,某些构建环节就可能失败,而且报错信息往往指向别的地方,让人完全猜不到原因。团队协作时尤其要注意,让所有人都把项目放在英文路径下,能省掉大量扯皮。
7. 从 Hello World 往后走,还能玩出什么花样
7.1 加一个按钮,把事件系统跑通
静态文字 + 打字机效果之后,下一步自然就是交互。在场景里创建一个 Button 节点(创建 -> 2D 对象 -> Button),它下面会自带一个 Label 子节点,改改文字就行。然后写这么一段:
import { _decorator, Component, Label, Button } from 'cc'; const { ccclass, property } = _decorator; @ccclass('HelloWorldPlus') export class HelloWorldPlus extends Component { @property(Label) public label: Label = null!; @property(Button) public button: Button = null!; private _counter: number = 0; onLoad() { this.button.node.on(Button.EventType.CLICK, this.onButtonClick, this); } private onButtonClick() { this._counter++; this.label.string = `Hello World x ${this._counter}`; } onDestroy() { this.button.node.off(Button.EventType.CLICK, this.onButtonClick, this); } }这里有个细节值得单独说:我在onLoad里注册事件,在onDestroy里注销。很多新手只写前者,在复杂场景里会导致节点销毁后回调还在,引擎访问已经释放的对象,然后报一堆莫名其妙的错。有注册就配一个注销,这个习惯从 Hello World 阶段就养成,能省掉未来无数个夜晚。
另外,事件回调的第三个参数this是绑定上下文用的。如果你写成this.button.node.on('click', this.onButtonClick),回调里的this会指向节点而不是组件,this._counter直接变成undefined。这种错误不报错,只是数值不对,排查起来相当费劲。
7.2 用 Prefab 管理重复节点
如果你打算做一排按钮,或者很多个一样的文字标签,手动复制节点会让场景文件越来越臃肿,改一处还要改十遍。正确做法是用Prefab(预制体):把配好的节点从层级管理器直接拖到资源管理器的assets目录下,它就会变成一个.prefab文件。之后要用就在资源管理器里拖进场景,所有实例共享同一份模板,改了模板所有实例一起变。
Prefab 的威力在动态生成时才真正显现。你可以在脚本里这样写:
import { _decorator, Component, Prefab, instantiate, Node } from 'cc'; const { ccclass, property } = _decorator; @ccclass('Spawner') export class Spawner extends Component { @property(Prefab) public itemPrefab: Prefab = null!; public spawnOne() { const node: Node = instantiate(this.itemPrefab); node.setParent(this.node); node.setPosition(0, 0); } }instantiate会克隆一份预制体的完整节点树,包括里面的组件和属性。这是所有列表、关卡、敌人刷新的基础套路。从 Hello World 到能用的 UI 界面,中间隔的其实就是这么一个"模板 + 实例化"的思维转变。
后续如果还想往下走,方向就很多了:用tween做入场动画、引入AudioSource播个音效、把文字换成图片精灵、接入脚本加密、尝试微信小游戏平台的构建。但无论走多远,这套"节点树 + 组件 + 生命周期 + 构建配置"的骨架都不会变,它们是你在这个引擎里所有表达的语法基础。
我个人在实际操作中的体会是,Hello World 这一步真的不该草草跳过。我见过太多人上来就照着教程做完整的小游戏,跑是跑起来了,但一遇到黑屏、文字不显示、点击无响应这类问题就彻底懵,因为他们不知道那个黑盒子里到底发生了什么。把 Hello World 从头到尾、从编辑器到 APK 都亲手走一遍,你脑子里会建立起一条完整的链路,后面所有的复杂度,都是在这条链路上加东西而已。