Animation Approaches
Spinning wheel mechanics can be driven by different animation models. A basic approach utilizes cc.tween with easing functions, which is straightforward but lacks dynamic pacing control. A more advanced method employs time interpolation combined with cc.tween.by, allowing for precise acceleration and deceleration phases while highlighting the active prize sector during rotation.
Dynamic Interpolation Implementation
The following implementation uses the interpolation-driven approach, utilizing async/await scheduling to sequentially process each sector step and update the visual highlight state.
SpinWheelManager Component
import WheelRotationEngine from "./WheelRotationEngine";
const { ccclass, property } = cc._decorator;
@ccclass
export default class SpinWheelManager extends cc.Component {
@property(cc.Node)
public prizeTemplate: cc.Node = null;
@property(cc.Node)
public prizeContainer: cc.Node = null;
@property(cc.Node)
public arrowIndicator: cc.Node = null;
@property(WheelRotationEngine)
public rotationEngine: WheelRotationEngine = null;
public sectorCount: number = 12;
public sectorAngle: number = 360 / this.sectorCount;
private sectorRadian: number = Math.PI * 2 / this.sectorCount;
private layoutRadius: number = 240;
private currentSector: number = 0;
private isSpinning: boolean = false;
start() {
this.layoutPrizes();
}
private layoutPrizes() {
for (let i = 0; i < this.sectorCount; i++) {
const radian = i * this.sectorRadian;
const posX = this.layoutRadius * Math.cos(radian);
const posY = this.layoutRadius * Math.sin(radian);
const item = cc.instantiate(this.prizeTemplate);
item.setPosition(posY, posX); // Coordinate system offset
item.parent = this.prizeContainer;
item.active = true;
item.getChildByName('label').getComponent(cc.Label).string = i.toString();
}
}
public onSpinButtonClicked() {
if (this.isSpinning) return;
this.isSpinning = true;
const target = Math.floor(Math.random() * this.sectorCount);
this.rotationEngine.initiateSpin(this, this.currentSector, target, 5, 3);
this.rotationEngine.onSpinFinished = () => {
this.currentSector = target;
this.isSpinning = false;
};
}
}WheelRotationEngine Component
import SpinWheelManager from "./SpinWheelManager";
const { ccclass, property } = cc._decorator;
@ccclass
export default class WheelRotationEngine extends cc.Component {
public onSpinFinished: () => void;
private finalRoundDuration: number = 2;
private startIndex: number;
private endIndex: number;
private totalDuration: number;
private loopCount: number;
private managerRef: SpinWheelManager;
private rotationOffset: number;
public initiateSpin(manager: SpinWheelManager, start: number, target: number, duration: number, loops: number) {
this.managerRef = manager;
this.rotationOffset = manager.sectorAngle * -1;
this.startIndex = start;
this.endIndex = target + manager.sectorCount;
this.totalDuration = duration > 3 ? duration : 3;
this.loopCount = loops;
this.runAnimation();
}
private async runAnimation() {
let t1 = 0, t2 = 0;
const totalSectors = this.loopCount * this.managerRef.sectorCount;
const avgTimePhase1 = (this.totalDuration - this.finalRoundDuration) / totalSectors;
for (let i = this.startIndex; i < totalSectors; i++) {
t1 = cc.misc.lerp(0, avgTimePhase1 * 2, (1 / totalSectors) * (i + 1));
cc.tween(this.managerRef.arrowIndicator).by(t1, { angle: this.rotationOffset }).start();
this.toggleHighlight(i);
await this.pauseExecution(t1);
}
const avgTimePhase2 = this.finalRoundDuration / this.endIndex;
for (let j = 0; j < this.endIndex; j++) {
t2 = cc.misc.lerp(t1, avgTimePhase2 * 2, (1 / this.endIndex) * (j + 1));
cc.tween(this.managerRef.arrowIndicator).by(t2, { angle: this.rotationOffset }).start();
this.toggleHighlight(j);
await this.pauseExecution(t2);
}
this.toggleHighlight(this.endIndex);
await this.pauseExecution(1);
if (this.onSpinFinished) {
this.onSpinFinished();
}
}
private pauseExecution(duration: number): Promise<void> {
return new Promise(resolve => this.scheduleOnce(resolve, duration));
}
private toggleHighlight(index: number) {
const modIndex = index % this.managerRef.sectorCount;
const prevIndex = modIndex - 1 < 0 ? this.managerRef.sectorCount - 1 : modIndex - 1;
const prevLabel = this.managerRef.prizeContainer.children[prevIndex]?.getChildByName("label");
if (prevLabel) {
prevLabel.color = cc.Color.WHITE;
}
const currentLabel = this.managerRef.prizeContainer.children[modIndex]?.getChildByName("label");
if (currentLabel) {
currentLabel.color = cc.Color.RED;
}
}
}