Events
Events are notifications sent by a sender to one or more subscribers indicating that an action has taken place.
Mouse and keyboard events are among the most commonly used events in OPENRNDR. Before we get to those in the next chapter, let’s examine how to create our own custom events.
We will often use events in class instances, so let’s create a simple class called Thing
. This class will include an update method that will emit an event every 60 times it’s called. Anyone listening to this event will receive it.
class Thing {
val timeEvent = Event<Int>("time-event")
private var frame = 0
fun update() {
if (++frame % 60 == 0) {
timeEvent.trigger(frame / 60)
}
}
}
Sending an event
Notice how events carry a payload, in this case Int
. This is convenient because it allows us to transmit information together with the event. Mouse and Keyboard events contain details about the mouse position or the key pressed. In this program we are free to choose any type, so lets just broadcasting a message containing the approximate time in seconds.
Passing a name in the event constructor is not necessary, but can be useful for logging and debugging.
Another thing to observe is that timeEvent
is a public variable. If it was private we couldn’t listen to it from outside by calling thing.timeEvent.listen { ... }
.
At some point in our program execution we need to call .trigger()
to broadcast the event. We can call it as many times as needed.
Listening to an event
The following small program shows how to listen to an event emitted by a class.
First, let’s create one instance of the class called thing
.
Next, listen to the event thing
can emit (timeEvent
).
fun main() = application {
program {
val thing = Thing()
extend {
thing.update()
}
thing.timeEvent.listen {
println("timeEvent triggered! $it")
}
}
}
We see a line appear every second:
timeEvent triggered! 1
timeEvent triggered! 2
timeEvent triggered! 3
...
Events in coroutines and threads
By default our OPENRNDR programs run in a single thread, which happens to be the “rendering thread”. But what would happen if we sent Events from different threads or coroutines? Lets find out.
The Blob
class is a copy of Thing
with three changes:
- To be able to spawn coroutines, we pass a
Program
in the constructor. - We add a second event called
doneWaiting
. We useUnit
as a type when we don’t want to pass any useful data. - When the class is constructed, we launch a coroutine to wait for 3 seconds, then trigger the new
doneWaiting
event.
class Blob(program: Program) {
val timeEvent = Event<Int>("time-event")
val doneWaiting = Event<Unit>("done-waiting")
private var frame = 0
fun update() {
if (++frame % 60 == 0) {
timeEvent.trigger(frame / 60)
}
}
init {
program.launch {
delay(3000)
doneWaiting.trigger(Unit)
}
}
}
Now lets use our Blob
class in a new program that listens to its two events:
fun main() = application {
program {
val blob = Blob(this)
extend {
blob.update()
}
blob.timeEvent.listen {
println("timeEvent triggered! $it")
}
blob.doneWaiting.listen {
println("done waiting")
}
}
}
timeEvent triggered! 1
timeEvent triggered! 2
timeEvent triggered! 3
done waiting
timeEvent triggered! 4
...
Seems to work! right? There’s one issue though: the doneWaiting.listen
function does not run on the rendering thread. This would be the case for events triggered due to external causes (loading a file from the Internet and waiting for its completion or an event coming from a hardware input device).
This will become apparent when we fail to draw on our window:
blob.doneWaiting.listen {
drawer.clear(ColorRGBa.WHITE) // <-- will not work
println("done waiting")
}
The solution is simple though: when constructing the Event
, we set the postpone
argument to true:
val doneWaiting = Event<Unit>("done-waiting", postpone = true)
Now triggering the event no longer sends it immediately, but queues it. The second part of the solution is to actually deliver the queued events by calling deliver()
.
doneWaiting.deliver()
It is essential to call deliver()
from the rendering thread. Since extend { }
executes in the rendering thread, and extend
calls update()
, we can let update()
call deliver()
and everything will work nicely.
This is the full program:
class Blob(program: Program) {
val timeEvent = Event<Int>("time-event")
val doneWaiting = Event<Unit>("done-waiting", postpone = true)
private var frame = 0
fun update() {
if (++frame % 60 == 0) {
timeEvent.trigger(frame / 60)
}
// Deliver any queued events
doneWaiting.deliver()
}
init {
program.launch {
delay(3000)
// Queue event outside the rendering thread
doneWaiting.trigger(Unit)
}
}
}
fun main() = application {
program {
val blob = Blob(this)
extend {
blob.update()
}
blob.timeEvent.listen {
println("timeEvent triggered! $it")
}
blob.doneWaiting.listen {
// White flash when this event is received
drawer.clear(ColorRGBa.WHITE)
println("done waiting")
}
}
}