Skip to main content

User Input

With a moving snake, you've done a good part of the game. This part lets you give the control into the players hands. WASM-4 supports both gamepad and mouse input, but for this tutorial we will only use gamepad input. If the player presses a certain button, you just have to change the values of the "Direction"-Property of your snake.

That's it.

But first, you need to understand how WASM-4 handles user input.

Gamepad Basics#

WASM-4 accepts up to 4 gamepads and provides 4 variables that represent the current state of this gamepads. It contains a value 0 (zero) if nothing has been pressed; otherwise, it contains a sum of the buttons pressed, based on the table below:

ButtonValue
Button 11
Button 22
Left16
Right32
Up64
Down128

So if the player presses "Right" and "Button 1", the value would be 33. Now are all values "a power of two", meaning you can set and check them using binary operators.

If you want to check if Button 1 is pressed, simply use the binary AND:

This is true for all other buttons too.

Keyboard Layout#

For the player side of things, WASM-4 tries to cover most keyboard layouts:

  • X and Space = Button 1
  • Y, C and Z = Button 2

This should cover QWERTY, QWERTZ and Dvorak layouts.

The gamepads for the players 2, 3 and 4 are currently not implemented.

Detecting justPressed#

Since the current state of the gamepad is stored in a single variable, you need to compare it to the previous state.

You can achieve this by using the bitwise XOR operator. To make it short, here is the code snippet you can use:

(local $gamepad i32)(local $just-pressed i32)
;; gamepad = *GAMEPAD;(local.set $gamepad (i32.load8_u (global.get $GAMEPAD1)))
;; just-pressed = gamepad & (gamepad ^ prev-state);(local.set $just-pressed  (i32.and    (local.get $gamepad)    (i32.xor      (local.get $gamepad)      (global.get $prev-state))))

The local variable just-pressed now holds all buttons that were pressed this frame. You can check the state of a single button like this:

(if (i32.and (local.get $just-pressed) (global.get $BUTTON_UP))  (then    ;; Do something  ))

If you don't care why that is, skip to the next part.

Like I explained in "Gamepad Basics", the value of Gamepad 1 is a combination of all currently pressed buttons. If we store it and use XOR or later on, we only get the differences.

Let's assume the right button is currently pressed. In that case Gamepad 1 has the value 32 (Right = 32). Now the player presses Button 1. The value changes from 32 to 33 (Button 1 + Right = 1 + 32 = 33). By using XOR, we get 1 as a result.

In the next step, we compare it to the current state. Like: What buttons are new this frame.

Here's a "hands-on" example:

Frame 0: Gamepad1 = 0 (No buttons are pressed)Frame 1: Gamepad1 = 32 (Right button is pressed)
Difference between Frame 0 and 1:  00000000 (0)^ 00100000 (32)= 00100000 (32)
What's new:  00100000 (32)& 00100000 (32)= 00100000 (32)
Result: "32" is new
----
Frame 2: Gamepad1 = 33 (Right button and Button 1 are pressed)
Differences between Frame 1 and 2:  00100000 (32)^ 00100001 (33)= 00000001 (1)
What's new:  00000001 (1)& 00100001 (33)= 00000001 (1)
Result: "1" is new
----
Frame 3: Gamepad1 = 1 (Button 1 is pressed, Right button got released)
Difference between Frame 2 and 3:  00100001 (33)^ 00000001 (1)= 00100000 (32)
What's new:  00100000 (32)& 00000001 (1)= 00000000 (0)
Result: No new key was pressed this frame.

Changing Directions#

Now that you know how to detect if a key was pressed in the current frame, it's time you let the player change the direction of the snake.

Like most of this tutorial, this is step is rather easy once you've grasped how it works.

For this, you need to change the update function of in the main file. Remember, this is how it currently looks like:

(func (export "update")  ;; frame-count = frame-count + 1;  (global.set $frame-count (i32.add (global.get $frame-count) (i32.const 1)))
  ;; if ((frame-count % 15) == 0) ...  (if (i32.eqz (i32.rem_u (global.get $frame-count) (i32.const 15)))    (then      (call $snake-update)))
  (call $snake-draw))

The classic processing loop goes like this: Input, Process the input, output the result. Or in case of most games: User-Input, Update, Render. The last two steps are already in place. Now it's time to add the first part.

It's a good idea to handle the input in its own function. Something like this could be on your mind:

(func $input  (local $gamepad i32)  (local $just-pressed i32)
  ;; gamepad = *GAMEPAD;  (local.set $gamepad (i32.load8_u (global.get $GAMEPAD1)))
  ;; just-pressed = gamepad & (gamepad ^ prev-state);  (local.set $just-pressed    (i32.and      (local.get $gamepad)      (i32.xor        (local.get $gamepad)        (global.get $prev-state))))
  (if (i32.and (local.get $just-pressed) (global.get $BUTTON_LEFT))    (then      ;; Do something    )  ))
(func (export "update")  ;; frame-count = frame-count + 1;  (global.set $frame-count (i32.add (global.get $frame-count) (i32.const 1)))
  (call $input)
  ;; if ((frame-count % 15) == 0) ...  (if (i32.eqz (i32.rem_u (global.get $frame-count) (i32.const 15)))    (then      (call $snake-update)))
  (call $snake-draw))

If you try to compile this, you should get an error: error: undefined local variable "$prev-state". This is easily fixed. Just create a $prev-state global variable:

(global $prev-state (mut i32) (i32.const 0))

To notice any change in the gamepad, you have to store the current state at the end of the input. This will make it the previous state. And while you're at it, why not add the other 3 directions along the way:

(func $input  (local $gamepad i32)  (local $just-pressed i32)
  ;; gamepad = *GAMEPAD;  (local.set $gamepad (i32.load8_u (global.get $GAMEPAD1)))
  ;; just-pressed = gamepad & (gamepad ^ prev-state);  (local.set $just-pressed    (i32.and      (local.get $gamepad)      (i32.xor        (local.get $gamepad)        (global.get $prev-state))))
  (if (i32.and (local.get $just-pressed) (global.get $BUTTON_LEFT))    (then      ;; Do something    )  )
  (if (i32.and (local.get $just-pressed) (global.get $BUTTON_RIGHT))    (then      ;; Do something    )  )
  (if (i32.and (local.get $just-pressed) (global.get $BUTTON_UP))    (then      ;; Do something    )  )
  (if (i32.and (local.get $just-pressed) (global.get $BUTTON_DOWN))    (then      ;; Do something    )  )
  (global.set $prev-state (local.get $gamepad)))

If you want to check if it works: Use the trace function provided by WASM-4. Here's an example:

  (import "env" "trace" (func $trace (param i32)))
  ;; Put the string somewhere unused in memory.  (data (i32.const 0x3000) "down\00")
  (func $input    ...
    ;; LEFT    (if (i32.and (local.get $just-pressed) (global.get $BUTTON_LEFT))      (then        (call $trace (i32.const 0x3000))      )    )
    ...  )

If you use trace in each if-statement, you should see the corresponding output in the console.

Now, instead of using trace to confirm everything works as intended, you should replace it with something like this:

    (if (i32.and (local.get $just-pressed) (global.get $BUTTON_LEFT))      (then        (call $snake-down)))

I'll leave it to you, to finish the other 3 directions.

You'll be - once again - rewarded with error messages:

main.wat:127:13: error: undefined function variable "$snake-left"      (call $snake-left)            ^^^^^^^^^^^main.wat:134:13: error: undefined function variable "$snake-right"      (call $snake-right)            ^^^^^^^^^^^^main.wat:141:13: error: undefined function variable "$snake-up"      (call $snake-up)            ^^^^^^^^^main.wat:148:13: error: undefined function variable "$snake-down"      (call $snake-down)            ^^^^^^^^^^^

To fix this, add those functions to your snake. Here's an example for down:

(func $snake-down  ;; if (direction.y == 0) {  ;;   direction.x = 0;  ;;   direction.y = 1;  ;; }  (if (i32.eq (i32.load (i32.const 0x19a4)) (i32.const 0))    (then      (i32.store (i32.const 0x19a0) (i32.const 0))      (i32.store (i32.const 0x19a4) (i32.const 1)))))

First, it checks if the direction is already changing the Y-Direction. Only if it isn't allow the change. And then change the Y-Direction to 1. The Up direction requires a Y-Direction of -1. Left and Right don't check the Y, but the X and change it accordingly (Left: -1, Right: 1).

With this knowledge, you should be able to implement them. If you're unsure, check the source in the repository.

Controlled Snake