Custom Entities, Triggers and Stylegrounds - EverestAPI/Resources GitHub Wiki
Important
This wiki page assumes that you have set up your code mod via the template.
Don't know what the template is all about? Check out the Code Mod Setup wiki page to get started.
Note
This is a guide which explains how to define a custom entity, trigger, or styleground.
For information on how to integrate an entity, trigger or styleground with Lönn for use in maps, please see the Lönn Wiki 🔗.
To create a custom entity (an object which you can place in a map), create a class that extends Monocle.Entity
and add using Celeste.Mod.Entities;
to the top of the file.
Then, add the [CustomEntity]
attribute, so that Everest can detect it when loading .bin
map data.
The ID passed to [CustomEntity]
must be unique. The most common format is [CustomEntity("ModName/EntityName")]
.
The class should look like so:
// ExampleEntity.cs
using Celeste.Mod.Entities;
using Monocle;
namespace Celeste.Mod.ExampleMod.Entities;
[CustomEntity("ExampleMod/ExampleEntity")]
public class ExampleEntity : Entity
{
// ...
}
Important
To be able to place your entity in Lönn, you will also have to create a Lönn plugin 🔗 for it.
When spawning your entity from map data, Everest looks for a special constructor and invokes it, if it finds one.
It checks for, in this exact order:
-
public ExampleEntity(EntityData data, Vector2 offset, EntityID id)
Useful when making an entity which doesn't respawn. -
public ExampleEntity(EntityData data, Vector2 offset)
The most common constructor signature. -
public ExampleEntity(Vector2 offset)
Not very commonly used, due to its lack of theEntityData
parameter. -
public ExampleEntity()
Common for "controller" entities.
(entities which are invisible and intangible, but which do stuff behind the scenes)
Let's explain what each parameter is:
-
data
- Contains the room-relative position of the entity and its attributes assigned in Lönn -
offset
- The offset of the top-left corner of the room -
id
- The entity's unique ID, commonly used to prevent an entity from loading again
Your constructor needs to call base
with a given absolute position (or none when making a "controller" entity), else it will spawn at the wrong place.
The absolute position can be calculated with data.Position + offset
.
Here is an example with a constructor defined:
// ExampleEntity.cs
using Celeste.Mod.Entities;
using Monocle;
using Microsoft.Xna.Framework;
namespace Celeste.Mod.ExampleMod.Entities;
[CustomEntity("ExampleMod/ExampleEntity")]
public class ExampleEntity : Entity
{
// Used when loading the entity from a map
// Remember to call base(data.Position + offset)!
public ExampleEntity(EntityData data, Vector2 offset)
: base(data.Position + offset)
{
// ...
}
}
Important
If Everest cannot find a constructor with one of these signatures, it won't be able to spawn your entity.
Mind the access level - the constructor must be public
!
Now you can read information from Lönn in the constructor, using various members of the data
parameter.
Here are a few:
-
data.Bool(...)
- Read abool
value -
data.Float(...)
- Read afloat
value -
data.String(...)
- Read astring
value -
data.HexColor(...)
- Read a hexadecimal color value -
data.Nodes
- An array of node positions relative to the entity
The Entity
class has two important methods you can override:
-
Update
- Called every frame while the entity isActive
-
Render
- Called every frame while the entity isVisible
There are also some lifecycle methods:
-
Added
- Called once when the entity is first added to the level -
Awake
- Called once when all entities are added to the level on this frame -
Removed
- Called once when the entity is being removed from the level (like after a transition, or when respawning)
The entity can define a custom collider, via the Collider
field. For a rectangular hitbox, you can assign a Hitbox
to Collider
:
// ExampleEntity.cs
public ExampleEntity(EntityData data, Vector2 offset)
: base(data.Position + offset)
{
// Create an 8x8 collider, offset 4 pixels left and 4 pixels up
// (which makes it centered)
Collider = new Hitbox(8, 8, -4, -4);
}
You can change the order in which an entity is rendered/updated by changing its Depth
.
Lower values mean that the entity is closer in front and updates later. Higher values mean that the entity is further back and updates earlier. Madeline uses depth 0
.
Some general depth constants can be found in the Depths
class.
Important
Update/render order for multiple entities at the same depth is not consistent and may change depending on the order the entities are added and/or removed in.
This is especially problematic at depth 0
, since Madeline is at that depth.
This can cause slight (but important) behavior differences seemingly randomly.
For example, if we want our entity to appear behind and update before background decals:
// ExampleEntity.cs
public ExampleEntity(EntityData data, Vector2 offset)
: base(data.Position + offset)
{
// Place us slightly behind background decals
Depth = Depths.BGDecals + 50;
}
Entities can have Components
, which are reusable bits of code that can be attached to multiple entities independently.
This helps prevent code duplication and simplifies logic.
Tip
Some useful components include:
-
Image
- Renders a static image -
Sprite
- Renders an animated sprite defined inSprites.xml
(create a sprite withGFX.SpriteBank.Create("sprite_name")
) -
PlayerCollider
- Runs code when Madeline collides with the entity (can accept non-default colliders) -
DashListener
- Runs code when Madeline dashes anywhere in the level -
Holdable
- Allows Madeline to pick up the entity just like Theo or a Jelly -
SoundSource
- Adds a localized source of sound*(wow!)* -
WindMover
- Runs code when your entity is being pushed by wind -
Coroutine
- Run a method which can be paused
Components can be added at any time by calling the Add
method on the entity.
This is commonly done directly in the constructor, but it can be done at any time.
Components are updated and rendered based on the order they have been added in.
Important
Remember to call base.Update()
and base.Render()
in Update
and Render
respectively!
These methods are responsible for running logic for the components you've added to your entities.
Here is an example which adds a torch sprite, lights it up if the player dashes, and extinguishes it if the player touches it:
// ExampleEntity.cs
using Celeste.Mod.Entities;
using Monocle;
using Microsoft.Xna.Framework;
namespace Celeste.Mod.ExampleMod.Entities;
[CustomEntity("ExampleMod/ExampleEntity")]
public class ExampleEntity : Entity
{
private readonly Sprite TorchSprite;
// Used when loading the entity from a map
// Remember to call base(data.Position + offset)!
public ExampleEntity(EntityData data, Vector2 offset)
: base(data.Position + offset)
{
// Define a centered 32x32px (4x4 tile) hitbox
Collider = new Hitbox(32f, 32f, -16f, -16f);
// Create a Sprite based on the Sprites.xml name
// Store its reference so that we can do stuff with it
Add(TorchSprite = GFX.SpriteBank.Create("torch"));
// Add a DashListener which runs OnDash when Madeline dashes
Add(new DashListener(OnDash));
// Add a PlayerCollider which runs OnPlayer when Madeline touches it
Add(new PlayerCollider(OnPlayer))
}
// Called when Madeline dashes
private void OnDash(Vector2 dashDirection)
{
// Play the "turnOn" animation defined in Sprites.xml
TorchSprite.Play("turnOn");
}
// Called when Madeline touches the entity
private void OnPlayer(Player player)
{
// Play the "off" animation defined in Sprites.xml
TorchSprite.Play("off");
}
}
[CustomEntity]
accepts multiple IDs, which is useful if you need backwards compatibility:
// ExampleEntity.cs
using Celeste.Mod.Entities;
using Monocle;
using Microsoft.Xna.Framework;
namespace Celeste.Mod.ExampleMod.Entities;
[CustomEntity("ExampleMod/ExampleEntityNew", "ExampleMod/ExampleEntity")]
public class ExampleEntity : Entity
{
// ...
}
Alternatively, you can provide generator methods, which can give you more control over how an entity is spawned.
By default, Everest looks for a generator named Load
. You can specify a different generator name by putting an equals sign after the ID in [CustomEntity]
.
Generator methods must be static, return Entity
(or a type which extends from it), and accept 4 parameters:
-
Level
- The level scene to which the entity should be spawned
If necessary, data related to the current session can be found inlevel.Session
-
LevelData
- The data about the level straight from the.bin
file -
Vector2
- The offset of the top-left corner of the room (same as in the constructor) -
EntityData
- Contains the room-relative position of the entity and its attributes assigned in Lönn (same as in the constructor)
Important
A generator method, if provided, takes precedence over the special constructors.
Here's an example of an entity which uses two different entity IDs and needs two different ways of loading:
// ExampleEntity.cs
using Celeste.Mod.Entities;
using Monocle;
using Microsoft.Xna.Framework;
namespace Celeste.Mod.ExampleMod.Entities;
[CustomEntity(
"ExampleMod/ExampleEntityNew",
"ExampleMod/ExampleEntity = LoadLegacy")]
public class ExampleEntity : Entity
{
// Called by Everest when loading a "ExampleMod/ExampleEntityNew" entity
// No generator name specified in CustomEntity, but it defaults to Load (if present)
public static ExampleEntity Load(
Level level,
LevelData levelData,
Vector2 offset,
EntityData entityData)
=> new ExampleEntity(entityData, offset, isLegacy: false);
// Called by Everest when loading a "ExampleMod/ExampleEntity" entity
// The generator name is specified in CustomEntity
public static ExampleEntity LoadLegacy(
Level level,
LevelData levelData,
Vector2 offset,
EntityData entityData)
=> new ExampleEntity(entityData, offset, isLegacy: true);
// This constructor won't be used by Everest!
// - The signature is different
// - We have defined generator methods, so Everest will use those
public ExampleEntity(EntityData data, Vector2 offset, bool isLegacy)
: base(data.Position + offset)
{
// ...
}
}
Custom triggers are done almost identically to Custom Entities.
The only difference is that they extend from Celeste.Trigger
, and not Monocle.Entity
.
The base constructor also two parameters instead of one: EntityData
and Vector2
.
Then, you can override one (or more) of these methods:
-
OnEnter
- Called once when the player enters the trigger -
OnStay
- Called every frame while the player stays in the trigger -
OnLeave
- Called once when the player exits the trigger
Here's an example trigger which counts how long the player has been in the trigger:
// ExampleTrigger.cs
using Celeste.Mod.Entities;
using Monocle;
using Microsoft.Xna.Framework;
namespace Celeste.Mod.ExampleMod.Entities;
[CustomEntity("ExampleMod/ExampleTrigger")]
public class ExampleTrigger : Trigger
{
private float Timer;
// Used when loading the trigger from a map
// Remember to call base(data, offset)!
public ExampleTrigger(EntityData data, Vector2 offset)
: base(data, offset)
{
// ...
}
// Log when Madeline enters the trigger
public override void OnEnter(Player player)
{
Logger.Info("ExampleMod/ExampleTrigger",
"The trigger has been entered.");
}
// Accumulate time while Madeline stays in the trigger
public override void OnStay(Player player)
{
Timer += Engine.DeltaTime;
}
// Log how long Madeline was inside when she exits the trigger
// Round the time to 2 decimal places
public override void OnEnter(Player player)
{
Logger.Info("ExampleMod/ExampleTrigger",
$"The trigger has been exited. Time: {Timer:F2} sec.");
Timer = 0;
}
}
There are also some extra attributes which you might find useful.
The [Tracked]
attribute registers your entity to the tracker. This allows you to find your entity/entities in a more efficient way.
Entity firstEntity;
// Slow - iterates through *every* entity in the scene
firstEntity = Scene.Entities.FindFirst<ExampleEntity>();
// Fast - the tracker keeps track of every entity type in a list
// Requires the [Tracked] attribute on ExampleEntity
firstEntity = Scene.Tracker.GetEntity<ExampleEntity>();
// Slow:
// - iterates through *every* entity in the scene
// - creates a new list every time, using up memory
foreach (ExampleEntity entity in Scene.Entities.FindAll<ExampleEntity>())
{
// ...
}
// Fast:
// - the tracker keeps track of every entity type in a list
// - returns the same list every time
// Requires the [Tracked] attribute on ExampleEntity
foreach (ExampleEntity entity in Scene.Tracker.GetEntities<ExampleEntity>())
{
// ...
}
Important
GetEntities<T>()
returns a reference to the list used internally by the tracker, not a copy.
As long as you use it in a read-only fashion (like iterating through it), everything is alright.
However, do not modify it. If you need a copy of the list of entities, use GetEntitiesCopy<T>()
instead.
It also allows you to use CollideCheck<T>()
(and overloads), where T
is an Entity
.
CollideCheck
expects that T
is marked with [Tracked]
, and will violently explo- erm, crash if it isn't.
This attribute has a parameter which determines whether child classes are included in the search as well. By default it's set to false
.
This means that if you have an entity which extends from your tracked entity...
// DerivedEntity.cs
using Celeste.Mod.Entities;
using Monocle;
using Microsoft.Xna.Framework;
namespace Celeste.Mod.ExampleMod.Entities;
[CustomEntity("ExampleMod/DerivedEntity")]
public class DerivedEntity : ExampleEntity
{
// ...
}
...then Scene.Tracker.GetEntities<MyEntity>()
will return:
- all
ExampleEntity
objects in the scene ifExampleEntity
is annotated with[Tracked]
- all
ExampleEntity
andDerivedEntity
objects ifExampleEntity
is annotated with[Tracked(true)]
If you annotate your entity with [TrackedAs(type)]
, it will be tracked in the exact same way as the type specified.
For example...
// ExampleCustomWater.cs
using Celeste.Mod.Entities;
using Monocle;
using Microsoft.Xna.Framework;
namespace Celeste.Mod.ExampleMod.Entities;
[TrackedAs(typeof(Water))]
[CustomEntity("ExampleMod/ExampleCustomWater")]
public class ExampleCustomWater : Water
{
// ...
}
...means "ExampleCustomWater
should be tracked in exactly the same way as Water
is".
This is beneficial for several reasons:
-
CollideCheck<Water>()
will also check collisions withExampleCustomWater
, making Madeline able to swim in your custom water with no extra code -
Scene.Tracker.GetEntities<Water>()
also returnsExampleCustomWater
entities - ...et cetera.
Used here in Spring Collab 2020 🔗.
This is useful when developing an entity extending a tracked vanilla one, when the vanilla one has [Tracked(false)]
making children not tracked by default.
Important
When using [TrackedAs]
, it is important that your entity extends from the type you provide.
Celeste and every mod expects that entities returned by Scene.Tracker.GetEntities<T>()
(where T
is an Entity
) are instances of T
, and will combus- uh, crash if there is an entity which does not extend from T
.
This attribute can be placed on any class that extends Strawberry
or implements IStrawberry
.
It allows custom strawberries to be taken into account correctly in the total strawberry count, or in the strawberry tracker in the pause menu for example.
Here's an example from Spring Collab 2020:
[RegisterStrawberry(isTracked: true, blocksNormalCollection: false)]
[CustomEntity("SpringCollab2020/CassetteFriendlyStrawberry")]
public class CassetteFriendlyStrawberry : Strawberry
{
// ...
}
This attribute has two parameters:
-
isTracked
- Whether the strawberry should be counted in the maximum berry count, and should show up on the checkpoint card / the pause menu tracker.
Its checkpoint ID and order will be auto-assigned by Everest in this case. -
blocksNormalCollection
- Whether the berry has specific collection rules, similarly to golden berries.
In this case, it will allow berries behind it in the "berry train" to be collected.
For reference, in vanilla:
- red berries are tracked and do not block normal collection
- golden berries are untracked and block normal collection
- the moon berry is untracked and does not block normal collection
If your custom berry doesn't extend Strawberry
and you want to have seeds behaving normally, you can have your custom berry implement IStrawberrySeeded
, then use the GenericStrawberrySeed
class instead of vanilla strawberry seeds.
See Spring Collab 2020's Glass Berry 🔗 for an example.
Custom stylegrounds are done in a very similar way to entities, with several key differences:
- The class must use
[CustomBackdrop]
, not[CustomEntity]
- The class must extend from
Backdrop
- The constructor/generator must accept one parameter of type
BinaryPacker.Element
- The generator (if any) must return
Backdrop
(or a type which extends from it)
Tip
BinaryPacker.Element
behaves very similarly to EntityData
.
Here's an example:
// ExampleStyleground.cs
using Celeste.Mod.Entities;
using Monocle;
using Microsoft.Xna.Framework;
namespace Celeste.Mod.ExampleMod.Stylegrounds;
// Note the [CustomBackdrop] attribute
[CustomBackdrop("ExampleMod/ExampleStyleground")]
public class ExampleStyleground : Backdrop
{
// No base() call needed, as the base constructor accepts 0 parameters
public ExampleStyleground(BinaryPacker.Element data)
{
// ...
}
}
It provides two extra methods on top of Update
and Render
:
-
BeforeRender
- Do stuff before rendering -
Ended
- Do cleanup after the level ends (like upon completion, Save and Quit, Return to Map, etc.)