Worlds
In TypeSpriteJS everything happens within one or more worlds. They help to organize entities, messages, activation states, screenflows and manage resources.
Basic Overview
The main object is the WorldManager
which is created by TypeSpriteJS automatically.
Using *.edf
files you populate one or more worlds with entities. An Entity is composed of one or more derived instances of Component
. Once per mainloop run each world and each entity and each component receives a call to onUpdate()
.
From your component you can access the global WorldManager
instance like this:
class MyComponent extends Component {
onUpdate() {
const manager = this.world.manager; // << the global world manager
}
}
World Definition (*.edf)
As a game developer you configure the worlds by creating EDF-Files. In those files you define the compositions of potential entities (game objects) and set which entities start with the world and in what order.
An EDF-File is a text file that you put next to your component classes into one of the asset folders. For the given example the folder might look like this:
mygame/
assets/
Component1.ts
Component2.ts
Component3.ts
world1.edf
In the EDF you define all entities that can run in the given world. The filename of the EDF-File is also the ID of the world: world1.edf
=> world1
.
By using the ini-like syntax you write down the entities you need:
# Static instance named: entity1
# Automatically created when the world is loaded
[!entity1]
@Component1
@Component2
@Component3
someProp = 1
# ...
# Just a definition
# Need to be instanced by code.
[entity4]
@Component1
@Component2
someProp = 1
In this case you created an entity definition with the name entity1
. By prefixing the name with an !
you also create an instance on world startup. Those start-entities are called static
.
As a developer one only has to create the Component- and EDF-Files. The entire management of this is done by TypeSpriteJS.
Need Examples?
If you just want to know how to create your own components and don't care for the multi worlds so much you can jump to the next chapter (Components).
Multi Worlds
For starters one world will be enough. However, eventually you want to implement things like screen flows, loading screens or maybe even a split screen mode. In those situations multiple worlds can help.
To create multiple worlds you simply create more EDF-Files. TypeSpriteJS will start them ALL automatically on startup. You can change that in the typesprite.config.mjs
:
import {defineConfig} from 'typesprite'
export default (defineConfig(({command}) => {
return {
run: {
startWorlds: "world1" // only start world1.edf
}
}
}))
World States
Worlds can only be populated when all resources are loaded. Loading can take some time and it's also possible that the world cannot be loaded for some reason. This leads to a range of states a World can be in it:
As many worlds can run at the same time they all have their own state. Depending on the state they have different characteristics:
\ | Empty | Loading | Populated | Error |
---|---|---|---|---|
onUpdate() called | ❌ | ❌ | ✅ | ❌ |
Holds Assets/Resources | ❌ | ✅/❌ | ✅ | ❌ |
Receives Messages | ❌ | ❌ | ✅ | ❌ |
onUpdate()
is only called for Entities in a populated world. If the world loads or unloads or is in error state no updates happen. Resources are loaded when a world is in loading state. During that time it slowly starts to consume more and more memory (GPU + CPU).
To change the state of a world one can use the following functions:
// Given a Component function:
onUpdate() {
const w1 = this.world.manager.getWorldByName("level");
w1.start(); // Loads and starts world: "level"
this.world.stop(); // Destorys the world we are currently in
}
Resources in Worlds
All worlds share one Resource Manager. Resources are requested by Entities and owned by their parent world. That way two entities within a world can share a resource. It's also possible that two entities of two different world share the same resource:
Resources are unloaded as soon as all worlds pointing to them are stopped/unloaded. So in the given example assets/levels/level1.json
is only retained by the world LevelWorld
and if the world gets stopped so the resource will be released. assets/sheets/uiskin1.png
on the other hand will only be unloaded if TitleScreen
and InGameUI
are stopped.
As a game developer this concept frees you from thinking too much about resources. You simply request the things you need in the components and that's it. TypeSpriteJS takes care of the inner details.
It's a common use case to have one world representing a level. The entities in there are the enemies, the main character and all other things filling the world. If the player dies the level needs to restart. In such a case it is recommended to use restart()
:
// given a Component function:
restartLevelAfterDeath() {
this.world.restart(); // ✅ DO THIS!
this.world.stop(); // ❌ NOT This!
this.world.start();
}
IF you call stop()
follwed by a start()
it means the engine will throw away all resources that are bound to the given world and load them again. restart()
on the other hand will only load and unload assets that changed during the restart.