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