跳到主要内容

第一个闭环:点击、计时、结算与重开

先把一个很小的游戏做完整:一局 30 秒,每点一次加 10 分,达到 200 分通关,超时失败,结算后可重开。这里不接网络、不用美术大包,集中验证开发链路。

先把玩法说成规则​

这是允许反复点击同一目标的入门玩法,不是每个宝石只能收集一次的成品设计。先保证闭环,再增加随机目标或更多规则。

场景装配​

按编辑器章节建立 Game、Target、三个 Label 和 RestartButton。把 ClickCollector.ts 放到 assets/scripts 并挂在 Game 上。

脚本字段拖入的组件用途
targetTarget 的 Button接收加分点击
restartButtonRestartButton 的 Button重新开始
scoreLabelScoreLabel 的 Label当前积分
timeLabelTimeLabel 的 Label剩余秒数
resultLabelResultLabel 的 Label通关或失败提示
roundSeconds / goalScore30 / 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 和状态为准。需要更精确的竞技计时,应改为明确时间戳与服务端裁决方案,而非依赖帧更新顺序。

必须亲手验收的六件事​

  1. 第一次预览显示 0 分、30 秒,无错误日志。
  2. 点一次只加 10 分;快速点也不会一次多次加分。
  3. 达标后显示成功,继续点目标不再加分。
  4. 等待超时后显示失败,分数停止变化。
  5. 连续重开三次,每秒仍只减少约一秒。
  6. 结束或重开时目标恢复原缩放,不越点越小。

脚本经过真实 3.8.8 声明的静态类型检查;上述装配、交互与微信真机结果需要你在对应环境实际验证,见验证矩阵。

下一步:把状态显式化、加入关卡配置。

依据:Button 组件、生命周期。