Adding first version of SuperLoader
This class is the Phaser loader as it should be: it comes with promises, can be easily run out of the "preload" phase and won't crash if the scene is destroyed before textures are loaded.
This commit is contained in:
parent
ea3088a876
commit
c529658ef4
@ -90,7 +90,7 @@ export abstract class Character extends Container implements OutlineableInterfac
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
return lazyLoadPlayerCharacterTextures(scene.load, [
|
return lazyLoadPlayerCharacterTextures(scene.superLoad, [
|
||||||
{
|
{
|
||||||
id: "color_22",
|
id: "color_22",
|
||||||
img: "resources/customisation/character_color/character_color21.png",
|
img: "resources/customisation/character_color/character_color21.png",
|
||||||
|
@ -2,6 +2,8 @@ import LoaderPlugin = Phaser.Loader.LoaderPlugin;
|
|||||||
import type { CharacterTexture } from "../../Connexion/LocalUser";
|
import type { CharacterTexture } from "../../Connexion/LocalUser";
|
||||||
import { BodyResourceDescriptionInterface, mapLayerToLevel, PlayerTextures, PlayerTexturesKey } from "./PlayerTextures";
|
import { BodyResourceDescriptionInterface, mapLayerToLevel, PlayerTextures, PlayerTexturesKey } from "./PlayerTextures";
|
||||||
import CancelablePromise from "cancelable-promise";
|
import CancelablePromise from "cancelable-promise";
|
||||||
|
import {SuperLoaderPlugin} from "../Services/SuperLoaderPlugin";
|
||||||
|
import Texture = Phaser.Textures.Texture;
|
||||||
|
|
||||||
export interface FrameConfig {
|
export interface FrameConfig {
|
||||||
frameWidth: number;
|
frameWidth: number;
|
||||||
@ -35,87 +37,31 @@ export const loadAllDefaultModels = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const loadWokaTexture = (
|
export const loadWokaTexture = (
|
||||||
loaderPlugin: LoaderPlugin,
|
superLoaderPlugin: SuperLoaderPlugin,
|
||||||
texture: BodyResourceDescriptionInterface
|
texture: BodyResourceDescriptionInterface
|
||||||
): CancelablePromise<BodyResourceDescriptionInterface> => {
|
): CancelablePromise<Texture> => {
|
||||||
return createLoadingPromise(loaderPlugin, texture, {
|
return superLoaderPlugin.spritesheet(texture.id, texture.img, {
|
||||||
frameWidth: 32,
|
frameWidth: 32,
|
||||||
frameHeight: 32,
|
frameHeight: 32,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const lazyLoadPlayerCharacterTextures = (
|
export const lazyLoadPlayerCharacterTextures = (
|
||||||
loadPlugin: LoaderPlugin,
|
superLoaderPlugin: SuperLoaderPlugin,
|
||||||
textures: BodyResourceDescriptionInterface[]
|
textures: BodyResourceDescriptionInterface[]
|
||||||
): CancelablePromise<string[]> => {
|
): CancelablePromise<string[]> => {
|
||||||
const promisesList: CancelablePromise<unknown>[] = [];
|
const promisesList: CancelablePromise<Texture>[] = [];
|
||||||
textures.forEach((texture) => {
|
for (const texture of textures) {
|
||||||
try {
|
promisesList.push(superLoaderPlugin.spritesheet(texture.id, texture.img, {
|
||||||
//TODO refactor
|
frameWidth: 32,
|
||||||
if (!loadPlugin.textureManager.exists(texture.id)) {
|
frameHeight: 32,
|
||||||
promisesList.push(
|
}));
|
||||||
createLoadingPromise(loadPlugin, texture, {
|
};
|
||||||
frameWidth: 32,
|
const returnPromise: CancelablePromise<Texture[]> = CancelablePromise.all(promisesList);
|
||||||
frameHeight: 32,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let returnPromise: CancelablePromise<Array<string | BodyResourceDescriptionInterface>>;
|
|
||||||
if (promisesList.length > 0) {
|
|
||||||
loadPlugin.start();
|
|
||||||
returnPromise = CancelablePromise.all(promisesList).then(() => textures);
|
|
||||||
} else {
|
|
||||||
returnPromise = CancelablePromise.resolve(textures);
|
|
||||||
}
|
|
||||||
|
|
||||||
//If the loading fail, we render the default model instead.
|
return returnPromise.then(() =>
|
||||||
return returnPromise.then((keys) =>
|
textures.map((key) => {
|
||||||
keys.map((key) => {
|
return key.id;
|
||||||
return typeof key !== "string" ? key.id : key;
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createLoadingPromise = (
|
|
||||||
loadPlugin: LoaderPlugin,
|
|
||||||
playerResourceDescriptor: BodyResourceDescriptionInterface,
|
|
||||||
frameConfig: FrameConfig
|
|
||||||
) => {
|
|
||||||
return new CancelablePromise<BodyResourceDescriptionInterface>((res, rej, cancel) => {
|
|
||||||
if (loadPlugin.textureManager.exists(playerResourceDescriptor.id)) {
|
|
||||||
return res(playerResourceDescriptor);
|
|
||||||
}
|
|
||||||
|
|
||||||
cancel(() => {
|
|
||||||
loadPlugin.off("loaderror");
|
|
||||||
loadPlugin.off("filecomplete-spritesheet-" + playerResourceDescriptor.id);
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
|
|
||||||
// If for some reason, the "img" is empty, let's reject the promise.
|
|
||||||
if (!playerResourceDescriptor.img) {
|
|
||||||
console.warn("Tried to load an empty texture for a Woka");
|
|
||||||
rej(playerResourceDescriptor);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
loadPlugin.spritesheet(playerResourceDescriptor.id, playerResourceDescriptor.img, frameConfig);
|
|
||||||
const errorCallback = (file: { src: string }) => {
|
|
||||||
if (file.src !== playerResourceDescriptor.img) return;
|
|
||||||
console.error("failed loading player resource: ", playerResourceDescriptor);
|
|
||||||
rej(playerResourceDescriptor);
|
|
||||||
loadPlugin.off("filecomplete-spritesheet-" + playerResourceDescriptor.id, successCallback);
|
|
||||||
loadPlugin.off("loaderror", errorCallback);
|
|
||||||
};
|
|
||||||
const successCallback = () => {
|
|
||||||
loadPlugin.off("loaderror", errorCallback);
|
|
||||||
res(playerResourceDescriptor);
|
|
||||||
};
|
|
||||||
|
|
||||||
loadPlugin.once("filecomplete-spritesheet-" + playerResourceDescriptor.id, successCallback);
|
|
||||||
loadPlugin.on("loaderror", errorCallback);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
@ -100,6 +100,7 @@ import type { CoWebsite } from "../../WebRtc/CoWebsite/CoWesbite";
|
|||||||
import type { VideoPeer } from "../../WebRtc/VideoPeer";
|
import type { VideoPeer } from "../../WebRtc/VideoPeer";
|
||||||
import CancelablePromise from "cancelable-promise";
|
import CancelablePromise from "cancelable-promise";
|
||||||
import { Deferred } from "ts-deferred";
|
import { Deferred } from "ts-deferred";
|
||||||
|
import {SuperLoaderPlugin} from "../Services/SuperLoaderPlugin";
|
||||||
export interface GameSceneInitInterface {
|
export interface GameSceneInitInterface {
|
||||||
initPosition: PointInterface | null;
|
initPosition: PointInterface | null;
|
||||||
reconnecting: boolean;
|
reconnecting: boolean;
|
||||||
@ -217,6 +218,7 @@ export class GameScene extends DirtyScene {
|
|||||||
private loader: Loader;
|
private loader: Loader;
|
||||||
private lastCameraEvent: WasCameraUpdatedEvent | undefined;
|
private lastCameraEvent: WasCameraUpdatedEvent | undefined;
|
||||||
private firstCameraUpdateSent: boolean = false;
|
private firstCameraUpdateSent: boolean = false;
|
||||||
|
public readonly superLoad: SuperLoaderPlugin;
|
||||||
|
|
||||||
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
|
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
|
||||||
super({
|
super({
|
||||||
@ -232,6 +234,7 @@ export class GameScene extends DirtyScene {
|
|||||||
this.createPromiseDeferred = new Deferred<void>();
|
this.createPromiseDeferred = new Deferred<void>();
|
||||||
this.connectionAnswerPromiseDeferred = new Deferred<RoomJoinedMessageInterface>();
|
this.connectionAnswerPromiseDeferred = new Deferred<RoomJoinedMessageInterface>();
|
||||||
this.loader = new Loader(this);
|
this.loader = new Loader(this);
|
||||||
|
this.superLoad = new SuperLoaderPlugin(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
//hook preload scene
|
//hook preload scene
|
||||||
@ -746,7 +749,7 @@ export class GameScene extends DirtyScene {
|
|||||||
.then((onConnect: OnConnectInterface) => {
|
.then((onConnect: OnConnectInterface) => {
|
||||||
this.connection = onConnect.connection;
|
this.connection = onConnect.connection;
|
||||||
|
|
||||||
lazyLoadPlayerCharacterTextures(this.load, onConnect.room.characterLayers)
|
lazyLoadPlayerCharacterTextures(this.superLoad, onConnect.room.characterLayers)
|
||||||
.then((layers) => {
|
.then((layers) => {
|
||||||
this.currentPlayerTexturesResolve(layers);
|
this.currentPlayerTexturesResolve(layers);
|
||||||
})
|
})
|
||||||
|
@ -3,39 +3,51 @@ import { BodyResourceDescriptionInterface, PlayerTexturesKey } from "../Entity/P
|
|||||||
import { loadWokaTexture } from "../Entity/PlayerTexturesLoadingManager";
|
import { loadWokaTexture } from "../Entity/PlayerTexturesLoadingManager";
|
||||||
import type CancelablePromise from "cancelable-promise";
|
import type CancelablePromise from "cancelable-promise";
|
||||||
import { PlayerTextures } from "../Entity/PlayerTextures";
|
import { PlayerTextures } from "../Entity/PlayerTextures";
|
||||||
|
import Texture = Phaser.Textures.Texture;
|
||||||
|
import {SuperLoaderPlugin} from "../Services/SuperLoaderPlugin";
|
||||||
|
|
||||||
export abstract class AbstractCharacterScene extends ResizableScene {
|
export abstract class AbstractCharacterScene extends ResizableScene {
|
||||||
protected playerTextures: PlayerTextures;
|
protected playerTextures: PlayerTextures;
|
||||||
|
protected superLoad: SuperLoaderPlugin;
|
||||||
|
|
||||||
constructor(params: { key: string }) {
|
constructor(params: { key: string }) {
|
||||||
super(params);
|
super(params);
|
||||||
this.playerTextures = new PlayerTextures();
|
this.playerTextures = new PlayerTextures();
|
||||||
|
this.superLoad = new SuperLoaderPlugin(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
loadCustomSceneSelectCharacters(): Promise<BodyResourceDescriptionInterface[]> {
|
loadCustomSceneSelectCharacters(): Promise<BodyResourceDescriptionInterface[]> {
|
||||||
const textures = this.playerTextures.getTexturesResources(PlayerTexturesKey.Woka);
|
const textures = this.playerTextures.getTexturesResources(PlayerTexturesKey.Woka);
|
||||||
const promises: CancelablePromise<BodyResourceDescriptionInterface>[] = [];
|
const promises: CancelablePromise<Texture>[] = [];
|
||||||
|
const bodyResourceDescriptions: BodyResourceDescriptionInterface[] = [];
|
||||||
if (textures) {
|
if (textures) {
|
||||||
for (const texture of Object.values(textures)) {
|
for (const texture of Object.values(textures)) {
|
||||||
if (texture.level === -1) {
|
if (texture.level === -1) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
promises.push(loadWokaTexture(this.load, texture));
|
promises.push(loadWokaTexture(this.superLoad, texture));
|
||||||
|
bodyResourceDescriptions.push(texture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Promise.all(promises);
|
return Promise.all(promises).then(() => {
|
||||||
|
return bodyResourceDescriptions;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSelectSceneCharacters(): Promise<BodyResourceDescriptionInterface[]> {
|
loadSelectSceneCharacters(): Promise<BodyResourceDescriptionInterface[]> {
|
||||||
const promises: CancelablePromise<BodyResourceDescriptionInterface>[] = [];
|
const promises: CancelablePromise<Texture>[] = [];
|
||||||
|
const bodyResourceDescriptions: BodyResourceDescriptionInterface[] = [];
|
||||||
for (const textures of this.playerTextures.getLayers()) {
|
for (const textures of this.playerTextures.getLayers()) {
|
||||||
for (const texture of Object.values(textures)) {
|
for (const texture of Object.values(textures)) {
|
||||||
if (texture.level !== -1) {
|
if (texture.level !== -1) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
promises.push(loadWokaTexture(this.load, texture));
|
promises.push(loadWokaTexture(this.superLoad, texture));
|
||||||
|
bodyResourceDescriptions.push(texture);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Promise.all(promises);
|
return Promise.all(promises).then(() => {
|
||||||
|
return bodyResourceDescriptions;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
84
front/src/Phaser/Services/SuperLoaderPlugin.ts
Normal file
84
front/src/Phaser/Services/SuperLoaderPlugin.ts
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
|
||||||
|
import LoaderPlugin = Phaser.Loader.LoaderPlugin;
|
||||||
|
import {BodyResourceDescriptionInterface} from "../Entity/PlayerTextures";
|
||||||
|
import CancelablePromise from "cancelable-promise";
|
||||||
|
import {FrameConfig} from "../Entity/PlayerTexturesLoadingManager";
|
||||||
|
import {Scene} from "phaser";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper around Phaser LoaderPlugin. Each method returns a (cancelable) Promise that resolves as soon as
|
||||||
|
* the file is loaded.
|
||||||
|
*
|
||||||
|
* As a bonus, if the scene is destroyed BEFORE the promise resolves, the promise is canceled automatically.
|
||||||
|
* So there is no risk of trying to add a resource on a closed scene.
|
||||||
|
*/
|
||||||
|
export class SuperLoaderPlugin {
|
||||||
|
constructor(private scene: Scene) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public spritesheet(key: string, url: string, frameConfig?: Phaser.Types.Loader.FileTypes.ImageFrameConfig, xhrSettings?: Phaser.Types.Loader.XHRSettingsObject)
|
||||||
|
{
|
||||||
|
return new CancelablePromise<Phaser.Textures.Texture>((res, rej, cancel) => {
|
||||||
|
if (this.scene.scene.settings.status === Phaser.Scenes.DESTROYED) {
|
||||||
|
rej(new Error('Trying to load a spritesheet in a Scene that is already destroyed.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.scene.load.textureManager.exists(key)) {
|
||||||
|
return res(this.scene.load.textureManager.get(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If for some reason, the "url" is empty, let's reject the promise.
|
||||||
|
if (!url) {
|
||||||
|
console.error("Tried to load an empty texture. URL is missing.");
|
||||||
|
rej(new Error('Failed loading spritesheet: URL is empty'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let destroySceneEventRegistered = false;
|
||||||
|
|
||||||
|
const unloadCallbacks = () => {
|
||||||
|
this.scene.load.off("filecomplete-spritesheet-" + key, successCallback);
|
||||||
|
this.scene.load.off("loaderror", errorCallback);
|
||||||
|
if (destroySceneEventRegistered) {
|
||||||
|
this.scene.events.off(Phaser.Scenes.Events.DESTROY, unloadCallbacks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorCallback = (file: { src: string }) => {
|
||||||
|
if (file.src !== url) return;
|
||||||
|
console.error("Failed loading spritesheet: ", url);
|
||||||
|
rej(new Error('Failed loading spritesheet: "' + url + '"'));
|
||||||
|
unloadCallbacks();
|
||||||
|
};
|
||||||
|
|
||||||
|
const successCallback = () => {
|
||||||
|
this.scene.load.off("loaderror", errorCallback);
|
||||||
|
this.scene.events.off(Phaser.Scenes.Events.DESTROY, unloadCallbacks);
|
||||||
|
res(this.scene.load.textureManager.get(key));
|
||||||
|
};
|
||||||
|
|
||||||
|
cancel(() => {
|
||||||
|
unloadCallbacks();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.scene.load.spritesheet(key, url, frameConfig, xhrSettings);
|
||||||
|
|
||||||
|
this.scene.load.once("filecomplete-spritesheet-" + key, successCallback);
|
||||||
|
this.scene.load.on("loaderror", errorCallback);
|
||||||
|
// When the scene is destroyed, let's remove our callbacks.
|
||||||
|
// We only need to register this destroy event is the scene is not in loading state (otherwise, Phaser
|
||||||
|
// will take care of that for us).
|
||||||
|
if (this.scene.scene.settings.status === Phaser.Scenes.LOADING) {
|
||||||
|
destroySceneEventRegistered = true;
|
||||||
|
this.scene.events.once(Phaser.Scenes.Events.DESTROY, unloadCallbacks);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.scene.scene.settings.status !== Phaser.Scenes.LOADING) {
|
||||||
|
this.scene.load.start();
|
||||||
|
// Due to a bug, if the loader is already started, additional items are not added.... unless we
|
||||||
|
// explicitly call the "update" method.
|
||||||
|
this.scene.load.update();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
@ -92,8 +92,10 @@ export class PositionDispatcher {
|
|||||||
|
|
||||||
public removeViewport(socket: ExSocketInterface): void {
|
public removeViewport(socket: ExSocketInterface): void {
|
||||||
// Also, let's stop listening on viewports
|
// Also, let's stop listening on viewports
|
||||||
for (const zone of socket.listenedZones) {
|
if (socket.listenedZones) {
|
||||||
this.stopListening(zone, socket);
|
for (const zone of socket.listenedZones) {
|
||||||
|
this.stopListening(zone, socket);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user