Published 3 years, 8 months ago
In this tutorial you'll experiment with the object event system to change the way vanilla objects work.
Note: This tutorial is for an outdated version of the mod runtime environment.
Contents
1 Source Structure
Since you'll be writing quite a bit more code in this tutorial, you should start thinking about how you want to structure you mod's source code files. While it's possible to keep everything in a single file, that starts to become quite unmaintainable for larger mods.
Right now the only source file you have is moddef.c
(and its header file). Based on its name, this module really ought to just be used to define your mod and nothing more, so you should move the function HandleRoomChange
to its own module. How you choose to divide your mod's source modules up is entirely up to you, but you should generally try to maximize
cohesion
and minimize
coupling.
For now, I'll offer a relatively straight forward structure for you to use.
AERModDef
's roomChangeListener
member is one of three so-called "pseudoevents" that the AER framework provides. They're called pseudoevents because they aren't real events handled by the Game Maker engine. Instead, they're fabricated by the framework to act as conveniences to mod authors. Therefore, having a module dedicated to holding these pseudoevents makes sense.
First, create a new header file for this module called pseudoevent.h
, and add the following contents to it:
/* pseudoevent.h */ #ifndef PSEUDOEVENT_H #define PSEUDOEVENT_H#include <stdint.h>
/* ----- INTERNAL FUNCTIONS ----- */
void HandleRoomChange(int32_t newRoomIdx, int32_t prevRoomIdx);
#endif /* PSEUDOEVENT_H */
In the last tutorial, the HandleRoomChange
function was private to the moddef
module since no other modules needed it (no other modules even existed). Now, however, moddef
is going to need to include this function from the new pseudoevent
module you're making, so it will need to be declared in the module's header file.
Next, you'll need to create this module's source file, pseudoevent.c
, but before that, you should add it to the list of source files in CMakeLists.txt
:
# CMakeLists.txt# Update this as you add new source files. set(SRC src/moddef.c src/pseudoevent.c )
Now create file pseudoevent.c
and move your HandleRoomChange
function from moddef.c
to pseudoevent.c
:
/* pseudoevent.c */ #include "aer/log.h" #include "aer/room.h"#include "pseudoevent.h"
/* ----- INTERNAL FUNCTIONS ----- */
void HandleRoomChange(int32_t newRoomIdx, int32_t prevRoomIdx) { AERLogInfo("Changed from room %i to room %i.", prevRoomIdx, newRoomIdx); if (newRoomIdx == AER_ROOM_AUTOSAVEMESSAGE) AERRoomGoto(AER_ROOM_TITLE);
return; }
Don't forget to remove the static
keyword from HandleRoomChange
since it's now internal instead of private.
Finally, go back to moddef.c
, remove the unnecessary includes and add pseudoevent.h
to the includes:
/* moddef.c */ #include "moddef.h" #include "export.h" #include "pseudoevent.h"/* ----- PUBLIC FUNCTIONS ----- */
MOD_EXPORT void DefineMod(AERModDef *def) { def->roomChangeListener = HandleRoomChange;
return; }
Now build and run your mod again to make sure this new source structure works.
2 Extending a Vanilla Object
With your source structure cleaned up, you're now ready to start extending and changing the functionality of a vanilla object.
When it comes to changing vanilla objects, the possibilities are effectively limitless, so I've somewhat arbitrarily decided which method you'll use to give the player unlimited ammunition for their gun.
2.1 Respawning Destructable Objects
One way of getting ammo in HLD is by destroying random stuff sitting around the room:
At first glance, it might look as if there are a wide variety of breakable objects that give ammo to the player, but it turns out that they are all instances of the same Game Maker object,
AER_OBJECT_DESTRUCTABLE.
So a very over-the-top yet fun way to give the player unlimited ammunition would be to immediately "respawn" instances of the AER_OBJECT_DESTRUCTABLE
object whenever they are destroyed.
To do this, you'll have to attach a custom event listener to that vanilla object's destroy event. There are a lot of different ways you could organize your source modules to do this, but for this tutorial you'll have one module for each object that you either extend or create (you'll learn how to define custom mod objects in a later tutorial) and one module to tie them all together.
2.1.1 Object Module
To start, create the files object.h
and object.c
to act as the module to tie all your mod's object code together (don't forget to add src/object.c
to CMakeLists.txt
) and give them the following code:
/* object.h */ #ifndef OBJECT_H #define OBJECT_H/* ----- INTERNAL FUNCTIONS ----- */
void RegisterObjectEventListeners(void);
#endif /* OBJECT_H */
/* object.c */ #include "object.h"/* ----- INTERNAL FUNCTIONS ----- */
void RegisterObjectEventListeners(void) { return; }
The function RegisterObjectEventListeners
will be responsible for attaching your custom event listeners. You might think you can then call this function from inside DefineMod
or inside a mod constructor (which your mod does not have right now), but object event registration actually needs to take place at a very specific stage in the AER framework (i.e. after custom object registration but before the main game loop starts). AERModDef
has a registerObjectListeners
callback for this very purpose.
Include object.h
in moddef.c
, and then pass this new function you just made to the MRE using your mod definition function:
/* moddef.c */ #include "moddef.h" #include "export.h" #include "object.h" #include "pseudoevent.h"/* ----- PUBLIC FUNCTIONS ----- */
MOD_EXPORT void DefineMod(AERModDef *def) { def->roomChangeListener = HandleRoomChange; def->registerObjectListeners = RegisterObjectEventListeners;
return; }
2.1.2 Destructable Module
Now you need to make a module dedicated to AER_OBJECT_DESTRUCTABLE
. It might be a good idea to create source and header sub directories for specific object-related code, so create the directories include/obj/hld/
src/obj/hld
. These will hold object modules that extend vanilla objects. Next, create files for the new destructable
module with the following contents:
/* obj/hld/destructable.h */ #ifndef OBJ_HLD_DESTRUCTABLE_H #define OBJ_HLD_DESTRUCTABLE_H/* ----- INTERNAL FUNCTIONS ----- */
void RegisterDestructableListeners(void);
#endif /* OBJ_HLD_DESTRUCTABLE_H */
/* obj/hld/destructable.c */ #include "obj/hld/destructable.h"/* ----- INTERNAL FUNCTIONS ----- */
void RegisterDestructableListeners(void) { return; }
Now call RegisterDestructableListeners
from inside object.c
's RegisterObjectEventListeners
function:
/* object.c */ #include "object.h" #include "obj/hld/destructable.h"/* ----- INTERNAL FUNCTIONS ----- */
void RegisterObjectEventListeners(void) { RegisterDestructableListeners();
return; }
With that, you're ready to create a custom event listener for AER_OBJECT_DESTRUCTABLE
's destroy event. Include
aer/object.h
from within obj/hld/destructable.c
. This will give you access to the enumeration AERObjectIndex
, which is quite similar to aer/room.h
's AERRoomIndex
enumeration in that it maps object names taken from the HLD executable to their respective room indexes. Including this header also gives you access to a number of functions for attaching event listeners to object events. The one to focus on is AERObjectAttachDestroyListener
, so add a call to this function from inside obj/hld/destructable.c
's RegisterDestructableListeners
function and hover your mouse over it:
The condensed documentation tells you that it takes an object index, which would be AER_OBJECT_DESTRUCTABLE
, and an event listener callback function. Unfortunately, this documentation snippet left out the part telling you to go
here in the API documentation
for more information about what this listener callback should do.
All object event listeners have the signature:
bool listener(AEREvent *event, AERInstance *target, AERInstance *other);
where argument target
is an instance (think entity) of AER_OBJECT_DESTRUCTABLE
object that is about to be destroyed. Argument other
is unused for destroy events, so just ignore that one. Argument event
is a reference to an instance of the
AEREvent
struct. Notice that this struct has a member function called handle
which has the exact same signature as object event listeners. That's because this function has the effect of calling the next event listener attached to object in question.
To help illustrate what this means, create your new destroy event listener function in obj/hld/destructable.c
like this:
/* obj/hld/destructable.c */ /* Includes... *//* ----- PRIVATE FUNCTIONS ----- */
static bool DestroyListener(AEREvent *event, AERInstance *target, AERInstance *other) { return event->handle(event, target, other); }
/* ----- INTERNAL FUNCTIONS ----- */ /* ... */
This could be thought of as an "identity" event listener. When an instance of the destructable object is about to be destroyed, the HLD engine would normally call the vanilla destroy event listener that Heart Machine wrote and attached to the destructable object's destroy event. With the AER framework installed, however, your custom listener will be called before the vanilla listener. If your mod is the only one that is loaded (which is probably the case right now), then calling event->handle
has the effect of calling the vanilla event listener that Heart Machine attached to this object. If there are multiple mods loaded, then calling event->handle
might call the vanilla listener, or it might call the next mod's listener in the chain (if that mod has a lower priority than yours). Either way, it shouldn't matter to your mod.
You may then be wondering what happens if you don't call event->handle
in your listener. In fact, this is (normally) a completely acceptable thing to do. By not calling event->handle
, the event effectively gets "canceled". If you do ever decide to cancel an event this way, make sure you always return false
. The value returned by event listeners basically says whether the event was handle (true
) or whether some mod event listener in the chain decided to cancel the event (false
).
That means that if you don't want to cancel an event (meaning you are expecting it to be handled normally), you should call event->handle
before you perform any custom event logic. Imagine that you want to free up some allocated memory related to this instance when the destroy event comes around, but there's a lower priority mod loaded that also has a listener attached to this same event. If you free that memory and then call event->handle
, that lower priority listener might decide it wants to cancel the event, so it doesn't call its event-handle
and it returns false
. That would be a problem since you already freed that memory.
For that exact reason, your event listener should usually first handle the event to see whether or not it gets canceled, and then only do its custom event logic if the event wasn't canceled. That would end up looking something like this:
/* obj/hld/destructable.c */ static bool DestroyListener(AEREvent *event, AERInstance *target, AERInstance *other) { if (!event->handle(event, target, other)) return false;/* Custom event logic here... */
return true; }
You might be thinking that choosing to always cancel this event would then result in the custom functionality you're after: instantly "respawning" destructable instances when they are destroyed. Unfortunately, destroy events happen to be the one event type that doesn't get canceled in the way you might think it would. While I'm hoping to add the functionality someday, I currently don't know how to cancel destroy events, so you generally shouldn't cancel them. Canceling any other type of event (like create ) does work how you think it should.
Regardless, you can achieve the behavior you're after by using a couple of functions from the
aer/instance.h
header. Update DestroyListener
to look like this (note that aer/instance.h
is automatically included when you include aer/object.h
:
/* obj/hld/destructable.c */ static bool DestroyListener(AEREvent *event, AERInstance *target, AERInstance *other) { if (!event->handle(event, target, other)) return false;/* Get attributes of current destructable instance. */ int32_t spriteIdx = AERInstanceGetSprite(target); float spriteFrame = AERInstanceGetSpriteFrame(target); float scaleX, scaleY; AERInstanceGetSpriteScale(target, &scaleX, &scaleY); 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);
/* Copy attributes to new instance. */ AERInstanceSetSprite(new, spriteIdx); AERInstanceSetSpriteFrame(new, spriteFrame); AERInstanceSetSpriteScale(new, scaleX, scaleY);
return true; }
Try hovering your mouse over all of those instance functions to see what they all do. The reason you should pass the result of AERInstanceGetObject
to AERInstanceCreate
rather than just AER_OBJECT_DESTRUCTABLE
is for the sake of mod compatibility. While just passing AER_OBJECT_DESTRUCTABLE
would work in this situation, if another mod created a custom object type that inherited from AER_OBJECT_DESTRUCTABLE
, whenever an instance of their destructable object gets destroyed, it'll eventually get to your event listener (since it's attached to the parent object of their custom object), and your listener would respawn it as a normal destructable object. You would probably want to respawn it as an instance of their object, so that's why you should call AERInstanceGetObject
in this situation.
All that's left is to attach this listener to the destroy event of AER_OBJECT_DESTRUCTABLE
inside RegisterDestructableListners
:
/* obj/hld/destructable.c */ void RegisterDestructableListeners(void) { AERObjectAttachDestroyListener(AER_OBJECT_DESTRUCTABLE, DestroyListener);return; }
Now try building and running your mod. You should be seeing results like this:
Note: This tutorial is for an outdated version of the mod runtime environment.
#dev #moddevtut