first commit
This commit is contained in:
290
assets/scripts/GameManager.ts
Normal file
290
assets/scripts/GameManager.ts
Normal file
@@ -0,0 +1,290 @@
|
||||
// assets/scripts/GameManager.ts
|
||||
import {
|
||||
_decorator,
|
||||
Component,
|
||||
Node,
|
||||
Prefab,
|
||||
instantiate,
|
||||
NodePool,
|
||||
UITransform,
|
||||
Vec3,
|
||||
v3,
|
||||
input,
|
||||
Input,
|
||||
EventTouch,
|
||||
SpriteFrame,
|
||||
Sprite,
|
||||
Label,
|
||||
RigidBody2D,
|
||||
Vec2,
|
||||
tween,
|
||||
AudioSource,
|
||||
sys,
|
||||
math,
|
||||
} from 'cc';
|
||||
import { FRUITS, clampLevel, randomStartLevel } from './FruitConfig';
|
||||
import { Fruit } from './Fruit';
|
||||
const { ccclass, property } = _decorator;
|
||||
|
||||
@ccclass('GameManager')
|
||||
export class GameManager extends Component {
|
||||
@property(Prefab)
|
||||
fruitPrefab!: Prefab; // 统一水果预制
|
||||
|
||||
@property([SpriteFrame])
|
||||
fruitSprites: SpriteFrame[] = []; // 长度需 >= FRUITS.length,按等级顺序
|
||||
|
||||
@property(Node)
|
||||
fruitRoot!: Node;
|
||||
|
||||
@property(Node)
|
||||
preview!: Node; // 顶部预览点,挂 Sprite
|
||||
|
||||
@property(Node)
|
||||
topSensor!: Node; // 仅用于场景绑定与可视化
|
||||
|
||||
@property(Node)
|
||||
walls!: Node; // Left/Right/Floor(用于取边界宽度,可留空)
|
||||
|
||||
@property(Label)
|
||||
scoreLabel!: Label;
|
||||
|
||||
@property(Label)
|
||||
bestLabel!: Label;
|
||||
|
||||
@property(Node)
|
||||
dropLine!: Node; // 危险红线(可选)
|
||||
|
||||
@property(AudioSource)
|
||||
sfx!: AudioSource; // 可选
|
||||
|
||||
// —— 可调参数 ——
|
||||
@property
|
||||
previewY = 560; // 预览/下落的 Y(根据分辨率调整)
|
||||
@property
|
||||
spawnCooldown = 0.2; // 连续投掷冷却
|
||||
@property
|
||||
previewClampPadding = 40; // 贴边留白
|
||||
@property
|
||||
randomStartMaxLevel = 5; // 随机初级水果最大等级
|
||||
|
||||
// —— 运行时状态 ——
|
||||
nextLevel = 1; // 将要投的等级
|
||||
queuedLevel = 1; // 下一颗的预告
|
||||
canDrop = true;
|
||||
isGameOver = false;
|
||||
dangerOn = false;
|
||||
score = 0;
|
||||
best = 0;
|
||||
|
||||
private pool = new NodePool();
|
||||
|
||||
onLoad() {
|
||||
// 把 SpriteFrame 注入配置(可选)
|
||||
for (let i = 0; i < FRUITS.length; i++) {
|
||||
FRUITS[i].sprite = this.fruitSprites[i] || FRUITS[i].sprite;
|
||||
}
|
||||
this.best = Number(sys.localStorage.getItem('best') || 0);
|
||||
if (this.bestLabel) this.bestLabel.string = `${this.best}`;
|
||||
if (this.scoreLabel) this.scoreLabel.string = `0`;
|
||||
}
|
||||
|
||||
start() {
|
||||
this.bindInput();
|
||||
this.reset();
|
||||
}
|
||||
|
||||
protected onDestroy() {
|
||||
this.unbindInput();
|
||||
}
|
||||
|
||||
bindInput() {
|
||||
input.on(Input.EventType.TOUCH_MOVE, this.onMove, this);
|
||||
input.on(Input.EventType.TOUCH_START, this.onMove, this); // 触摸开始也更新预览位置
|
||||
input.on(Input.EventType.TOUCH_END, this.onDrop, this);
|
||||
}
|
||||
|
||||
unbindInput() {
|
||||
input.off(Input.EventType.TOUCH_MOVE, this.onMove, this);
|
||||
input.off(Input.EventType.TOUCH_START, this.onMove, this);
|
||||
input.off(Input.EventType.TOUCH_END, this.onDrop, this);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.isGameOver = false;
|
||||
this.canDrop = true;
|
||||
this.updateScore(0);
|
||||
|
||||
// 清场(放回对象池而非直接删)
|
||||
const children = [...this.fruitRoot.children];
|
||||
for (const c of children) this.recycleFruit(c);
|
||||
|
||||
// 初始化两个预报等级
|
||||
this.nextLevel = randomStartLevel(1, this.randomStartMaxLevel);
|
||||
this.queuedLevel = randomStartLevel(1, this.randomStartMaxLevel);
|
||||
|
||||
this.refreshPreview();
|
||||
this.setDanger(false);
|
||||
if (this.dropLine) this.dropLine.active = false;
|
||||
}
|
||||
|
||||
refreshPreview() {
|
||||
if (!this.preview) return;
|
||||
const sp = this.preview.getComponent(Sprite)!;
|
||||
const def = FRUITS[this.nextLevel - 1];
|
||||
sp.spriteFrame = def.sprite!;
|
||||
|
||||
const ui = this.preview.getComponent(UITransform)!;
|
||||
ui.setContentSize(def.radius * 2, def.radius * 2);
|
||||
|
||||
// 初始位置居中(仅设置 X,Y 固定在预览线)
|
||||
const p = this.preview.position;
|
||||
this.preview.active = true;
|
||||
this.preview.setPosition(v3(0, this.previewY, p.z));
|
||||
}
|
||||
|
||||
onMove(e: EventTouch) {
|
||||
if (this.isGameOver) return;
|
||||
const loc = e.getUILocation();
|
||||
// 将屏幕坐标转本地 Canvas 坐标(父节点是 Canvas)
|
||||
const ui = this.node.getComponent(UITransform)!;
|
||||
const local = ui.convertToNodeSpaceAR(new Vec3(loc.x, loc.y, 0));
|
||||
|
||||
// 约束 X
|
||||
const halfW = ui.contentSize.width / 2;
|
||||
const padding = this.previewClampPadding;
|
||||
const x = math.clamp(local.x, -halfW + padding, halfW - padding);
|
||||
|
||||
const p = this.preview.position;
|
||||
this.preview.setPosition(v3(x, this.previewY, p.z));
|
||||
}
|
||||
|
||||
onDrop() {
|
||||
if (this.isGameOver || !this.canDrop) return;
|
||||
this.canDrop = false;
|
||||
|
||||
const dropPos = this.preview.worldPosition;
|
||||
this.spawnFruit(this.nextLevel, dropPos);
|
||||
|
||||
// 队列前移 & 随机新队列
|
||||
this.nextLevel = this.queuedLevel;
|
||||
this.queuedLevel = randomStartLevel(1, this.randomStartMaxLevel);
|
||||
this.refreshPreview();
|
||||
|
||||
// 冷却
|
||||
this.scheduleOnce(() => (this.canDrop = true), this.spawnCooldown);
|
||||
}
|
||||
|
||||
spawnFruit(level: number, worldPos: Vec3, inheritVel?: Vec2) {
|
||||
const node = this.pool.size() > 0 ? this.pool.get()! : instantiate(this.fruitPrefab);
|
||||
node.parent = this.fruitRoot;
|
||||
node.active = true;
|
||||
|
||||
// 转换到 FruitRoot 的本地坐标
|
||||
const local = this.fruitRoot.getComponent(UITransform)!.convertToNodeSpaceAR(worldPos.clone());
|
||||
node.setPosition(local);
|
||||
node.setScale(1, 1, 1);
|
||||
node.setRotationFromEuler(0, 0, 0);
|
||||
|
||||
const fruit = node.getComponent(Fruit)!;
|
||||
const index = Math.max(0, Math.min(level - 1, FRUITS.length - 1));
|
||||
const sf = FRUITS[index].sprite!;
|
||||
fruit.init(level, sf, this);
|
||||
|
||||
// 让它有一点随机水平初速度,手感更“弹”
|
||||
const rb = node.getComponent(RigidBody2D)!;
|
||||
const vx = Math.random() * 200 - 100;
|
||||
rb.linearVelocity = inheritVel || new Vec2(vx, 0);
|
||||
|
||||
// 轻微缩放弹出效果
|
||||
node.setScale(0.9, 0.9, 1);
|
||||
tween(node).to(0.1, { scale: v3(1, 1, 1) }).start();
|
||||
|
||||
// 可选音效:下落
|
||||
// this.playSfx(0.1);
|
||||
|
||||
return fruit;
|
||||
}
|
||||
|
||||
// 被 Fruit 调用:把两颗同级水果合成到 nextLevel
|
||||
spawnMerged(nextLevel: number, worldMid: Vec3, a: Fruit, b: Fruit) {
|
||||
if (this.isGameOver) return;
|
||||
|
||||
// 读两者速度,合成后给一个小的向上速度,避免“坍塌”
|
||||
const va = a.getComponent(RigidBody2D)!.linearVelocity;
|
||||
const vb = b.getComponent(RigidBody2D)!.linearVelocity;
|
||||
const inherit = new Vec2((va.x + vb.x) * 0.25, Math.max(120, (va.y + vb.y) * 0.25));
|
||||
|
||||
// 回收/删除旧的
|
||||
this.recycleFruit(a.node);
|
||||
this.recycleFruit(b.node);
|
||||
|
||||
// 生成新的
|
||||
const f = this.spawnFruit(clampLevel(nextLevel), worldMid, inherit);
|
||||
|
||||
// 计分:按目标等级给分
|
||||
this.updateScore(this.score + FRUITS[clampLevel(nextLevel) - 1].score);
|
||||
|
||||
// 合成特效:放大闪烁
|
||||
const n = f.node;
|
||||
const s0 = n.scale.clone();
|
||||
tween(n)
|
||||
.to(0.06, { scale: v3(s0.x * 1.15, s0.y * 1.15, 1) })
|
||||
.to(0.08, { scale: v3(1, 1, 1) })
|
||||
.start();
|
||||
|
||||
// 音效:合成
|
||||
this.playSfx(0.2);
|
||||
}
|
||||
|
||||
recycleFruit(node: Node) {
|
||||
const fr = node.getComponent(Fruit);
|
||||
if (fr) fr.removed = true;
|
||||
node.removeFromParent();
|
||||
this.pool.put(node);
|
||||
}
|
||||
|
||||
updateScore(v: number) {
|
||||
this.score = v;
|
||||
if (this.scoreLabel) this.scoreLabel.string = `${this.score}`;
|
||||
if (this.score > this.best) {
|
||||
this.best = this.score;
|
||||
sys.localStorage.setItem('best', String(this.best));
|
||||
}
|
||||
if (this.bestLabel) this.bestLabel.string = `${this.best}`;
|
||||
}
|
||||
|
||||
setDanger(on: boolean) {
|
||||
this.dangerOn = on;
|
||||
if (this.dropLine) this.dropLine.active = on;
|
||||
}
|
||||
|
||||
onGameOver() {
|
||||
if (this.isGameOver) return;
|
||||
this.isGameOver = true;
|
||||
this.canDrop = false;
|
||||
this.setDanger(true);
|
||||
|
||||
// 所有水果轻微变暗/缩放(可选)
|
||||
this.fruitRoot.children.forEach((n) =>
|
||||
tween(n).to(0.2, { scale: v3(0.95, 0.95, 1) }).start()
|
||||
);
|
||||
|
||||
// 失败音
|
||||
this.playSfx(0.35);
|
||||
}
|
||||
|
||||
restart() {
|
||||
this.reset();
|
||||
this.canDrop = true;
|
||||
}
|
||||
|
||||
playSfx(vol = 0.2) {
|
||||
if (!this.sfx || !this.sfx.clip) return;
|
||||
try {
|
||||
this.sfx.volume = vol;
|
||||
this.sfx.playOneShot(this.sfx.clip, vol);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user