Building Lottery Wheel Animations in CocosCreator

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;
        }
    }
}

Tags: CocosCreator game development animation Lottery Wheel TypeScript

Posted on Fri, 14 Aug 2026 16:44:46 +0000 by malec