Angular调用Phaser3场景类autoScratch方法无效果问题排查
Angular + Phaser3 刮卡游戏自动刮卡失效问题修复
核心问题分析
- 重复创建Scene实例:初始化时已在
gameConfig中注册ScratchCard场景,后续又重复添加,导致Angular组件操作的是无效实例 - 定时器与Phaser渲染不同步:原生
setInterval被Angular的Zone.js拦截,擦除操作未进入Phaser渲染管线 - 初始值缺失:
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
相关产品推荐
相关产品推荐

