Help me understand deferred loading of assets and Scene lifecycle #2768
-
Hey, One of the Examples in the Docs is trying to defer loading of assets into the initialization code of the scene, I tried to make that work but either I'm doing something wrong, or I'm misunderstanding the purpose of it. The Scene lifecycle is saying that onInitialize is called once when the engine goes to the scene for the very first time. Here's an example: I have a level with an entity (a sign) made in tiled. I try to load that and set the scene with all its actors in the onInitialize. This normally wouldn't cause issues if both onInitialize and onActivate are synchronous, but it does in this case. export class TiledLevel extends Level {
levelLoader: Loader;
resource: TiledMapResource;
resourceData: TiledMap | null = null;
constructor(public id: number, public name: string, private _path: string) {
super(id, name);
this.resource = new TiledMapResource(_path);
this.resource.convertPath = (origin, path) => {
const relativeSplit = path.split('/');
relativeSplit.shift(); // remove the nav to parent folder
return ['/mystic_woods', ...relativeSplit].join('/');
}
this.levelLoader = new Loader([this.resource]);
this.levelLoader.loadingBarColor = Color.White;
this.levelLoader.backgroundColor = Color.Black.toHex();
this.levelLoader.loadingBarPosition = vec(40, 270);
this.levelLoader.playButtonText = 'Let\'s Go!';
}
async onInitialize(_engine: Engine) {
await this.levelLoader.load();
this.resourceData = this.resource.data;
this.resource.addTiledMapToScene(this);
const { x, y } = this.resourceData.getObjectLayerByName('signs').objects[0];
const sign = new Sign(x,y);
this.add(sign)
this.add(new Player(10,10));
console.log('onInitialize ran');
}
onActivate(ctx: SceneActivationContext) {
(this.actors.find(actor => actor instanceof Sign) as Sign)?.setMessage(ctx?.data?.message);
}
} On another note, the change of color and backgroundcolor don't have an effect on the loader, but the text of the button is changed, confused on whats happening there too. |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
Hi @Joshuahelmle! Many thanks for the kind words, and please do send us any examples you come up with we'd be more than happy to include them in the documentation! Documentation updates are much appreciated! You are correct To work around this current limitation is to wrap the scene transition api API with an async handler, have it handle the async initialization like loading assets, then have it call the sync goto api with the appropriate @mattjennings did some cool work a while back to implement animated scene transitions which uses loaders under the hood, which could in theory be used for loading actual assets as well! Check it out https://github.com/mattjennings/excalibur-router If you don't want to try Matt's excalibur-router to make this happen here is some light sample code import { Engine, Scene } from "excalibur";
export interface AsyncScene {
isAsyncInitialized: boolean;
onAsyncInitialize(engine: Engine): Promise<void>;
}
export function isAsyncScene(scene: Scene | AsyncScene): scene is AsyncScene {
return (scene as any).isAsyncInitialized !== undefined && (scene as any).onAsyncInitialize;
}
export class SceneRouter {
scenes = new Map<string, AsyncScene | Scene>();
constructor(public engine: Engine) {}
addScene(name: string, scene: AsyncScene | Scene) {
this.scenes.set(name, scene);
this.engine.addScene(name, scene as Scene);
}
async goToSceneAsync(sceneName: string) {
const scene = this.scenes.get(sceneName);
if (scene && isAsyncScene(scene)) {
if (!scene.isAsyncInitialized) {
await scene.onAsyncInitialize(this.engine);
scene.isAsyncInitialized = true;
}
this.engine.goToScene(sceneName);
}
}
} Then to use it have your scene implement the async init export class LoadingScene extends Scene implements AsyncScene {
isAsyncInitialized: boolean = false;
async onAsyncInitialize(engine: Engine) {
console.log("async init load started");
// await async init code
console.log("async init load complete");
}
onInitialize() {
console.log("sync init started");
console.log("sync init complete");
}
onActivate() {
console.log("sync activate started");
console.log("sync activate complete");
}
} const engine = new Engine({
width: 800,
height: 600
});
const sceneRouter = new SceneRouter(engine);
sceneRouter.addScene('root', new SyncScene());
sceneRouter.addScene('async-scene', new LoadingScene());
engine.start();
engine.input.keyboard.on('press', e => {
if (e.key === Keys.Enter) {
sceneRouter.goToSceneAsync('async-scene');
}
})
@Joshuahelmle Oh that seems like a bug... I'll look into that today! That should work like you expect. I had some success get the colors to change in my sample by hooking into the engine post draw onPostDraw(ctx: ExcaliburGraphicsContext) {
if (!this.loader.isLoaded()) {
this.loader.canvas.draw(ctx, 0, 0);
}
}
async onAsyncInitialize(engine: Engine) {
console.log("async init load started");
// Optionally hook into the post to draw the loader
engine.onPostDraw = (ctx) => this.onPostDraw(ctx);
} I've pushed up all this code in a working sample! |
Beta Was this translation helpful? Give feedback.
Hi @Joshuahelmle! Many thanks for the kind words, and please do send us any examples you come up with we'd be more than happy to include them in the documentation! Documentation updates are much appreciated!
You are correct
onInitialize
doesn't take into account async initialization so in this example it will fire and forget unfortunately and your assets won't be loaded in time. I think async scene initialization is definitely a gap right now in Excalibur (definitely one we want to fix) right now Excalibur really wants to load all the assets up front before the game starts.To work around this current limitation is to wrap the scene transition api API with an async handler, have it handle…