Excellent, that solved my texture issues with previous attempts thanks!
Complete code for test setup is now below and i can just replace as needed with single phaser texture/image.
Since you are so responsive, maybe you could also hint to if its possible for me to create my SpineGameObject without having an Atlas on first creation? So that i could just provide or apply individual attachments for each of the slots defined in the Skeleton without loading initial atlas?
import { SERVER_URL } from "@/data/constants";
import { TINY_GAME } from "@/data/games";
import { Game, Types } from "phaser";
import React, { useEffect, useRef } from "react";
import { GLTexture, RegionAttachment, SpineGameObject, SpinePlugin, TextureRegion } from "@esotericsoftware/spine-phaser";
type Props = {
children?: React.ReactNode;
};
export function TinyGameScene({ children }: Props) {
const loaded = useRef(false);
const [currentGun, setCurrentGun] = React.useState(1);
const gameRef = useRef<Game | null>(null);
const sceneRef = useRef<Phaser.Scene | null>(null);
const spineObjectRef = useRef<SpineGameObject | null>(null);
const swapGun = () => {
const spineGameObject = spineObjectRef.current;
const scene = sceneRef.current;
const spinePlugin = scene?.spine;
if (!spineGameObject || !scene || !spinePlugin || !spinePlugin.webGLRenderer?.context) {
console.error("Spine object, scene or plugin is not available or misconfigured");
return;
}
const slot = spineGameObject.skeleton.findSlot("gun");
if (!slot || !slot.attachment) {
console.error("Slot 'gun' not found");
return;
}
const newExternalGun = `external-gun${currentGun}`;
setCurrentGun((prev) => (prev === 1 ? 0 : 1));
const phaserTexture = scene.textures.get(newExternalGun).getSourceImage();
const spineTexture = new GLTexture(spinePlugin.webGLRenderer?.context, phaserTexture as HTMLImageElement | ImageBitmap);
const region = new TextureRegion();
region.u2 = region.v2 = 1;
region.width = region.originalWidth = phaserTexture.width;
region.height = region.originalHeight = phaserTexture.height;
region.texture = spineTexture;
const newAttachment: RegionAttachment = slot.attachment.copy() as RegionAttachment;
newAttachment.region = region;
newAttachment.updateRegion();
slot.setAttachment(newAttachment);
};
const addCharacter = () => {
const sceneObject = sceneRef.current;
if (!sceneObject) {
console.error("Scene is not available");
return;
}
const spineObject = sceneObject.make.spine({
x: 200,
y: 500,
scale: 0.5,
dataKey: "spineCharacter",
atlasKey: "spineAtlas",
});
spineObject.setInteractive();
spineObjectRef.current = spineObject;
spineObject.on("pointerdown", (pointer: SpineGameObject) => {
spineObject.animationState.setAnimation(0, "jump", false);
});
spineObject.animationState.addListener({
complete: (entry) => {
if (entry.animation?.name === "jump") {
spineObject.animationState.setAnimation(0, "walk", true);
}
},
});
spineObject.animationState.setAnimation(0, "walk", true);
sceneObject.add.existing(spineObject);
};
useEffect(() => {
if (loaded.current) {
return;
}
loaded.current = true;
function preload(this: Phaser.Scene) {
if (this.load.spineJson) {
this.load.spineJson("spineCharacter", `${SERVER_URL}/tiny/character/spineboy-ess.json`);
this.load.spineAtlas("spineAtlas", `${SERVER_URL}/tiny/character/spineboy-split.atlas`, false);
this.load.image("external-gun0", `${SERVER_URL}/tiny/character/external-gun0.png`);
this.load.image("external-gun1", `${SERVER_URL}/tiny/character/external-gun1.png`);
} else {
console.error("Spine plugin not available");
}
}
function create(this: Phaser.Scene) {
sceneRef.current = this;
addCharacter();
}
function update(this: Phaser.Scene) {}
const config: Types.Core.GameConfig = {
type: Phaser.AUTO,
parent: "gamecanvas",
backgroundColor: TINY_GAME.colours.bright,
width: "100%",
antialias: true,
height: "100%",
scene: {
preload,
create,
update,
},
plugins: {
scene: [
{
key: "SpinePlugin",
plugin: SpinePlugin,
mapping: "spine",
},
],
},
};
gameRef.current = new Game(config);
return () => {
if (gameRef.current) {
gameRef.current.destroy(true);
gameRef.current = null;
}
};
}, []);
return (
<div className="relative w-full h-full" id="gamecanvas">
<div className="absolute flex flex-row ">
<div
className="px-5 mt-10 cursor-pointer"
onClick={() => {
addCharacter();
}}
>
Add character
</div>
<div
className="px-5 mt-10 cursor-pointer"
onClick={() => {
swapGun();
}}
>
Test swap
</div>
</div>
</div>
);
}