Events
Every entity in Ceramic implements the Events
interface, making it compatible with Ceramic events syntax.
How to create events?
Let's imagin a silly example in a restaurant with a kitchen
and a waiter
. The waiter is waiting for the kitchen to have a dish ready to serve. When a dish is ready, we want the kitchen
to fire the dishReady
event so that the waiter
can serve that dish.
In Ceramic, that can be done with classes that inherit from Entity
and custom events. First, let's create a Kitchen
entity class and add that dishReady
event declaration inside:
import ceramic.Entity;
import ceramic.Timer;
class Kitchen extends Entity {
/**
* A custom `dishReady` event that
* accepts a dish name (`String`) parameter.
*/
function dishReady(name:String);
}
That's it: to add a custom event to an Entity
subclass, you simply need to write a function
declaration prefixed with the @event
metadata like in the example above.
Under the hood, and thanks to the powerful Haxe macro system, Ceramic automatically generates methods at compile-time from this single dishReady
event declaration. Here is what it will provide:
onDishReady(owner, callback) |
Bind a callback that will be called everytime the dishReady event is fired. |
onceDishReady(owner, callback) |
Bind a callback that will be called one time, next time the dishReady event is fired (same as onDishReady except that the callback is garanteed to be called only once and then unbound). |
offDishReady(?callback) |
Unbind the given callback that was previously bound to the dishReady event, or unbind all callbacks bound to the dishReady event if no specific callback argument is provided. |
listensDishReady() |
Returns true if there is any callback bound and listening to the dishReady event, false otherwise. |
emitDishReady(name) |
Emit the dishReady event. This method is usually called from inside the class that have the event declaration. |
You don't know what the owner
argument is about yet? No worries, we'll get back to it just below!
Good to know: Because all these event-related methods are generated at compile time, they don't require dynamic access. Everything is statically typed and quite efficient. You can create and use a lot of events in Ceramic without worrying too much about performance penalties. Thanks Haxe and its compile-time macros!
Listening to an event
Here we are, we have a Kitchen
class with a dishReady
event. Now, we still need to create a Waiter
class that listens to the event from the Kitchen
class.
Let's add this Waiter
class with a constructor that takes a kitchen
as parameter. It will listen to that kitchen and wait for dishReady
events to be emitted.
import ceramic.Entity;
class Waiter extends Entity {
public function new(kitchen:Kitchen) {
super();
// Listen to `dishReady` event and call
// `serveDish()` everytime the event is fired
kitchen.onDishReady(this, serveDish);
}
function serveDish(name:String) {
trace('The waiter served the dish: ' + name);
}
}
Let's modify Kitchen
class so that it will emit the dishReady
event with a random dish name at a regular interval:
import ceramic.Entity;
import ceramic.Timer;
import ceramic.ReadOnlyArray;
using ceramic.Extensions;
class Kitchen extends Entity {
/**
* A list of available dishes
*/
static final AVAILABLE_DISHES:ReadOnlyArray<String> = [
'Pizza', 'Tiramisu', 'Pasta', 'Gelato'
];
/**
* A custom `dishReady` event that
* accepts a dish name (`String`) parameter.
*/
function dishReady(name:String);
/**
* Constructor
*/
public function new() {
super();
// Finish a dish every 1 second
Timer.interval(this, 1.0, finishDish);
}
function finishDish() {
// Pick a dish name randomly
var name = AVAILABLE_DISHES.randomElement();
// Emit the `dishReady` event with that name
emitDishReady(name);
}
}
About this
and the owner
argument
Why to we pass this
as first argument of kitchen.onDishReady(this, ...)
, as well as Timer.interval(this, ...)
?
It is the owner
argument: most methods provided by Ceramic API that take a callback as argument, which is expected to be called later, also accept a first argument: the owner
of the binding. An owner
is an Entity
instance that owns the binding (here, a binding is an assignation of a callback to an event, a timer...). What does it mean in practice? It means that if that owner is destroyed
, the binding is removed safely and automatically.
In our example with kitchen.onDishReady(this, ...)
, if our Waiter
instance (which is an Entity
subclass) is destroyed
(via the destroy()
method), the binding to the dishReady
event will be removed automatically. This mecanism ensures there won't be any memory leak because an entity is accidentally retained by another object. The binding will also be removed if the kitchen
object is destroyed
, not because it is the owner (it is not), but because it is the object emitting the event so destroying that object will remove any possibility of the dishEvent
from being emitted anyway and make the related bindings irrelevant. It also prevents any risk of memory leak as well: the kitchen
object won't be accidentally retained by the Waiter
instance, thanks to the binding being safely removed.
Similar rules apply to the Timer.interval(this, ...)
binding: if the owner
(this
) is destroyed
, it will be unplugged from the Timer
class safely and the callback won't be accidentally called.
In practice, the owner
of a binding to an event, a timer etc... should be the object than contains the method that will be called as callback. It ensures that when the owner is destroyed
, its methods won't be accidentally called later by a fired event from another object, everything is unplugged correctly and safely. You'll often simply use this
as owner if your callback is a method of the current class anyway, like in our Waiter
class example.
These rules might be a bit uneasy to grasp at first, but they are not very complicated. Don't hesitate to read the previous paragraphs again to understand correctly!
Better together
Alright, we have a Kitchen
and a Waiter
class, but at the moment, nothing will happen until we instanciate and plug these objects together. let's do it:
// Nothing very difficult here,
// We first create a kitchen
var kitchen = new Kitchen();
// Then we create a waiter for that kitchen
var waiter = new Waiter(kitchen);
That is all! You can try to create this kitchen and waiter logic in a test project to see it work for real. If everything goes well, you should see logs similar to this appear at regular intervals:
The waiter served the dish: Pasta
The waiter served the dish: Tiramisu
The waiter served the dish: Tiramisu
The waiter served the dish: Pizza
To test the behaviour we explained in the owner
/this
section, you can try to destroy either the kitchen
or the waiter
object, after some time.
You should notice that the logs will stop as soon as one of the two objects of the binding is destroyed
.
Timer.delay(kitchen, 10, kitchen.destroy);
Timer.delay(waiter, 15, waiter.destroy);
Look how we used the owner
argument this time: the owner is the object having the method that will be called as callback by Timer
, so the owner
is the kitchen
object if we bind the destroy()
method from that kitchen
object. It is the waiter
object if we bind the destroy()
method of that waiter
object. In case the owner was destroyed before the timer delay has passed, the callback wouldn't be called at all. Basically, when you destroy
an entity
, you are sure that Ceramic events and timer bindings are removed and no method you previously assigned as callback will be called anymore!
You now know how to create entities
, add custom events to them and make other entities listen to these events. Continue reading to learn more about the Visual
class, an important type in Ceramic that inherits from Entity
and provides its own built-in events.
Continue reading ➔ Visuals