So I've been building a Tetris clone for a day or so and had to learn how to handle keyboard events in :wx, so I figured I'd pass that knowledge along. Let's get started.

Project

The project I'm working on is called extris and it's available on my github account. You can check out the tag before_episode_114 to see the code as it stands before this episode.

Overview of existing code

I wanted to show you the existing code. First, off, let's just run it:

iex -S mix
# click the rotation buttons some

So here is the first part of my tetris clone. At this point, I'm capable of rendering all of the shapes and rotating them around on a canvas via button presses.

Obviously, button presses aren't sufficient, and that's why we're adding keyboard handling, but first let's spelunk through the code to see how this works.

First, have a look at the shapes. Open up lib/extris/shapes.ex. Here, you can see that we've defined each of the base shapes, and then our shapes module attribute returns a map for each shape, and each of its possible orientations. We won't dig through the rotator code yet, just trust me that it rotates (well actually, there are tests, you don't have to trust me).

The real core of the graphical part of this app is the Window module. Open up lib/extris/window.ex.

  # look in do_init
  def do_init(_config) do
    #...
  end

Here's where we're doing the entirety of the window. We set up a window to have a couple of buttons that say left and right, then we show the window and enter the main loop. Once that loop exits, we destroy the window.

It's important to note that we passed a State object into the loop, so let's look at that:

  defmodule State do
    defstruct shape: :ell, rotation: 0
  end

So the state just contains the currently falling shape and its rotation, for now. This way the game board knows what to draw. We generate the shape randomly.

Let's look at the loop:

  def loop(state, frame) do
    #...
  end

So here you can see that for each pass through the loop, we draw the game board and then await input. If the input was the left or right buttons being pressed, we increase the rotation by 1 mod 4, and we loop again. This is how our current rotation works.

Adding Keypress Observers

In order to start making an actual game out of this, we need to be able to respond to user keyboard input. In wx, this is handled by wxKey events. First, we need to import the record from the wx hrl file:

  Record.defrecordp :wxKey, Record.extract(:wxKey, from_lib: "wx/include/wx.hrl")

Next, we need to tell wx to notify our process of any events:

  def do_init(_config) do
    #...
    for action <- [:key_down, :key_up, :char] do
      :wxWindow.connect(frame, action)
    end
    #...
  end

Finally, when those events come in they have a keyCode property, which is what we care about. For now, we'll make 'a' rotate clockwise by 90 degrees, and 'd' rotate counterclockwise by 90 degrees.

  def loop(state, frame) do
    receive do
      #...
      wx(event: wxKey(keyCode: 65)) ->
        state = %State{state | rotation: rem(state.rotation + 1, 4)}
        loop(state, frame)
      wx(event: wxKey(keyCode: 68)) ->
        state = %State{state | rotation: rem(state.rotation - 1, 4)}
        loop(state, frame)
      #...
    end
  end

Alright, we'll go ahead and run it again (,,). I have to click the buttons once before the keypresses start making it into the loop, and I haven't figured that out yet, but apart from that this just works like I want it to.

(( press a and d a fair bit ))

Summary

That's it for today's episode. Now you know how to handle keyboard events using wx. See you soon!

Resources