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