当前位置: 首页 > news >正文

2048游戏笔记3 游戏开始与结束 cocos3.8.7

游戏开始界面

开始游戏按钮

start() {this.startButton.node.on(Button.EventType.CLICK, this.onStartGame, this);}
  • this.startButton:指向脚本中通过 @property(Button) 声明的按钮组件实例,是需要监听点击事件的按钮。
  • .node:获取按钮组件所在的节点(Node),因为事件监听是注册在节点上的。
  • .on():节点的事件注册方法,用于监听特定类型的事件,接收三个主要参数:
    1. Button.EventType.CLICK:事件类型,这里指定为按钮点击事件(Cocos 内置的按钮事件常量)。
    2. this.onStartGame:事件触发时要执行的回调函数(即点击按钮后要执行的逻辑)。
    3. this:回调函数的上下文(this 指向),确保在 onStartGame 函数中使用 this 时,能正确指向当前脚本实例。

作用:当 startButton 被点击时,会自动调用当前脚本中的 onStartGame 方法,实现点击按钮后的逻辑(比如开始游戏、切换界面等)。


GameReadyUI

点击按钮,GameReadyUI界面禁用

private onStartGame() {// 通知游戏管理器开始游戏GameManager.inst().StartGame();// 隐藏开始界面this.node.active = false;}

在代码中使用 this.node.active = false 来隐藏开始界面,是基于 Cocos Creator 引擎的节点激活状态机制设计的,具体原因和优势如下:

 Cocos Creator 中节点激活状态的作用

在 Cocos Creator 中,node.active 是控制节点及其子节点是否可见、是否参与交互的核心属性:

  • 当 active = false 时:
    • 节点及其所有子节点会从渲染树中移除,不再显示在屏幕上(实现隐藏效果)。
    • 节点上的组件(如按钮、碰撞体等)会暂停工作,不再响应事件(避免隐藏后仍能被点击等误操作)。
  • 当 active = true 时:
    • 节点及其子节点重新显示,组件恢复正常功能。

滑动事件的启用和禁用

protected start(): void {this.transitionToReadyState();}transitionToReadyState(){this.curGS = GameState.Ready;// 初始禁用滑动if ( this.chessboard ){this.chessboard.setSwipeEnable(false);}}StartGame(){this.curGS = GameState.Gaming;// 启用滑动if( this.chessboard ){this.chessboard.setSwipeEnable(true);}}
  • 避免空引用错误if (this.chessboard) 检查确保 chessboard 实例已正确初始化(在 Cocos Creator 中,通过编辑器绑定的属性可能因配置问题为空),防止调用 setSwipeEnable 时出现报错。
  • 单一职责原则:滑动功能的具体实现(如触摸检测、方向判断)封装在 Chessboard 中,GameManager 仅负责 “开关控制”,不关心滑动的具体逻辑,降低代码耦合。
    private isSwipeEnabled: boolean = false;public setSwipeEnable(enabled: boolean) {this.isSwipeEnabled = enabled;console.log(`滑动功能${enabled ? '启用' : '禁用'}`);}

isSwipeEnabled 标记和 setSwipeEnable 方法的设计,是为了精准控制滑动功能的可用性,确保滑动交互仅在正确的游戏阶段响应。以下是具体解析:

一、核心作用

private isSwipeEnabled: boolean = false; // 滑动功能是否启用的标记
public setSwipeEnable(enabled: boolean) { ... } // 控制标记的方法

这两个成员共同实现了对滑动功能的 “开关控制”,本质是通过状态标记限制滑动事件的处理逻辑

二、逐部分解析设计逻辑

1. private isSwipeEnabled: boolean = false
  • 访问权限 private:确保只有 Chessboard 自身能直接修改该标记,外部(如 GameManager)必须通过 setSwipeEnable 方法操作,避免标记被意外篡改(例如其他脚本误将其设为 true 导致滑动提前启用)。

  • 初始值 false:游戏默认处于 “准备状态”(GameState.Ready),此时不应响应滑动操作(如点击开始按钮前,玩家滑动屏幕不应有反应),因此初始禁用滑动符合逻辑。

  • 作为状态标记的意义:它是滑动事件处理的 “闸门”,在 handleSwipe 方法中会先检查该标记:

    private handleSwipe(direction: SwipeDirection) {if (!this.isSwipeEnabled) { // 若未启用,直接忽略事件console.log("滑动功能未启用,忽略滑动事件");return;}// 只有启用时才处理滑动逻辑...
    }
    

    这种设计确保了滑动交互仅在允许的状态下生效。

2. public setSwipeEnable(enabled: boolean) 方法
  • 访问权限 public:允许外部模块(如 GameManager)通过该方法控制滑动功能的开关,例如:

    // GameManager 中切换状态时启用/禁用滑动
    transitionToReadyState() {this.chessboard.setSwipeEnable(false); // 准备阶段禁用
    }
    StartGame() {this.chessboard.setSwipeEnable(true);  // 游戏阶段启用
    }
    

    这体现了模块化协作GameManager 负责管理游戏状态,Chessboard 负责实现滑动逻辑,通过该方法完成状态同步。

  • 参数 enabled 与日志输出

    • 方法接收布尔值参数,直接修改 isSwipeEnabled 标记,逻辑简洁直观。
    • 日志 console.log(...) 用于调试,方便开发时确认滑动功能的状态变化(例如控制台打印 “滑动功能启用”,可验证 StartGame 方法是否正确调用)。

游戏结束界面

再来一次按钮

点击按钮,重新开始游戏,同时隐藏游戏结束界面

 @property(Button)AgainButton: Button = null;start() {this.AgainButton.node.on(Button.EventType.CLICK,this.onPlayAgainGame,this);}onPlayAgainGame(){// 点击按钮之后,调用// 游戏开始GameManager.inst().StartGame();// 隐藏游戏结束界面this.node.active = false;}

GameReadyUI和GameOverUI的启用与禁用

根据游戏状态来启用和禁用ui界面。

游戏开始界面

 @property(Button)startButton: Button = null;start() {// 当 startButton 被点击时,会自动调用当前脚本中的 onStartGame 方法,this.startButton.node.on(Button.EventType.CLICK, this.onStartGame, this);// 启用自身uithis.EnableGameReadyUI();}private onStartGame() {// 通知游戏管理器开始游戏GameManager.inst().StartGame();// 隐藏开始界面this.DisableGameReadyUI();}DisableGameReadyUI(){// 隐藏开始界面this.node.active = false;}EnableGameReadyUI(){// 显示开始界面this.node.active = true;}

游戏结束界面

@property(Button)AgainButton: Button = null;start() {this.AgainButton.node.on(Button.EventType.CLICK,this.onPlayAgainGame,this);// 启用自身ui,在这里不可以直接禁用gameReadyui,初始化会冲突this.EnableGameOverUI();}onPlayAgainGame(){// 点击按钮之后,调用// 游戏开始GameManager.inst().StartGame();// 隐藏游戏结束界面this.DisableGameOverUI();}public DisableGameOverUI(){// 隐藏游戏结束界面this.node.active = false;}EnableGameOverUI(){// 展示游戏结束界面this.node.active = true;}

GameManager

@property(GameReadyUI)
gameReadyUI:GameReadyUI = null;@property(GameOverUI)
gameOverUI: GameOverUI = null;curGS: GameState = GameState.Ready;protected start(): void {this.transitionToReadyState();}transitionToReadyState(){this.curGS = GameState.Ready;// 启用uiif(this.gameReadyUI){this.gameReadyUI.EnableGameReadyUI();}if(this.gameOverUI){this.gameOverUI.DisableGameOverUI();}// 初始禁用滑动if ( this.chessboard ){this.chessboard.setSwipeEnable(false);}}StartGame(){this.curGS = GameState.Gaming;// 启用滑动if( this.chessboard ){this.chessboard.setSwipeEnable(true);}}GameOver(){this.curGS = GameState.GameOver;// 启用uiif(this.gameOverUI){this.gameOverUI.EnableGameOverUI();}if(this.gameReadyUI){this.gameReadyUI.DisableGameReadyUI();}// 禁用滑动if(this.chessboard){this.chessboard.setSwipeEnable(false);}}

问题(在GameReadyUI中调用禁用GameOverUI方法会冲突)

一、先解决循环引用的错误(关键前提)

报错明确指出 GameOverUI 和 GameReadyUI 相互导入,形成了循环引用,导致类型定义异常(undefined),进而可能引发 UI 控制逻辑失效。

问题代码分析:
  • GameOverUI.ts 导入了 GameReadyUI,同时 GameReadyUI.ts 又导入了 GameOverUI,形成闭环。
  • 循环引用会导致 TypeScript 在解析时无法正确识别类类型,使得 @property(GameReadyUI) 这类定义失效,可能导致组件引用为 null
  • 初始化冲突

    • GameReadyUI 的 start 方法会调用 EnableGameReadyUI,其中尝试禁用 GameOverUI
    • GameOverUI 的 start 方法会调用 EnableGameOverUI,其中尝试禁用 GameReadyUI
    • 两个 start 方法在游戏启动时几乎同时执行,可能导致相互禁用失败(此时对方组件可能还未初始化)。
解决方案:移除相互导入,通过 GameManager 间接访问
  1. 态唯一GameManager 的 curGS 始终只有一个状态,确保同一时间只会触发对应状态的 UI 逻辑。
  2. 显隐成对出现:每个状态切换时,都会同时处理 “显示当前 UI” 和 “隐藏另一个 UI”,例如:
    • 准备状态:显示 GameReadyUI + 隐藏 GameOverUI
    • 结束状态:显示 GameOverUI + 隐藏 GameReadyUI
  3. UI 解耦GameReadyUI 和 GameOverUI 互不感知对方存在,所有联动逻辑由 GameManager 统一调度,避免循环依赖或冲突。

游戏结束状态

无空格且无相邻可合并方块则游戏结束

private CheckGameOver(): boolean{// 检查是否有空格子for (let i=0; i<this.rowCount; i++){for(let j=0; j<this.colCount; j++){if(this.values[i][j] == 0){return false;}}}// 检查是否有相邻相同数值(水平方向)for( let i=0; i<this.rowCount; i++){for( let j=0; j<this.colCount-1; j++){if(this.values[i][j] == this.values[i][j+1]){return false;}}   }// 垂直方向for(let j=0;j< this.colCount; j++){for(let i=0; i<this.rowCount-1; i++){if( this.values[i][j] === this.values[i+1][j]){return false;}}}return true;}

分数记录

 // 向上移动private moveUp(): boolean {let moved = false;for (let j = 0; j < this.colCount; j++) { // 遍历每一列for (let i = 1; i < this.rowCount; i++) { // 从第二行开始向上移动if (this.values[i][j] !== 0) {let k = i;// 移动到最上方空位while (k > 0 && this.values[k - 1][j] === 0) {this.values[k - 1][j] = this.values[k][j];this.values[k][j] = 0;k--;moved = true;}// 合并上方相同数值if (k > 0 && this.values[k - 1][j] === this.values[k][j]) {this.values[k - 1][j] *= 2;this.values[k][j] = 0;moved = true;this.node.emit('score', this.values[k - 1][j]);}}}}this.updateGridViews();return moved;}
this.node.emit('score', this.values[k - 1][j]);

✅ 作用解释(一句话):

把“本次合并得到的分数”广播出去,让别的脚本(如 Score.ts)听见并处理。


✅ 拆分解释:

表格

复制

片段含义
this.node当前脚本所在的节点(这里是 Chessboard 节点)
.emit('score', ...)向 同一节点 发送一个自定义事件,事件名叫 'score'
this.values[k - 1][j]合并后新生成的数字(比如 2+2=4,这里就是 4)

✅ 事件流(整个链条):

  1. 用户滑动 → 两个 2 合并成 4

  2. 执行 this.values[k - 1][j] *= 2 → 格子变成 4

  3. 执行 this.node.emit('score', 4) → 发出事件

  4. GameManager 监听到 → 调用 Score.onScore(4)

  5. 分数增加 4 → UI 更新


Score.ts

import { _decorator, Component, Label, Node } from 'cc';
const { ccclass, property } = _decorator;@ccclass('Score')
export class Score extends Component {// 游戏中实时显示的“当前得分”标签@property(Label)presentScore: Label = null;// 游戏中实时显示的“历史最高分”标签@property(Label)highestScoreEver: Label = null;// 游戏结束界面显示的“本局分数”标签@property(Label)currentScore: Label = null;// 游戏结束界面显示的“历史最佳分数”标签@property(Label)bestScore: Label = null;// 当前局内得分(内存变量)private _presentScore: number = 0;// 历史最高分(从本地存储读取)private _highestScoreEver: number = 0;// 组件加载时触发:读取存档、初始化显示、注册得分事件protected onLoad(): void {// 从浏览器本地存储读取历史最高分,不存在则默认为 0this._highestScoreEver = parseInt(localStorage.getItem('bestScore') || '0');// 同步所有 Label 的初始文字this.UpDataLabels();// 监听同节点发出的自定义事件 'score',收到后调用 onScore 方法this.node.on('score', this.onScore, this);}// 得分事件回调:value 为合并后新生成的数字(如 4、8、16...)public onScore(value: number): void {// 累加到当前局内得分this._presentScore += value;// 如果突破历史最高分,则更新并持久化if (this._presentScore > this._highestScoreEver) {this._highestScoreEver = this._presentScore;// 写入浏览器本地存储,key 为 'bestScore'localStorage.setItem('bestScore', this._highestScoreEver.toString());}// 刷新所有相关 Label 的显示this.UpDataLabels();}// 统一刷新四个 Label 的显示文字private UpDataLabels(): void {if (this.presentScore) {this.presentScore.string = this._presentScore.toString();}if (this.highestScoreEver) {this.highestScoreEver.string = this._highestScoreEver.toString();}if (this.currentScore) {this.currentScore.string = this._presentScore.toString();}if (this.bestScore) {this.bestScore.string = this._highestScoreEver.toString();}}// 重新开始时重置当前局内得分并刷新显示public resetScore(): void {this._presentScore = 0;this.UpDataLabels();}start() { }update(deltaTime: number) { }
}

this._highestScoreEver = parseInt(localStorage.getItem('bestScore') || '0');

✅ 作用一句话:

从浏览器本地存储中读取历史最高分,如果之前没存过,就默认为 0。


✅ 拆分解释:

片段含义
localStorage浏览器提供的本地存储对象,游戏退出后数据仍在
.getItem('bestScore')尝试读取键为 'bestScore' 的值(字符串)
`'0'`如果读不到(第一次玩或清过缓存),就用 '0' 作为默认值
parseInt(...)把字符串转成整数,方便后续比较和计算
this._highestScoreEver = ...存到内存变量里,供本局逻辑使用

得分事件监听

onLoad() {GameManager._insc = this;// 监听棋盘得分事件if (this.chessboard) {this.chessboard.node.on('score', (value: number) => {const score = this.node.getComponent(Score);if (score) {score.onScore(value); // 直接调用得分逻辑}});}}

这段代码写在 GameManageronLoad 生命周期里,负责 “搭桥”——把棋盘里发出的得分事件转交给分数组件。逐句拆解如下:


✅ 1. 单例赋值

GameManager._insc = this;

把当前实例存到类的静态变量 _insc,方便全局随时通过 GameManager.inst() 拿到这个唯一的管理器。


✅ 2. 监听棋盘事件

if (this.chessboard) {this.chessboard.node.on('score', (value: number) => { ... });
}
  • 如果编辑器里给 GameManager 拖了 Chessboard 引用,就监听它的节点。

  • 事件名必须是 'score'(与棋盘合并时 this.node.emit('score', ...) 保持一致)。

  • 回调参数 value 就是合并后新生成的数字(例如 4、8、16 ...)。


✅ 3. 转发给分数组件

const score = this.node.getComponent(Score);
if (score) {score.onScore(value);   // 直接调用 Score 脚本里的累加方法
}
  • this.nodeGameManager 自己所挂的节点。

  • 如果同一个节点上还有 Score 组件,就立即把分数值塞给它,让它累加、刷新界面、更新最高纪录。


http://www.dtcms.com/a/549070.html

相关文章:

  • 【AI WorkFow】n8n 源码分析-核心结构体设计思考(六)
  • 网站诊断内容工程建设教育培训
  • Opencv(四):自适应二值化
  • GitHub 热榜项目 - 日榜(2025-10-30)
  • 网络:4.应用层自定义协议与序列化
  • Word崩溃打不开?实测三款Word文档修复工具!
  • 哪个网站做签约插画师好网站域名过期后续费多长时间生效
  • 《R for Data Science (2e)》免费中文翻译 (第11章) --- Communication(2)
  • 扩展阅读:CSV格式的目标检测(Object Detection)标注文件示例
  • 行政单位门户网站建设规定久久建筑网20g三维图集下载
  • Kotlin Multiplatform Mobile(KMM):实现 iOS 与 Android 共享业务逻辑
  • 利用Selenium和PhantomJS提升网页内容抓取与分析的效率
  • QML学习笔记(四十七)QML与C++交互:上下文对象
  • 农业物联网实践:基于 ESP8266 与土壤传感器的智能灌溉系统开发与部署
  • 【Windows 10 企业版 LSTC】下安装【英特尔® 显卡控制中心】
  • Linux常用操作命令详解
  • 十堰专业网站建设公司网站建设预算
  • 深圳网站设计+建设首选网站开发iis怎么配置
  • Angular【起步】
  • Unity ComputeShader入门指南
  • 铜鼻子冷压端子视觉检测机 尺寸外观瑕疵自动化检测设备
  • 强化学习(RL)简介及其在大语言模型中的应用
  • 沈阳自主建站模板网站代理维护
  • 东莞做展示网站的公司济南网络科技公司排名
  • 云栖实录 | 阿里云发布Elasticsearch Serverless 2.0,重塑AI搜索时代基础设施
  • 解决 InfiniteScroll 滚动 BUG
  • Python实现随机选播视频的示例代码
  • 做网站开发多少钱制作网站步骤
  • CSS实现渐变色边框(Gradient borders)
  • 本地部署集成全能平台 Team.IDE 并实现外部访问