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

Angular调用Phaser3场景类autoScratch方法无效果问题排查

Angular + Phaser3 刮卡游戏自动刮卡失效问题修复

核心问题分析

  1. 重复创建Scene实例:初始化时已在gameConfig中注册ScratchCard场景,后续又重复添加,导致Angular组件操作的是无效实例
  2. 定时器与Phaser渲染不同步:原生setInterval被Angular的Zone.js拦截,擦除操作未进入Phaser渲染管线
  3. 初始值缺失:lastPointerPosition初始为null,导致插值函数跳过执行

修复步骤及代码修改

1. 清理Angular组件中的重复Scene逻辑

// main.component.ts
import { Component, OnInit, AfterViewInit, OnDestroy } from '@angular/core';
import * as Phaser from 'phaser';
import { ScratchCard } from './../../../logic/game.logic';
import { ApiService } from '../../../services/api.service';
import { NgZone } from '@angular/core';

@Component({
  selector: 'app-game',
  templateUrl: './game.component.html',
  styleUrls: ['./game.component.scss']
})
export class GameComponent implements OnInit, AfterViewInit, OnDestroy {
  private scratchCard!: ScratchCard;
  public game!: Phaser.Game;

  constructor(public apiService: ApiService, private ngZone: NgZone) { }

  public readonly gameConfig: Phaser.Types.Core.GameConfig = {
    type: Phaser.AUTO,
    width: 500,
    height: 500,
    parent: 'scratch-card-game',
    scene: [ScratchCard]
  };

  ngOnInit() {
    // 在Angular Zone外创建游戏,避免Zone监控所有游戏异步操作
    this.ngZone.runOutsideAngular(() => {
      this.game = new Phaser.Game(this.gameConfig);
    });
  }

  ngAfterViewInit() {
    this.game.events.once('ready', () => {
      // 直接获取已初始化的场景实例,不要重复创建
      this.scratchCard = this.game.scene.getScene('scratchcard') as ScratchCard;
    });
  }

  public runCard() {
    if (this.scratchCard) {
      this.ngZone.runOutsideAngular(() => {
        this.scratchCard.autoScratch(250, 5, 5);
      });
    } else {
      console.log('场景未初始化');
    }
  }

  ngOnDestroy() {
    this.ngZone.runOutsideAngular(() => {
      this.game.destroy(true);
    });
  }
}

2. 替换原生定时器为Phaser时间管理器

// game.logic.ts
import * as Phaser from 'phaser';
import { ApiService } from '../services/api.service';

export class ScratchCard extends Phaser.Scene {
  background!: Phaser.GameObjects.Image;
  renderTexture!: Phaser.GameObjects.RenderTexture;
  canvasSize!: number;
  brush!: string;
  lastPointerPosition!: Phaser.Math.Vector2;

  constructor(public apiService: ApiService) {
    super({ key: 'scratchcard' });
  }

  preload() {
    this.load.image('surface', '../../assets/game1/images/wallpaper.png');
    this.load.image('background', '../../assets/game1/images/background.png');
  }

  create() {
    this.canvasSize = this.scale.width;

    // 生成圆形画笔
    const circleBrush = (scene: Phaser.Scene, radius: number, color: number): string => {
      const graphics = scene.make.graphics({ x: 0, y: 0, add: false });
      graphics.fillStyle(color);
      graphics.fillCircle(radius, radius, radius);
      graphics.generateTexture('scratch-brush', radius * 2, radius * 2);
      return 'scratch-brush';
    };

    // 创建背景与刮涂层
    this.background = this.add.image(this.canvasSize / 2, this.canvasSize / 2, 'background');
    this.background.setDisplaySize(this.canvasSize, this.canvasSize);

    this.renderTexture = this.add.renderTexture(this.canvasSize / 2, this.canvasSize / 2, this.canvasSize, this.canvasSize);
    this.renderTexture.setOrigin(0.5, 0.5);
    this.renderTexture.draw('surface', 0, 0);

    this.brush = circleBrush(this, this.canvasSize / 15, 0x000000);
    // 初始化指针位置,避免插值函数跳过
    this.lastPointerPosition = new Phaser.Math.Vector2(this.canvasSize / 2, 0);

    // 保留原有手动刮卡逻辑
    this.input.on('pointerdown', (pointer: Phaser.Input.Pointer) => {
      this.renderTexture.erase(this.brush, pointer.x - 50, pointer.y - 50);
      this.lastPointerPosition = new Phaser.Math.Vector2(pointer.x, pointer.y);
    });

    this.input.on('pointermove', (pointer: Phaser.Input.Pointer) => {
      if (pointer.isDown) {
        this.renderTexture.erase(this.brush, pointer.x - 20, pointer.y - 20);
        this.interpolatePoints(pointer, this.lastPointerPosition, () => {
          this.renderTexture.erase(this.brush, pointer.x - 50, pointer.y - 50);
        });
        this.lastPointerPosition = new Phaser.Math.Vector2(pointer.x, pointer.y);
      }
    });
  }

  // 重写自动刮卡方法,使用Phaser时间事件
  public autoScratch(amplitude: number, frequency: number, speed: number) {
    let time = 0;
    // 创建Phaser循环事件,与渲染循环同步
    const scratchLoop = this.time.addEvent({
      delay: 16, // 60FPS间隔
      loop: true,
      callback: () => {
        time += speed;
        // 计算波浪路径坐标
        const pointerX = this.canvasSize / 2 + Math.sin(Math.cos(time * frequency)) * amplitude;
        const pointerY = this.lastPointerPosition.y + 5; // 缓慢向下移动
        const currentPoint = new Phaser.Math.Vector2(pointerX, pointerY);

        // 插值填充路径,避免跳步
        this.interpolatePoints(currentPoint, this.lastPointerPosition, () => {
          this.renderTexture.erase(this.brush, pointerX - 50, pointerY - 50);
        });

        this.lastPointerPosition = currentPoint;
      }
    });

    // 定时停止刮卡
    this.time.delayedCall(25555, () => {
      scratchLoop.remove();
    });
  }

  // 完善插值逻辑,确保路径连续
  public interpolatePoints(currentPoint: Phaser.Math.Vector2, lastPoint: Phaser.Math.Vector2, callback: () => void) {
    const distance = Phaser.Math.Distance.Between(lastPoint.x, lastPoint.y, currentPoint.x, currentPoint.y);
    const steps = Math.floor(distance / 5); // 每5像素一步,保证路径连续
    if (steps <= 0) {
      callback();
      return;
    }
    const stepX = (currentPoint.x - lastPoint.x) / steps;
    const stepY = (currentPoint.y - lastPoint.y) / steps;
    for (let i = 1; i <= steps; i++) {
      const x = lastPoint.x + stepX * i;
      const y = lastPoint.y + stepY * i;
      this.renderTexture.erase(this.brush, x - 50, y - 50);
    }
  }
}

关键修复说明

  • 移除重复Scene创建逻辑,确保操作的是同一个有效场景实例
  • 用Phaser内置time.addEvent替代原生定时器,保证与游戏渲染循环同步
  • 初始化lastPointerPosition,避免插值函数因null值跳过执行
  • 将游戏创建和自动刮卡操作放在NgZone.runOutsideAngular中,避免Zone.js干扰Phaser异步流程
  • 完善插值函数,确保波浪路径连续无断点

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 22:29:56