Tutorial 5 - Sprites & RNG

Published 1 month, 1 week ago

In this tutorial you'll learn the basics of sprites. This includes replacing vanilla sprites and adding new ones.


1 Assets

Up to this point you haven't needed an assets directory, but now you do so you can put sprites there. The actual place you put the sprites within this directory doesn't matter, but make sure that both sprite filenames and directory names consist of only lowercase ASCII letter, numbers and underscores (this is a weird restriction that the engine places). The one exception to all of this is the assets license. If you choose to include one in your mod, it must be named exactly assets/LICENSE.txt and it must not appear in a subdirectory.

Create a directory called assets and then make a subdirectory called assets/sprites.

2 Sprite Format

For the engine to be able to load a sprite, it must be a PNG file. It may optionally have an alpha channel. To create an animated sprite, tile the frames out horizontally, making sure each frame is the same size. As far as I can tell, there is no practical limit on either the size of a single frame (in pixels) or the number of frames a sprite can have.

For this tutorial, you'll be using these two animated sprites from the example Balloons mod:

CC0 1.0 by Garrett Fairburn

CC0 1.0 by Garrett Fairburn

As you can see, they each have 6 frames- resulting in an animation of the balloon bobbing up and down. The actual speed of the animation is controlled by the code, and you'll learn more later in this tutorial.

Save these images to two files named assets/sprites/balloon_red.png and assets/sprites/balloon_blue.png.

3 Sprite Module

As was the case for objects, you'll also be creating a dedicated sprite module. The only difference this time is that you won't be create separate modules for individual sprites; all sprite code will go in this single sprite module. Also like you object module, you'll be using an internal RegisterSprites function.

Create the files sprite.h and sprite.c and give them the following code:

/* sprite.h */
#ifndef SPRITE_H
#define SPRITE_H

/* ----- INTERNAL FUNCTIONS ----- */

void RegisterSprites(void);

#endif /* SPRITE_H */

/* sprite.c */
#include "aer/sprite.h"

#include "sprite.h"

/* ----- INTERNAL FUNCTIONS ----- */

void RegisterSprites(void) { return; }

Then pass a reference to RegisterSprites to the MRE using your mod definition function:

/* moddef.c */
MOD_EXPORT void DefineMod(AERModDef *def) {
  def->roomChangeListener = HandleRoomChange;
  def->registerObjectListeners = RegisterObjectEventListeners;
  def->registerSprites = RegisterSprites;

return; }

4 Replacing a Vanilla Sprite

Note the inclusion of aer/sprite.h in sprite.c. This module contains a function called AERSpriteReplace which, as the name would suggest, lets you replace a vanilla sprite with a custom mod sprite. As was the case with objects and rooms, the AER framework also provides the enumeration AERSpriteIndex which maps sprite names taken from the executable to their respective sprite indexes.

If you'll recall from the previous tutorial, you were manipulating destructable objects. One of them looked like this:

This particular sprite is AER_SPRITE_NBOOKS16. Try replacing this sprite with the red balloon sprite you saved earlier by doing this:

/* sprite.c */
void RegisterSprites(void) {
  AERSpriteReplace(AER_SPRITE_NBOOKS16, "sprites/balloon_red.png", 6, 6, 40);

return; }

When replacing and registering sprites, paths are relative to your mod's assets directory.

Now build and run your mod. You should see all of those books replaced with the red balloon sprite:

There is an issue, though. The balloon sprite isn't animated. That's because the destructable object has a default animation speed of 0.0f. You'll learn how to fix that in the next section.

5 Registering a New Sprite

When replacing a sprite, the framework simply overrides whatever is at the provided sprite index. To register new sprites, you need a way to keep track of the new sprite indexes the framework assigns to your sprites. So go ahead and make the following changes to your sprite module:

/* sprite.h */
/* Includes... */

/* ----- INTERNAL TYPES ----- */

typedef struct Sprites { int32_t balloonRed; int32_t balloonBlue; } Sprites;

/* ----- INTERNAL GLOBALS ----- */

extern Sprites sprites;

/* ----- INTERNAL FUNCTIONS ----- */ /* ... */

/* sprite.c */
/* Includes... */

/* ----- INTERNAL GLOBALS ----- */

Sprites sprites = {0};

/* ----- INTERNAL FUNCTIONS ----- */ /* ... */

This first defines a new struct called Sprites for holding the indexes of your custom sprites. It then declares an internal global instance of this struct called sprites. Finally, sprite.c defines this global and zero-initializes it. Note that globals should usually be declared with the extern keyword, but defined without that keyword (but not always). Here's an explanation of the extern keyword when applied to globals.

Now change your RegisterSprites function to look like this:

/* sprite.c */
void RegisterSprites(void) {
  sprites.balloonRed =
      AERSpriteRegister("BalloonRed", "sprites/balloon_red.png", 6, 6, 40);
  sprites.balloonBlue =
      AERSpriteRegister("BalloonBlue", "sprites/balloon_blue.png", 6, 6, 40);

return; }

AERSpriteRegister has a somewhat similar signature to AERSpriteReplace, but the first argument is a unique name to give to the sprite rather than a sprite to replace, and it returns the index to your newly allocated sprite. You then save that index to the relevant member of the sprites global so that other modules in your mod can reference it.

Go back to the obj/hld/destructable.c file you created in the last tutorial and make these changes:

/* obj/hld/destructable.c */
#include "aer/object.h"
#include "aer/rand.h"

#include "obj/hld/destructable.h" #include "sprite.h"

/* ----- PRIVATE FUNCTIONS ----- */

static bool DestroyListener(AEREvent *event, AERInstance *target, AERInstance *other) { if (!event->handle(event, target, other)) return false;

/* Get attributes of current destructable instance. */ float posX, posY; AERInstanceGetPosition(target, &posX, &posY);

/* Spawn new instance of object being destroyed at position of target. */ AERInstance *new = AERInstanceCreate(AERInstanceGetObject(target), posX, posY);

/* Override attributes on new instance. */ AERInstanceSetSprite(new, (AERRandBool()) ? sprites.balloonRed : sprites.balloonBlue); AERInstanceSetSpriteSpeed(new, 0.0375f);

return true; }

/* ----- INTERNAL FUNCTIONS ----- */ /* ... */

When you run your mod, now, the original AER_SPRITE_NBOOKS16 should be there again, but once you destroy a destructable instance, it should turn into either a red or blue balloon that slowly bobs up and down.

The value you pass to AERInstanceSetSpriteSpeed is what controls the animation speed of the sprite. It's in units of frames/step, so if you know which speed your animation should be running at in frames/second, just divide that number 60 since there are 60 steps/second in HLD.

5.1 RNG

Also notice that you're using a new module, aer/rand.h, to generate a random boolean value when selecting the sprite. This module uses a modern and very high quality pseudo-random number generation algorithm at its core, and its functions are carefully designed to avoid introducing any distribution-related bias.

The function AERRandBool uses a shared, global random number generator that's automatically seeded when the game starts using the current time. This same generator is also used for all other functions of the form AERRand<type>. If you need control over the seed of the generator or you don't want to share generator state with other mods, you can create and use a self-managed generator using the functions of the form AERRandGen<type>.

I'd highly recommend using the RNG functions provided by this module instead of the rand and srand functions from stdlib.h, which are particularly old, low-quality and non-user-friendly.


#dev #moddevtut