第一个闭环:点击、计时、结算与重开
先把一个很小的游戏做完整:一局 30 秒,每点一次加 10 分,达到 200 分通关,超时失败,结算后可重开。这里不接网络、不用美术大包,集中验证开发链路。
先把玩法说成规则
这是允许反复点击同一目标的入门玩法,不是每个宝石只能收集一次的成品设计。先保证闭环,再增加随机目标或更多规则。
场景装配
按编辑器章节建立 Game、Target、三个 Label 和 RestartButton。把 ClickCollector.ts 放到 assets/scripts 并挂在 Game 上。
| 脚本字段 | 拖入的组件 | 用途 |
|---|---|---|
| target | Target 的 Button | 接收加分点击 |
| restartButton | RestartButton 的 Button | 重新开始 |
| scoreLabel | ScoreLabel 的 Label | 当前积分 |
| timeLabel | TimeLabel 的 Label | 剩余秒数 |
| resultLabel | ResultLabel 的 Label | 通关或失败提示 |
| roundSeconds / goalScore | 30 / 200 | 本关配置 |
目标和重开按钮各自需要可见图形与 UITransform。不要额外在 Button 的 Click Events 里再连一次逻辑。保存场景后再预览。
完整脚本
下载 ClickCollector.ts。下方内容与下载文件使用同一份源码,避免两份代码互相漂移。
展开完整脚本:字段、监听、规则、计时与动画清理
ClickCollector.ts
import { _decorator, Button, Component, Label, Node, tween, Tween, Vec3 } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('ClickCollector')
export class ClickCollector extends Component {
@property(Button) target: Button | null = null;
@property(Button) restartButton: Button | null = null;
@property(Label) scoreLabel: Label | null = null;
@property(Label) timeLabel: Label | null = null;
@property(Label) resultLabel: Label | null = null;
@property roundSeconds = 30;
@property goalScore = 200;
private phase: 'playing' | 'settled' = 'settled';
private score = 0;
private remaining = 0;
private shownSeconds = -1;
private initialScale = new Vec3(1, 1, 1);
private feedback: Tween<Node> | null = null;
onLoad() {
if (this.target) this.initialScale.set(this.target.node.scale);
}
onEnable() {
this.target?.node.on(Button.EventType.CLICK, this.collect, this);
this.restartButton?.node.on(Button.EventType.CLICK, this.restart, this);
}
start() {
if (!this.target || !this.restartButton || !this.scoreLabel ||
!this.timeLabel || !this.resultLabel || !Number.isFinite(this.roundSeconds) ||
this.roundSeconds <= 0 || !Number.isFinite(this.goalScore) || this.goalScore <= 0) {
console.error('ClickCollector: 请绑定全部字段,并设置有效的回合时间与目标分数');
this.enabled = false;
return;
}
this.restart();
}
onDisable() {
this.target?.node.off(Button.EventType.CLICK, this.collect, this);
this.restartButton?.node.off(Button.EventType.CLICK, this.restart, this);
this.stopFeedback();
}
private restart() {
this.stopFeedback();
this.score = 0;
this.remaining = this.roundSeconds;
this.shownSeconds = -1;
this.phase = 'playing';
if (this.target) this.target.interactable = true;
if (this.scoreLabel) this.scoreLabel.string = '积分 0';
if (this.resultLabel) this.resultLabel.string = '';
this.renderTime();
}
private collect() {
if (this.phase !== 'playing' || !this.target) return;
// 截止点只结算超时,避免已耗尽时间的点击仍加分。
if (this.remaining <= 0) { this.settle(false); return; }
this.score += 10;
if (this.scoreLabel) this.scoreLabel.string = `积分 ${this.score}`;
if (this.score >= this.goalScore) { this.settle(true); return; }
this.stopFeedback();
const node = this.target.node;
const base = this.initialScale.clone();
const small = new Vec3(base.x * 0.9, base.y * 0.9, base.z);
this.feedback = tween(node)
.to(0.06, { scale: small })
.to(0.06, { scale: base })
.start();
}
update(dt: number) {
if (this.phase !== 'playing') return;
this.remaining = Math.max(0, this.remaining - dt);
this.renderTime();
if (this.remaining <= 0) this.settle(false);
}
private renderTime() {
const value = Math.ceil(this.remaining);
if (value !== this.shownSeconds && this.timeLabel) {
this.shownSeconds = value;
this.timeLabel.string = `剩余 ${value} 秒`;
}
}
private settle(won: boolean) {
if (this.phase !== 'playing') return;
this.phase = 'settled';
this.stopFeedback();
if (this.target) this.target.interactable = false;
if (this.resultLabel) this.resultLabel.string = won ? '目标达成!' : '时间到,再试一次';
}
private stopFeedback() {
this.feedback?.stop();
this.feedback = null;
if (this.target) this.target.node.setScale(this.initialScale);
}
}
按这个顺序读代码
先看 phase、score、remaining,它们是规则状态;再看 collect 和 settle,它们改变状态;最后看 Label 与 Tween,它们负责表现。settle 只有从 playing 才能进入,因此重复调用不会重复结算。
计时按 update(dt) 累减,显示用向上取整;只有显示秒数变化时才更新 Label。这个示例没有接微信前后台事件,因此不能据此推断后台时间怎样计入。正式接入时按调试章节补上时间规则。
当点击和超时恰好处于边界时,脚本以当前已更新的 remaining 和状态为准。需要更精确的竞技计时,应改为明确时间戳与服务端裁决方案,而非依赖帧更新顺序。
必须亲手验收的六件事
- 第一次预览显示 0 分、30 秒,无错误日志。
- 点一次只加 10 分;快速点也不会一次多次加分。
- 达标后显示成功,继续点目标不再加分。
- 等待超时后显示失败,分数停止变化。
- 连续重开三次,每秒仍只减少约一秒。
- 结束或重开时目标恢复原缩放,不越点越小。
脚本经过真实 3.8.8 声明的静态类型检查;上述装配、交互与微信真机结果需要你在对应环境实际验证,见验证矩阵。