Wiki

Clone wiki

SpecGine / Render

Adding game state with rendering system

First we start by creating GameState. Game states in SpecGine are based on entities, but we will start from empty one.

Creating and linking empty game state

We will place our game state in file core/src/main/scala/Play.scala.

#!scala
package com.specdevs.ping

import com.specdevs.specgine.states.GameState

class Play extends GameState {
  def initialize() {}
}

We register our game state in core/src/main/scala/Ping.scala.

#!scala
  def initialize() {
    addGroup(new Menus, "Menus")
    addState(new MainMenu, "MainMenu", "Menus")
    addState(new Intro, "Intro", default=true)
    addState(new Play, "Play")
  }

Finally we need to link button in main menu so it will go into game state when "New game" is clicked.

#!scala
  def create() {
    //...
    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("Play")
      }
    })

    table.row()
    //...
  }
When "New game" will be clicked, we issue command pushState. It means that "Play" will be new current state, but when we leave it game will return to main menu state. To read more about state stack check documentation for StateManager.

Creating entities

To populate our world we have to create enitities, which are represented by class Entity. For our game we need table, ball and two paddles. To display them we will need to define their position, body, layer and optionally color. All of those are components, which are sub-types of Component. We place them all in core/src/main/scala/components.scala.

#!scala
package com.specdevs.ping

import com.specdevs.specgine.states.Component

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

sealed trait Shape

case class Ball(val radius: Float) extends Shape

case class Paddle(val width: Float, val height: Float) extends Shape

case class Table(val width: Float, val height: Float, val margin: Float) extends Shape

case class Body(val shape: Shape) extends Component

case class Color(val r: Float, val g: Float, val b: Float) extends Component

case class Layer(val layer: Int) extends Component

With defined components, we can proceed to creating entities. We do so in initialize method in Play class.

#!scala
  def initialize() {
    val tableWidth = 10f/3f
    val tableHeight = 2f
    val paddleWidth = 0.05f
    val paddleHeight = 1f/3f

    // ball
    createEntity(
      Position(0, 0),
      Body(Ball(0.05f)),
      Layer(1)
    )

    // table
    createEntity(
      Position(-tableWidth/2f, -tableHeight/2f),
      Body(Table(width=tableWidth, height=tableHeight, margin=paddleWidth)),
      Layer(0)
    )

    // paddleA
    createEntity(
      Position(tableWidth/2f-paddleWidth, -paddleHeight/2f),
      Body(Paddle(paddleWidth, paddleHeight)),
      Color(1.0f, 0.5f, 0.5f),
      Layer(1)
    )

    // paddleB
    createEntity(
      Position(-tableWidth/2f, -paddleHeight/2f),
      Body(Paddle(paddleWidth, paddleHeight)),
      Color(0.5f, 0.5f, 1.0f),
      Layer(1)
    )

    ()
  }
We have just populated our world with entities. Now we need to create render system, so we can see them.

Rendering system

Systems in SpecGine are modeled by EntitySystem. Our render system will be SingleEntitySystem that processes all entities with Position, Body, Layer and optionally Color components. We will implement this in file core/src/main/scala/RenderSystem.scala.

#!scala
package com.specdevs.ping

import com.specdevs.specgine.states.{ComponentsSpec,Renderer,SingleEntitySystem,Entity}
import com.specdevs.specgine.macros.states.{withManager,Needed,Optional}

class RenderSystem extends SingleEntitySystem with Renderer {
  class RenderSpec extends ComponentsSpec {
    val position = new Needed[Position]
    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)
  }

  val components = withManager[RenderSpec]

  def initialize() {}

  def create() {}

  def enter() {}

  def leave() {}

  def dispose() {}
}
We use entity grouping to preserve drawing order. Entities with smaller layer value will be drawn first. We add just created RenderSystem to Play class.

#!scala
  def intialize() {
    addSystem(new RenderSystem)

    //...
  }

Now we need to implement rendering in our system. We will use ShapeRenderer from Libgdx to render objects in game. We also need Libgdx OrthographicCamera. The most complicated method which will be implemented bellow will be resize method. It will guarantee us that table will preserve aspect ratio and fill as much space of screen as possible. We add this code RenderSystem.

#!scala
//...

import com.badlogic.gdx.Gdx
import com.badlogic.gdx.graphics.{GL20=>GdxGL20,OrthographicCamera=>GdxOrthographicCamera}
import com.badlogic.gdx.graphics.glutils.{ShapeRenderer=>GdxShapeRenderer}

class RenderSystem extends SingleEntitySystem with Renderer {
  //...

  private val camera = new GdxOrthographicCamera

  private var renderer: GdxShapeRenderer = null  

  def initialize() {
    renderer = new GdxShapeRenderer
  }

  def enter() {
    Gdx.gl20.glClearColor(0f, 0f, 0f, 1f)
  }

  def dispose() {
    renderer.dispose()
    renderer = null
  }

  override def beginRender(alpha: Float) {
    Gdx.gl20.glClear(GdxGL20.GL_COLOR_BUFFER_BIT)
    renderer.begin(GdxShapeRenderer.ShapeType.Filled)
  }

  override def endRender(alpha: Float) {
    renderer.end()
  }

  override def resize(x: Int, y: Int) {
    val width = x.toFloat
    val height = y.toFloat
    val aspect = width/height
    if (aspect > 800f/480f) {
      camera.setToOrtho(false, 2f*aspect, 2.0f)
      camera.translate(-aspect, -1.0f)
    } else {
      camera.setToOrtho(false, 2f*800f/480f, 2f*800f/480f/aspect)
      camera.translate(-800f/480f, -800f/480f/aspect)
    }
    camera.update()
    renderer.setProjectionMatrix(camera.combined)
  }
}

First, when state is initialized, we create instance of ShapeRenderer. We need to dispose it, as it is Disposable. When state is activated (executing enter method) we cleat color to black. It is used to clear screen each frame in beginRender method. Also in beginRender method we initialize ShapeRenderer, which stays active until end of frame when it is flushed to graphics card.

In resize method we first check aspect ratio of screen. If screen is wider than default table will fill whole height, leaving black borders on the sides. Analogously, if screen is too tall, table will fill whole width and leave borders on top and bottom.

We now implement rendering of entities.

Rendering ball

The simplest entity to draw is ball. We add renderBall method to RenderSystem.

#!scala
  private def renderBall(pos: Position, ball: Ball) {
    renderer.setColor(0.9f, 0.9f, 0.9f, 1f)
    renderer.circle(pos.x, pos.y, ball.radius, 100)
  }

Rendering paddles

Paddles are slightly harder because we need to address different colors. We add renderPaddle method to RenderSystem.

#!scala
  private def renderPaddle(pos: Position, p: Paddle, c: Option[Color]) {
    val col = c.getOrElse(Color(0.5f, 0.5f, 0.5f))
    renderer.setColor(col.r, col.g, col.b, 1f)
    renderer.rect(pos.x, pos.y, p.width, p.height)
  }

Rendering table

Rendering table is the hardest out of those three. We add renderTable method to RenderSystem.

#!scala
//...
import scala.math.min
//...
  private def renderTable(pos: Position, t: Table) {
    val circleRadius = 1f/6f*min(t.width, t.height)
    val lineWidth = 0.02f*circleRadius
    val tableColour = Color(0.5f, 1.0f, 0.5f)

    renderer.setColor(tableColour.r, tableColour.g, tableColour.b, 1f)
    renderer.rect(pos.x, pos.y, t.width, t.height)

    renderer.setColor(1f, 1f, 1f, 1f)
    renderer.circle(0f, 0f, circleRadius, 100)
    renderer.setColor(tableColour.r, tableColour.g, tableColour.b, 1f)
    renderer.circle(0f, 0f, circleRadius-lineWidth, 100)

    renderer.setColor(1f, 1f, 1f, 1f)
    renderer.rect(-lineWidth/2f, -t.height/2f, lineWidth, t.height)
    renderer.rect(-(t.width/2f-t.margin)-lineWidth/2f, pos.y, lineWidth, t.height)
    renderer.rect((t.width/2f-t.margin)-lineWidth/2f, pos.y, lineWidth, t.height)
  }

First we draw table background in green, than circle in the middle in white. Finally, we add some vertical lines.

Putting rendering implementation all together

We put rendering in render method with pattern matching on Shape type.

#!scala
  override def render(alpha: Float, e: Entity) {
    val pos = components.position(e)
    val shape = components.renderable(e).shape
    shape match {
      case b: Ball => renderBall(pos, b)
      case p: Paddle => renderPaddle(pos, p, components.color(e))
      case t: Table => renderTable(pos, t)
      case _ => ()
    }
  }

At this point we should have a rendering system which will display static image of our scene. Because everything is static we can ignore alpha parameter, but we will revisit render method in next section when we add ball movement.

Having troubles with this step?

Here you can download project with all above steps completed.

Updated