Skip to content

Part 5: Handling Events

Actors

Now we're going to get into a really powerful part of the Kywy engine, and how it's meant to be used: with Actors and Events.

If you want to build a game or program that has some meat to it things can get really complicated really fast, especially if you're trying to run everything from a single loop function.

The Kywy engine offers a solution to this in the form of Actors and Events.

Actors are buckets of code that run on their own, recieve Events, respond (or handle) those Events, and even send out (or publish) Events of their own.

The real kicker is that you can have multiple actors, all running simultaneously. It's like having multiple loop functions that can each have a bit of the responsibility.

Events are the glue between these separately running bits of code. In other words: Actors talk to each other through Events.

The Simplest Actor

Let's look at the most basic Actor we can create.

#include "Kywy.hpp"

Kywy::Engine engine;

// an actor is a class that inherits from Actor::Actor
class NothingDoer : public Actor::Actor {
public:
  // an actor must have a `handle` function to recieve messages
  void handle(::Actor::Message *message) {}
} nothingDoer; // create an instance of a NothingDoer called nothingDoer

void setup() {
  engine.start();
  nothingDoer.start(); // an actor is started just like the engine
}

void loop() {
  delay(1000); // don't churn the CPU running an empty loop
}

This actor does, you may be surprised to learn, nothing. But it does have all the critical parts of an Actor:

  • A class definition that inherits from Actor::Actor
  • a handle function that takes in an ::Actor::Message *
  • a call to start()

Responding to an Event

Let's make our NothingDoer actually do something (I guess we'll have to rename it as well).

We'll start with responding to our first event: Kywy::Events::TICK. The engine will automatically create these TICK events at a regular interval, and our actors can respond in kind.

To handle this event, we'll need to update our handle function. For now, we'll simply make it print out "Tock" whenever it get's a TICK.

#include "Kywy.hpp"

Kywy::Engine engine;

class Tocker : public Actor::Actor {
public:
  void handle(::Actor::Message *message) {
    switch (message->signal) {
      case Kywy::Events::TICK:
        Serial.println("Tock");
        break;
    }
  }
} tocker;

void setup() {
  engine.start();
  tocker.start();
}

void loop() {
  delay(1000);
}

A few notes:

  • We see what type of message we're dealing with by checking message->signal
  • We use a switch statement here because we very often (and will below) expand the handle function to respond to multiple types of events.

If we run this and check our serial monitor we should see...nothing.

That's because we've missed one crucial part: subscription.

By default, actors don't actually listen for any events. They just plug their ears and sit there. We have to tell an actor who it should listen to by calling the subscribe method on another actor.

In other words, one actor "subscribes" to another actors events.

Here's where we reveal that the Kywy engine is actually an actor--well, several actors--itself!

One of those actors you've already seen: engine.input. We'll use that actor later, but you can probably guess that it publishes events related to button presses.

The actor we need here is engine.clock, which periodically fires the TICK event and drives forward our program.

So lets add this subscription:

#include "Kywy.hpp"

Kywy::Engine engine;

class Tocker : public Actor::Actor {
public:
  void handle(::Actor::Message *message) {
    switch (message->signal) {
      case Kywy::Events::TICK:
        Serial.println("Tock");
        break;
    }
  }
} tocker;

void setup() {
  engine.start();

  // this is all we need to add, it tells our tocker
  // to listed for events from the engine clock
  tocker.subscribe(&engine.clock); 
  tocker.start();
}

void loop() {
  delay(1000);
}

Now our serial monitor should be endlessly printing

Tock
Tock
Tock
Tock
Tock
Tock
Tock
...

Info

You can also unsubscribe from other actors. For example, if we wanted our tocker to stop tocking at some point we could call tocker.unsubscribe(&engine.clock) and it would no longer recieve those events.

Bouncing Ball Actor

Now let's apply this to our bouncing ball. Instead of running our physics simulation in loop, we're going to move it to a new actor that will run the simulation every TICK.

#include "Kywy.hpp"

Kywy::Engine engine;

class Ball : public Actor::Actor {
public:
  // make our global variables class variables instead
  float x = KYWY_DISPLAY_WIDTH / 2;
  float y = KYWY_DISPLAY_HEIGHT / 2;
  float xVelocity = 5;
  float yVelocity = 0;

  void handle(::Actor::Message *message) {
    switch (message->signal) {
      // copy in our physics code, EXCEPT THE `delay`
      // that's handled by the engine.clock now.
      case Kywy::Events::TICK:
        // x and y position and velocity changes
        x = x + xVelocity;
        y = y + yVelocity;

        // collisions and bounces
        if (x < 12) {
          xVelocity = abs(xVelocity);
          x = 12;
        }

        if (x > KYWY_DISPLAY_WIDTH - 12) {
          xVelocity = -1 * abs(xVelocity);
          x = KYWY_DISPLAY_WIDTH - 12;
        }

        if (y < 12) {
          yVelocity = abs(yVelocity);
          y = 12;
        }

        if (y > KYWY_DISPLAY_HEIGHT - 12) {
          yVelocity = -1 * abs(yVelocity);
          y = KYWY_DISPLAY_HEIGHT - 12;
        }

        // gravity
        yVelocity += 1;

        // draw to the screen
        engine.display.clear();
        engine.display.drawCircle(
          (int)x, (int)y,
          25,
          Display::Object2DOptions()
            .origin(Display::Origin::Object2D::CENTER));
        engine.display.update();
        break;
    }
  }
} ball;

void setup() {
  engine.start();

  // subscribe our ball to the clock and start it!
  ball.subscribe(&engine.clock); 
  ball.start();
}

// remove all code from `loop` and just put in
// a delay so that it doesn't run often
void loop() { 
  delay(1000);
}

We should now see... exactly what we did before!

Handling Inputs

So why did we go to all this trouble?

Consider a new behavior we might want for our program: giving the ball a "kick" whenever we press up on the joystick.

How would you accomplish this with our previous method? Well we'd have to: * keep track of whether the button was pressed or not * whenever it changes, check if it's changing to pressed or to not-pressed * only apply a "kick" when it's changing to pressed

Not too bad, but still a bit clunky.

With actors we can just listen for more events. Luckily for us, the Kywy engine has just the one we need: Kywy::Events::D_PAD_UP_PRESSED. This event comes from the engine.input actor so we'll also have to remember to subscribe our ball to that.

So lets add a -15 (upwards) kick to our yVelocity whenever we get a D_PAD_UP_PRESSED event.

We should only need 4 lines of code for this. Our new event handler:

      case Kywy::Events::D_PAD_UP_PRESSED:
        yVelocity -= 15;
        break;

and our new subscription:

  ball.subscribe(&engine.input);

In total we have:

#include "Kywy.hpp"

Kywy::Engine engine;

class Ball : public Actor::Actor {
public:
  float x = KYWY_DISPLAY_WIDTH / 2;
  float y = KYWY_DISPLAY_HEIGHT / 2;
  float xVelocity = 5;
  float yVelocity = 0;

  void handle(::Actor::Message *message) {
    switch (message->signal) {
      case Kywy::Events::TICK:
        // x and y position and velocity changes
        x = x + xVelocity;
        y = y + yVelocity;

        // collisions and bounces
        if (x < 12) {
          xVelocity = abs(xVelocity);
          x = 12;
        }

        if (x > KYWY_DISPLAY_WIDTH - 12) {
          xVelocity = -1 * abs(xVelocity);
          x = KYWY_DISPLAY_WIDTH - 12;
        }

        if (y < 12) {
          yVelocity = abs(yVelocity);
          y = 12;
        }

        if (y > KYWY_DISPLAY_HEIGHT - 12) {
          yVelocity = -1 * abs(yVelocity);
          y = KYWY_DISPLAY_HEIGHT - 12;
        }

        // gravity
        yVelocity += 1;

        // draw to the screen
        engine.display.clear();
        engine.display.drawCircle(
          (int)x, (int)y, 25,
          Display::Object2DOptions()
            .origin(Display::Origin::Object2D::CENTER));
        engine.display.update();
        break;

      // our new event handler
      case Kywy::Events::D_PAD_UP_PRESSED:
        yVelocity -= 15;
        break;
    }
  }
} ball;

void setup() {
  engine.start();

  // don't forget to subscribe to input events
  ball.subscribe(&engine.input);
  ball.subscribe(&engine.clock); 
  ball.start();
}

void loop() { 
  delay(1000);
}

And there we have it! A Kywy style ball bouncing around the screen and responding to button presses!

Info

The full list of events that you can recieve from the Kywy engine is here.

You can also create your own events to publish and handle! For more advanced Actor techniques check out our Actors Guide.