Cocos Creator实战:从零开发回合制游戏《击败老大》

Cocos Creator实战:从零开发回合制游戏《击败老大》
1. 项目概述与核心思路最近在捣鼓Cocos Creator想找个有意思的切入点来系统性地学习一下。直接啃官方文档和教程案例总觉得少了点“实战感”那些案例太“干净”了离我们实际想做的项目总隔着一层。于是我决定自己动手从零开始做一个小游戏把引擎的各个模块串起来用一遍。我选择的这个项目叫《击败老大》它是一个非常经典且结构清晰的回合制对战小游戏原型。麻雀虽小五脏俱全它几乎涵盖了Cocos Creator开发中UI、动画、状态管理、脚本通信、资源管理等核心环节。通过复现它我们能快速建立起对Cocos Creator工作流的直观认知这比单纯看理论要有效得多。这个“第一段”我们聚焦于最基础的部分搭建项目骨架、创建核心场景、制作静态UI并让角色能够“站”在场景里。听起来简单但这里面每一步都藏着新手容易踩的坑比如Canvas适配、节点结构设计、Prefab预制体的初次使用等。我会结合我实际开发中遇到的问题把每个步骤拆开了、揉碎了讲清楚目标是让你看完就能跟着做出来并且理解为什么这么做。2. 开发环境准备与项目初始化2.1 Cocos Creator版本选择与安装目前Cocos Creator的主流版本是3.x系列。对于新手而言我强烈建议直接使用官网提供的最新稳定版。不必过于纠结某个子版本因为我们的学习目标在于掌握核心工作流和API这些在稳定版中差异不大。访问Cocos官网下载对应操作系统的安装包即可。安装过程没什么特别的一路下一步就行。注意安装路径请避免使用中文或特殊字符这是很多开发工具的通用准则能避免一些潜在的、难以排查的路径解析问题。安装完成后首次打开Cocos Dashboard仪表板你需要用Cocos账号登录。没有的话注册一个很简单。登录后在“项目”标签页点击“新建”按钮。这里会让我们选择项目模板。2.2 新建项目模板与参数详解在新建项目面板你会看到“Empty”、“Hello World”、“Simple Game”等多种模板。对于《击败老大》这个项目我们选择“Empty (3D)”模板。有同学可能会问我们做2D游戏为什么选3D模板这里有个关键点Cocos Creator 3.x是真正的3D引擎其2D渲染是建立在3D空间基础上的。选择“Empty (3D)”模板会为我们创建一个最干净、包含完整3D功能支持的项目结构而我们完全可以在其中进行2D游戏开发。如果选择“2D”模板它其实也是基于3D的但可能会预置一些2D相关的设置对于我们这种希望从底层了解的学习项目来说“Empty”反而更纯粹干扰更少。项目名称我填的是“DefeatTheBoss”位置选择一个你熟悉的文件夹。下方的“项目路径”会自动生成检查一下没有中文就行。“编辑器版本”默认就是你安装的版本。“初始场景类型”选择“3D”。不用担心我们马上就会把它改造成2D场景。点击“创建并打开”引擎会自动初始化项目并打开编辑器。2.3 初识编辑器界面与基础设置项目打开后你会看到Cocos Creator的编辑器界面。它主要分为几个面板正中间的场景编辑器Scene右上角的层级管理器Hierarchy左下角的资源管理器Assets右侧的属性检查器Inspector和控制台Console。第一次接触可能会觉得有点复杂别慌我们一步步来。首先我们需要将项目设置为以2D为主。点击编辑器顶部菜单栏的“项目” - “项目设置”。在打开的窗口中找到“功能裁剪”分页。这里我们需要确保“3D”相关的模块是开启的因为我们用了3D模板但同时要关注2D。实际上对于空项目2D功能默认是开启的。我们只需要确认一下“Sprite”、“UI”等2D渲染组件是可用的即可。更重要的设置在于场景的渲染管线。回到场景编辑器选中层级管理器中的“Canvas”节点。在右侧属性检查器中找到“Canvas”组件。这里有一个“Design Resolution”设计分辨率我设置为960 x 640。这是一个比较经典的横屏移动端分辨率比例是3:2。同时将“Fit Height”和“Fit Width”都勾选上这能确保我们的UI在不同屏幕比例下都能较好地自适应。实操心得设计分辨率的选择没有绝对标准但建议在项目开始时就确定下来并保持统一。960x640、1136x64016:9、1334x750等都是常见选择。勾选“Fit Height”和“Fit Width”意味着Canvas会始终缩放以填满屏幕可能导致在某些极端比例下UI被拉伸或裁剪但对于小游戏原型这是一个简单有效的策略。更精细的适配我们后续可以再深入。3. 构建游戏场景与基础UI3.1 场景结构设计与节点创建一个清晰的节点结构是项目可维护性的基石。在层级管理器Hierarchy中默认有一个“Canvas”节点和一个“Main Camera”节点。Canvas是所有UI元素的根容器而Main Camera是3D相机我们需要把它改造成2D相机。首先右键点击“Main Camera”选择“重命名”改为“UICamera”。在属性检查器中找到“Camera”组件将“Projection”从“Perspective”透视改为“Orthographic”正交。正交投影是2D游戏的标准它消除了近大远小的透视效果。然后调整“Ortho Height”参数。这个参数决定了相机垂直方向能看到的世界单位范围。我们可以先设置为5。接着调整相机的位置Position将Z轴坐标设置为10左右确保相机在场景前方能拍到所有物体。现在我们来规划场景的节点树。一个好的习惯是将不同类型的节点分组管理。在Canvas下我创建了以下几个空节点右键Canvas - 创建节点 - 创建空节点Background用于放置背景图片。GameLayer用于放置所有游戏内的动态元素如玩家角色、敌人角色、技能特效等。这是我们的“游戏世界”。UILayer用于放置所有用户界面元素如血条、按钮、文字提示等。UI层通常渲染在游戏层之上。这样分层后我们可以独立控制每一层的渲染顺序、缩放和交互逻辑非常清晰。3.2 导入资源与设置背景游戏开发离不开美术资源。我们首先需要准备一些图片。对于这个演示项目你可以在一些免费的素材网站如Kenney.nl、OpenGameArt.org找到简单的角色和UI素材。我准备了以下几张PNG图片一张背景图background.png、一张玩家角色图player.png、一张敌人角色图boss.png以及血条用的红色和绿色长条图。将你的素材图片直接拖拽到资源管理器Assets面板的“assets”目录下或者在其下新建“textures”目录再拖入。Cocos Creator会自动导入这些图片并生成对应的“.texture”资源。制作背景非常简单。在层级管理器中选中“Background”节点然后在属性检查器中点击“添加组件” - “渲染组件” - “Sprite”。你会看到节点上多了一个Sprite组件。将资源管理器里的“background.png”拖拽到Sprite组件的“Sprite Frame”属性框中。如果图片没有显示或者显示过大/过小我们需要调整节点和图片的尺寸。首先确保Background节点的“Position”是(0,0,0)。然后查看Sprite组件将“Type”设置为“SIMPLE”简单模式。接着我们需要让背景图铺满整个屏幕。这里有一个技巧选中Background节点在属性检查器顶部点击“添加组件” - “UI组件” - “Widget”对齐挂件。添加Widget组件后勾选上“Top”、“Bottom”、“Left”、“Right”四个对齐选项并将它们的值都设置为0。这意味着Background节点的四条边将分别对齐到其父节点即Canvas的四条边从而实现全屏拉伸。最后在Sprite组件里将“Size Mode”设置为“CUSTOM”然后你就可以手动输入宽高或者更简单直接点击Widget组件旁边的“对齐到父节点”按钮一个带四向箭头的图标背景就会自动适配了。3.3 创建角色Prefab预制体在游戏中角色玩家和老大是需要被反复使用和可能被实例化多个的实体。最佳实践是将其制作成Prefab预制体。Prefab相当于一个蓝图你可以基于这个蓝图在场景中创建多个完全一样的实例并且修改Prefab蓝图所有实例都会同步更新。首先我们在资源管理器的assets目录下右键 - 创建 - 文件夹命名为“prefabs”。然后在场景中创建角色实例。在层级管理器选中“GameLayer”节点右键 - 创建节点 - 创建空节点命名为“Player”。在Player节点下再创建一个空节点命名为“Sprite”专门用来挂载显示图片的组件。选中“Sprite”节点为其添加“Sprite”组件并将“player.png”拖拽赋值。调整Sprite节点的Scale缩放比如设置为(0.5, 0.5, 1)让角色大小合适。现在Player节点包含了代表其外观的Sprite子节点。未来我们还可以为Player节点添加碰撞体、动画等组件。最关键的一步来了将整个“Player”节点从层级管理器拖拽到资源管理器的“prefabs”文件夹里。你会看到“prefabs”文件夹下生成了一个蓝色的“Player.prefab”文件。此时场景中的Player节点会变成蓝色这表示它已经是一个Prefab实例了。注意事项创建Prefab后永远不要直接修改场景中的实例节点来调整Prefab的通用属性。比如如果你想修改所有玩家的基础缩放应该这样做在资源管理器中双击打开“Player.prefab”文件这时编辑器会进入Prefab编辑模式层级管理器显示的是Prefab的根节点。在这里修改Sprite节点的Scale然后保存。你会发现场景中所有的Player实例都自动更新了。如果需要为某个特定实例做一些个性化调整比如位置可以在场景中选中该实例在属性检查器中修改非锁定的属性如Position这些修改只会影响当前实例。重复上述步骤用“boss.png”创建一个“Boss.prefab”。创建好后你可以将场景中的Player和Boss实例删除右键节点 - 删除因为我们后续会通过脚本动态创建它们或者需要时再从资源管理器拖入。保留prefabs资源即可。4. 制作基础UI血条与按钮4.1 使用ProgressBar组件制作血条血条是游戏UI的经典元素。在Cocos Creator中我们可以用“ProgressBar”进度条组件快速实现。在层级管理器选中“UILayer”节点右键 - 创建节点 - 渲染组件 - ProgressBar。将新节点命名为“PlayerHPBar”。选中“PlayerHPBar”节点在属性检查器中可以看到ProgressBar组件。它主要由三部分组成Bar Sprite代表当前进度即红色血条部分。Background进度条的背景即血条的总长度背景框。Total Length进度条的总长度。首先我们需要为它设置图片。ProgressBar需要一个“Sliced”九宫格切片类型的精灵图这样在拉伸时边角不会变形。我们的血条素材如果是简单长条可以在导入后设置九宫格。在资源管理器中选中你的血条背景图比如一个灰色长条和前景图红色长条在属性检查器中将“Type”从“Simple”改为“Sliced”。然后在“Border”属性中设置九宫格的边距例如左3右3上0下0这告诉引擎图片的哪些部分是可以拉伸的。设置好后将背景图拖给ProgressBar组件的“Background”的“Sprite Frame”将前景图拖给“Bar Sprite”的“Sprite Frame”。然后调整“Total Length”比如设为200让血条有一个合适的显示长度。血条的方向通常是从左到右减少。ProgressBar的“Direction”属性默认为“LEFT_TO_RIGHT”这符合我们的习惯。进度值“Progress”范围是0-11代表满血。我们可以先在编辑器里把“Progress”设为0.5看看效果。为了让血条跟随玩家我们还需要为其添加“Widget”对齐挂件。但血条是动态跟随的更适合用代码控制其世界坐标转换到屏幕坐标。作为第一段我们先把它静态地放在屏幕左上角。给PlayerHPBar节点添加Widget组件对齐方式设置为“Top”和“Left”并设置一定的偏移量比如Top: 20, Left: 20。用同样的方法我们再创建一个“BossHPBar”放在屏幕右上角设置Widget为Top和Right。4.2 创建交互按钮接下来我们创建游戏的控制按钮比如“攻击”按钮。在“UILayer”下右键 - 创建节点 - UI组件 - Button。命名为“AttackButton”。选中AttackButton节点你会发现它自动带了一个子节点“Label”。这是按钮上的文字。我们可以在属性检查器中找到Button组件和Label组件。Button组件设置Interactable: 勾选表示按钮可交互。Transition: 按钮状态过渡效果。选择“SCALE”缩放这样点击时按钮会有轻微的缩放动画反馈感更好。Normal Color, Pressed Color, Hover Color, Disabled Color: 分别设置按钮在不同状态下的颜色。例如Normal设为白色Pressed设为灰色。Click Events: 这是一个数组用于绑定按钮点击后要执行的函数。我们稍后通过脚本绑定。Label组件设置在AttackButton/Label节点上String: 输入文字如“攻 击”。Font Size: 设置字号如32。Color: 设置字体颜色如黑色。Horizontal Align和Vertical Align: 都设置为“CENTER”让文字在按钮中居中。同样使用Widget组件将按钮定位到屏幕底部。例如设置对齐方式为“Bottom”和“Center”然后调整“Bottom”的偏移量为50。我们可以复制这个按钮选中节点CtrlC, CtrlV创建出“防御”、“技能”等按钮并修改它们的Label文字和位置排列在屏幕底部。4.3 UI的层级与渲染顺序现在我们的UILayer下有了血条和按钮。在2D渲染中节点的渲染顺序谁在上面谁在下面由两个因素决定一是节点在层级管理器中的顺序从上到下越下面的越后渲染显示在越上层二是节点的Layer属性。默认情况下所有UI节点都在同一个“UI_2D”层。此时渲染顺序完全由它们在层级管理器中的兄弟节点顺序决定。你可以通过拖拽节点来调整上下顺序。例如如果你希望某个提示框显示在所有按钮之上就把它放在按钮节点的下面。实操心得养成好习惯在UILayer下用空节点进行功能分组。例如可以创建“TopUI”放血条、积分、“BottomUI”放操作按钮、“PopupUI”放弹窗等子节点。这样不仅结构清晰也便于整体控制某一组UI的显示/隐藏。5. 编写第一个脚本让角色动起来5.1 创建脚本与挂载静态的UI和场景有了现在我们需要用代码让游戏活起来。Cocos Creator支持TypeScript和JavaScript作为脚本语言。我强烈推荐使用TypeScript因为它提供了静态类型检查能在编码阶段就发现很多潜在错误对大型项目管理和团队协作帮助巨大。在资源管理器的assets目录下右键 - 创建 - TypeScript命名为“PlayerController”。双击这个脚本文件它会在你的默认代码编辑器如VSCode中打开。回到Cocos编辑器在层级管理器中我们之前已经把Player的Prefab实例删除了。现在我们从资源管理器将“Player.prefab”拖拽到“GameLayer”节点下创建一个新的实例。确保这个实例被选中然后在属性检查器底部点击“添加组件” - “自定义脚本” - “PlayerController”。这样PlayerController脚本就被挂载到了这个Player预制体实例上。5.2 脚本基础结构与生命周期打开PlayerController.ts你会看到如下基础模板import { _decorator, Component, Node } from cc; const { ccclass, property } _decorator; ccclass(PlayerController) export class PlayerController extends Component { start() { // 初始化逻辑 } update(deltaTime: number) { // 每一帧渲染前调用的逻辑 } }ccclass(PlayerController): 这是一个装饰器将当前类注册到Cocos引擎中类名必须与脚本文件名一致。export class PlayerController extends Component: 定义了一个继承自Component的类。所有挂载到节点上的脚本都必须继承Component。start(): 组件首次激活时通常在第一帧update之前调用一次用于初始化。update(deltaTime: number): 每一帧渲染前调用deltaTime是上一帧到当前帧的时间间隔秒用于编写与帧率无关的动画或运动逻辑。5.3 实现键盘控制移动让我们先实现一个简单的功能用键盘WASD或方向键控制玩家角色移动。这需要我们在update函数中监听键盘输入。首先我们需要获取对角色Sprite节点或直接对当前节点的引用以修改其位置。我们通过属性声明的方式在编辑器中将节点拖拽赋值给脚本。修改PlayerController.ts:import { _decorator, Component, Node, Vec3, input, Input, KeyCode } from cc; const { ccclass, property } _decorator; ccclass(PlayerController) export class PlayerController extends Component { // 声明一个属性用于在编辑器面板中关联移动速度 property public moveSpeed: number 200; // 声明一个属性用于关联Sprite子节点可选如果直接移动本节点则不需要 // property(Node) // public spriteNode: Node null; start() { // 启用键盘输入监听 input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this); input.on(Input.EventType.KEY_UP, this.onKeyUp, this); } onDestroy() { // 组件销毁时移除监听防止内存泄漏 input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this); input.off(Input.EventType.KEY_UP, this.onKeyUp, this); } // 记录当前按下的方向键 private _curMoveDir: Vec3 new Vec3(0, 0, 0); onKeyDown(event: EventKeyboard) { const v this._curMoveDir; switch(event.keyCode) { case KeyCode.KEY_W: case KeyCode.ARROW_UP: v.y 1; break; case KeyCode.KEY_S: case KeyCode.ARROW_DOWN: v.y -1; break; case KeyCode.KEY_A: case KeyCode.ARROW_LEFT: v.x -1; break; case KeyCode.KEY_D: case KeyCode.ARROW_RIGHT: v.x 1; break; } } onKeyUp(event: EventKeyboard) { const v this._curMoveDir; switch(event.keyCode) { case KeyCode.KEY_W: case KeyCode.ARROW_UP: if (v.y 0) v.y 0; break; case KeyCode.KEY_S: case KeyCode.ARROW_DOWN: if (v.y 0) v.y 0; break; case KeyCode.KEY_A: case KeyCode.ARROW_LEFT: if (v.x 0) v.x 0; break; case KeyCode.KEY_D: case KeyCode.ARROW_RIGHT: if (v.x 0) v.x 0; break; } } update(deltaTime: number) { // 根据当前方向向量和速度计算位移 const moveVec new Vec3(this._curMoveDir); // 归一化方向向量确保斜向移动速度一致 if (moveVec.length() 0) { moveVec.normalize(); } // 计算本帧位移 const moveDistance moveVec.multiplyScalar(this.moveSpeed * deltaTime); // 应用到节点位置 const curPos this.node.position; const newPos curPos.add(moveDistance); this.node.setPosition(newPos); } }代码解析属性声明property装饰器将moveSpeed暴露到编辑器面板我们可以随时调整这个值而无需修改代码。输入监听在start中我们通过input.on监听键盘按下(KEY_DOWN)和抬起(KEY_UP)事件并绑定对应的处理函数onKeyDown和onKeyUp。第三个参数this用于指定回调函数执行时的this上下文。方向向量_curMoveDir是一个Vec3向量用于累积当前按下的方向。例如同时按住W和D向量会是(1,1,0)。按键处理在onKeyDown中根据按下的键设置方向向量的分量1或-1。在onKeyUp中当键松开时将对应的分量归零。运动计算在update中每一帧都检查_curMoveDir。首先将其归一化normalize()确保斜向移动的速度与轴向移动相同向量长度变为1。然后用归一化后的方向向量乘以速度moveSpeed再乘以时间增量deltaTime得到本帧的位移向量。最后将位移加到节点的当前位置上。保存脚本回到Cocos编辑器。因为脚本已经被挂载编辑器会自动编译。选中场景中的Player节点在属性检查器中可以看到PlayerController组件并且有一个“Move Speed”参数可以调整试试改成300。点击编辑器上方的“预览”按钮一个小三角图标在浏览器中运行游戏现在你应该可以用键盘控制玩家角色移动了5.4 让血条跟随角色世界坐标转屏幕坐标目前血条是固定在屏幕左上角的。我们需要让它跟随玩家角色移动。这涉及到将角色的世界坐标在GameLayer中的位置转换为屏幕坐标在Canvas上的位置然后赋值给血条UI节点。我们需要修改PlayerController脚本让它能控制血条。同时我们需要另一个脚本来管理Boss和它的血条。为了简化我们先创建一个通用的“角色数据”脚本。首先创建一个新的TypeScript脚本命名为“ActorData”。这个脚本将持有角色的基础属性如生命值、最大生命值并负责更新血条。import { _decorator, Component, Node, ProgressBar } from cc; const { ccclass, property } _decorator; ccclass(ActorData) export class ActorData extends Component { property maxHP: number 100; private _currentHP: number 100; property(ProgressBar) hpBar: ProgressBar null; // 在编辑器中将血条ProgressBar节点拖拽到这里 start() { this._currentHP this.maxHP; this.updateHPBar(); } // 受到伤害 takeDamage(damage: number) { this._currentHP - damage; if (this._currentHP 0) this._currentHP 0; this.updateHPBar(); // 可以在这里触发受伤动画或音效 if (this._currentHP 0) { this.onDefeated(); } } // 治疗 heal(amount: number) { this._currentHP amount; if (this._currentHP this.maxHP) this._currentHP this.maxHP; this.updateHPBar(); } private updateHPBar() { if (this.hpBar) { this.hpBar.progress this._currentHP / this.maxHP; } } private onDefeated() { console.log(this.node.name 被击败了); // 触发击败效果比如播放死亡动画销毁节点等 // this.node.destroy(); } }然后我们修改PlayerController让它获取ActorData组件并每帧更新血条位置。同时我们需要一个对Canvas下UILayer中血条节点的引用。// 在PlayerController.ts顶部增加导入 import { ActorData } from ./ActorData; import { UITransform, Camera, view } from cc; export class PlayerController extends Component { // ... 之前已有的属性 ... property(Node) public hpBarNode: Node null; // 在编辑器中将UILayer下的PlayerHPBar节点拖拽到这里 property(Camera) public uiCamera: Camera null; // 在编辑器中将UICamera节点拖拽到这里 private _actorData: ActorData null; start() { // ... 原有的输入监听 ... this._actorData this.getComponent(ActorData); if (!this._actorData) { console.warn(PlayerController: ActorData component not found!); } } update(deltaTime: number) { // ... 原有的移动逻辑 ... this.updateHPBarPosition(); } private updateHPBarPosition() { if (!this.hpBarNode || !this.uiCamera) return; // 1. 获取玩家角色的世界坐标 const worldPos this.node.worldPosition; // 2. 将世界坐标转换为屏幕坐标归一化坐标范围0-1 const screenPos this.uiCamera.worldToScreen(worldPos); // 3. 将屏幕坐标转换为UI坐标系下的坐标 // 这里需要获取Canvas的UITransform组件来进行转换 const canvasTransform this.hpBarNode.parent.getComponent(UITransform); const uiPos new Vec3(); canvasTransform.convertToNodeSpaceAR(screenPos, uiPos); // 4. 设置血条节点的位置通常我们只需要X和Y坐标并加上一个偏移量比如在角色头顶上方50像素 this.hpBarNode.setPosition(uiPos.x, uiPos.y 50); } }操作步骤将ActorData脚本挂载到Player节点上。在Player节点的ActorData组件中将“Hp Bar”属性关联到层级管理器中的“PlayerHPBar”节点即ProgressBar节点本身。在Player节点的PlayerController组件中将“Hp Bar Node”属性关联到“PlayerHPBar”节点整个节点将“Ui Camera”属性关联到“UICamera”节点。为Canvas根节点添加“UITransform”组件如果还没有的话。因为我们的hpBarNode的父节点是UILayer而UILayer的父节点是Canvas所以用this.hpBarNode.parent可以获取到Canvas节点。现在运行预览你会发现血条会跟随玩家角色在世界中的移动而移动始终显示在角色头顶。这里用到的worldToScreen和convertToNodeSpaceAR是2D UI跟随3D/2D世界物体的核心API务必理解其转换过程。6. 核心战斗逻辑框架搭建6.1 游戏状态管理初步对于一个回合制游戏我们需要一个中心控制器来管理游戏状态当前是玩家回合还是敌人回合角色是否可以行动这个控制器我们通常称为“GameManager”。它是一个单例在整个游戏中只有一个实例方便各个系统访问。创建脚本“GameManager.ts”import { _decorator, Component, Node, director } from cc; const { ccclass, property } _decorator; // 定义游戏状态枚举 export enum GameState { PLAYER_TURN, // 玩家回合 ENEMY_TURN, // 敌人回合 PLAYER_WIN, PLAYER_LOSE, } ccclass(GameManager) export class GameManager extends Component { // 静态实例用于全局访问 public static instance: GameManager null; private _currentState: GameState GameState.PLAYER_TURN; onLoad() { // 实现单例模式 if (GameManager.instance null) { GameManager.instance this; // 可选使该节点在场景切换时不销毁 // director.addPersistRootNode(this.node); } else { this.node.destroy(); return; } } start() { this.startPlayerTurn(); } get currentState(): GameState { return this._currentState; } startPlayerTurn() { this._currentState GameState.PLAYER_TURN; console.log(玩家回合开始); // 这里可以触发UI更新比如高亮玩家可操作的按钮 // 例如EventSystem.emit(turn_start, GameState.PLAYER_TURN); } startEnemyTurn() { this._currentState GameState.ENEMY_TURN; console.log(敌人回合开始); // 触发敌人AI行动 this.performEnemyAction(); } private performEnemyAction() { // 模拟敌人AI简单延迟后攻击玩家 setTimeout(() { if (this._currentState GameState.ENEMY_TURN) { console.log(老大发动了攻击); // 这里应该调用玩家角色的受伤逻辑 // 例如PlayerController.instance.takeDamage(10); this.endEnemyTurn(); } }, 1000); } endPlayerTurn() { if (this._currentState GameState.PLAYER_TURN) { this.startEnemyTurn(); } } endEnemyTurn() { if (this._currentState GameState.ENEMY_TURN) { this.startPlayerTurn(); } } checkGameOver() { // 检查玩家或敌人血量更新游戏状态 // if (playerHP 0) { this._currentState GameState.PLAYER_LOSE; } // if (enemyHP 0) { this._currentState GameState.PLAYER_WIN; } } }在场景中创建一个空节点命名为“GameManager”然后将GameManager脚本挂载上去。这样游戏中就有一个全局可访问的GameManager.instance了。6.2 连接按钮与战斗指令现在我们需要让“攻击”按钮点击后触发玩家的攻击动作并切换到敌人回合。这涉及到UI按钮事件与游戏逻辑的通信。首先修改PlayerController增加攻击方法并让它受游戏状态控制export class PlayerController extends Component { // ... 原有属性 ... // 攻击力 property attackPower: number 15; // 攻击目标Boss节点 property(Node) targetBoss: Node null; private _actorData: ActorData null; private _targetActorData: ActorData null; // Boss的ActorData start() { // ... 原有初始化 ... this._actorData this.getComponent(ActorData); if (this.targetBoss) { this._targetActorData this.targetBoss.getComponent(ActorData); } } // 提供给按钮调用的攻击方法 public onAttackButtonClicked() { // 检查当前是否玩家回合 if (GameManager.instance GameManager.instance.currentState GameState.PLAYER_TURN) { this.performAttack(); // 玩家行动结束后结束回合 GameManager.instance.endPlayerTurn(); } else { console.log(现在不是你的回合); } } private performAttack() { if (!this._targetActorData) return; console.log(玩家攻击了老大造成${this.attackPower}点伤害); this._targetActorData.takeDamage(this.attackPower); // 这里可以播放攻击动画、音效 } // ... update等其他方法 ... }然后我们需要在编辑器中将按钮的点击事件绑定到onAttackButtonClicked方法。选中“AttackButton”节点。在属性检查器的Button组件中找到“Click Events”。将数组大小设为1。在元素0中“Target”拖拽层级管理器中的Player节点到这里。“Component”选择“PlayerController”。“Handler”选择“onAttackButtonClicked”下拉列表中会出现脚本中声明的方法。现在点击预览当你点击“攻击”按钮时控制台会输出攻击信息并且Boss的血条如果你已经为Boss也挂载了ActorData并关联了BossHPBar会减少。同时游戏状态会切换到敌人回合1秒后敌人会进行反击目前只是在控制台输出。6.3 简单的敌人AI与回合切换反馈为了让回合切换更明显我们可以修改GameManager在切换回合时触发一些UI更新比如改变按钮的交互状态。首先我们创建一个简单的事件系统为了简化这里使用Cocos内置的EventTarget对于小型项目足够。在GameManager中声明一个事件发射器// 在GameManager.ts顶部导入 import { EventTarget } from cc; export class GameManager extends Component { public static instance: GameManager null; public eventTarget: EventTarget new EventTarget(); // 自定义事件发射器 // ... 其他代码 ... startPlayerTurn() { this._currentState GameState.PLAYER_TURN; console.log(玩家回合开始); // 发射回合开始事件 this.eventTarget.emit(turn-start, GameState.PLAYER_TURN); } startEnemyTurn() { this._currentState GameState.ENEMY_TURN; console.log(敌人回合开始); // 发射回合开始事件 this.eventTarget.emit(turn-start, GameState.ENEMY_TURN); this.performEnemyAction(); } }然后我们创建一个UI控制器脚本“UIController.ts”来监听这个事件并更新按钮状态import { _decorator, Component, Node, Button, EventTarget } from cc; import { GameManager, GameState } from ./GameManager; const { ccclass, property } _decorator; ccclass(UIController) export class UIController extends Component { property(Button) attackButton: Button null; onLoad() { // 监听回合开始事件 if (GameManager.instance) { GameManager.instance.eventTarget.on(turn-start, this.onTurnStart, this); } } onDestroy() { if (GameManager.instance) { GameManager.instance.eventTarget.off(turn-start, this.onTurnStart, this); } } onTurnStart(state: GameState) { switch(state) { case GameState.PLAYER_TURN: this.attackButton.interactable true; // 可以改变按钮颜色提示 break; case GameState.ENEMY_TURN: this.attackButton.interactable false; break; } } }将UIController脚本挂载到Canvas或UILayer节点上并在属性检查器中将“Attack Button”关联到我们的攻击按钮节点。现在当切换到敌人回合时攻击按钮会变灰不可点击切换回玩家回合时按钮恢复可点击状态。这给了玩家清晰的回合制反馈。7. 项目调试与常见问题排查7.1 使用浏览器开发者工具Cocos Creator预览游戏时默认会在浏览器中打开。浏览器开发者工具按F12是我们调试的利器。Console控制台查看console.log输出的信息、警告和错误。所有脚本中的console语句都会在这里显示。这是追踪逻辑流程、查看变量值最基本的方法。Sources源代码可以在这里找到你编写的TypeScript脚本通常被编译、合并后放在assets目录下的.js文件中并设置断点进行单步调试。这对于排查复杂的逻辑错误至关重要。Network网络查看资源加载情况如果游戏卡在加载界面可以在这里检查是否有资源加载失败。Elements元素查看DOM结构虽然Cocos游戏最终渲染在Canvas上但一些UI调试信息有时也有用。7.2 Cocos Creator编辑器内调试属性检查器实时修改在预览运行时你可以在属性检查器中实时修改组件的属性值如速度、血量并立即看到效果。这对于平衡游戏数值非常方便。节点树与组件状态在层级管理器中可以实时看到节点的激活状态、位置变化。选中节点属性检查器会显示其当前所有组件的实时数据。控制台面板编辑器底部也有控制台面板会显示编译错误、运行时错误和日志与浏览器控制台同步。7.3 常见问题与解决方案速查表问题现象可能原因排查步骤与解决方案脚本修改后预览无变化1. 脚本编译错误。2. 脚本未保存。3. 浏览器缓存。1. 查看编辑器控制台是否有红色错误信息修正语法错误。2. 确保代码编辑器已保存文件CtrlS。3. 在浏览器预览页按CtrlF5强制刷新或关闭Cocos Creator的预览进程重新点击预览。节点在场景中不显示1. 节点或父节点未激活。2. 渲染组件如Sprite未设置SpriteFrame。3. 节点位置在相机视野外。4. 节点Scale为0。1. 检查层级管理器节点眼睛图标是否睁开激活。2. 检查Sprite组件的SpriteFrame属性是否为空。3. 检查节点Position确保其在相机Ortho Height范围内。4. 检查节点Scale是否为(0,0,0)。按钮点击无反应1. 按钮节点或父节点未激活。2. Button组件Interactable未勾选。3. 点击事件绑定错误目标、组件、方法名。4. 按钮被其他UI元素遮挡。1. 检查节点激活状态。2. 检查Button组件属性。3. 仔细核对Click Events中的Target、Component和Handler选择是否正确方法名是否公开public。4. 检查层级顺序确保按钮节点在遮挡它的节点之上。Prefab修改后场景实例未更新1. 直接在场景实例上修改了Prefab覆盖属性。2. Prefab编辑后未保存。1. 在资源管理器中双击打开Prefab进行编辑修改根节点或子节点的属性然后保存CtrlS。2. 确保Prefab资源文件已保存。脚本中获取的组件为null1. 使用property声明的属性未在编辑器赋值。2. 使用getComponent获取的组件不存在于该节点。3. 脚本执行时机问题节点还未初始化完成。1. 检查属性检查器确保所有property修饰的属性都已正确拖拽关联。2. 确认节点上确实挂载了该类型的组件。3. 将获取组件的代码放在start()或onLoad()生命周期函数中而不是构造函数里。移动或动画卡顿1.update中逻辑过于复杂或每帧计算量过大。2. 未使用deltaTime进行与帧率无关的运动计算。3. 图片资源尺寸过大。1. 优化代码逻辑避免在update中做不必要的计算或频繁查找节点/组件。2. 确保运动计算乘以了deltaTime。3. 检查导入的图片尺寸过大的图片会占用大量内存和显存适当压缩。7.4 性能优化小贴士初期注意事项在项目初期就养成好习惯能避免后期大量重构减少update中的开销只在需要持续变化的逻辑里使用update。对于不常变化的状态使用事件驱动。善用节点池NodePool对于需要频繁创建和销毁的对象如子弹、特效一定要使用节点池进行复用这是避免GC垃圾回收卡顿的关键。合图Auto Atlas将大量小图打包成一张大图能显著减少Draw Call提升渲染性能。在Cocos Creator的“项目设置”-“功能裁剪”中确保开启了“Auto Atlas”并在资源管理器中对纹理资源进行合理配置。避免在update中频繁find或getComponent这些调用都有开销。在start或onLoad中获取一次并缓存引用。至此我们已经完成了《击败老大》游戏第一段的核心内容从零搭建Cocos Creator项目创建场景与UI实现角色Prefab编写控制脚本搭建基础的回合制战斗逻辑框架并完成了UI与逻辑的联动。这个框架虽然简单但已经包含了组件化开发、节点管理、输入处理、坐标转换、事件通信等核心概念。在下一段中我们将深入丰富战斗细节添加技能系统、动画效果、音效以及更完善的游戏状态管理。