Several weeks ago, I developed a product backlog for my game programming class, and I tagged the first two tasks --- the highest priority tasks --- for demonstration purposes. Specifically, the plan was to complete these two tasks on Wednesday of the first week of classes (tomorrow):
- Create a Position component.
- Create an ImageRenderable component with corresponding ImageRenderSystem.
The first is quite easy and provides a good introduction to TDD. The Position component doesn't do anything: it's just a bounding rectangle in 2D space, so the test is really just over accessors and mutators.
For the second, I wrote a little note to myself that my unit test should load a 1x1 image and check that the color values match as expected. Rather routinely this morning, I decided to make sure I remembered how to do this before the class meeting tomorrow. The best I can figure now is that I never actually wrote this before, I only saw that
Slick's
Graphics class has a
getPixel method. The mere presence of this method means that the test should be a simple one, right?
Well, no. Slick sits on top of
LWJGL, and so one cannot simply create a Graphics object: there is all sorts of OpenGL initialization that happens behind the scenes. In fact, a similar thing happens when just trying to create a Slick
Image object: this fails if done outside of the context of a Slick
Game in a
GameContainer since the OpenGL subsystem is not initialized.
Mock objects really won't help here, since the best one could do is check whether the ImageRenderSystem calls a specific rendering method on a mock Graphics object. This would be fragile and only test how the ImageRenderSystem uses the Slick API, not whether it behaves as it should.
After some lunchtime tinkering, I've come up with a solution that seems to work fine. The crux of it is to create one-off
BasicGame implementations within the test case and embed the test code within. BasicGame is the default starting place for a Slick game implementation, itself implementing the Game interface. BasicGame is abstract and requires implementation of three methods: init, update, and render. So, let's look at the "easier" one first. This is what ImageRenderable looks like:
import org.newdawn.slick.Image;
public class ImageRenderable implements Component {
private Image image;
public Image image() {
return image;
}
public ImageRenderable image(Image image) {
this.image = image;
return this;
}
}
Easy enough, as long as you don't mind my Smalltalk-esque style — using the same method name for both an accessor and mutator, and returning this
from the mutator.
This conceptually-simple unit test just checks that if we set an image, that the property accessor returns that image:
@Test
public void testImageAccessorAndMutator() {
final String RESOURCE_LOC = "images/white-1x1.png";
try {
new AppGameContainer(new BasicGame("Test") {
@Override
public void render(GameContainer container, Graphics g)
throws SlickException {
final Image image = new Image(RESOURCE_LOC);
ImageRenderable ir = new ImageRenderable().image(image);
assertEquals(image, ir.image());
container.exit();
}
@Override
public void update(GameContainer container, int delta)
throws SlickException {
}
@Override
public void init(GameContainer container) throws SlickException {
}
}).start();
} catch (SlickException e) {
fail("Failed to create game: " + e);
}
}
Here's a breakdown:
- The game needs a container in order to run, so we create an AppGameContainer.
This is the class used when launching games as applications, in contrast with
AppletGameContainer.
- The inner class overrides the three methods required, but only render is interesting to this test, so it's the only one that matters.
- The
image
variable references the actual Slick image object, which is then referenced by ir
. We don't actually need any Entity
objects here: we're just testing the component.
assertEquals
ensures that the image we put into the component is the one we get out.
- After the test, we call the container's
exit
method to gracefully shut down Slick.
- The start() call on the game container actually starts the test game.
Unit tests for accessors and mutators are nigh unnecessary, but this set-up allows us to do something more interesting with the system that processes these components. Here's a simple implementation of ImageRenderSystem
, which assumes that its Entity
arguments have both Position
and ImageRenderable
components.
import java.util.List;
import org.newdawn.slick.Graphics;
public class ImageRenderSystem {
public void render(Graphics g, List entities) {
for (Entity e : entities)
render(g, e);
}
public void render(Graphics g, Entity e) {
Position p = e.as(Position.class);
ImageRenderable i = e.as(ImageRenderable.class);
g.drawImage(i.image(), p.x(), p.y());
}
}
Simple. Here's the corresponding unit test (which was written first, natch).
@Test
public void testImageRendering() throws Exception {
new AppGameContainer(new BasicGame("Test") {
private Entity e;
@Override
public void render(GameContainer container, Graphics g)
throws SlickException {
ImageRenderSystem sys = new ImageRenderSystem();
sys.render(g, e);
assertEquals(Color.white, g.getPixel(0, 0));
container.exit();
}
@Override
public void update(GameContainer container, int delta)
throws SlickException {
}
@Override
public void init(GameContainer container) throws SlickException {
BasicEntityManager mgr = new BasicEntityManager();
e = mgr.createEntity();
e.add(new Position().x(0).y(0));
e.add(new ImageRenderable().image(new Image(
"assets/white-1x1.png")));
}
}).start();
}
This has the same structure as the previous one. I am using the init
method to set up the test. Technically, this could be done in render
as in the previous example, but it seems that init
is a good place to do initialization. The render method calls out to the ImageRenderSystem
render
method, and then we check that the pixel in the upper-left corner is indeed a white one. To finish the test, we should check that the rest are black, but that's left as an exercise for the reader.
This shows how to set up rendering-related unit tests in Slick. The real moral of the story, though, is: make sure you know what you're doing before you show up in class. This was supposed to take about fifteen minutes, and it took me over an hour to puzzle through all the parts. It's always easier the second time.
Thanks to David Craft for the post explaining how to set up SyntaxHighlighter in blogger.