Wiki
Clone wikiSpecGine / 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)) //... }
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() {} }
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 } }
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 } } }
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 _ => () } } }
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