Safely Sending Signals (Part 1)

In many Blinks games, we need to send messages to a whole bunch of connected blinks. In normal programming, you can set variables and send messages globally with no trouble. But Blinks are distributed: each Blink is its own little world, and sending reliable, secure messages across a field is tricky.

There are plenty of ways to do this, but we came up with a simple way to do it that we used in pretty much all of the launch titles. Let’s set up our basic game.

First, we need to create some communication states. We’ll start by making an enum of our signal states, then a byte to hold the current state.

enum signalStates {INERT, GO, RESOLVE};
byte signalState = INERT;

Now we’ll just make a super quick display method that shows our state, and add it to loop()

void loop() {
  displaySignalState();
}

void displaySignalState() {
  switch (signalState) {
    case INERT:
      setColor(CYAN);
      break;
    case GO:
      setColor(RED);
      break;
    case RESOLVE:
      setColor(YELLOW);
      break;
  }
}

If we install this on a Blink, you’ll just get a cyan Blink that… doesn’t do anything. To make it do something, we’ll need to build out a bit more of the game’s architecture. We’ll start by making a bunch of separate game loops.

void loop() {
  switch (signalState) {
    case INERT:
      inertLoop();
      break;
    case GO:
      goLoop();
      break;
    case RESOLVE:
      resolveLoop();
      break;
  }
  displaySignalState();
}

void inertLoop() {
}

void goLoop() {
}

void resolveLoop() {
}

Inside of inertLoop, we want to listen for single clicks, and when we hear one, we will move into GO. It looks like this:

void inertLoop() {
  //set myself to GO
  if (buttonSingleClicked()) {
    signalState = GO;
  }
}

Install that, and you’ll see the blink turn red on single press. What should this now do? Well, we need to tell all of our neighbors to also transition to “GO” and, at the same time, we should make sure they are listening. We’ll do this by adding a “setValueSentOnAllFaces” to the main loop, and a basic listener in our INERT loop.

void loop() {
  switch (signalState) {
    case INERT:
      inertLoop();
      break;
    case GO:
      goLoop();
      break;
    case RESOLVE:
      resolveLoop();
      break;
  }

  displaySignalState();

  setValueSentOnAllFaces(signalState);
}

void inertLoop() {
  //set myself to GO
  if (buttonSingleClicked()) {
    signalState = GO;
  }

  //listen for neighbors in GO
  FOREACH_FACE(f) {
    if (!isValueReceivedOnFaceExpired(f)) {//a neighbor!
      if (getLastValueReceivedOnFace(f) == GO) {//a neighbor saying GO!
        signalState = GO;
      }
    }
  }
}

So now if you install this on a set of Blinks, you’ll notice that clicking a Blink will transition all of the Blinks into GO, because they’re all listening. How do we go from GO to RESOLVE? Another listener, of course. But this one is special: it listens to make sure that ALL neighbors have heard the GO news, and that it’s safe to begin resolving. It looks like this:

void goLoop() {
  signalState = RESOLVE;//I default to this at the start of the loop. Only if I see a problem does this not happen

  //look for neighbors who have not heard the GO news
  FOREACH_FACE(f) {
    if (!isValueReceivedOnFaceExpired(f)) {//a neighbor!
      if (getLastValueReceivedOnFace(f) == INERT) {//This neighbor doesn't know it's GO time. Stay in GO
        signalState = GO;
      }
    }
  }
}

If you install again, you’ll see that the Blinks now transition from INERT to GO, and then super quickly into RESOLVE. In fact, you may only see a brief flash of red. But now they’re stuck in RESOLVE. To go back to INERT, we’ll make a very similar listening loop as above.

void resolveLoop() {
  signalState = INERT;//I default to this at the start of the loop. Only if I see a problem does this not happen

  //look for neighbors who have not moved to RESOLVE
  FOREACH_FACE(f) {
    if (!isValueReceivedOnFaceExpired(f)) {//a neighbor!
      if (getLastValueReceivedOnFace(f) == GO) {//This neighbor isn't in RESOLVE. Stay in RESOLVE
        signalState = RESOLVE;
      }
    }
  }
}

Install this and you’ll see the entire process happen on the Blinks. Click any Blink to send it to GO, watch that signal propogate and then transition to RESOLVE, and then back to INERT. It should happen super quickly. GO and RESOLVE will be quick red and yellow flashes respectively.

So why did we do all this? Well, what we’ve created here is a way to send a signal (in this case “GO”) that is safely received by every Blink a single time. In most cases, this is used to create a game-wide state change. To demonstrate, we’ll build a super quick state so that we can change it. First, we’ll make a boolean up at the top, and then we’ll add a little check in the display code so you can see it visually.

bool gameState = true;//creates 2 simple game states. I'll associate a color with each

...

void displaySignalState() {
  switch (signalState) {
    case INERT:
      if (gameState == true) {
        setColor(CYAN);
      } else {
        setColor(BLUE);
      }
      break;
    case GO:
      setColor(RED);
      break;
    case RESOLVE:
      setColor(YELLOW);
      break;
  }
}

With that in, we just need to figure out when and where to change this state. Generally speaking, we would want to do this on a transition. For reasons that will become clear later, we’ll do it when you go from INERT to GO. Also, we’ll do it in its own little function so we can call it from two different places without repeating the code.

void inertLoop() {
  //set myself to GO
  if (buttonSingleClicked()) {
    signalState = GO;
    changeGameState();
  }

  //listen for neighbors in GO
  FOREACH_FACE(f) {
    if (!isValueReceivedOnFaceExpired(f)) {//a neighbor!
      if (getLastValueReceivedOnFace(f) == GO) {//a neighbor saying GO!
        signalState = GO;
        changeGameState();
      }
    }
  }
}

...

void changeGameState() {
  gameState = !gameState;//toggles from TRUE to FALSE
}

Again, install this on a few Blinks and you will see the state change happening. Just in case you missed a step, the entire game should look like this:

enum signalStates {INERT, GO, RESOLVE};
byte signalState = INERT;

bool gameState = true;//creates 2 simple game states. I'll associate a color with each

void setup() {

}

void loop() {
  switch (signalState) {
    case INERT:
      inertLoop();
      break;
    case GO:
      goLoop();
      break;
    case RESOLVE:
      resolveLoop();
      break;
  }

  displaySignalState();

  setValueSentOnAllFaces(signalState);
}

void inertLoop() {
  //set myself to GO
  if (buttonSingleClicked()) {
    signalState = GO;
    changeGameState();
  }

  //listen for neighbors in GO
  FOREACH_FACE(f) {
    if (!isValueReceivedOnFaceExpired(f)) {//a neighbor!
      if (getLastValueReceivedOnFace(f) == GO) {//a neighbor saying GO!
        signalState = GO;
        changeGameState();
      }
    }
  }
}

void goLoop() {
  signalState = RESOLVE;//I default to this at the start of the loop. Only if I see a problem does this not happen

  //look for neighbors who have not heard the GO news
  FOREACH_FACE(f) {
    if (!isValueReceivedOnFaceExpired(f)) {//a neighbor!
      if (getLastValueReceivedOnFace(f) == INERT) {//This neighbor doesn't know it's GO time. Stay in GO
        signalState = GO;
      }
    }
  }
}

void resolveLoop() {
  signalState = INERT;//I default to this at the start of the loop. Only if I see a problem does this not happen

  //look for neighbors who have not moved to RESOLVE
  FOREACH_FACE(f) {
    if (!isValueReceivedOnFaceExpired(f)) {//a neighbor!
      if (getLastValueReceivedOnFace(f) == GO) {//This neighbor isn't in RESOLVE. Stay in RESOLVE
        signalState = RESOLVE;
      }
    }
  }
}

void changeGameState() {
  gameState = !gameState;//toggles from TRUE to FALSE
}

void displaySignalState() {
  switch (signalState) {
    case INERT:
      if (gameState == true) {
        setColor(CYAN);
      } else {
        setColor(BLUE);
      }
      break;
    case GO:
      setColor(RED);
      break;
    case RESOLVE:
      setColor(YELLOW);
      break;
  }
}

As you can see, the state change happens quickly across the entire field, and everything is in sync. Well… everything is mostly in sync. You’ll notice that if you pull a Blink out by itself and click it, it’ll change state. Then, when you reintroduce it and click, it will permanently be out of sync with the others. To fix this, we’ll need to learn about some more advanced signal techniques. Check out the next tutorial to find out more!

4 Likes

Great introduction for solid game architecture, thanks for writing this up!

Thanks for the tutorials

I found a problem when using the dev kit together with 5 other blinks and setting them up in a circle.
Often 1 remains in GO and the 2 connected blinks in RESOLVE. I think that the signal from the dev kit traverses the empty space but the signal from the normal blink doesn’t, resulting in miscommunication.

But even without the dev kit there is a problem in the circle setup. Quite often one blink will get out of sync because it receives the GO from both sides simultaneously therefore toggling the game state twice.
I was able to resolve it by breaking out of the for each loop once the GO signal has been received.

void inertLoop() {
  //set myself to GO
  if (buttonSingleClicked()) {
    signalState = GO;
    changeGameState();
  }
  //listen for neighbors in GO
  FOREACH_FACE(f) {
    if (!isValueReceivedOnFaceExpired(f)) {//a neighbor!
      if (getLastValueReceivedOnFace(f) == GO) {//a neighbor saying GO!
        signalState = GO;
        changeGameState();
        break; //exit the loop to prevent changing the game state twice
      }
    }
  }
}

Lachi,

So I have not actually re-run this code on the new dev blinks, but thanks for pointing this out! I will do some investigation about that.

As for the async, that’s a little bit on purpose here. The signals in this tutorial are slightly insecure until you implement the solutions in Part 2. So the behavior you’re seeing is expected, and is purposefully breakable so that the second tutorial has something to fix. Hope that helps!

Got here from @jbobrow’s topic.

If I understand the algorithm, it seems like there is still a race condition.

Say I have two adjacent tiles A & B, both in INERT. Clicking A will send it to GO and send the signal to cause B to enter GO.

As soon as tile B enters GO it will send back the signal, which might cause A to move on to RESOLVE. But tile B might still be waiting for its other neighbors to enter GO.

If tile A enters RESOLVE fast enough, it will no longer be in GO and tile B will be stuck. Tile B is waiting for all of its neighbors to be in GO, but tile A has already moved on to RESOLVE.

This may never happen in practice, of course.

Did you see my approach to this?