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

Phaser 3+React+Vite中update函数CPU占用过高问题排查

问题描述

我正在使用Phaser 3搭配React和Vite进行开发,将玩家移动逻辑放在update函数中,怀疑由于Phaser以60fps运行,else块每秒被调用60次来停止玩家移动,导致CPU占用高到难以接受。

我的游戏场景代码:

import Phaser from 'phaser'
import { CustomCursorKeys } from '../types/customCursorKeys';

export default class GameScene extends Phaser.Scene {
  private cursors: CustomCursorKeys | null = null
  private player: Phaser.Physics.Arcade.Sprite | null = null;
  private speed: number = 200
  constructor() {
    super('GameScene')
  }

    private resizeCamera() {
        // Responsively set camera size to window size
        const width = window.innerWidth;
        const height = window.innerHeight;
        this.cameras.main.setSize(width, height);
        this.cameras.main.setScroll(0, 0);
    }

    preload() {
        // Load spritesheet and tileset images
    this.load.spritesheet('sokoban', 'sprites/sokoban_tilesheet.png', {
            frameWidth: 64
        })
        this.load.image('tiles', 'maps/test_tiles.png')
        this.load.tilemapTiledJSON('tilemap', 'maps/groundtrees.json')
  }

  create() {
        // Create world map using tileset image
    const map = this.make.tilemap({ key: 'tilemap' })
    const tileset = map.addTilesetImage('test_tiles', 'tiles')
    
        // Create map layers
    map.createLayer('ground', tileset)
    map.createLayer('trees', tileset)

        // Create player sprite and add physics
    this.player = this.physics.add.sprite(map.widthInPixels / 2, map.heightInPixels / 2, 'sokoban', 52);

        // Create player animations
        const createAnimation = (prefix: string, startFrame: number, endFrame: number) => {
            this.anims.create({
                key: `${prefix}-idle`,
                frames: [{ key: 'sokoban', frame: startFrame }],
                frameRate: 1
            })
            
            this.anims.create({
                key: `${prefix}-walk`,
                frames: this.anims.generateFrameNumbers('sokoban', { start: startFrame, end: endFrame }),
                frameRate: 10,
                repeat: -1,
            })
        }

        createAnimation('down', 52, 54)
        createAnimation('up', 55, 57)
        createAnimation('left', 81, 83)
        createAnimation('right', 78, 80)

        // Set up cursor keys for player movement
        this.cursors = {
      ...this.input.keyboard.createCursorKeys(),
      ...(this.input.keyboard.addKeys('W,S,A,D')),
    }
    
        // Set up camera to follow player and bound to map size
        this.cameras.main.setBounds(0, 0, map.widthInPixels, map.heightInPixels);
        this.resizeCamera();
        window.addEventListener('resize', () => {
            this.resizeCamera();
        });
        this.cameras.main.startFollow(this.player);
  }

    update() {
        if(!this.cursors || !this.cursors.A || !this.cursors.D || !this.cursors.W || !this.cursors.S || !this.player){
            return
        }
        /*
         Check if arrow keys or WASD keys is down,
         Set player's velocity and movement,
         Play walk animation
        */
        if (this.cursors.left.isDown || this.cursors.A.isDown) {
            this.player.setVelocity(-this.speed, 0)
            this.player.play('left-walk', true)
        } 
        else if (this.cursors.right.isDown || this.cursors.D.isDown) {
            this.player.setVelocity(this.speed, 0)
            this.player.play('right-walk', true)
        } 
        else if (this.cursors.up.isDown || this.cursors.W.isDown) {
            this.player.setVelocity(0, -this.speed)
            this.player.play('up-walk', true)
        } 
        else if (this.cursors.down.isDown || this.cursors.S.isDown) {
            this.player.setVelocity(0, this.speed)
            this.player.play('down-walk', true)
        } 
        /*
         If no arrow key is pressed stop the player's movement,
         Get the key of the current animation being played,
         If an animation is currently playing,
         Get the direction from the animation key,
         Play the idle animation for the current direction
        */
        else {
            this.player?.setVelocity(0, 0)
            const key = this.player?.anims.currentAnim?.key
            if (key) {
                let direction = key?.split('-')[0]
                this.player?.play(`${direction}-idle`, true)
            }      
        }
    }

  destroy() {
    window.removeEventListener('resize', this.resizeCamera.bind(this))
  }
}

Phaser配置文件:

import Phaser from 'phaser'
import GameScene from './scenes/GameScene'

const config = {
  type: Phaser.AUTO,
  parent: 'game-content',
  backgroundColor: '#123456',
  scale: {
    mode: Phaser.Scale.ScaleModes.RESIZE,
    width: window.innerWidth,
    height: window.innerHeight,
  },
  physics: {
    default: 'arcade',
    arcade: {
      gravity: { y: 0, x: 0 },
    },
  },
  scene: [GameScene],
}

export default new Phaser.Game(config)

奇怪的是:如果在else块后添加return语句,空闲CPU占用率约为60%;如果按原样运行else块,CPU占用率会降至20%。我看到所有Phaser教程都采用类似的停止玩家移动逻辑,请问是否存在操作错误导致CPU占用过高?

问题分析与解决

核心问题出在冗余操作的频繁触发,和是否加return的关联只是表象:

  1. 动画重复播放的性能浪费
    你在else块里每次都调用player.play(${direction}-idle, true),哪怕当前已经在播放该idle动画。Phaser每次调用play时,都会检查动画状态、重置帧计数器,频繁触发这些操作会产生额外CPU开销。
    优化方案:仅当当前动画不是目标idle动画时才切换:

    else {
        this.player?.setVelocity(0, 0)
        const key = this.player?.anims.currentAnim?.key
        if (key && !key.endsWith('-idle')) {
            let direction = key.split('-')[0]
            this.player?.play(`${direction}-idle`, true)
        }      
    }
    
  2. 不必要的Velocity重复设置
    当玩家已经静止时,每秒60次调用setVelocity(0,0)会强制Physics引擎重复更新玩家物理状态,属于完全冗余的操作。优化方案:仅当玩家当前速度不为0时才设置静止:

    else {
        if (this.player?.body?.velocity.x !== 0 || this.player?.body?.velocity.y !== 0) {
            this.player.setVelocity(0, 0)
        }
        // 动画判断逻辑同上
    }
    
  3. 关于加return后CPU更高的原因
    加return后CPU飙升,是因为update函数提前返回后,React与Phaser的渲染同步逻辑可能出现无意义的频繁触发,但这不是核心问题。解决前两点的冗余操作后,无论是否加return,CPU占用都会回归合理水平。

  4. 额外优化建议

    • 在create阶段确保cursors初始化完成,避免update函数中每次都做一堆非空判断;
    • 用键盘事件监听替代update轮询:通过input.keyboard.on('keydown')和keyup事件控制移动与动画,减少update中的重复判断,进一步降低CPU消耗。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 03:54:54