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

如何在Phaser 3中上传Spine文件夹并正确加载Spine动画对象?

问题:Phaser 3 上传Spine文件夹后无法加载新动画对象

我正在做一个Phaser 3项目来学习Spine动画的加载与显示,需求是支持上传包含Spine文件(.json和.atlas等)的文件夹,并加载对应的动画对象。目前初始加载的Spine动画正常运行,但尝试3天仍无法实现上传文件后显示新Spine对象,最近一次尝试中animationsData返回undefined。

我使用带有webkitdirectory属性的HTML input标签上传文件夹,代码如下:

<div id="dropZone">
  <input
    type="file"
    id="fileInputControl"
    webkitdirectory="true"
    multiple
  />
</div>

通过控制台日志确认文件已成功上传,但问题出在Spine对象的加载与添加逻辑上。以下是我的Phaser.Scene子类代码:

preload() {
  this.load.setPath(spinePath);
  this.loadSpineData();
}

create() {
  fileInputControl.addEventListener("change", () => {
    fileLoaded = true;
  });

  this.initializeAnimationSelect();
  this.setupSpineObject();
}

update() {
  if (fileLoaded) {
    selectedFiles = fileInputControl.files;
    keyName = selectedFiles?.[0]?.name.split(".")[0] || keyName;
    this.loadSpineData();
    this.initializeAnimationSelect();

    fileLoaded = false;
  }
}

changeAnimation() {
  return this.animationSelect.value;
}

initializeAnimationSelect() {
  let animationsData = this.cache.json.get(keyName)?.animations;
  console.log(animationsData);

  this.animationSelect = document.getElementById("animationSelect");
  this.animationSelect.innerHTML = "";

  for (let animationName in animationsData) {
    if (animationsData.hasOwnProperty(animationName)) {
      let option = document.createElement("option");
      option.value = animationName;
      option.text = animationName;
      this.animationSelect.add(option);
    }
  }
}

loadSpineData() {
  let atlasName = keyName + ".atlas";
  let jsonName = keyName + ".json";

  console.log("JSON name: " +jsonName);
  console.log("Atlas name: " + atlasName);

  this.load.spine({
    key: keyName,
    jsonURL: jsonName,
    atlasURL: [atlasName],
    preMultipliedAlpha: false,
  });

  this.selectedAnimation = this.animationSelect?.options[0]?.value || "";
}

setupSpineObject() {
  let spineObject = this.add.spine(
    400,
    300,
    keyName,
    [this.selectedAnimation],
    true
  );

  this.animationSelect.addEventListener("change", () => {
    this.selectedAnimation = this.changeAnimation();
    spineObject.setAnimation(0, this.selectedAnimation, true);
  });
}

问题分析

  1. 异步加载未处理:Phaser的Loader是异步的,你在update中调用loadSpineData()后立刻执行initializeAnimationSelect(),此时文件还未加载完成,cache.json.get(keyName)自然返回undefined。
  2. 本地文件路径错误:上传的文件是本地文件,this.load.spine中直接使用文件名作为URL,Phaser会从服务器静态资源路径加载,无法访问本地上传的文件。
  3. 全局变量滥用:fileLoaded、keyName等全局变量容易导致状态混乱,无法准确控制加载流程。
  4. 未清理旧对象:加载新Spine对象时未销毁旧对象,可能导致显示冲突和内存泄漏。

解决方案

核心修改点

  • 将本地上传文件转换为Blob URL,让Phaser能够加载这些文件。
  • 监听Loader的complete事件,确保在文件加载完成后再初始化动画选择器和创建Spine对象。
  • 封装状态变量到Scene类内部,避免全局变量干扰。
  • 加载新对象前销毁旧对象,清理资源。

修改后的完整代码

class SpineScene extends Phaser.Scene {
  constructor() {
    super('SpineScene');
    // 封装状态变量到类内部
    this.fileLoaded = false;
    this.selectedFiles = null;
    this.keyName = 'default-spine-key'; // 初始动画key
    this.spineObject = null;
    this.animationSelect = null;
  }

  preload() {
    this.load.setPath(spinePath);
    this.loadSpineData();
    // 初始加载完成后初始化UI和动画
    this.load.on('complete', () => {
      this.initializeAnimationSelect();
      this.setupSpineObject();
    });
    this.load.start();
  }

  create() {
    const fileInputControl = document.getElementById('fileInputControl');
    fileInputControl.addEventListener('change', (e) => {
      this.selectedFiles = e.target.files;
      this.fileLoaded = true;
    });

    // 提前获取动画选择器元素
    this.animationSelect = document.getElementById('animationSelect');
    this.animationSelect.addEventListener('change', () => {
      if (this.spineObject) {
        const selectedAnim = this.animationSelect.value;
        this.spineObject.setAnimation(0, selectedAnim, true);
      }
    });
  }

  update() {
    if (this.fileLoaded) {
      this.fileLoaded = false;
      // 从上传文件中筛选出需要的.json和.atlas文件
      const jsonFile = Array.from(this.selectedFiles).find(f => f.name.endsWith('.json'));
      const atlasFile = Array.from(this.selectedFiles).find(f => f.name.endsWith('.atlas'));

      if (!jsonFile || !atlasFile) {
        console.error('上传的文件夹缺少必要的Spine文件(.json或.atlas)');
        return;
      }

      this.keyName = jsonFile.name.split('.')[0];
      // 生成Blob URL供Phaser加载
      const jsonUrl = URL.createObjectURL(jsonFile);
      const atlasUrl = URL.createObjectURL(atlasFile);

      // 重置Loader队列,加载新文件
      this.load.reset();
      this.load.spine({
        key: this.keyName,
        jsonURL: jsonUrl,
        atlasURL: atlasUrl,
        preMultipliedAlpha: false,
      });

      // 监听本次加载完成事件(使用once避免重复绑定)
      this.load.once('complete', () => {
        // 释放Blob URL,避免内存泄漏
        URL.revokeObjectURL(jsonUrl);
        URL.revokeObjectURL(atlasUrl);
        
        // 销毁旧的Spine对象
        if (this.spineObject) {
          this.spineObject.destroy();
        }

        // 初始化新的动画选择器和Spine对象
        this.initializeAnimationSelect();
        this.setupSpineObject();
      });

      this.load.start();
    }
  }

  initializeAnimationSelect() {
    const animationsData = this.cache.json.get(this.keyName)?.animations;
    console.log(animationsData);

    this.animationSelect.innerHTML = '';
    if (!animationsData) {
      const emptyOption = document.createElement('option');
      emptyOption.value = '';
      emptyOption.text = '未获取到动画数据';
      this.animationSelect.add(emptyOption);
      return;
    }

    for (const animationName in animationsData) {
      if (animationsData.hasOwnProperty(animationName)) {
        const option = document.createElement('option');
        option.value = animationName;
        option.text = animationName;
        this.animationSelect.add(option);
      }
    }
  }

  setupSpineObject() {
    const selectedAnimation = this.animationSelect?.options[0]?.value || '';
    this.spineObject = this.add.spine(400, 300, this.keyName, selectedAnimation, true);
  }

  loadSpineData() {
    const atlasName = this.keyName + '.atlas';
    const jsonName = this.keyName + '.json';
    this.load.spine({
      key: this.keyName,
      jsonURL: jsonName,
      atlasURL: atlasName,
      preMultipliedAlpha: false,
    });
  }
}

关键说明

  1. Blob URL转换:通过URL.createObjectURL()将本地File对象转换为浏览器可访问的临时URL,解决Phaser无法直接加载本地文件的问题。
  2. Loader事件监听:使用load.once('complete')确保每次加载完成后只执行一次后续逻辑,避免重复绑定导致的错误。
  3. 资源清理:加载完成后调用URL.revokeObjectURL()释放Blob URL,销毁旧Spine对象,避免内存泄漏。
  4. 错误处理:增加文件检查逻辑,当缺少必要文件时给出明确提示,方便调试。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 11:59:51