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 = $GAMEPAD1local just_pressed = gamepad & (gamepad ~ prev_state)

The constant just_pressed now holds all buttons that were pressed this frame. You can check the state of a single button like this:

if just_pressed & BUTTON_UP ~= 0 then  -- Do somethingend

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:

local function update()  frame_count = frame_count + 1
  if frame_count % 15 == 0 then    snake:update()  end
  snake:draw()end

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:

local function input()  local gamepad = $GAMEPAD1  local just_pressed = gamepad & (gamepad ~ prev_state)
  if just_pressed & BUTTON_UP ~= 0 then    -- Do something  endend
local function update()  frame_count = frame_count + 1
  input()
  if frame_count % 15 == 0 then    snake:update()  end
  snake:draw()end

If you try to compile this, you should get an error: error: undeclared symbol 'prev_state'. This is easily fixed. Just place the prev_state into the var-section:

local snake = Snake.init()local frame_count = 0local prev_state = 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:

local function input()  local gamepad = $GAMEPAD1  local just_pressed = gamepad & (gamepad ~ prev_state)
  if just_pressed & BUTTON_LEFT ~= 0 then    -- Do something  end  if just_pressed & BUTTON_RIGHT ~= 0 then    -- Do something  end  if just_pressed & BUTTON_UP ~= 0 then    -- Do something  end  if just_pressed & BUTTON_DOWN ~= 0 then    -- Do something  end
  prev_state = gamepadend

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

if just_pressed & BUTTON_DOWN ~= 0 then  trace("down")end

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 just_pressed & BUTTON_DOWN ~= 0 then  snake:down()end

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

You'll be - once again - rewarded with an error message:

src/main.nelua:1:1: from: AST node Blockrequire "wasm4"^~~~~~~~~~~~~~~src/main.nelua:19:3: from: AST node Block  local gamepad = $GAMEPAD1  ^~~~~~~~~~~~~~~~~~~~~~~~~src/main.nelua:23:5: from: AST node Block    snake:left()    ^~~~~~~~~~~~src/main.nelua:23:10: error: cannot index meta field 'left' for type 'snake.Snake'    snake:left()         ^~~~~~~

Nelua will only list this first unknown it finds, but you'll get an error for each function as you add them. To fix the errors, add those functions to your snake. Here's an example for down:

function Snake:down()  if self.direction.y == 0 then    self.direction = {x = 0, y = 1}  endend

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