Entities & Components & Props
In TypeSpriteJS everything is composed of Entities and Components.
An Entity is the class that represent your game objects. A game object can be really anything: the main character, the enemies, the UI element that represents your health-bar.
To create an Entity you compose them of one or more Components that you develop specifically for your game. Everytime an Entity is spawned its Components will run through a set of lifecycle steps.
Define an Entity
Entities are defined using the Entity-Definition-Format (EDF). It's a simple textfile that looks like this:
[!MyEntity]
@MyCmp
firePower = 100
[!MyOtherEntity]
@MyCmp
life = 200
firePower = 200
In this example we created two entity-definitions: MyEntity
and MyOtherEntity
. Both use the component MyCmp
and set some default properties.
Entity Properties with: prop()
export class MyCmp extends Component {
@prop('number', 100)
private life:number;
@prop('string', 'fire', {allowed: ["fire", "water", "ice"]})
private mainSpell:string;
@prop('number')
private firePower:number;
onInit() {
// Will be called for every Entity with this
// component attached.
//
// All @prop-variables are ready to use here
}
// {...}
}
The variable life
has a default value of 100. However, it can be overwritten in the EDF file (see MyOtherEntity above).
mainSpell
is of type string but must be one of the allowed values.
firePower
, is without a default value. If we forget to set it in the EDF file the entity will not spawn and cause a console.error.
TIP
The parser that tries to make sense of the property is something you can write yourself. With a simple function you can basically parse anything you like.
See Property Parser.
Entity Resources: res()
export class MyCmp extends Component {
@res('json', 'assets/someConfig.json')
private config:Record<string, any>;
@res('image', 'assets/images/tex1.png')
private img1:HTMLImageElement;
@res('texture', 'assets/images/tex1.png')
private tex1:ManagedTexture;
onInit() {
// All @res-variables are ready to use here
// and everything is loaded!
}
// {...}
}
Resources and assets like textures and configuration are automatically managed by the engine. The resource loader makes sure that the requested resources will be loaded.
If a file is not found, the Entity will not instantiate.
All resources are only loaded once and shared throughout the running game/world.
As the example shows, it's possible to load one file (assets/images/tex1.png
) in two different loaders. The way the loaders are implemented makes sure that the texture is created from the same image.
TIP
You can write your own ResourceLoader and adjust this behavior any way you like. Also import custom file formats that fits your game best.
See Resource Loader
Component Reference
If a component requires the existence of another component we can express that dependency by using cmp()
.
Let's create an Entity with two components:
[!MyEntity]
@MyCmp1
@MyCmp2
export class MyCmp1 extends Component {
public someValue:number;
// {...}
}
export class MyCmp2 extends Component {
@cmp('MyCmp1')
private cmp1:MyCmp1;
@cmp('MyOptionalCmp2', true)
private cmp2:MyCmp2|null;
onInit() {
// All @link-variables are ready to use here
}
// {...}
}
In the example we can use MyCmp2
only in Entities that also have a MyCmp1
component. If not the entity won't be spawned.
MyOptionalCmp2
on the other hand is not required - but if it exists will be set.
TIP
It's also possible to use functions like this.entity.findComponent(...)
to search and check for other Components.
Dependency Order & Linking
If a world becomes active all Entities with an !
are spawned automatically. The order is not guaranteed. However, we can express the dependency order:
[!InitGUI]
@MyGuiManager
[!MyGui1->InitGUI] # read "MyGui1 needs InitGui"
@MyGuiWindow1
export class InitGUI extends Component {
public guiVisible:number = 0;
onInit() {
}
// {...}
}
export class MyGui1 extends Component {
@link('InitGUI')
private initGui:InitGUI;
onInit() {
// We can use this.initGui here!
}
// {...}
}
TypeSpriteJS knows now, that MyGui1
needs to wait for InitGUI
to be inited. This way we can safely expect that MyGuiManager
is inited when MyGui1::onInit()
is called.
Using the decorator link()
we can access this in a component.
This behavior avoids the need for singletons classes.
TIP
Keep in mind that starting/stopping the world will destroy and recreate the given Entities.
An elegant solution to this is to create an extra world that holds game global state objects that shall live throughout the game.
Link to another world is easy too: link("initGui", "GlobalWorld")
Spawn Children
So far we only used static entities (the one beginning with !
). In practice it's likely that we spawn most of them via code.
Imagine we have a player that can throw bombs.
[!Player]
@PlayerCmp
[Bomb]
@BombCmp
export class BombCmp extends Component {
@prop('number', 5)
private timeToExplode:number;
@prop('number', 10)
private damageRadius:number;
onInit() {
// called for every new bomb
}
// {...}
}
export class PlayerCmp extends Component {
@child("Bomb")
private throwEntity:string;
onUpdate() {
if (someCondition) {
this.world.spawn(this.throwEntity);
}
}
// {...}
}
In this example the Player Entity is instantiated on startup. However, the bomb is not. But the child()
property ensures that all resources are loaded so the player can create a bomb entity.
This way your game only needs to load the entities that are needed for the current scenario.
You can also change the properties when spawning. Check out the following code:
export class PlayerCmp extends Component {
@child("Bomb")
private throwEntity:string;
onUpdate() {
if (someCondition) {
this.world.spawn(this.throwEntity,
{
timeToExplode: 10,
}
)
}
}
}
Another important detail is that child
is just another property. We can simply overwrite it in the edf:
[!Player]
@PlayerCmp
throwEntity = SuperBomb
[Bomb]
@BombCmp
[SuperBomb]
@BombCmp
damageRadius = 50