把规则接进 Cocos 场景
级别:首作必学。 这一步先使用文字牌面,验证规则、输入、生命周期和重开。图像、动画和声音在下一阶段加入。
1. 创建场景与引用
在你的 Creator 3.8 工程中新建 Main 场景:
Canvas
├─ Board
│ ├─ Card0 … Card5 每张牌有 UITransform 和 Label
├─ Status Label,显示阶段与剩余回合
├─ Restart 有可见文字与足够 UITransform 尺寸
└─ GameController 挂载下面的 MemoryController
使用固定六张牌,先手动摆成两行三列。每张卡牌的 Label 在其自身节点,给 UITransform 设置可点击的宽高;确保 Camera 能看到对应 UI Layer。Restart 可用普通 UI 节点,不再配置第二套点击回调。
将下载的 model.ts复制为 assets/scripts/model.ts,新建同目录的 MemoryController.ts。在属性检查器中把六个 Label 按 0 到 5 顺序拖入 cards,绑定 status 和 restartButton。
2. 最小控制器
以下是需要在 Creator 中执行的完整组件示例,本站未做引擎与真机运行验证。它不依赖本仓库的 React 或 Node 测试库。
import {_decorator, Component, Label, Node, game, Game as EngineGame} from 'cc';
import {createGame, flip, resolve, shuffle, Game} from './model';
const {ccclass, property} = _decorator;
const FACES = ['A', 'B', 'C', 'A', 'B', 'C'];
@ccclass('MemoryController')
export class MemoryController extends Component {
@property([Label]) cards: Label[] = [];
@property(Label) status: Label | null = null;
@property(Node) restartButton: Node | null = null;
private state: Game = createGame(FACES, 6);
private hidden = false;
private taps: Array<() => void> = [];
onLoad() {
if (this.cards.length !== 6 || this.cards.some(c => !c) ||
!this.status || !this.restartButton) {
throw new Error('请绑定 6 个卡牌 Label、状态 Label 和重开节点');
}
this.taps = this.cards.map((_, id) => () => this.tap(id));
game.on(EngineGame.EVENT_HIDE, this.onHidden, this);
game.on(EngineGame.EVENT_SHOW, this.onShown, this);
this.reset();
}
onEnable() {
this.cards.forEach((c, i) => c.node.on(Node.EventType.TOUCH_END, this.taps[i]));
this.restartButton!.on(Node.EventType.TOUCH_END, this.reset, this);
this.paint();
this.armMismatch();
}
onDisable() {
this.cards.forEach((c, i) => c.node.off(Node.EventType.TOUCH_END, this.taps[i]));
this.restartButton?.off(Node.EventType.TOUCH_END, this.reset, this);
this.unschedule(this.finishMismatch);
}
onDestroy() {
game.off(EngineGame.EVENT_HIDE, this.onHidden, this);
game.off(EngineGame.EVENT_SHOW, this.onShown, this);
}
private reset() {
this.unschedule(this.finishMismatch);
this.state = createGame(shuffle(FACES), 6);
this.paint();
}
private tap(id: number) {
if (this.hidden) return;
const next = flip(this.state, id);
if (next === this.state) return;
this.state = next;
this.paint();
this.armMismatch();
}
private finishMismatch = () => {
this.state = resolve(this.state);
this.paint();
};
private armMismatch() {
this.unschedule(this.finishMismatch);
if (!this.hidden && this.state.phase === 'resolving') {
this.scheduleOnce(this.finishMismatch, 0.7);
}
}
private onHidden() {
this.hidden = true;
this.unschedule(this.finishMismatch);
}
private onShown() {
this.hidden = false;
if (!this.enabledInHierarchy) return;
this.paint();
this.armMismatch();
}
private paint() {
this.state.cards.forEach((c, i) => {
this.cards[i].string = c.face === 'down' ? '?' :
c.face === 'matched' ? `${c.value} ✓` : c.value;
});
const text = {
playing: '请选择两张牌', resolving: '不匹配,稍后盖回',
won: '配对成功!可以重开', lost: '回合用完,可以重试',
}[this.state.phase];
this.status!.string = `${text} · 剩余 ${this.state.turnsLeft} 回合`;
}
}
0.7 秒是观察规则的教学初值,后续结合翻牌动画和试玩调整。此版本用文字显示结算;需要遮罩与专门结果面板时,仍由同一 state.phase 驱动。
输入监听仅在组件启用期间存在;应用前后台监听则保留到组件销毁。这样即使组件在后台被禁用、前台恢复后才重新启用,也不会错过 SHOW 后永久保留暂停标志。应用规模扩大时,可把这份持续状态移到统一协调层。
3. 接线验收
先检查浏览器预览:点击牌面、匹配、不匹配、最后一回合、重开、禁用再启用组件。再构建微信,分别在选中一张和展示两张时切后台,以及后台禁用、回前台后重新启用的组合。前台恢复会重新提供一次展示窗口,不补扣回合。
“新局被旧任务盖回”时,检查 reset 是否取消同一个 finishMismatch 引用;“点击两次执行”时,检查是否还配置了 Button 点击或其他触摸监听。
完成标准:六张文字牌能完成胜负与重开;所有编辑器字段已绑定;前后台和反复重开均有实际测试记录。然后进入反馈与配置。