Basic Time Functions

Many Blinks games use time-based mechanics. And unlike many board games, where timers need to be used to enforce these mechanics, Blinks can do this themselves! In this tutorial we’ll go over the two major ways of using time, as well as some convenience functions that can help.

Let’s start with the “millis()” function. If you call millis() in your script, it will return the milliseconds elapsed since the script began. This is essentially a stopwatch that starts when the game starts, and never ends. Okay, technically it rolls back to 0 after a huge amount of time, but for our purposes it just counts up. This is useful in a couple of ways, like tracking the timing of an event in your script, but mostly we use it to make pulses. Let’s do that now.

We’re going to use the dim() function we learned earlier to dim our Blinks in a pulsing pattern. We know the dim command only accepts values from 0 to 255, so we’ll truncate our values to match like this:

void loop() {
  byte dimness = millis() % 256;
  setColor(dim(WHITE, dimness));
}

Install that on a Blink and it’ll give you very rapid fade up to white over and over again. There are two ways we might want to adjust this pattern. For starters, it might be nice if it didn’t drop immediately from full brightness to off each time. We can solve that by using a sine wave pattern, which we can conventiently get by using the built in sin8_C() function, like this:

byte dimness = sin8_C(millis() % 256);

Put this back on your Blink and the pattern will now be a smooth transition from off to on and back off again. This is because the sin8_C() command takes a byte from 0-255 and transforms it using a sin function. Before our value went from 0-255 in a linear fashion, then back to 0. Transformed, it goes from 0 up to 255 and back to 0 in the same time.

But it’s kinda hard to make out the smoothness with how fast it’s moving. We need to figure out a way to make the pulse time… longer. There are many ways you could do this using simple arithmetic or other methods, but the Blinks library has another great convenience function that would help here. Before we implement it, we’ll just do a bit more setup.

At the top of the script, we’re going to define a “PULSE_LENGTH” like this:

#define PULSE_LENGTH 2000

Then we’re going to change the code in our loop to look like this:

void loop() {
  //get progress from 0 - MAX
  int pulseProgress = millis() % PULSE_LENGTH;

  //transform that progress to a byte (0-255)
  byte pulseMapped = map(pulseProgress, 0, PULSE_LENGTH, 0, 255);

  //transform that byte with sin
  byte dimness = sin8_C(pulseMapped);

  //set color
  setColor(dim(WHITE, dimness));
}

If you haven’t already, install that onto your Blink so you can see your smooth 2-second pulse. That new piece of code you’re seeing is the map() function. It is probably the most convenient of the convenience functions, and you will see it pretty much everywhere in our code. What it does is it takes a number, in this case “pulseProgress,” which lies between an input minimum and an input maximum, in this case 0 and PULSE_LENGTH, then maps it to the number it would be between an output minimum and and output maximum, in this case 0 and 255. So, for example, if pulseProgress was 1000, that would put it halfway between 0 and PULSE_LENGTH, which is defined as 2000. Therefore its output value, mapped to 0-255, would be 127, which is halfway between the two.

This function has some restrictions, though. For instance, minimums and maximums must maintain a low-to-high relationship - you can’t get tricky and try to set a higher number as a minimum and get something to map in reverse. You’ll need to do that after the fact. In addition, we don’t recommend inputting mapping values that are beyond the bounds of the input min and max - not that it won’t work, only that it’s not designed to work that way and can cause issues in some cases. But for the most part, this shouldn’t be an issue. Frankly, in most cases you’ll be using it to do exactly what we’re doing here: mapping a big number down to 0-255 so you can use it in visual codes.

Now it’s time for us to move to the next big way we deal with time, and that is by using Timers. Timers work pretty much exactly how you’d expect, so I’m just going to make a super quick example that uses all three timer commands, and adds an interesting little flourish to our existing code.

We’ll start by declaring a timer at the top, along with a new duration definition for that timer.

#define TIMER_LENGTH 6000
Timer redTimer;

We’ll also use the first Timer command, Timer.set(), in the setup function:

void setup() {
  redTimer.set(TIMER_LENGTH);
}

This sets the timer to run for the allotted number of milliseconds, which in this case is 6000, or 6 seconds. What we’re going to do now is turn the Blink red while the timer is running. To do this, we need to know if the timer is still running, and we’ll do that by checking if the Timer is expired in loop(). If it is, we’ll adjust the way we display color. This is how we’ll update the code:

//check if timer is expired
  byte saturation = 0;
  if (!redTimer.isExpired()) {
    saturation = 255;
  }

  //set color
  setColor(makeColorHSB(0, saturation, dimness));

isExpired() returns true if the Timer has run out of time, and false if it’s still going. For our purposes, we’re actually more interested in times when it’s not expired, that’s why we’ve thrown the “!” before our statement. This code will change the color of the Blink to red in the first 6 seconds after you start the code. But wouldn’t it be cooler if instead of just staying solidly red, it faded back to white? Of course it would, and we can do that by slowly changing the saturation over time. We’ll get the values we need by using the last Timer function, Timer.getRemaining(). This returns the amount of time remaining on the timer, from the full set time to 0. Let’s take that time and map it 0-255 using the map function, like this:

  byte saturation = 0;
  if (!redTimer.isExpired()) {
    int timerProgress = redTimer.getRemaining();
    saturation = map(timerProgress, 0, TIMER_LENGTH, 0, 255);
  }

If you install this code, you’ll see the flashes start out red, then slowly desaturate to white over the first 6 seconds. It’s not necessarily the best-looking effect, but through experimentation you can find all sorts of interesting fades and effects.

And those are all the time-based functions, plus some other convenience functions to boot! As always, here’s the full code of what we made in case you missed a step.

#define PULSE_LENGTH 2000
#define TIMER_LENGTH 6000
Timer redTimer;

void setup() {
  redTimer.set(TIMER_LENGTH);
}

void loop() {
  //get progress from 0 - MAX
  int pulseProgress = millis() % PULSE_LENGTH;

  //transform that progress to a byte (0-255)
  byte pulseMapped = map(pulseProgress, 0, PULSE_LENGTH, 0, 255);

  //transform that byte with sin
  byte dimness = sin8_C(pulseMapped);

  //check if timer is expired
  byte saturation = 0;
  if (!redTimer.isExpired()) {
    int timerProgress = redTimer.getRemaining();
    saturation = map(timerProgress, 0, TIMER_LENGTH, 0, 255);
  }

  //set color
  setColor(makeColorHSB(0, saturation, dimness));
}
2 Likes

Forgive me for digging up this old post, but I’m trying to implement something similar to the first section (using sin8_C function to create a pulsating light fade). Is it possible to set a “floor/ceiling”, or a value where the sine wave would stop before traveling back in the opposite direction?

I’m trying to implement a pulsating light for a game, but I don’t want it to fade all the way down to 0. Instead, I’m trying to get the dimness to stop at, say, 100. I’ve been tinkering with the sample code in this post but I can’t seem to get that working properly. Does anyone have any suggestions?

Hi @jrcwest,

here’s a quick option

  byte bri = 127 + sin8_C(millis())/2;
  setColor(dim(RED, bri));

A slightly more descriptive version is here:

  byte low = 127;
  byte high = 255;
  byte t = millis();
  byte range = 256 / (high-low);
  
  byte bri = low + sin8_C(t) / range;
  setColor(dim(RED, bri));

note that integer math will make this second example a little wonky depending on values.

1 Like

Here is one way to get rid of that wonky integer math

  byte low = 100;
  byte high = 255;
  byte t = millis();

  byte bri = low + (((high-low) * sin8_C(t) )/ 255);
  setColor(dim(RED, bri));
1 Like

Math is your friend! :slight_smile: I will not generally comment on the usage patterns discussed in the OP (I would never use the map function except for the most basic games as it takes a lot of space and the same effect can be achieved in other ways), but here is the solution to what you want.

First, you want the minimum value to be 100, so let’s do that.

byte dimness = sin8_C(pulseMapped) + 100;

The above does not quite do what you want, the minimum value would be, in theory, 100, but the maximum value is now 355 and a byte can not hold this value so it will wrap-around, the effect being you are back at the same range.

So now you need to make sure the maximum value is still 255. To do that, you will need to compress the range which was originally 0 to 255 to 100 to 255. Here is the full solution:

byte original_dimness = sin8_C(pulseMapped);

byte new_dimness = (byte)(((int)original_dimness * (int)155 / (int)255) + (int)100);

The int cast is because we are doing integer arithmetic and if you do not do that, you are working with very small numbers and will not get the results you would expect. You also do not need all the cast but doing them is easier than explaining the C conversion rules. Converting to int improves things a bit (then we convert back to byte).

Now we can test it:

if original_dimness is 0:

new_dimness = (0 * 155 / 255) + 100 = 100

And if it is 255:

new_dimness = (255 * 155 / 255) + 100 = 255

Which is what you wanted.

Here is the general rule to convert to a range to another one, you can make a function out of it if you want:

old_range = (old_max - old_min);
new_range = (new_max - new_min);
new_value = (((old_value - old_min) * new_range) / old_range) + new_min

2 Likes

@BGA @jbobrow Thanks for the replies! I’ll try both of your suggestions when I get the next opportunity to open up my code. And thanks for formatting your responses in ways that are easy to understand.

This was one of the first things I tried, but I wasn’t sure how the Blink would handle the excess above the max value. Part of me just assumed it would cap out at 255, but it’s good to know what was actually happening. Thanks for that explanation!

No worries, glad to help.

I wrote the reply too early in the morning and when doing the casts to int, I want to multiply all numbers by 100 before doing the math and them dividing the final result by 100 before converting to byte. The reason is that this helps with the precision of the integer operations (specially the division) and it is always a good thing to do.

1 Like

@BGA I went ahead with your solution and it worked perfectly. it was pretty easy to tweak as well to get the dimness just where I wanted. Thanks again!