Game development for Apple platforms

Adding a repeat-rate for long-held buttons

In the previous post I looked at handling game controller input. The approach was:

One thing that was missing was emitting repeated signals if a button was held down for a longer time. Similar to how your spacebar behaves in a text editing program. For example, if you press and hold it, you might expect this behavior:

<space> ......................... <space> <space> <space> <space>

I.e., you would receive an initial signal, followed by a short delay. After this delay - if you are still holding the space bar - you would start receiving signals in rapid succession.

I played around with Combine a bit and achieved this same effect. Let me walk you through how I did it.

Sidenote: initial prototyping with Swift Playgrounds

As a side note, I wanted to mention that I did the initial prototype in Swift Playgrounds on an iPad. I was very tired and didn't feel like working at the computer. So I lounged on the couch, put on Bob's Burgers in the background, and fired up Swift Playgrounds.

It worked out surprisingly well:

It let me have a UIKit button (to send the raw input signal) and play with Combine in the left pane, to see how to achieve the desired effect.

After about two hours of messing around, I had something that worked, and that could be transplanted to the actual game code.

Step 1 - stream of last-pressed events

The first piece to the puzzle was something I realized I needed very late in the process (during testing).

When using the DPAD on a game controller, if you want to switch directions, you would likely release the currently-pressed direction, and then press a new direction. Thus you would receive these events:

left pressed -> left released -> down pressed -> down released

However, with a thumbstick, you might receive them in a different order:

left pressed -> down pressed -> left released -> down released

This messed up my implementation because while I would keep holding down, the previous state would be released (by the left button), and the stream stopped.

So I added a stream to basically only emit a signal when a different button is pressed. This will later be used to disregard certain signals.

In other words, I am making the assumption that I will only ever care about the most recently pressed button.

Here is an illustration of how this Combine stream works:

Step 2 - ignoring events with the help from step 1

Now that I always know which button I care about, I can combine this stream with the raw input stream to filter out events I don't care about, and simplify later processing:

Note that every event is doubled. This is intentional. In the game controller value change handlers, I send every change to my raw input stream twice. You'll see why in the next step.

Step 3 - aggregating events into panes of 2

I care about different event states:

To identify when the state for a button changes, I can't just look at a single state. I need to keep track of the last 2 states. So if two subsequent states are [Released, Pressed] - that is a button press event. If two subsequent states are [Pressed, Released] then a button was released. If two subsequent states are [Pressed, Pressed], then a button is continuously held.

I use the Combine scan() operator to aggregate events and look at them in groups of 2. However, if I don't duplicate events in the raw input stream, I run into a problem.

What will the event stream look like, if a button is pressed and released, and every input event is reported only once?

If I want to repeat the signal while the key is being held, I am going to use a timer. If I use combineLatest() with this stream, then every timer tick will keep emitting [down, up] - i.e. it will keep reporting that the key has been continuously let go.

Whereas if we duplicate the raw input events:

Now we get a more precise description of what is happening:

This is why reporting every raw input event twice gives a more precise picture of what's happening.

The overall schema of the third step is this:

When I say - in the diagram - "map into a Pane structure" I just mean I map the tuples of the last two events into a single Swift struct, which semantically maps to a single change event.

The final action is to combine the change event stream with a timer. This is nifty, because:

So now we have almost everything we need:

The final step is to put this all together.

Step 4 - the final stream

After the initial delay has passed (in this example - 0.3 seconds), then the repeat rate is determined by the timer interval.

And voilà! With this, we can have a neat stream that maps raw input events (button pressed or released) and outputs events with a nice repeat-rate and initial delay.

#combine #game controller #prototype #swift #swift playgrounds