如何在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); }); }
问题分析
- 异步加载未处理:Phaser的Loader是异步的,你在
update中调用loadSpineData()后立刻执行initializeAnimationSelect(),此时文件还未加载完成,cache.json.get(keyName)自然返回undefined。 - 本地文件路径错误:上传的文件是本地文件,
this.load.spine中直接使用文件名作为URL,Phaser会从服务器静态资源路径加载,无法访问本地上传的文件。 - 全局变量滥用:
fileLoaded、keyName等全局变量容易导致状态混乱,无法准确控制加载流程。 - 未清理旧对象:加载新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, }); } }
关键说明
- Blob URL转换:通过
URL.createObjectURL()将本地File对象转换为浏览器可访问的临时URL,解决Phaser无法直接加载本地文件的问题。 - Loader事件监听:使用
load.once('complete')确保每次加载完成后只执行一次后续逻辑,避免重复绑定导致的错误。 - 资源清理:加载完成后调用
URL.revokeObjectURL()释放Blob URL,销毁旧Spine对象,避免内存泄漏。 - 错误处理:增加文件检查逻辑,当缺少必要文件时给出明确提示,方便调试。
内容的提问来源于stack exchange,提问作者Josh
相关产品推荐
相关产品推荐

