I would like to load textures in a class and only after all textures have been loaded may the class be called. Loading the textures works but i don’t know how and where to call the class at the right place so that it is returned to the main class. I have reduced the code to the absolute neccessary to show my problem
class Environment {
constructor(params) {
this._Init(params);
}
static init(params) {
const getTextures = () => new Promise((resolve, reject)=>{
const manager = new THREE.LoadingManager(()=>resolve(textures));
const loader = new THREE.TextureLoader(manager);
const textures = [
'./resources/simplex-noise.png',
//next texture,
//next texture,
//next texture,
].map(filename=>loader.load(filename));
});
getTextures().then(tex => {
return new Environment({params, tex}); //here something is wrong.
//I need "new Environment({params, tex});" as return for "static init"
});
//return new Environment(params); //this is working but here i have no preloaded tex
}
_Init(params) {
this.params_ = params;
//....
}
}
class Main {
constructor(){
this._Initialize();
}
_Initialize() {
//...
this.OnAppStarted_();
//...
}
OnAppStarted_() {
this.LoadControllers_();
}
LoadControllers_() {
this.obj = Environment.init({
//params
});
//...
}
//...
}
you would typically use Promise.all to await multiple promises, the result you get back corresponds to the input in order.
async function app() {
const loader = new THREE.TextureLoader()
const [a, b, c] = await Promise.all(
['a', 'b','c'].map(
(name) => new Promise(
(res, rej) =>
loader.load(`/resources/${name}.png`, res, undefined, rej)
)
)
)
// here you can now access textures a, b and c
this has multiple benefits:
the fetch requests run in parallel not in a waterfall
you can add all the assets and loaders you need, textures, gltf’s, etc
by the time it completes you have access to all assets
ps one more hint, side effects in the class constructor are an anti pattern. you want to avoid this at all cost because it is hard to control and leads to race conditions and memory leaks.
the correct way is to init and dispose explicitely
const env = new Environment()
env.init()
// and later
env.dispose()
So I shouldn’t control my app from a main class, but from an async main function from which I can call any static async functions of classes that then call the classes after asynchronous operations. Do I understand this in the right manner?
So asynchronous always via async functions and only then synchronous class calls, right?
The app management is then always function-controlled and not controlled by the main class in order to have the freedom to use async methods
if your app is async the entry point is usually, too. threejs is inherently async, every loading operation is. top level await exists in modern browsers, too, no function required in that case. class functions can be async as well, just prepend the async keyword.
But then I always have to use my own async constructor, right? e.g. “async constructor*” because with the normal constructor there are difficulties with await async, right? Then I have to call the constructor* explicitly after creating a class object because only the normal constructor is executed automatically.
constructors cannot be async. this is what i was saying, constructors should never have side effects, if yours does (it does in the code you posted) you can consider it a bug.
instead of
// ❌ Will make a fetch request inside the constructor (which cannot be awaited or managed)
const env = new Environment(url)
always do this
const env = new Environment()
// ✅ This is safe
await env.init(url)
a side effect is anything that is is impure, as in it’s asynchronous, calls globals, fetches remotely, etc. a pure function is deterministic and doesn’t change anything outside of its own scope. this is programming 1x1 but something very easy to mess up nonetheless. even threejs has constructor side effects.