Wiki

Clone wiki

SpecGine / Score

Adding score and win state

Game is already playable, but remembering score can be hard. We will now add support for automatic score counting and display.

Adding score container into root group

First, we add score container into root group. We do so by setting two values, "playerA_score" and "playerB_score" to zero in initialize method of Play.

#!scala
//...
import com.specdevs.specgine.core.StateInfo
//...
  def initialize() {
    setIn[StateInfo,Int]("playerA_score", 0)
    setIn[StateInfo,Int]("playerB_score", 0)
    //...
  }

Displaying score

We will render score using bitmap font, after rendering of all entities is finished. We will use Libgdx SpireBatch, so we need to create one and make sure it is correctly sized. We modify RenderSystem to accommodate new features.

#!scala
//...
import com.badlogic.gdx.graphics.g2d.{BitmapFont=>GdxBitmapFont,SpriteBatch=>GdxSpriteBatch}
import com.badlogic.gdx.math.{Matrix4=>GdxMatrix4}
//...
class RenderSystem extends SingleEntitySystem with Renderer {
  //...

  private val viewMatrix = new GdxMatrix4

  private var batch: GdxSpriteBatch = null

  private var font: GdxBitmapFont = null

  def initialize() {
    batch = new GdxSpriteBatch
    font = new GdxBitmapFont
    //...
  }

  def create() {
    font.setColor(0f, 0f, 0f, 1f)
  }

  def dispose() {
    font.dispose()
    font = null
    batch.dispose()
    batch = null
    //...
  }

  override def resize(x: Int, y: Int) {
    //...
    if (aspect > 800f/480f) {
      //...
      font.setScale(2f*height/480f)
    } else {
      //...
      font.setScale(2f*width/800f)
    }
    viewMatrix.setToOrtho2D(0f, 0f, width, height)
    batch.setProjectionMatrix(viewMatrix)
    //...
  }
}

Now, that we have SpriteBatch ready, we can modify endRener method. We will use camera project method to position values in top corners.

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

import com.badlogic.gdx.math.{Vector3=>GdxVector3}
//...
  override def endRender(alpha: Float) {
    renderer.end()
    batch.begin()

    val vecA = new GdxVector3(4f/3f, 0.9f, 0f)
    camera.project(vecA)
    val vecB = new GdxVector3(-4f/3f, 0.9f, 0f)
    camera.project(vecB)
    val vec2 = new GdxVector3(1f/6f, 0, 0f)
    camera.project(vec2)

    val alignA = GdxBitmapFont.HAlignment.RIGHT
    val scoreA = getFrom[StateInfo,Int]("playerA_score").toString
    font.drawMultiLine(batch, scoreA, vecA.x-vec2.x, vecA.y, vec2.x, alignA)

    val alignB = GdxBitmapFont.HAlignment.LEFT
    val scoreB = getFrom[StateInfo,Int]("playerB_score").toString
    font.drawMultiLine(batch, scoreB, vecB.x, vecB.y, vec2.x, alignB)

    batch.end()
  }

Counting points

Now that we can see score, we need to set it when ball miss paddle. Notice, that when player A misses, player B scores point. That's why we will join score for player A with paddle of player B, and score of player B with paddle of player A.

Adding score label component

We now add new component for score label in core/src/main/scala/components.scala, to connect paddle with correct score value.

#!scala
//...

case class Score(val name: String) extends Component

We now add new components to paddles.

#!scala
    //...

    // 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"),
      Score("playerB_score")
    )

    // 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"),
      Score("playerA_score")
    )

    //...

Adjusting collision system

We now need to adjust collision system to keep track of points. We need optional Score component in Colidee.

#!scala
class CollisionSystem extends PairEntitySystem(processPriority=1) with Processor {
  //...

  class CollideeSpec extends ComponentsSpec {
    val position = new Needed[Position]
    val collidee = new Needed[Body]
    val layer = new Needed[Layer]
    val score = new Optional[Score]
    override def group(e: Entity): Option[Int] = Some(-layer(e).layer)
  }

  //...
}

We will now add code to award points when ball misses the paddle. We will increase by one and set again the score inside awardPoint method.

#!scala
//...
import com.specdevs.specgine.core.StateInfo
//...
class CollisionSystem extends PairEntitySystem(processPriority=1) with Processor {
  //...

  private def awardPoint(s: Score) {
    val oldScore = getFrom[StateInfo,Int](s.name)
    setIn[StateInfo,Int](s.name, oldScore + 1)
  }

  private def ballPaddle(pos1: Position, v1: Velocity, b: Ball, pos2: Position, p: Paddle, s: Score) {
    if (pos2.x>0 && pos1.x+b.radius>pos2.x) {
      if (pos1.y>=pos2.y && pos1.y<=pos2.y+p.height) {
        //...
      } else {
        //...
        awardPoint(s)
      }
    }
    if (pos2.x<0 && pos1.x-b.radius<pos2.x+p.width) {
      if (pos1.y>=pos2.y && pos1.y<=pos2.y+p.height) {
        //...
      } else {
        //...
        awardPoint(s)
      }
    }
  }

  override def process(dt: Float, e1: Entity, e2: Entity) {
    if (e1 != e2) {
      val score = components2.score(e2)
      //...
            case p: Paddle => ballPaddle(pos1, vel, b, pos2, p, score.get)
      //...
    }
  }
}

Adding win screen

We will now implement screen that we display when one of players gets five points.

Adding empty state

We will implement win screen using menu state. We create file core/src/main/scala/WinMenu.scala.

#!scala
package com.specdevs.ping

import com.specdevs.specgine.states.MenuState

class WinMenu extends MenuState {
  def initialize() {}

  def create() {}

  def dispose() {}
}

We now add matching screen in file core/src/main/scala/WinScreen.scala.

#!scala
package com.specdevs.ping

import com.specdevs.specgine.states.gdx.MenuScreen

class WinScreen extends MenuScreen(width=Some(800), height=Some(480)) {
  def initialize() {}

  def create() {}

  def dispose() {}
}

and register it inside initialize method of WinMenu.

#!scala
  def initialize() {
    addMenuScreen("Win", new WinScreen)
  }

Now, we can register state inside Ping class.

#!scala
  def initialize() {
    //...
    addState(new WinMenu, "WinMenu")
  }

Finally, we push this screen when score equals to four. We modify awardPoint method from CollisionSystem.

#!scala
  private def awardPoint(s: Score) {
    //...
    if (oldScore == 4) {
      pushState("WinMenu")
    }
  }

Adding buttons and background

We now modify win screen to provide buttons in background. We do so analogously to pause menu.

#!scala
//...
import com.specdevs.specgine.assets.Asset
import com.specdevs.specgine.assets.gdx.ImplicitSkinAsset

import com.badlogic.gdx.scenes.scene2d.{Actor=>GdxActor}
import com.badlogic.gdx.scenes.scene2d.ui.{Skin=>GdxSkin,TextButton=>GdxTextButton}
import com.badlogic.gdx.scenes.scene2d.utils.{ChangeListener=>GdxChangeListener}
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener.{ChangeEvent=>GdxChangeEvent}
//...
class WinScreen extends MenuScreen(width=Some(800), height=Some(480)) {
  //...

  def create() {
    val skin = get[Asset,GdxSkin]("defaultSkin")

    val button1 = new GdxTextButton("New game", skin)
    table.add(button1).width(120).pad(10)
    button1.addListener(new GdxChangeListener {
      def changed(event: GdxChangeEvent, actor: GdxActor) {
        pushState("MainMenu")
        pushState("Play")
      }
    })

    table.row()

    val button0 = new GdxTextButton("Quit", skin)
    table.add(button0).width(120).pad(10)
    button0.addListener(new GdxChangeListener {
      def changed(event: GdxChangeEvent, actor: GdxActor) {
        quit()
      }
    })

    ()
  }

  //...

  override def renderBackground(alpha: Float) {
    renderFrozenState(alpha)
  }
}

What remains is to add label saying which player won.

Extending skin to support color labels

We start by extending "defaultSkin" to support labels in two colors, "RED" and "BLUE". We modify it in Menus state group in core/src/main/scala/Menus.scala file.

#!scala
//...
import com.badlogic.gdx.scenes.scene2d.ui.Label.{LabelStyle=>GdxLabelStyle}
//...
class Menus extends StateGroup {
  //...

  def create() {
    //...

    val labelStyleRed = new GdxLabelStyle
    labelStyleRed.font = skin.getFont("default")
    labelStyleRed.fontColor = GdxColor.RED
    skin.add("RED", labelStyleRed)

    val labelStyleBlue = new GdxLabelStyle
    labelStyleBlue.font = skin.getFont("default")
    labelStyleBlue.fontColor = GdxColor.BLUE
    skin.add("BLUE", labelStyleBlue)

    set[Asset,GdxSkin]("defaultSkin", skin)
  }

  //...
}

Displaying labels in menu

We modify WinScreen to display congratulation string to winner.

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

import com.badlogic.gdx.scenes.scene2d.ui.{Label=>GdxLabel}
//...
class WinScreen extends MenuScreen(width=Some(800), height=Some(480)) {
  //...

  def create() {
    val skin = get[Asset,GdxSkin]("defaultSkin")

    val scoreA = getFrom[StateInfo,Int]("playerA_score")
    val scoreB = getFrom[StateInfo,Int]("playerB_score")
    val winner = if (scoreA > scoreB) "RED" else "BLUE"

    val label = new GdxLabel(s"$winner player wins!", skin, winner)
    table.add(label).pad(20)

    table.row()

    //...
  }

  //...
}

We now have fully playable game with all designed features, but it still is desktop-only. In next section we will fix configuration for other platforms.

Having troubles with this step?

Here you can download project with all above steps completed.

Updated