Game development in sge/C++: Part II (sprites)

Introduction

Note: There’s a git repository at https://github.com/Phillemann/sgetutorial which includes limited installation instructions for the tutorial files as well as sge, fcppt and so on. Enjoy!

In the last tutorial, we set up a little framework to build our game upon. We only created a window which can be closed via the “escape” key. What we’re going to do now is add a spaceship – nothing more. But in the process, we’re going to discuss how to draw and manipulate arbitrary 2D objects (also called sprites) in sge. I’m first going to explain two concepts: sprites and atlasing.

Various Sprites

Fig. 1: Various Sprites, top left: A square, axis-aligned sprite with a texture, but no transparency, top right: a rotated rectangle with a texture but no transparency, bottom left: a square axis-aligned sprite with a color but no texture, bottom right: the same as top right but with an alpha channel (transparency)

So what are sprites exactly? For us, a sprite is just a rectangle. This rectangle might be rotated, it might have a texture or it might be invisible. This doesn’t seem to bear much “substance”, but most 2D games consist of nothing but textured rectangles, so having an engine which supports them in an elegant and performant way is extremely important.

 

Performance

A simple analysis

But those are just rectangles, why care much about performance? Today’s graphics cards can render up to a gazillion of them at once! Correct, but this only applies to static geometry. Sprites are mostly dynamic, changing each frame (think of the player/enemies/projectiles/debris moving and/or rotating). This means that each frame, you have to update most of their data – which is done on the CPU – and send the new data to the GPU. Let’s see how much data we’re pumping to the GPU each second. Since a rectangle is represented as two triangles, we have 6 vertices per sprite. Each vertex has a color (4 bytes), a position (3 floats), and a texture coordinate (2 floats). Assuming you have 10.000 sprites, you get2:

10.000 \cdot (4 \textrm{ bytes} + (3 + 2) \cdot 4\textrm{ bytes}) \cdot 6 \textrm{ vertices} \cdot 60 \textrm{fps} \approx 70 \textrm{MiB/s}

This is quite a lot, considering we’re only dealing with rectangles here. It would be good if we could specify exactly which attributes we want for a group of sprites. This way, if we never change a sprite’s color, for example, we save 4 bytes of traffic each sprite, each frame.

The woes of transparency

There are at least two other performance-degrading factors which come into play: texture switches and the number of render calls. You might think that the sprite rendering function looks something like this:

foreach (texture in registered_sprite_textures)
{
  renderer.activate_texture(texture);
  draw_all_sprites_with_texture(texture);
}

which would mean that we have ‘n’ render calls for ‘n’ textures. But it’s a little bit more complicated. The problem is that if you use transparency, you cannot just draw all the sprites at once, because drawing two objects isn’t a commutative operation anymore. You have to sort the sprites based on their depth (z coordinate), see [1], [2] and [3] for more information. Luckily, that’s also managed by sge, but you have to at least be aware of it to use sge::sprite best.

Atlasing

Now to texture switches and atlasing: First of all, in all graphic APIs you have at least one “active” texture. This is the texture that’s used when you render a textured rectangle, for example.1 If you want to draw three sprites with different textures, you have to switch textures thrice. Now, texture switching is an expensive operation which we want to avoid. One obvious solution would be to cram all the smaller textures into one bigger texture and then only use that texture. This technique is called atlasing. See figure 2 for a depiction.

Demonstration of atlasing.

Figure 2: Demonstration of atlasing. The dashed lines represent textures, the other images are the sprites that use the texture.


For our game, however, we won’t be using atlasing, but as you’ll see later you still need to know that it’s there.

Choices

Okay, so now, finally, we can get to the code which utilizes sge::sprite. Before we can instantiate our sprites, we have to define a few types. At the most elementary level, we have to decide which integer and float type we want to use and also which color format (if we use colors, that is). This gives us another degree of freedom, since we might not be satisfied with, say, integer coordinates or float precision. The three choices are aggregated in the type_choices structure:

#include <sge/sprite/sprite.hpp>
#include <sge/image/color/rgba8_format.hpp>

typedef
sge::sprite::type_choices
<
  int,
  float,
  sge::image::color::rgba8_format
>
sprite_type_choices;

As I said above, we would be lucky if we could decide which attributes (color, texture, …) our sprites have – and indeed we can! The next typedef defines exactly what a sprite contains:

#include <boost/mpl/vector.hpp>

typedef
sge::sprite::choices
<
  sprite_type_choices,
  boost::mpl::vector
  <
    sge::sprite::with_dim,
    sge::sprite::with_color,
    sge::sprite::with_texture,
    sge::sprite::with_rotation
  >
>
sprite_choices;

Don’t be puzzled by the boost::mpl stuff in the code. We can ignore that for now and just say that an mpl::vector is able to somehow aggregate arbitrary types, like the with_ types above. As you can see, we’ll be using colors, textures and rotations for our sprites. But what about that with_dim thingy, don’t all sprites have a dimension? Well, no. sge::sprite also supports so called point sprites which are not rectangles but squares. We’ll get to that topic later.

In the meantime, if you’re curious as to what other choices you have, here’s an exhaustive table of all sprite attributes:

Type Description
with_color self-explanatory
with_depth Adds a z coordinate to the sprite (available via sprite.z()).
with_dim This sprite has a dimension (available via sprite.size()).
with_repetition The sprite’s texture can be repeated (tiled) (available via sprite.repetition()).
with_rotation_center When you set a sprite’s rotation, the rectangle is rotated around its center. With this, you can change the rotation pivot (available via sprite.rotation_center()).
with_rotation self-explanatory
with_texture_coordinates Gives you the ability to change the texture coordinates, which are usually (0,0), (1,0), (0,1), (1,1) for the four vertices (and a little different if you use repetition)
with_texture self-explanatory
with_unspecified_dim Reserved for point-sprite (see below)
with_visibility Enables you to make a sprite invisible via sprite.visible(false). Invisible sprites aren’t sent to the GPU, of course.
intrusive::tag This takes a bit longer to explain, see below.

The most important typedefs

We’re almost finished typedeffing stuff. Two things are missing: The actual sprite type and the sprite system type. The sprite system is the structure responsible for rendering and caching stuff. It contains the vertex buffer and the index buffer and is able to reuse buffers across render calls to save performance. The sprites act more like a container and have virtually no inherent logic. The definition is simple3:

typedef
sge::sprite::object<choices>
sprite_object;

// You can ignore the "::type" at the end for now, I'll explain metafunctions in a later article.
typedef
sge::sprite::system<choices>::type
sprite_system;

Creating a sprite

Finally, let’s create a sprite. Let’s say it should be positioned in the center of the screen and have a texture which is stored in a file called “ship.png”. The sprite’s size should correspond to the size of the texture and the color should be white. Voila:

#include <sge/texture/texture.hpp>
#include <sge/image2d/image2d.hpp>
#include <fcppt/math/dim/dim.hpp>

typedef
sge::sprite::parameters<sprite_choices>
sprite_parameters;

sprite_object ship(
  sprite_parameters()
    .texture(
      sge::texture::part_ptr(
        new sge::texture::part_raw(
          sge::renderer::texture::create_planar_from_view(
            sys.renderer(),
            *sys.image_loader().load(
              FCPPT_TEXT("ship.png"))->view(),
            sge::renderer::texture::filter::linear,
            sge::renderer::texture::address_mode2(
              sge::renderer::texture::address_mode::clamp),
            sge::renderer::resource_flags::none))))
    .texture_size()
    .any_color(
      sge::image::colors::white())
    .center(
      sprite_object::vector(
        512,384))
    .elements());

The “named parameter” idiom

I guess you thought it was more straightforward, but there are a few “quirks” to see here. First of all, a sprite is initialized with a “helper structure” called sprite::parameters. In C++, you don’t have the ability to pass “named parameters” to functions, as in:

f(name = "foobar",age = 10,weight = 67.4);

You can only do

f("foobar",10,67.4);

Which is ugly and unsafe. Think about what would happen if you wrongly remembered the order to be first “weight”, then “age”. The compiler might complain, or it might not. Also, you might want to have more than one default parameter, which you cannot easily do in C++. So we construct a helper class:

class helper
{
public:
  helper()
  :
    name_("name not specified"),
    age_(-1),
    weight_(-1.0)
  {
  }

  helper &name(std::string _name) { name_ = _name; return *this; }
  helper &weight(double _weight) { weight_ = _weight; return *this; }
  helper &age(int _age) { age_ = _age; return *this; }
  
  std::string name_;
  int age_;
  double weight_;
};

Now you can say:

f(helper().name("foobar").age(67).weight(37.6));

And you can even omit parameters, which will then be initialized to default values in the constructor of helper. sprite::parameters, however, doesn’t default-initialize the values you don’t specify, they’re undefined. There’s sprite::default_parameters which does default-initialization. Also note the call to elements() at the end of the initialization. This is mandatory and you will get a nasty compiler error if you omit it.

Textures and parts

The texture creation looks a bit more complex, but every part of it is easily explained. I’ll explain it “from the inside out”. As you can see, we use the systems’ image_loader to load an image, which is returned as a shared_ptr. To create a planar (2D) texture from it, we need a “view” of that image. Think of it as a “complete” representation of the image, since to be really type safe, you cannot just return a raw char* or a void*, you need to encode more information in the return value. The other parameters to the create_planar_from_view function are self-explanatory. We choose linear filtering on the texture (arbitrary choice, really) and the texture has no “special” flags like “readable”. Also, you can ignore the address_mode stuff for now.

Which leaves us with the texture::part stuff. But that we already covered: It’s atlasing. You do not want the sprite to take a whole “raw” texture, just a part of it. But since we’re not using atlasing, we have to wrap our texture in a part_raw which means “take this texture, and take all of it”.

The rest

The sprite’s color is chosen from a predefined set of colors so we don’t have to specify all four channels. The last statement, where the position is assigned, might be a bit puzzling. What’s this structure_cast doing there? And what’s a dim? It’s really simple: In fcppt, you have a vector type and a dim type. A vector has operations like “dot product” and “multiplication with a matrix” defined, a dim does not, since it’s nothing you usually want to do with a “size” type. This distinction is fundamental: Things which are similar but are used in a (completely) different context should have different types, or at least types with different names (typedefs)! This is not to annoy the library’s users but to help them. Many bugs are introduced because of some automatic conversion between types, or even identical types with different names (typedefs).

So, since center accepts a point but we’re giving it a dimension (the screen size), we have to structure_cast it to a vector.

Drawing a sprite

Ok, we’ve got our sprite, but it merely sits there and isn’t drawn on the screen. Luckily, that’s much easier to explain than the sprite’s creation. First, the code:

sprite_system sprite_sys(
  sys.renderer());

while (running)
{
  sys.window().dispatch();

  sge::renderer::scoped_block block(
    sys.renderer());

  sge::sprite::render_one(
    sprite_sys,
    ship);
}

Not much to explain here. sprite has a function render_one to render exactly one sprite (there are, of course, functions to render more sprites as well). The scoped_block is a very simple class which calls renderer->begin_rendering() on construction and renderer->end_rendering() on destruction. If you don’t call those functions, nothing will be drawn.

Not quite there yet

If you compile and run the program at this point, however, it will crash. That’s because we’re trying to load an image but didn’t request an image loader from sge::systems. So, warp to the sge::systems initialization, add the following:

#include <sge/all_extensions.hpp>

sge::systems::instance sys(
  sge::systems::list()
    (sge::systems::image_loader(
      sge::image::capabilities_field::null(),
      sge::all_extensions))
    /* add rest of code here */);

Result of part 2

Result of part 2


Compile, run, and see a cute spaceship in the middle of the screen (you have to watch out that “ship.png” is in your current working directory).

And we’re done for today. Long article for a small result, but it will not be the last time you’ve seen sge::sprite and I hope you have a good first impression of it. See you in part 3, where I’ll introduce input callbacks, so we can move the ship around.

Footnotes

1 With multitexturing, you have multiple active textures, but that doesn’t matter here, you have to do a switch at some point.
2 We assume 4 color channels, one byte each channel, as well as 3 components for the position. Plus, we assume that floats are 4 bytes.
3 The astute reader might have noticed that we’re breaking our own rule here: Instead of putting everything sprite-related into a namespace, we prefix all of it with sprite_. We’ll remedy this situation later, when we’re giving the code some structure. For now, forgive me.

Advertisements
This entry was posted in Uncategorized and tagged , , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s