You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Phaser 3补间动画无法启动求助:确认后轮盘不转动

Phaser 3 模态框确认后轮盘补间动画不启动问题排查

问题描述

我有一个Phaser 3项目,游戏场景内的按钮会打开一个HTML实现的确认模态框。当用户点击确认按钮时,应调用游戏场景中的spinWheel()函数使轮盘转动,但该函数虽已执行,补间动画却未启动。

相关代码

模态框代码

<div class="modal fade" id="Modal" onclick="runGameScene()" tabindex="-1" aria-labelledby="ModalLabel" aria-hidden="true">
    <div class="modal-dialog">
      <div class="modal-content">
        <div class="modal-body">
            <div style="text-align:center;">
                Do you want to spin the wheel?
            </div>
            <div class="modal-footer">
                <div style="margin:auto">
                    <button type="button" class="btn btn-primary blue-btn" onclick="startWheelSpin()">yes</button>
                    <button type="button" class="btn btn-secondary red-btn" onclick="runGameScene()" data-dismiss="modal"> no </button>
                </div>
            </div>
        </div>       
      </div>
    </div>
  </div> 

game.js代码

var game;

var gameOptions = {
slices: 6,
sliceColors: [0x16A085, 0x2989B9,  0x8E44AD, 0xF39C12, 0xD35400, 0xC0392B],
slicePrizes: ["RED", "GREEN", "BLUE", "WHITE", "CYAN", "YELLOW", "GREY", "PURPLE"],
rotationTime: 3000,
wheelRadius: 100,
spinCounter: 5
}

const ratio = Math.max(window.innerWidth / window.innerHeight, window.innerHeight / window.innerWidth)
const DEFAULT_HEIGHT = 2400 // any height you want
const DEFAULT_WIDTH = ratio * DEFAULT_HEIGHT
var gameConfig = {
   type: Phaser.AUTO,
   width: DEFAULT_WIDTH,
   height: DEFAULT_HEIGHT,
   scale: {
      mode: Phaser.Scale.FIT,
      autoCenter: Phaser.Scale.CENTER_BOTH,
      width: DEFAULT_WIDTH,
      height: DEFAULT_HEIGHT
   },
   backgroundColor: 0xd7eef7,
   scene: [playGame]
 };
 window.onload = function() {
   // game constructor
   game = new Phaser.Game(gameConfig);
 }

游戏场景代码

class playGame extends Phaser.Scene {
constructor(){
    super("PlayGame");
}
create(){
    var sliceDegrees = 360 / gameOptions.slices;
    // making a graphic object without adding it to the game
    var graphics = this.make.graphics({x: 0,y: 0,add: false});
    for(var i = 0; i < gameOptions.slices; i++){
        graphics.fillStyle(gameOptions.sliceColors[i], 1);
        graphics.slice(gameOptions.wheelRadius, gameOptions.wheelRadius, gameOptions.wheelRadius, Phaser.Math.DegToRad(270 + i * sliceDegrees), Phaser.Math.DegToRad(270 + (i  + 1) * sliceDegrees), false);
        graphics.fillPath();
    }
    graphics.generateTexture("wheel", gameOptions.wheelRadius * 2, gameOptions.wheelRadius * 2);

    // creating a sprite with wheel image as if it was a preloaded image
    this.wheel = this.add.sprite(game.config.width / 2, game.config.height / 2, "wheel");
    // spin circle
    this.spinBtn = new RoundedButton(this, game.config.width / 2, game.config.height / 2, 100, "spin", () => {this.confirmSpin()}); // a custom button with confirmSpin() as callback
    this.add.existing(this.spinBtn);
    // the game has just started = we can spin the wheel
    this.canSpin = true;
}

// function to spin the wheel
spinWheel() {
    if(this.canSpin){
        var rounds = Phaser.Math.Between(2, 4);
        var degrees = Phaser.Math.Between(0, 360);
        var prize = gameOptions.slices - 1 - Math.floor(degrees / (360 / gameOptions.slices));

        // now the wheel cannot spin because it's already spinning
        this.canSpin = false;

        this.tweens.add({
            targets: [this.wheel],
            angle: 360 * rounds + degrees,
            duration: gameOptions.rotationTime,
            ease: "Cubic.easeOut",
            callbackScope: this,
            onStart: function() {
                console.log("tween starts.");
            },
            onComplete: function(tween){
                console.log("prize:", prize);
                // player can spin again
                this.canSpin = true;
            }
        });
    }
}
confirmSpin() {
    this.scene.pause('PlayGame');
    $('#spentModal').modal('show');      // open confirm spin modal
}

调用spinWheel的代码

runGameScene = function() {
   game.scene.run('PlayGame');
}
startWheelSpin = function(){
   $('#Modal').modal('hide');
   runGameScene();
   const scene = game.scene.keys.PlayGame;
   scene.spinWheel();
}

问题根源

  1. 场景暂停导致tween系统休眠:调用this.scene.pause('PlayGame')暂停场景后,Phaser的补间(tween)系统会随场景一起暂停,无法处理新的补间请求。
  2. 场景恢复的异步性:game.scene.run()恢复暂停场景时,场景的更新循环和tween系统需要一帧时间重新激活,直接在其后调用spinWheel()会导致补间被添加到未完全恢复的系统中,无法执行。
  3. 模态框误触发场景恢复:模态框根元素绑定的onclick="runGameScene()"会导致点击模态框内任意区域都触发场景恢复,造成状态混乱。
  4. 模态框ID不匹配:confirmSpin中打开的是#spentModal,但实际模态框ID是Modal,可能导致模态框显示异常。

修复方案

1. 移除模态框根元素的冗余点击事件

删掉模态框最外层的onclick="runGameScene()",避免误触发:

<div class="modal fade" id="Modal" tabindex="-1" aria-labelledby="ModalLabel" aria-hidden="true">
    <!-- 内部内容保持不变 -->
</div>

2. 确保场景完全恢复后执行补间

修改startWheelSpin函数,利用Phaser的延迟调用确保tween系统恢复后再启动动画:

startWheelSpin = function(){
    $('#Modal').modal('hide');
    const scene = game.scene.keys.PlayGame;
    
    if (scene.scene.isPaused()) {
        scene.scene.resume();
        // 延迟到下一帧执行,确保场景tween系统已激活
        scene.time.delayedCall(0, () => {
            scene.spinWheel();
        }, [], scene);
    } else {
        scene.spinWheel();
    }
}

3. 修正场景恢复函数的逻辑

将runGameScene改为针对暂停场景的恢复操作,避免重复启动:

runGameScene = function() {
    const scene = game.scene.keys.PlayGame;
    if (scene.scene.isPaused()) {
        scene.scene.resume();
    }
}

4. 统一模态框ID

修正confirmSpin中的模态框ID,确保正确显示:

confirmSpin() {
    this.scene.pause('PlayGame');
    $('#Modal').modal('show'); // 匹配实际模态框ID
}

原理说明

Phaser场景暂停时,所有的tween、定时器和更新循环都会停止。恢复场景后,这些系统需要一帧时间重新初始化。使用time.delayedCall(0)可以让代码在下一帧执行,此时tween系统已完全激活,补间能被正常加入到更新队列中运行。

内容的提问来源于stack exchange,提问作者Faezeh Keshmiri

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 05:55:22