1. 项目概述与核心思路拆解“大鱼吃小鱼”这个游戏概念相信大家都不陌生。它本质上是一个经典的成长型游戏玩家操控一条小鱼通过不断吃掉比自己小的鱼来成长壮大同时要躲避比自己大的鱼的追捕。这个玩法简单、反馈直接非常适合作为2D游戏开发的入门项目。选择Cocos Creator作为开发工具是因为它对于2D游戏开发来说门槛相对较低可视化编辑器能极大提升场景搭建和UI制作的效率而其背后的Cocos2d-x引擎则提供了稳定可靠的底层支持。用Cocos Creator来复现“大鱼吃小鱼”我们不仅能快速实现核心玩法还能深入理解游戏对象管理、物理碰撞、状态机、UI交互等游戏开发的核心模块。这个教程的目标是带你从零开始一步步构建一个完整的、可玩的“大鱼吃小鱼”游戏。我们会涵盖从项目创建、资源准备、玩家控制、AI敌人、成长系统、游戏逻辑到最终打包的全流程。过程中我会穿插很多我实际开发中踩过的“坑”和总结的技巧让你不仅能做出这个游戏更能理解为什么这么做。2. 开发环境搭建与项目初始化2.1 Cocos Creator编辑器安装与配置首先你需要去Cocos官网下载Cocos Dashboard。我强烈建议使用Dashboard来管理你的Cocos Creator版本和项目这比单独安装编辑器要方便得多。下载安装后打开Dashboard你会看到一个清晰的界面。在“编辑器”标签页选择最新的LTS长期支持版本进行安装。对于新手LTS版本是最稳定的选择能避免很多因版本迭代带来的兼容性问题。安装过程可能需要一些时间取决于你的网络速度。安装完成后回到Dashboard的“项目”标签页点击“新建”按钮。这里你会看到多种项目模板。对于我们的2D游戏选择“Empty(2D)”这个空白模板就足够了。给项目起个名字比如“BigFishEatSmallFish”然后选择一个你熟悉的目录存放。点击“创建并打开”Cocos Creator编辑器就会启动并加载你的新项目。注意项目路径和名称尽量不要使用中文或特殊字符虽然新版Cocos Creator对此支持已经好了很多但使用纯英文路径和名称能从根本上避免一些潜在的、奇怪的编码或路径问题。2.2 项目结构初探与资源管理规范项目创建好后让我们先花几分钟熟悉一下编辑器的界面和项目结构。编辑器主要分为几个区域中间的“场景编辑器”是你搭建游戏世界的地方左上方的“层级管理器”以树状结构展示场景中的所有节点右下方的“资源管理器”是你项目所有资源图片、脚本、声音等的家右边的“属性检查器”会显示当前选中节点的所有可配置属性。在“资源管理器”面板我习惯先建立几个文件夹来分类管理资源这会让后续开发井井有条textures: 存放所有图片资源如精灵图、UI图标。scripts: 存放所有的TypeScript脚本。scenes: 存放游戏场景文件。prefabs: 存放预制体Prefab这是一种可复用的节点模板。audios: 存放音效和背景音乐。fonts: 存放字体文件。你可以通过右键点击“资源管理器”的assets根目录选择“新建 - 文件夹”来创建。良好的目录结构是项目可维护性的第一步千万别把所有文件都扔在根目录下。3. 核心游戏对象创建与基础配置3.1 玩家角色大鱼的创建与控制我们的主角——大鱼本质上是一个带有精灵Sprite和碰撞体Collider的节点。在“层级管理器”中右键点击“Canvas”节点选择“创建节点 - 创建渲染节点 - Sprite”。将这个新节点重命名为“Player”。接下来你需要为玩家准备一张图片。你可以自己绘制或者从一些免费的资源网站要遵守版权协议获取一张鱼的图片。将图片文件如player_fish.png拖入assets/textures文件夹。然后在“层级管理器”中选中“Player”节点在右侧“属性检查器”中找到“Sprite”组件下的“Sprite Frame”属性。点击那个小小的文件夹图标在弹出的资源选择窗口中找到并选中你刚导入的player_fish.png。现在你的玩家还只是一个静态的图片。我们需要让它动起来并响应用户输入。在“资源管理器”的scripts文件夹上右键选择“新建 - TypeScript”创建一个名为PlayerController的脚本。双击打开它我们会编写控制逻辑。首先我们需要让鱼能跟着鼠标或触摸移动。一个常见的做法是让鱼平滑地朝向鼠标位置移动。以下是PlayerController.ts的核心移动逻辑import { _decorator, Component, Vec2, Vec3, input, Input, EventTouch, EventMouse } from cc; const { ccclass, property } _decorator; ccclass(PlayerController) export class PlayerController extends Component { // 移动速度 property moveSpeed: number 500; // 平滑移动的缓动系数值越小越平滑 property smoothFactor: number 0.1; private _targetPos: Vec3 new Vec3(); private _isMoving: boolean false; start() { // 监听鼠标按下/移动事件PC端 input.on(Input.EventType.MOUSE_DOWN, this.onMouseDown, this); input.on(Input.EventType.MOUSE_MOVE, this.onMouseMove, this); // 监听触摸事件移动端 input.on(Input.EventType.TOUCH_START, this.onTouchStart, this); input.on(Input.EventType.TOUCH_MOVE, this.onTouchMove, this); } onMouseDown(event: EventMouse) { this.setTargetPosition(event.getLocationX(), event.getLocationY()); } onMouseMove(event: EventMouse) { if (event.getButton() 0) { // 左键拖拽 this.setTargetPosition(event.getLocationX(), event.getLocationY()); } } onTouchStart(event: EventTouch) { const touch event.touch!; this.setTargetPosition(touch.getLocationX(), touch.getLocationY()); } onTouchMove(event: EventTouch) { const touch event.touch!; this.setTargetPosition(touch.getLocationX(), touch.getLocationY()); } // 将屏幕坐标转换为世界坐标并设置为目标位置 setTargetPosition(screenX: number, screenY: number) { // 这里需要将屏幕坐标转换到Canvas节点下的坐标。 // 为了简化我们假设Canvas的锚点在中心且设计分辨率与屏幕匹配。 // 更严谨的做法是使用UITransform的convertToNodeSpaceAR等方法。 const canvas this.node.parent!; const canvasPos canvas.getComponent(UITransform)!.convertToNodeSpaceAR(new Vec3(screenX, screenY, 0)); this._targetPos.set(canvasPos.x, canvasPos.y, 0); this._isMoving true; } update(deltaTime: number) { if (!this._isMoving) return; const currentPos this.node.getPosition(); // 计算朝向目标的方向向量 const direction new Vec3(this._targetPos.x - currentPos.x, this._targetPos.y - currentPos.y, 0); const distance direction.length(); // 如果已经非常接近目标点则停止移动 if (distance 5) { this._isMoving false; return; } // 归一化方向向量并计算本帧位移 direction.normalize(); // 计算平滑移动当前位置向目标位置插值 const newX currentPos.x (this._targetPos.x - currentPos.x) * this.smoothFactor; const newY currentPos.y (this._targetPos.y - currentPos.y) * this.smoothFactor; this.node.setPosition(newX, newY, 0); // 让鱼朝向移动方向可选 if (distance 10) { // 避免在很近时频繁旋转 const angle Math.atan2(direction.y, direction.x) * 180 / Math.PI; this.node.setRotationFromEuler(0, 0, angle); } } onDestroy() { // 记得移除事件监听防止内存泄漏 input.off(Input.EventType.MOUSE_DOWN, this.onMouseDown, this); input.off(Input.EventType.MOUSE_MOVE, this.onMouseMove, this); input.off(Input.EventType.TOUCH_START, this.onTouchStart, this); input.off(Input.EventType.TOUCH_MOVE, this.onTouchMove, this); } }编写完脚本后回到编辑器将PlayerController组件拖拽到“层级管理器”中的“Player”节点上。你可以在“属性检查器”中调整moveSpeed和smoothFactor参数直到找到你觉得手感最好的数值。实操心得在setTargetPosition方法中坐标转换是关键。上述简化方法在Canvas适配模式为SHOW_ALL或FIXED_WIDTH等情况下可能会不准。更健壮的做法是使用view.getVisibleSize()和view.getVisibleOrigin()来获取视口信息再进行转换。对于新手如果发现点击位置和鱼移动的位置对不上首先检查Canvas的Align设置和设计分辨率。3.2 小鱼食物/AI的生成与管理游戏中的小鱼玩家前期的食物后期的敌人需要被动态生成和管理。我们将创建一个“小鱼”预制体并编写一个生成器。首先创建一个小鱼节点。和创建玩家类似新建一个Sprite节点命名为“SmallFish”并赋予它一张小鱼的图片。然后为了让鱼能游动我们可以为它添加一个简单的AI脚本SmallFishAI.tsimport { _decorator, Component, Vec3, math } from cc; const { ccclass, property } _decorator; ccclass(SmallFishAI) export class SmallFishAI extends Component { property moveSpeed: number 100; property directionChangeInterval: number 2; // 方向改变间隔秒 private _moveDirection: Vec3 new Vec3(1, 0, 0); private _timer: number 0; start() { // 初始随机方向 this.randomizeDirection(); } randomizeDirection() { const angle math.randomRange(0, Math.PI * 2); this._moveDirection.x Math.cos(angle); this._moveDirection.y Math.sin(angle); this._moveDirection.normalize(); } update(deltaTime: number) { this._timer deltaTime; if (this._timer this.directionChangeInterval) { this.randomizeDirection(); this._timer 0; } const newPos this.node.position.clone(); newPos.x this._moveDirection.x * this.moveSpeed * deltaTime; newPos.y this._moveDirection.y * this.moveSpeed * deltaTime; this.node.setPosition(newPos); // 简单边界检查碰到边界就反弹可选更复杂可以做成环绕 const screenSize view.getVisibleSize(); const halfWidth screenSize.width / 2; const halfHeight screenSize.height / 2; const pos this.node.position; if (Math.abs(pos.x) halfWidth) { this._moveDirection.x * -1; // 防止卡在边界 newPos.x math.clamp(newPos.x, -halfWidth, halfWidth); this.node.setPosition(newPos); } if (Math.abs(pos.y) halfHeight) { this._moveDirection.y * -1; newPos.y math.clamp(newPos.y, -halfHeight, halfHeight); this.node.setPosition(newPos); } // 朝向移动方向 const angle Math.atan2(this._moveDirection.y, this._moveDirection.x) * 180 / Math.PI; this.node.setRotationFromEuler(0, 0, angle); } }将这个脚本挂载到“SmallFish”节点上。然后我们将这个配置好的节点拖拽到“资源管理器”的assets/prefabs文件夹中。这时你会看到它变成了一个蓝色的预制体资源而场景中的那个实例可以删除了。预制体就像是一个模板我们可以在代码中动态创建它的多个实例。接下来创建一个“FishSpawner”节点并为其添加一个FishSpawner.ts脚本负责定时生成小鱼import { _decorator, Component, Prefab, instantiate, Node, CCInteger } from cc; const { ccclass, property } _decorator; ccclass(FishSpawner) export class FishSpawner extends Component { property(Prefab) smallFishPrefab: Prefab | null null; property(CCInteger) maxFishCount: number 20; // 场景中最多存在的小鱼数量 property spawnInterval: number 1.0; // 生成间隔秒 private _spawnTimer: number 0; private _activeFishes: Node[] []; start() { // 初始生成一些鱼 for (let i 0; i 5; i) { this.spawnFish(); } } update(deltaTime: number) { this._spawnTimer deltaTime; if (this._spawnTimer this.spawnInterval this._activeFishes.length this.maxFishCount) { this.spawnFish(); this._spawnTimer 0; } // 可以在这里添加清理超出屏幕外鱼的逻辑 } spawnFish() { if (!this.smallFishPrefab) return; const fish instantiate(this.smallFishPrefab); this.node.addChild(fish); // 将鱼添加到生成器节点下方便管理 // 在屏幕边缘随机位置生成 const screenSize view.getVisibleSize(); const side math.randomRangeInt(0, 4); // 0:上1:右2:下3:左 let x 0, y 0; switch (side) { case 0: // 上边 x math.randomRange(-screenSize.width / 2, screenSize.width / 2); y screenSize.height / 2; break; case 1: // 右边 x screenSize.width / 2; y math.randomRange(-screenSize.height / 2, screenSize.height / 2); break; case 2: // 下边 x math.randomRange(-screenSize.width / 2, screenSize.width / 2); y -screenSize.height / 2; break; case 3: // 左边 x -screenSize.width / 2; y math.randomRange(-screenSize.height / 2, screenSize.height / 2); break; } fish.setPosition(x, y, 0); this._activeFishes.push(fish); } // 当鱼被吃掉或销毁时从数组中移除 removeFish(fishNode: Node) { const index this._activeFishes.indexOf(fishNode); if (index -1) { this._activeFishes.splice(index, 1); fishNode.destroy(); // 销毁节点 } } }将FishSpawner脚本挂载到场景中的一个空节点上比如就叫“FishSpawner”然后在“属性检查器”中将我们之前创建的SmallFish预制体拖拽到smallFishPrefab属性栏中。运行游戏你应该能看到小鱼不断地从屏幕边缘生成并游动。4. 游戏核心逻辑实现碰撞、成长与状态4.1 碰撞检测与“吃”的逻辑“大鱼吃小鱼”的核心就是碰撞。我们需要为玩家和小鱼添加碰撞体。Cocos Creator提供了多种2D碰撞组件对于这种简单的圆形或矩形判断使用CircleCollider2D或BoxCollider2D都很方便。添加碰撞体在“层级管理器”中选中“Player”节点点击“属性检查器”下方的“添加组件”按钮选择“Physics 2D - Circle Collider 2D”。调整Radius半径使其大致匹配你的鱼图片大小。用同样的方法为“SmallFish”预制体添加碰撞体记得在预制体模式下编辑或者编辑完实例后点击“预制体”面板的“保存”。启用物理系统碰撞检测需要物理系统。在“层级管理器”中选中“Canvas”或任意节点添加组件“PhysicsSystem2D”。通常保持默认设置即可但你可以调整gravity重力为(0, 0)因为我们是在水中不需要重力。编写碰撞逻辑我们需要修改PlayerController和SmallFishAI或者新建一个Fish脚本来检测碰撞。通常我们会在玩家身上监听碰撞事件。在PlayerController.ts中增加以下代码import { Collider2D, Contact2DType, IPhysics2DContact } from cc; export class PlayerController extends Component { // ... 之前已有的属性 ... // 玩家当前等级/大小 private _fishSize: number 1; property baseScale: number 1.0; // 基础缩放 start() { // ... 之前的事件监听 ... // 获取碰撞体组件并监听碰撞 const collider this.getComponent(Collider2D); if (collider) { collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); } } onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) { // 判断碰撞到的物体是否是“可吃的鱼” // 我们可以通过节点的分组Group或者标签Tag来判断这里使用分组更规范。 // 假设我们为小鱼设置了特定的分组例如“SmallFish” if (otherCollider.group 2) { // 假设2是小鱼的分组 console.log(吃到了一条小鱼); // 1. 增长玩家 this.grow(); // 2. 销毁小鱼 // 我们需要一个方法来通知FishSpawner或直接销毁小鱼节点 // 这里假设小鱼节点上有一个SmallFish脚本里面提供了beEaten方法 const fishScript otherCollider.node.getComponent(SmallFish); // 或 SmallFishAI if (fishScript fishScript.beEaten) { fishScript.beEaten(); } else { // 如果没有脚本直接销毁不推荐不利于管理 // otherCollider.node.destroy(); } } // 还可以在这里判断是否撞到了更大的鱼游戏结束 } grow() { this._fishSize 0.1; // 每次成长增加0.1 // 更新视觉大小 const newScale this.baseScale * this._fishSize; this.node.setScale(newScale, newScale, 1); // 可以同时增加移动速度让大鱼游得更快 // this.moveSpeed 10; } // ... update等其他方法 ... }相应地我们需要为小鱼SmallFishAI脚本添加分组和beEaten方法设置分组在编辑器中选择“SmallFish”预制体根节点在“属性检查器”最上方找到“Layer”旁边有个下拉菜单点击“添加分组...”。在项目设置Project - Project Settings - Layers中可以添加一个名为“SmallFish”的分组并分配一个索引例如2。然后为“SmallFish”节点选择这个分组。同样为“Player”节点设置一个分组比如“Player”索引1。修改脚本在SmallFishAI.ts中添加export class SmallFishAI extends Component { // ... 已有属性 ... // 引用生成器用于通知自己被吃掉 private _spawner: FishSpawner | null null; // 可以在生成时由Spawner设置 setSpawner(spawner: FishSpawner) { this._spawner spawner; } beEaten() { if (this._spawner) { this._spawner.removeFish(this.node); } else { this.node.destroy(); } } // ... 其他方法 ... }同时需要修改FishSpawner.ts的spawnFish方法在生成鱼后为其设置生成器引用spawnFish() { // ... 创建和设置位置的代码 ... this._activeFishes.push(fish); // 获取小鱼身上的AI脚本并设置生成器 const fishAI fish.getComponent(SmallFishAI); if (fishAI) { fishAI.setSpawner(this); } }注意事项物理碰撞的分组和掩码Mask设置是关键。你需要确保玩家的碰撞体能检测到小鱼的碰撞体。在“项目设置 - 物理 - 碰撞矩阵”中确保“Player”分组和“SmallFish”分组之间的复选框是勾选的表示它们之间会发生碰撞检测。4.2 游戏状态管理与UI交互一个完整的游戏需要有开始、进行中、结束等状态以及相应的UI界面。创建UI在“层级管理器”中右键点击“Canvas”节点选择“创建节点 - 创建UI节点 - Button”。将其重命名为“Btn_Start”。你可以修改它的Label文字为“开始游戏”。用同样的方法创建两个Text节点分别命名为“Txt_Score”和“Txt_GameOver”用于显示分数和游戏结束信息。将它们摆放在合适的位置。创建游戏管理器新建一个脚本GameManager.ts它将作为游戏的大脑管理状态、分数和UI。import { _decorator, Component, Label, Button, director } from cc; const { ccclass, property } _decorator; enum GameState { GS_INIT, // 初始化/菜单 GS_PLAYING, // 游戏中 GS_OVER, // 游戏结束 } ccclass(GameManager) export class GameManager extends Component { // 单例模式方便其他脚本访问 private static _instance: GameManager | null null; public static get instance(): GameManager { return GameManager._instance!; } property(Label) scoreLabel: Label | null null; property(Label) gameOverLabel: Label | null null; property(Button) startButton: Button | null null; property(Button) restartButton: Button | null null; private _currentState: GameState GameState.GS_INIT; private _score: number 0; onLoad() { if (GameManager._instance GameManager._instance ! this) { this.node.destroy(); return; } GameManager._instance this; // 防止切换场景时被销毁 director.addPersistRootNode(this.node); } start() { this.setState(GameState.GS_INIT); } setState(state: GameState) { this._currentState state; switch (state) { case GameState.GS_INIT: this._score 0; this.updateScoreUI(); if (this.gameOverLabel) this.gameOverLabel.node.active false; if (this.startButton) this.startButton.node.active true; if (this.restartButton) this.restartButton.node.active false; // 暂停游戏逻辑如果有 break; case GameState.GS_PLAYING: if (this.gameOverLabel) this.gameOverLabel.node.active false; if (this.startButton) this.startButton.node.active false; if (this.restartButton) this.restartButton.node.active false; // 恢复或启动游戏逻辑 break; case GameState.GS_OVER: if (this.gameOverLabel) { this.gameOverLabel.node.active true; this.gameOverLabel.string 游戏结束得分${this._score}; } if (this.restartButton) this.restartButton.node.active true; // 暂停游戏逻辑 break; } } // 由PlayerController调用增加分数 addScore(points: number) { if (this._currentState ! GameState.GS_PLAYING) return; this._score points; this.updateScoreUI(); // 可以在这里添加分数达到一定值后玩家升级的逻辑 } updateScoreUI() { if (this.scoreLabel) { this.scoreLabel.string 分数${this._score}; } } // UI按钮回调方法 onStartButtonClicked() { this.setState(GameState.GS_PLAYING); } onRestartButtonClicked() { // 简单重启重新加载当前场景 director.loadScene(director.getScene()!.name); // 更优雅的做法是重置所有游戏对象的状态而不是重载场景 } // 游戏结束条件触发例如撞到大鱼 gameOver() { this.setState(GameState.GS_OVER); } }关联UI与逻辑将GameManager脚本挂载到一个空节点如“GameManager”上。然后将场景中的UI节点Txt_Score, Txt_GameOver, Btn_Start, Btn_Restart拖拽到GameManager组件对应的属性栏中。设置按钮点击事件选中“Btn_Start”按钮在“属性检查器”的“Button”组件下方找到“Click Events”。将数量设置为1然后将“GameManager”节点拖拽到第一个“Target”栏在“Component”下拉菜单中选择“GameManager”在“Handler”下拉菜单中选择“onStartButtonClicked”。对“Btn_Restart”按钮做类似操作关联到onRestartButtonClicked方法。整合玩家与游戏管理器修改PlayerController的grow方法在增长时调用GameManager.instance.addScore(10)来增加分数。同时你需要在玩家碰撞检测中添加判断是否撞到“大鱼”可以是一个新的分组如“BigFish”如果撞到则调用GameManager.instance.gameOver()。5. 功能扩展与性能优化5.1 添加更多游戏元素基础的“吃”和“被吃”逻辑完成后我们可以让游戏变得更有趣多种鱼类创建不同大小、不同速度、不同分数的小鱼和大鱼预制体。修改FishSpawner使其能随机生成不同类型的鱼。玩家只能吃比自己小的鱼碰到比自己大的鱼则游戏结束。成长系统玩家大小不仅影响外观还应影响可吞噬鱼类的范围。可以定义一个“吞噬半径”或“吞噬等级”只有等级比玩家低的鱼才能被吃。道具系统添加一些特殊道具如“加速”、“无敌”、“磁铁”自动吸引小鱼等。这些道具可以随机生成被玩家碰撞后触发效果。关卡与难度递增随着分数增加可以增加生成鱼的速度、数量或者出现更多的大鱼提高游戏难度。5.2 性能优化与注意事项当屏幕上有几十上百条鱼在游动时性能可能成为问题。以下是一些优化技巧对象池Object Pooling这是游戏开发中最重要的优化手段之一。不要频繁地instantiate创建和destroy销毁对象尤其是小鱼。我们可以预先创建一定数量的小鱼对象不用时隐藏或回收需要时再取出激活。Cocos Creator内置了NodePool类来实现对象池。// 在FishSpawner中实现一个简单的对象池 import { NodePool } from cc; export class FishSpawner extends Component { private _fishPool: NodePool new NodePool(); // ... 其他属性 ... start() { // 初始化对象池预先创建一些鱼 for (let i 0; i this.maxFishCount; i) { const fish instantiate(this.smallFishPrefab!); this._fishPool.put(fish); // 放入池中默认会失活节点 } } spawnFish() { let fish: Node; if (this._fishPool.size() 0) { fish this._fishPool.get(); // 从池中取会自动激活 } else { fish instantiate(this.smallFishPrefab!); // 池空了才创建新的 } // ... 设置位置、初始化 ... fish.parent this.node; this._activeFishes.push(fish); } removeFish(fishNode: Node) { const index this._activeFishes.indexOf(fishNode); if (index -1) { this._activeFishes.splice(index, 1); // 不销毁而是放回对象池 this._fishPool.put(fishNode); } } }绘制调用合并Draw Call Batching对于大量使用相同材质和纹理的静态或动态精灵比如所有同一种小鱼Cocos Creator会自动尝试进行合批以减少GPU绘制调用。确保它们的渲染顺序layer接近并且使用相同的纹理图集SpriteAtlas能提高合批成功率。避免在update中做复杂计算如果每帧都需要遍历所有鱼来检查距离等当鱼数量多时开销很大。可以考虑使用四叉树Quadtree等空间分区数据结构来优化碰撞检测范围。资源管理将小鱼的图片打包成纹理图集能减少纹理切换提升渲染效率。在Cocos Creator的“资源管理器”中可以将多张图片拖到一个文件夹然后右键选择“创建 - Sprite Atlas”来创建图集。5.3 常见问题与排查技巧碰撞不生效检查分组和掩码确保发生碰撞的两个节点的碰撞体分组在“项目设置 - 物理 - 碰撞矩阵”中是相互勾选的。检查碰撞体大小和位置在场景编辑器中碰撞体会以绿色线框显示。确保线框确实包裹住了你的精灵图像。检查物理系统是否启用场景中需要有PhysicsSystem2D组件。检查回调函数是否正确绑定确认on(Contact2DType.BEGIN_CONTACT, ...)这行代码被执行了。预制体修改后实例不更新在“场景编辑器”中修改了预制体实例后记得点击该节点“属性检查器”顶部的“预制体”选项卡中的“保存”按钮才能将修改应用回预制体资源。反之修改了预制体资源后场景中的实例可能需要手动点击“预制体”选项卡的“回退”或“应用”来同步。脚本编译错误或找不到组件Cocos Creator使用TypeScript编译错误会在“控制台”面板以红色显示。仔细阅读错误信息通常是语法错误或类型错误。如果脚本中使用了getComponent(‘YourScript’)但找不到请检查脚本类名上的ccclass(‘YourScript’)装饰器中的字符串是否与getComponent里的字符串完全一致包括大小写。游戏在移动端触摸不准坐标转换问题。UI触摸事件获取的是屏幕坐标需要正确转换到Canvas节点的本地坐标。确保你的Canvas适配模式如FIXED_WIDTH设置正确并使用UITransform的convertToNodeSpaceAR等方法进行转换。性能突然下降打开Chrome浏览器的开发者工具F12切换到“Performance”或“Memory”标签页录制一段时间游戏运行过程。查看是否有内存泄漏Memory Leak内存使用量持续增长或某段脚本执行时间Scripting过长。对象池未正确管理是常见的内存泄漏原因。走到这一步一个具备核心玩法的“大鱼吃小鱼”游戏已经完成了。从角色控制、AI生成、碰撞交互到状态管理我们走完了2D游戏开发的一个典型流程。你可以在此基础上继续添加更多功能比如更精美的美术资源、粒子特效吃鱼时的泡泡、背景音乐和音效甚至是一些简单的剧情关卡。我个人在实际开发这类休闲游戏时最深的一点体会是快速原型和迭代比一开始就追求完美更重要。先用简单的方块和圆圈把核心玩法移动、碰撞、成长跑通验证乐趣所在然后再逐步替换美术资源、添加细节。这能帮你牢牢抓住项目的核心避免在次要细节上过度消耗时间。
Cocos Creator实战:从零开发2D游戏《大鱼吃小鱼》全流程指南
1. 项目概述与核心思路拆解“大鱼吃小鱼”这个游戏概念相信大家都不陌生。它本质上是一个经典的成长型游戏玩家操控一条小鱼通过不断吃掉比自己小的鱼来成长壮大同时要躲避比自己大的鱼的追捕。这个玩法简单、反馈直接非常适合作为2D游戏开发的入门项目。选择Cocos Creator作为开发工具是因为它对于2D游戏开发来说门槛相对较低可视化编辑器能极大提升场景搭建和UI制作的效率而其背后的Cocos2d-x引擎则提供了稳定可靠的底层支持。用Cocos Creator来复现“大鱼吃小鱼”我们不仅能快速实现核心玩法还能深入理解游戏对象管理、物理碰撞、状态机、UI交互等游戏开发的核心模块。这个教程的目标是带你从零开始一步步构建一个完整的、可玩的“大鱼吃小鱼”游戏。我们会涵盖从项目创建、资源准备、玩家控制、AI敌人、成长系统、游戏逻辑到最终打包的全流程。过程中我会穿插很多我实际开发中踩过的“坑”和总结的技巧让你不仅能做出这个游戏更能理解为什么这么做。2. 开发环境搭建与项目初始化2.1 Cocos Creator编辑器安装与配置首先你需要去Cocos官网下载Cocos Dashboard。我强烈建议使用Dashboard来管理你的Cocos Creator版本和项目这比单独安装编辑器要方便得多。下载安装后打开Dashboard你会看到一个清晰的界面。在“编辑器”标签页选择最新的LTS长期支持版本进行安装。对于新手LTS版本是最稳定的选择能避免很多因版本迭代带来的兼容性问题。安装过程可能需要一些时间取决于你的网络速度。安装完成后回到Dashboard的“项目”标签页点击“新建”按钮。这里你会看到多种项目模板。对于我们的2D游戏选择“Empty(2D)”这个空白模板就足够了。给项目起个名字比如“BigFishEatSmallFish”然后选择一个你熟悉的目录存放。点击“创建并打开”Cocos Creator编辑器就会启动并加载你的新项目。注意项目路径和名称尽量不要使用中文或特殊字符虽然新版Cocos Creator对此支持已经好了很多但使用纯英文路径和名称能从根本上避免一些潜在的、奇怪的编码或路径问题。2.2 项目结构初探与资源管理规范项目创建好后让我们先花几分钟熟悉一下编辑器的界面和项目结构。编辑器主要分为几个区域中间的“场景编辑器”是你搭建游戏世界的地方左上方的“层级管理器”以树状结构展示场景中的所有节点右下方的“资源管理器”是你项目所有资源图片、脚本、声音等的家右边的“属性检查器”会显示当前选中节点的所有可配置属性。在“资源管理器”面板我习惯先建立几个文件夹来分类管理资源这会让后续开发井井有条textures: 存放所有图片资源如精灵图、UI图标。scripts: 存放所有的TypeScript脚本。scenes: 存放游戏场景文件。prefabs: 存放预制体Prefab这是一种可复用的节点模板。audios: 存放音效和背景音乐。fonts: 存放字体文件。你可以通过右键点击“资源管理器”的assets根目录选择“新建 - 文件夹”来创建。良好的目录结构是项目可维护性的第一步千万别把所有文件都扔在根目录下。3. 核心游戏对象创建与基础配置3.1 玩家角色大鱼的创建与控制我们的主角——大鱼本质上是一个带有精灵Sprite和碰撞体Collider的节点。在“层级管理器”中右键点击“Canvas”节点选择“创建节点 - 创建渲染节点 - Sprite”。将这个新节点重命名为“Player”。接下来你需要为玩家准备一张图片。你可以自己绘制或者从一些免费的资源网站要遵守版权协议获取一张鱼的图片。将图片文件如player_fish.png拖入assets/textures文件夹。然后在“层级管理器”中选中“Player”节点在右侧“属性检查器”中找到“Sprite”组件下的“Sprite Frame”属性。点击那个小小的文件夹图标在弹出的资源选择窗口中找到并选中你刚导入的player_fish.png。现在你的玩家还只是一个静态的图片。我们需要让它动起来并响应用户输入。在“资源管理器”的scripts文件夹上右键选择“新建 - TypeScript”创建一个名为PlayerController的脚本。双击打开它我们会编写控制逻辑。首先我们需要让鱼能跟着鼠标或触摸移动。一个常见的做法是让鱼平滑地朝向鼠标位置移动。以下是PlayerController.ts的核心移动逻辑import { _decorator, Component, Vec2, Vec3, input, Input, EventTouch, EventMouse } from cc; const { ccclass, property } _decorator; ccclass(PlayerController) export class PlayerController extends Component { // 移动速度 property moveSpeed: number 500; // 平滑移动的缓动系数值越小越平滑 property smoothFactor: number 0.1; private _targetPos: Vec3 new Vec3(); private _isMoving: boolean false; start() { // 监听鼠标按下/移动事件PC端 input.on(Input.EventType.MOUSE_DOWN, this.onMouseDown, this); input.on(Input.EventType.MOUSE_MOVE, this.onMouseMove, this); // 监听触摸事件移动端 input.on(Input.EventType.TOUCH_START, this.onTouchStart, this); input.on(Input.EventType.TOUCH_MOVE, this.onTouchMove, this); } onMouseDown(event: EventMouse) { this.setTargetPosition(event.getLocationX(), event.getLocationY()); } onMouseMove(event: EventMouse) { if (event.getButton() 0) { // 左键拖拽 this.setTargetPosition(event.getLocationX(), event.getLocationY()); } } onTouchStart(event: EventTouch) { const touch event.touch!; this.setTargetPosition(touch.getLocationX(), touch.getLocationY()); } onTouchMove(event: EventTouch) { const touch event.touch!; this.setTargetPosition(touch.getLocationX(), touch.getLocationY()); } // 将屏幕坐标转换为世界坐标并设置为目标位置 setTargetPosition(screenX: number, screenY: number) { // 这里需要将屏幕坐标转换到Canvas节点下的坐标。 // 为了简化我们假设Canvas的锚点在中心且设计分辨率与屏幕匹配。 // 更严谨的做法是使用UITransform的convertToNodeSpaceAR等方法。 const canvas this.node.parent!; const canvasPos canvas.getComponent(UITransform)!.convertToNodeSpaceAR(new Vec3(screenX, screenY, 0)); this._targetPos.set(canvasPos.x, canvasPos.y, 0); this._isMoving true; } update(deltaTime: number) { if (!this._isMoving) return; const currentPos this.node.getPosition(); // 计算朝向目标的方向向量 const direction new Vec3(this._targetPos.x - currentPos.x, this._targetPos.y - currentPos.y, 0); const distance direction.length(); // 如果已经非常接近目标点则停止移动 if (distance 5) { this._isMoving false; return; } // 归一化方向向量并计算本帧位移 direction.normalize(); // 计算平滑移动当前位置向目标位置插值 const newX currentPos.x (this._targetPos.x - currentPos.x) * this.smoothFactor; const newY currentPos.y (this._targetPos.y - currentPos.y) * this.smoothFactor; this.node.setPosition(newX, newY, 0); // 让鱼朝向移动方向可选 if (distance 10) { // 避免在很近时频繁旋转 const angle Math.atan2(direction.y, direction.x) * 180 / Math.PI; this.node.setRotationFromEuler(0, 0, angle); } } onDestroy() { // 记得移除事件监听防止内存泄漏 input.off(Input.EventType.MOUSE_DOWN, this.onMouseDown, this); input.off(Input.EventType.MOUSE_MOVE, this.onMouseMove, this); input.off(Input.EventType.TOUCH_START, this.onTouchStart, this); input.off(Input.EventType.TOUCH_MOVE, this.onTouchMove, this); } }编写完脚本后回到编辑器将PlayerController组件拖拽到“层级管理器”中的“Player”节点上。你可以在“属性检查器”中调整moveSpeed和smoothFactor参数直到找到你觉得手感最好的数值。实操心得在setTargetPosition方法中坐标转换是关键。上述简化方法在Canvas适配模式为SHOW_ALL或FIXED_WIDTH等情况下可能会不准。更健壮的做法是使用view.getVisibleSize()和view.getVisibleOrigin()来获取视口信息再进行转换。对于新手如果发现点击位置和鱼移动的位置对不上首先检查Canvas的Align设置和设计分辨率。3.2 小鱼食物/AI的生成与管理游戏中的小鱼玩家前期的食物后期的敌人需要被动态生成和管理。我们将创建一个“小鱼”预制体并编写一个生成器。首先创建一个小鱼节点。和创建玩家类似新建一个Sprite节点命名为“SmallFish”并赋予它一张小鱼的图片。然后为了让鱼能游动我们可以为它添加一个简单的AI脚本SmallFishAI.tsimport { _decorator, Component, Vec3, math } from cc; const { ccclass, property } _decorator; ccclass(SmallFishAI) export class SmallFishAI extends Component { property moveSpeed: number 100; property directionChangeInterval: number 2; // 方向改变间隔秒 private _moveDirection: Vec3 new Vec3(1, 0, 0); private _timer: number 0; start() { // 初始随机方向 this.randomizeDirection(); } randomizeDirection() { const angle math.randomRange(0, Math.PI * 2); this._moveDirection.x Math.cos(angle); this._moveDirection.y Math.sin(angle); this._moveDirection.normalize(); } update(deltaTime: number) { this._timer deltaTime; if (this._timer this.directionChangeInterval) { this.randomizeDirection(); this._timer 0; } const newPos this.node.position.clone(); newPos.x this._moveDirection.x * this.moveSpeed * deltaTime; newPos.y this._moveDirection.y * this.moveSpeed * deltaTime; this.node.setPosition(newPos); // 简单边界检查碰到边界就反弹可选更复杂可以做成环绕 const screenSize view.getVisibleSize(); const halfWidth screenSize.width / 2; const halfHeight screenSize.height / 2; const pos this.node.position; if (Math.abs(pos.x) halfWidth) { this._moveDirection.x * -1; // 防止卡在边界 newPos.x math.clamp(newPos.x, -halfWidth, halfWidth); this.node.setPosition(newPos); } if (Math.abs(pos.y) halfHeight) { this._moveDirection.y * -1; newPos.y math.clamp(newPos.y, -halfHeight, halfHeight); this.node.setPosition(newPos); } // 朝向移动方向 const angle Math.atan2(this._moveDirection.y, this._moveDirection.x) * 180 / Math.PI; this.node.setRotationFromEuler(0, 0, angle); } }将这个脚本挂载到“SmallFish”节点上。然后我们将这个配置好的节点拖拽到“资源管理器”的assets/prefabs文件夹中。这时你会看到它变成了一个蓝色的预制体资源而场景中的那个实例可以删除了。预制体就像是一个模板我们可以在代码中动态创建它的多个实例。接下来创建一个“FishSpawner”节点并为其添加一个FishSpawner.ts脚本负责定时生成小鱼import { _decorator, Component, Prefab, instantiate, Node, CCInteger } from cc; const { ccclass, property } _decorator; ccclass(FishSpawner) export class FishSpawner extends Component { property(Prefab) smallFishPrefab: Prefab | null null; property(CCInteger) maxFishCount: number 20; // 场景中最多存在的小鱼数量 property spawnInterval: number 1.0; // 生成间隔秒 private _spawnTimer: number 0; private _activeFishes: Node[] []; start() { // 初始生成一些鱼 for (let i 0; i 5; i) { this.spawnFish(); } } update(deltaTime: number) { this._spawnTimer deltaTime; if (this._spawnTimer this.spawnInterval this._activeFishes.length this.maxFishCount) { this.spawnFish(); this._spawnTimer 0; } // 可以在这里添加清理超出屏幕外鱼的逻辑 } spawnFish() { if (!this.smallFishPrefab) return; const fish instantiate(this.smallFishPrefab); this.node.addChild(fish); // 将鱼添加到生成器节点下方便管理 // 在屏幕边缘随机位置生成 const screenSize view.getVisibleSize(); const side math.randomRangeInt(0, 4); // 0:上1:右2:下3:左 let x 0, y 0; switch (side) { case 0: // 上边 x math.randomRange(-screenSize.width / 2, screenSize.width / 2); y screenSize.height / 2; break; case 1: // 右边 x screenSize.width / 2; y math.randomRange(-screenSize.height / 2, screenSize.height / 2); break; case 2: // 下边 x math.randomRange(-screenSize.width / 2, screenSize.width / 2); y -screenSize.height / 2; break; case 3: // 左边 x -screenSize.width / 2; y math.randomRange(-screenSize.height / 2, screenSize.height / 2); break; } fish.setPosition(x, y, 0); this._activeFishes.push(fish); } // 当鱼被吃掉或销毁时从数组中移除 removeFish(fishNode: Node) { const index this._activeFishes.indexOf(fishNode); if (index -1) { this._activeFishes.splice(index, 1); fishNode.destroy(); // 销毁节点 } } }将FishSpawner脚本挂载到场景中的一个空节点上比如就叫“FishSpawner”然后在“属性检查器”中将我们之前创建的SmallFish预制体拖拽到smallFishPrefab属性栏中。运行游戏你应该能看到小鱼不断地从屏幕边缘生成并游动。4. 游戏核心逻辑实现碰撞、成长与状态4.1 碰撞检测与“吃”的逻辑“大鱼吃小鱼”的核心就是碰撞。我们需要为玩家和小鱼添加碰撞体。Cocos Creator提供了多种2D碰撞组件对于这种简单的圆形或矩形判断使用CircleCollider2D或BoxCollider2D都很方便。添加碰撞体在“层级管理器”中选中“Player”节点点击“属性检查器”下方的“添加组件”按钮选择“Physics 2D - Circle Collider 2D”。调整Radius半径使其大致匹配你的鱼图片大小。用同样的方法为“SmallFish”预制体添加碰撞体记得在预制体模式下编辑或者编辑完实例后点击“预制体”面板的“保存”。启用物理系统碰撞检测需要物理系统。在“层级管理器”中选中“Canvas”或任意节点添加组件“PhysicsSystem2D”。通常保持默认设置即可但你可以调整gravity重力为(0, 0)因为我们是在水中不需要重力。编写碰撞逻辑我们需要修改PlayerController和SmallFishAI或者新建一个Fish脚本来检测碰撞。通常我们会在玩家身上监听碰撞事件。在PlayerController.ts中增加以下代码import { Collider2D, Contact2DType, IPhysics2DContact } from cc; export class PlayerController extends Component { // ... 之前已有的属性 ... // 玩家当前等级/大小 private _fishSize: number 1; property baseScale: number 1.0; // 基础缩放 start() { // ... 之前的事件监听 ... // 获取碰撞体组件并监听碰撞 const collider this.getComponent(Collider2D); if (collider) { collider.on(Contact2DType.BEGIN_CONTACT, this.onBeginContact, this); } } onBeginContact(selfCollider: Collider2D, otherCollider: Collider2D, contact: IPhysics2DContact | null) { // 判断碰撞到的物体是否是“可吃的鱼” // 我们可以通过节点的分组Group或者标签Tag来判断这里使用分组更规范。 // 假设我们为小鱼设置了特定的分组例如“SmallFish” if (otherCollider.group 2) { // 假设2是小鱼的分组 console.log(吃到了一条小鱼); // 1. 增长玩家 this.grow(); // 2. 销毁小鱼 // 我们需要一个方法来通知FishSpawner或直接销毁小鱼节点 // 这里假设小鱼节点上有一个SmallFish脚本里面提供了beEaten方法 const fishScript otherCollider.node.getComponent(SmallFish); // 或 SmallFishAI if (fishScript fishScript.beEaten) { fishScript.beEaten(); } else { // 如果没有脚本直接销毁不推荐不利于管理 // otherCollider.node.destroy(); } } // 还可以在这里判断是否撞到了更大的鱼游戏结束 } grow() { this._fishSize 0.1; // 每次成长增加0.1 // 更新视觉大小 const newScale this.baseScale * this._fishSize; this.node.setScale(newScale, newScale, 1); // 可以同时增加移动速度让大鱼游得更快 // this.moveSpeed 10; } // ... update等其他方法 ... }相应地我们需要为小鱼SmallFishAI脚本添加分组和beEaten方法设置分组在编辑器中选择“SmallFish”预制体根节点在“属性检查器”最上方找到“Layer”旁边有个下拉菜单点击“添加分组...”。在项目设置Project - Project Settings - Layers中可以添加一个名为“SmallFish”的分组并分配一个索引例如2。然后为“SmallFish”节点选择这个分组。同样为“Player”节点设置一个分组比如“Player”索引1。修改脚本在SmallFishAI.ts中添加export class SmallFishAI extends Component { // ... 已有属性 ... // 引用生成器用于通知自己被吃掉 private _spawner: FishSpawner | null null; // 可以在生成时由Spawner设置 setSpawner(spawner: FishSpawner) { this._spawner spawner; } beEaten() { if (this._spawner) { this._spawner.removeFish(this.node); } else { this.node.destroy(); } } // ... 其他方法 ... }同时需要修改FishSpawner.ts的spawnFish方法在生成鱼后为其设置生成器引用spawnFish() { // ... 创建和设置位置的代码 ... this._activeFishes.push(fish); // 获取小鱼身上的AI脚本并设置生成器 const fishAI fish.getComponent(SmallFishAI); if (fishAI) { fishAI.setSpawner(this); } }注意事项物理碰撞的分组和掩码Mask设置是关键。你需要确保玩家的碰撞体能检测到小鱼的碰撞体。在“项目设置 - 物理 - 碰撞矩阵”中确保“Player”分组和“SmallFish”分组之间的复选框是勾选的表示它们之间会发生碰撞检测。4.2 游戏状态管理与UI交互一个完整的游戏需要有开始、进行中、结束等状态以及相应的UI界面。创建UI在“层级管理器”中右键点击“Canvas”节点选择“创建节点 - 创建UI节点 - Button”。将其重命名为“Btn_Start”。你可以修改它的Label文字为“开始游戏”。用同样的方法创建两个Text节点分别命名为“Txt_Score”和“Txt_GameOver”用于显示分数和游戏结束信息。将它们摆放在合适的位置。创建游戏管理器新建一个脚本GameManager.ts它将作为游戏的大脑管理状态、分数和UI。import { _decorator, Component, Label, Button, director } from cc; const { ccclass, property } _decorator; enum GameState { GS_INIT, // 初始化/菜单 GS_PLAYING, // 游戏中 GS_OVER, // 游戏结束 } ccclass(GameManager) export class GameManager extends Component { // 单例模式方便其他脚本访问 private static _instance: GameManager | null null; public static get instance(): GameManager { return GameManager._instance!; } property(Label) scoreLabel: Label | null null; property(Label) gameOverLabel: Label | null null; property(Button) startButton: Button | null null; property(Button) restartButton: Button | null null; private _currentState: GameState GameState.GS_INIT; private _score: number 0; onLoad() { if (GameManager._instance GameManager._instance ! this) { this.node.destroy(); return; } GameManager._instance this; // 防止切换场景时被销毁 director.addPersistRootNode(this.node); } start() { this.setState(GameState.GS_INIT); } setState(state: GameState) { this._currentState state; switch (state) { case GameState.GS_INIT: this._score 0; this.updateScoreUI(); if (this.gameOverLabel) this.gameOverLabel.node.active false; if (this.startButton) this.startButton.node.active true; if (this.restartButton) this.restartButton.node.active false; // 暂停游戏逻辑如果有 break; case GameState.GS_PLAYING: if (this.gameOverLabel) this.gameOverLabel.node.active false; if (this.startButton) this.startButton.node.active false; if (this.restartButton) this.restartButton.node.active false; // 恢复或启动游戏逻辑 break; case GameState.GS_OVER: if (this.gameOverLabel) { this.gameOverLabel.node.active true; this.gameOverLabel.string 游戏结束得分${this._score}; } if (this.restartButton) this.restartButton.node.active true; // 暂停游戏逻辑 break; } } // 由PlayerController调用增加分数 addScore(points: number) { if (this._currentState ! GameState.GS_PLAYING) return; this._score points; this.updateScoreUI(); // 可以在这里添加分数达到一定值后玩家升级的逻辑 } updateScoreUI() { if (this.scoreLabel) { this.scoreLabel.string 分数${this._score}; } } // UI按钮回调方法 onStartButtonClicked() { this.setState(GameState.GS_PLAYING); } onRestartButtonClicked() { // 简单重启重新加载当前场景 director.loadScene(director.getScene()!.name); // 更优雅的做法是重置所有游戏对象的状态而不是重载场景 } // 游戏结束条件触发例如撞到大鱼 gameOver() { this.setState(GameState.GS_OVER); } }关联UI与逻辑将GameManager脚本挂载到一个空节点如“GameManager”上。然后将场景中的UI节点Txt_Score, Txt_GameOver, Btn_Start, Btn_Restart拖拽到GameManager组件对应的属性栏中。设置按钮点击事件选中“Btn_Start”按钮在“属性检查器”的“Button”组件下方找到“Click Events”。将数量设置为1然后将“GameManager”节点拖拽到第一个“Target”栏在“Component”下拉菜单中选择“GameManager”在“Handler”下拉菜单中选择“onStartButtonClicked”。对“Btn_Restart”按钮做类似操作关联到onRestartButtonClicked方法。整合玩家与游戏管理器修改PlayerController的grow方法在增长时调用GameManager.instance.addScore(10)来增加分数。同时你需要在玩家碰撞检测中添加判断是否撞到“大鱼”可以是一个新的分组如“BigFish”如果撞到则调用GameManager.instance.gameOver()。5. 功能扩展与性能优化5.1 添加更多游戏元素基础的“吃”和“被吃”逻辑完成后我们可以让游戏变得更有趣多种鱼类创建不同大小、不同速度、不同分数的小鱼和大鱼预制体。修改FishSpawner使其能随机生成不同类型的鱼。玩家只能吃比自己小的鱼碰到比自己大的鱼则游戏结束。成长系统玩家大小不仅影响外观还应影响可吞噬鱼类的范围。可以定义一个“吞噬半径”或“吞噬等级”只有等级比玩家低的鱼才能被吃。道具系统添加一些特殊道具如“加速”、“无敌”、“磁铁”自动吸引小鱼等。这些道具可以随机生成被玩家碰撞后触发效果。关卡与难度递增随着分数增加可以增加生成鱼的速度、数量或者出现更多的大鱼提高游戏难度。5.2 性能优化与注意事项当屏幕上有几十上百条鱼在游动时性能可能成为问题。以下是一些优化技巧对象池Object Pooling这是游戏开发中最重要的优化手段之一。不要频繁地instantiate创建和destroy销毁对象尤其是小鱼。我们可以预先创建一定数量的小鱼对象不用时隐藏或回收需要时再取出激活。Cocos Creator内置了NodePool类来实现对象池。// 在FishSpawner中实现一个简单的对象池 import { NodePool } from cc; export class FishSpawner extends Component { private _fishPool: NodePool new NodePool(); // ... 其他属性 ... start() { // 初始化对象池预先创建一些鱼 for (let i 0; i this.maxFishCount; i) { const fish instantiate(this.smallFishPrefab!); this._fishPool.put(fish); // 放入池中默认会失活节点 } } spawnFish() { let fish: Node; if (this._fishPool.size() 0) { fish this._fishPool.get(); // 从池中取会自动激活 } else { fish instantiate(this.smallFishPrefab!); // 池空了才创建新的 } // ... 设置位置、初始化 ... fish.parent this.node; this._activeFishes.push(fish); } removeFish(fishNode: Node) { const index this._activeFishes.indexOf(fishNode); if (index -1) { this._activeFishes.splice(index, 1); // 不销毁而是放回对象池 this._fishPool.put(fishNode); } } }绘制调用合并Draw Call Batching对于大量使用相同材质和纹理的静态或动态精灵比如所有同一种小鱼Cocos Creator会自动尝试进行合批以减少GPU绘制调用。确保它们的渲染顺序layer接近并且使用相同的纹理图集SpriteAtlas能提高合批成功率。避免在update中做复杂计算如果每帧都需要遍历所有鱼来检查距离等当鱼数量多时开销很大。可以考虑使用四叉树Quadtree等空间分区数据结构来优化碰撞检测范围。资源管理将小鱼的图片打包成纹理图集能减少纹理切换提升渲染效率。在Cocos Creator的“资源管理器”中可以将多张图片拖到一个文件夹然后右键选择“创建 - Sprite Atlas”来创建图集。5.3 常见问题与排查技巧碰撞不生效检查分组和掩码确保发生碰撞的两个节点的碰撞体分组在“项目设置 - 物理 - 碰撞矩阵”中是相互勾选的。检查碰撞体大小和位置在场景编辑器中碰撞体会以绿色线框显示。确保线框确实包裹住了你的精灵图像。检查物理系统是否启用场景中需要有PhysicsSystem2D组件。检查回调函数是否正确绑定确认on(Contact2DType.BEGIN_CONTACT, ...)这行代码被执行了。预制体修改后实例不更新在“场景编辑器”中修改了预制体实例后记得点击该节点“属性检查器”顶部的“预制体”选项卡中的“保存”按钮才能将修改应用回预制体资源。反之修改了预制体资源后场景中的实例可能需要手动点击“预制体”选项卡的“回退”或“应用”来同步。脚本编译错误或找不到组件Cocos Creator使用TypeScript编译错误会在“控制台”面板以红色显示。仔细阅读错误信息通常是语法错误或类型错误。如果脚本中使用了getComponent(‘YourScript’)但找不到请检查脚本类名上的ccclass(‘YourScript’)装饰器中的字符串是否与getComponent里的字符串完全一致包括大小写。游戏在移动端触摸不准坐标转换问题。UI触摸事件获取的是屏幕坐标需要正确转换到Canvas节点的本地坐标。确保你的Canvas适配模式如FIXED_WIDTH设置正确并使用UITransform的convertToNodeSpaceAR等方法进行转换。性能突然下降打开Chrome浏览器的开发者工具F12切换到“Performance”或“Memory”标签页录制一段时间游戏运行过程。查看是否有内存泄漏Memory Leak内存使用量持续增长或某段脚本执行时间Scripting过长。对象池未正确管理是常见的内存泄漏原因。走到这一步一个具备核心玩法的“大鱼吃小鱼”游戏已经完成了。从角色控制、AI生成、碰撞交互到状态管理我们走完了2D游戏开发的一个典型流程。你可以在此基础上继续添加更多功能比如更精美的美术资源、粒子特效吃鱼时的泡泡、背景音乐和音效甚至是一些简单的剧情关卡。我个人在实际开发这类休闲游戏时最深的一点体会是快速原型和迭代比一开始就追求完美更重要。先用简单的方块和圆圈把核心玩法移动、碰撞、成长跑通验证乐趣所在然后再逐步替换美术资源、添加细节。这能帮你牢牢抓住项目的核心避免在次要细节上过度消耗时间。