Published 3 years, 10 months ago
In this tutorial you'll learn the basics of sprites. This includes replacing vanilla sprites and adding new ones.
Note: This tutorial is for an outdated version of the mod runtime environment.
Contents
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 creating 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.
Note: This tutorial is for an outdated version of the mod runtime environment.
#dev #moddevtut