Wiki
Clone wikiSpecGine / 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