Wiki

Clone wiki

SpecGine / Movement

Adding movement and collisions systems

There are two closely related systems that we will code right now, movement system and collision system. It will allow us to see the ball moving around the scene.

Adding movement

We will now add some movement to our game. Most important thing to grasp is that we render in non-fixed delays, but all processing like movement and physics is done in fixed steps, by default equal 1/100th of second. If between two frames there is time t equal for example 0.031 seconds, process method will be called 3 times with dt equal 0.01 second, and then render method will be issued with remainder, i.e. 0.1 (fraction of full dt). To make rendering fluent in this method, we need to interpolate between two states using this remainder, named alpha in our system. This is why we not only need Position component, but also OldPosition, to store old state to interpolate with. This approach is described in article Fix Your Timestep! by Glenn Fiedler.

New components

We state by adding components into core/src/main/scala/components.scala.

#!scala
//...

case class OldPosition(var x: Float, var y: Float) extends Component

case class Velocity(var x: Float, var y: Float) extends Component

Next, we will add helper method to create random velocity component, to make game less predictable. This is why we create file core/src/main/scala/utils.scala with following content:

#!scala
package com.specdevs.ping

import scala.math.{sin,cos,random}

package object utils {
  def randomVelocity(): Velocity = {
    val speed: Float = 0.5f
    val phi = (0.5+random)*(if (random>0.5) 0.5 else -0.5)
    Velocity(speed*cos(phi).toFloat, speed*sin(phi).toFloat)
  }
}

Now, we add new components to ball definition in initialize method of Play class from core/src/main/scala/Play.scala.

#!scala
    //...

    // ball
    createEntity(
      Position(0, 0),
      OldPosition(0, 0),
      utils.randomVelocity(),
      Body(Ball(0.05f)),
      Layer(1)
    )

    //...

Movement System

Now, we create another SingleEntitySystem, this time devoted to processing. We will place it in file core/src/main/scala/MovementSystem.scala.

#!scala
package com.specdevs.ping

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

class MovementSystem extends SingleEntitySystem with Processor {
  class MovementSpec extends ComponentsSpec {
    val pos = new Needed[Position]
    val oldpos = new Needed[OldPosition]
    val vel = new Needed[Velocity]
  }

  val components = withManager[MovementSpec]

  def initialize() {}

  def create() {}

  def enter() {}

  def leave() {}

  def dispose() {}
}

For something to move, it needs to have Position, OldPosition and Velocity. This new system needs to be registered in Play initialize method.

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

Now, with empty system ready to process entities, we need to implement process method in MovementSystem.

#!scala
//...
import com.specdevs.specgine.states.Entity
//...
  override def process(dt: Float, e: Entity) {
    components.oldpos(e).x = components.pos(e).x
    components.oldpos(e).y = components.pos(e).y
    components.pos(e).x += components.vel(e).x*dt
    components.pos(e).y += components.vel(e).y*dt
  }

Now, when the game starts, the ball will move right, always with same velocity but at random angle. The movement while perfect, might look jumpy in some cases, because in renderer implementation we ignored alpha parameter. We will now fix it.

Adding render interpolation

To add render interpolation for moving objects, we first need to inform our system, that there is optional OldPosition. We do so by adjusting RenderSpec in core/src/main/scala/RenderSystem.scala.

#!scala
  class RenderSpec extends ComponentsSpec {
    val position = new Needed[Position]
    val oldPosition = new Optional[OldPosition]
    val renderable = new Needed[Body]
    val layer = new Needed[Layer]
    val color = new Optional[Color]
    override def group(e: Entity): Option[Int] = Some(layer(e).layer)
  }

We now have access to this field. We only need to modify render method to take oldPosition into account. We remove old definition for pos and create new one.

#!scala
  override def render(alpha: Float, e: Entity) {
    val newpos = components.position(e)
    val oldpos = components.oldPosition(e).getOrElse(OldPosition(newpos.x, newpos.y))
    val pos = Position(newpos.x*alpha+oldpos.x*(1-alpha), newpos.y*alpha+oldpos.y*(1-alpha))
    //...
  }
Here we use simple fact, that if object does not have OldPosition component, it cannot move (OldPosition is needed by MovementSystem), so we can assume its oldPosition is equal to position value of Position.

This concludes movement system implementation, but ball moves in one direction and never comes back, even if it hits edge of table or paddle. We will fix it implementing simple collision system.

Adding collisions

Collisions are usually tricky, but here we can apply few simple rules. We want:

  • Ball bouncing from paddle.
  • Ball bouncing from top or bottom edge of table.
  • Ball returning to center and moving in opposite direction if it crossed left or right border on table, but did not touched paddle.

Empty collision system

We first create empty collision system, which implements PairEntitySystem in core/src/main/scala/CollisionSystem.scala.

#!scala
package com.specdevs.ping

import com.specdevs.specgine.states.{ComponentsSpec,Entity,PairEntitySystem,Processor}
import com.specdevs.specgine.macros.states.{withManager,Needed,Optional}

class CollisionSystem extends PairEntitySystem(processPriority=1) with Processor {
  class ColliderSpec extends ComponentsSpec {
    val position = new Needed[Position]
    val velocity = new Needed[Velocity]
    val collider = new Needed[Body]
  }

  class CollideeSpec extends ComponentsSpec {
    val position = new Needed[Position]
    val collidee = new Needed[Body]
  }

  val components1 = withManager[ColliderSpec]

  val components2 = withManager[CollideeSpec]

  def initialize() {}

  def create() {}

  def enter() {}

  def leave() {}

  def dispose() {}
}
Notice, that we set processing priority to 1, so collisions are always checked after movement finished (because it has default priority equal to 0). We now register it in Play.

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

We now implement collisions between different shapes.

Ball-table collision

Ball-table collision is simples collision we handle. We have to only take care for top and bottom edge of table, borders will be handled together with paddles. We add new method to CollisionSystem.

#!scala
  private def ballWall(pos1: Position, v1: Velocity, b: Ball, pos2: Position, t: Table) {
    if (pos1.y+b.radius>pos2.y+t.height) {
      v1.y = -v1.y
      pos1.y = 2*(pos2.y+t.height-b.radius)-pos1.y
    }
    if (pos1.y-b.radius<pos2.y) {
      v1.y = -v1.y
      pos1.y = 2*(pos2.y+b.radius)-pos1.y
    }
  }
These collisions simply change sign of vertical velocity. The change to position is needed, because collision could occur in between simulation steps. That is why we move ball away from border exactly same amount it crossed it.

Ball-paddle collision

Ball-paddle collision is most important one. We add new method to CollisionSystem.

#!scala
  private def ballPaddle(pos1: Position, v1: Velocity, b: Ball, pos2: Position, p: Paddle) {
    if (pos2.x>0 && pos1.x+b.radius>pos2.x) {
      if (pos1.y>=pos2.y && pos1.y<=pos2.y+p.height) {
        v1.x = -1.1f*v1.x
        v1.y = 1.1f*v1.y
        pos1.x = 2*(pos2.x-b.radius)-pos1.x
      } else {
        val newVel = utils.randomVelocity()
        newVel.x *= -1
        v1.x = newVel.x
        v1.y = newVel.y
        pos1.x = 0f
        pos1.y = 0f
      }
    }
    if (pos2.x<0 && pos1.x-b.radius<pos2.x+p.width) {
      if (pos1.y>=pos2.y && pos1.y<=pos2.y+p.height) {
        v1.x = -1.1f*v1.x
        v1.y = 1.1f*v1.y
        pos1.x = 2*(pos2.x+p.width+b.radius)-pos1.x
      } else {
        val newVel = utils.randomVelocity()
        v1.x = newVel.x
        v1.y = newVel.y
        pos1.x = 0f
        pos1.y = 0f
      }
    }
  }
First, we check if ball crossed right border. If yes, we check if it hit paddle or not. In case when it hit paddle, we change sign of its horizontal velocity and increase both velocity components by 10%. If ball missed paddle, we reset it to center with random velocity pointing left. Checking for collision with left paddle is analogous.

Putting it all together

It remains to put all defined methods together. We define process method in CollisionSystem.

#!scala
  override def process(dt: Float, e1: Entity, e2: Entity) {
    if (e1 != e2) {
      val shape1 = components1.collider(e1).shape
      val shape2 = components2.collidee(e2).shape
      val pos1 = components1.position(e1)
      val pos2 = components2.position(e2)
      val vel = components1.velocity(e1)
      shape1 match {
        case b: Ball => {
          shape2 match {
            case p: Paddle => ballPaddle(pos1, vel, b, pos2, p)
            case t: Table => ballWall(pos1, vel, b, pos2, t)
            case _ => ()
          }
        }
        case _ => ()
      }     
    }     
  }
Here, if we have two different entities, we check collision using simple pattern matching and previously defined helper functions.

This concludes movement and collision systems of our game. In next part we will add input handling, so you can move paddles around.

Having troubles with this step?

Here you can download project with all above steps completed.

Updated