Wiki

Clone wiki

SpecGine / Input

Adding configuration support and input handling

To properly support input, we should not hard-code key bindings. This is why we start from adding configuration.

Keys configuration

Configuration in SpecGine is persistent, with Libgdx mechanics for file creation. File name is based on unique name we already set in Ping class when extending Game. Right now, we will extend Play class to set default configuration if it was not set earlier. Open core/src/main/scala/Play.scala and add following lines to initialize method.

#!scala
//...
import com.specdevs.specgine.core.Config

import com.badlogic.gdx.Input.{Keys=>GdxKeys}
//...
  def initialize() {
    setIfNotSet[Config,Int]("playerA_up", GdxKeys.UP)
    setIfNotSet[Config,Int]("playerA_down", GdxKeys.DOWN)
    setIfNotSet[Config,Int]("playerB_up", GdxKeys.W)
    setIfNotSet[Config,Int]("playerB_down", GdxKeys.S)
    //...
  }
//...

We will prefix key name with either "playerA" or "playerB" to distinguish between right and left paddle. We now need to connect paddles with inputs. We do so using new component added to core/src/main/scala/components.scala.

#!scala
//...

case class KeyBindings(val name: String) extends Component

Now, again in initialize method of Play class we include new component in paddles.

#!scala
    //...

    // paddleA
    createEntity(
      Position(tableWidth/2f-paddleWidth, -paddleHeight/2f),
      Body(Paddle(paddleWidth, paddleHeight)),
      Color(1.0f, 0.5f, 0.5f),
      Layer(1),
      KeyBindings("playerA")
    )

    // paddleB
    createEntity(
      Position(-tableWidth/2f, -paddleHeight/2f),
      Body(Paddle(paddleWidth, paddleHeight)),
      Color(0.5f, 0.5f, 1.0f),
      Layer(1),
      KeyBindings("playerB")
    )

    //...

Now, with keys configured, we can proceed to creating input system.

Input system

We will place input system inside file core/src/main/scala/InputSystem.scala. We start from simple definition.

#!scala
package com.specdevs.ping

import com.specdevs.specgine.states.{ComponentsSpec,Processor,SingleEntitySystem}
import com.specdevs.specgine.macros.states.{withManager,Needed}

class InputSystem extends SingleEntitySystem with Processor {
  class InputSpec extends ComponentsSpec {
    val velocity = new Needed[Velocity]
    val keyBindings = new Needed[KeyBindings]
  }

  val components = withManager[InputSpec]

  def initialize() {}

  def create() {}

  def enter() {}

  def leave() {}

  def dispose() {}
}

And then add it to Ping class.

#!scala
  def initialize() {
    //...
    addSystem(new InputSystem)
    //...
  }

With empty system in place, we can start implementing first type of input handling.

Key input handling

To implement key processing we will use standard Scala queue into which we will place all events. We need to take special care to deal with key-repeat on some systems present, when key is held for longer time.

We add KeyReceiver to list of classes extended by InputSystem. We also add queue for key events and field for last pressed key. Then, we add keyUp and keyDown methods. We should also ensure, that when state is reinitialized, queue and last key are reset.

#!scala
//...
import com.specdevs.specgine.input.KeyReceiver

import scala.collection.mutable.Queue
//...
class InputSystem extends SingleEntitySystem with KeyReceiver with Processor {
  //...

  private val keysQueue = new Queue[(Int, Boolean)]

  private var lastKey = -1

  //...

  def enter() {
    lastKey = -1
    keysQueue.clear()
  }

  //...

  override def keyDown(key: Int) = {
    if (lastKey != key) {
      keysQueue.enqueue((key, true))
      lastKey = key
      true
    } else {
      false
    }
  }   

  override def keyUp(key: Int) = {
    keysQueue.enqueue((key, false))
    lastKey = -1
    true
  }
}

Now that we have have queue, we can use it during call process. To simplify it, we will keep the queue untouched for all entities and clear it in end method, when all processing is done.

#!scala
//...
import com.specdevs.specgine.core.Config
import com.specdevs.specgine.states.Entity
//...
  private def issueCommand(e: Entity, up: Boolean, state: Boolean) {
    val keyDirection = if (up) 1 else -1
    val stateDirection = if (state) 1 else -1
    val direction = keyDirection*stateDirection
    components.velocity(e).y += direction*2f/3f
  }

  override def process(dt: Float, e: Entity) {
    val bindingsName = components.keyBindings(e).name
    val upKey = get[Config,Int](bindingsName+"_up")
    val downKey = get[Config,Int](bindingsName+"_down")
    for ((key, state) <- keysQueue) {
      key match {
        case `upKey` => issueCommand(e, true, state)
        case `downKey` => issueCommand(e, false, state)
        case _ => ()
      }
    }
  }

  override def end(dt: Float) {
    keysQueue.clear()
  }

Making paddles move

Unfortunately, paddles does not move yet. This is because they do not have Velocity and OldPosition components. We add them in Play class, where they are defined.

#!scala
  def initialize() {
    //...

    // paddleA
    createEntity(
      Position(tableWidth/2f-paddleWidth, -paddleHeight/2f),
      OldPosition(tableWidth/2f-paddleWidth, -paddleHeight/2f),
      Velocity(0f, 0f),
      Body(Paddle(paddleWidth, paddleHeight)),
      Color(1.0f, 0.5f, 0.5f),
      Layer(1),
      KeyBindings("playerA")
    )

    // paddleB
    createEntity(
      Position(-tableWidth/2f, -paddleHeight/2f),
      OldPosition(-tableWidth/2f, -paddleHeight/2f),
      Velocity(0f, 0f),
      Body(Paddle(paddleWidth, paddleHeight)),
      Color(0.5f, 0.5f, 1.0f),
      Layer(1),
      KeyBindings("playerB")
    )

    //...
  }

Now, paddles move up and down when keys are pressed. Still, they can go beyond the screen. We will fix it by adding case for it inside CollisionSystem in core/src/main/scala/CollisionSystem.scala.

#!scala
  private def paddleWall(pos1: Position, v1: Velocity, p: Paddle, pos2: Position, t: Table) {
    if (pos1.y+p.height>pos2.y+t.height) {
      pos1.y = pos2.y+t.height-p.height
    }
    if (pos1.y<pos2.y) {
      pos1.y = pos2.y
    }
  }

  //...

  override def process(dt: Float, e1: Entity, e2: Entity) {
    if (e1 != e2) {
      //...
      shape1 match {
        case p: Paddle => {
          shape2 match {
            case t: Table => paddleWall(pos1, vel, p, pos2, t)
            case _ => ()
          }
        }
        //...
      }
    }
  }

With all above, if we play using keyboard, we have first playable version of game. We will now add mouse and touchscreen input.

Mouse and touchscreen input

To implement mouse and touchscreen input we will use same keys queue used by keyboard input. We will use PointerReceiver and split screen into four equally sized quadrants. We will need to store current screen size to translate pointer coordinates into events. We also need to remember where each pointer clicked, so pressing finger on one quadrant of screen and releasing it on another still works. We will use standard Scala hash map for this.

#!scala
//...
import com.specdevs.specgine.input.PointerReceiver

import scala.collection.mutable.HashMap
//...
class InputSystem extends SingleEntitySystem with KeyReceiver with PointerReceiver with Processor {
  //...

  private var width = 0

  private var height = 0

  private val screenPressed = new HashMap[Int,Int]

  //...

  override def resize(x: Int, y: Int) {
    width = x
    height = y
  }

  override def touchDown(x: Int, y: Int, pointer: Int, button: Int): Boolean = {
    val player = if (x*2>width) "A" else "B"
    val direction = if (y*2<height) "up" else "down"
    val key = get[Config,Int]("player"+player+"_"+direction)
    keysQueue.enqueue((key, true))
    screenPressed += pointer -> key
    true
  }

  override def touchUp(x: Int, y: Int, pointer: Int, button: Int): Boolean = {
    keysQueue.enqueue((screenPressed(pointer), false))
    screenPressed -= pointer
    true
  }

This completes handling of input to move paddles. In next part we will add pause menu available after pressing escape key or back button on Android.

Having troubles with this step?

Here you can download project with all above steps completed.

Updated