Skip to content

Object Events

Richard Kettering edited this page Feb 12, 2016 · 10 revisions

The fundamental "thing that makes anything happen" in Anura is our event-handling system. The only way an object can execute any code is by receiving a event from the engine that "triggers" that code to run. Ultimately, object files are just lists of two things: data, and FFL code triggered by events. The code you'll write that's triggered by events is typically in two places; either written directly inside the events, or calls to code written in the properties block of an object (you also will call code that's provided by builtin functions of the engine or external libraries, but you don't have to write that code, yourself).

Events trigger everything - not just when specific, noteworthy things happen (like two objects colliding in the game, or the user pressing a button), but also any ongoing "loops" of constant, frame-by-frame behavior. Events typically are triggered in one of a few ways; either some game state automatically triggers them (such as two objects colliding, or an object being created), some periodic timer fires an event (such as on_process, or on_timer), or objects can explicitly fire events at themselves or another object via the fire_event() function. This page documents all the events that objects can receive, and what causes the event to trigger.

For an example of a basic event, the following code inside an object: on_create="debug('hi')" will print 'hi' to screen when the object it's in gets created. The code debug('hi'), between the double-quotes, is just an example; you can put any FFL you want in there.

Some events have arguments describing important information that such an event would be mostly useless, without (i.e. if someone clicks the mouse, you almost always want to know where). In non-strict mode, these are automatically inserted into scope as new variables you can access. In strict mode, they are accessed through the arg variable. For example, in on_click, you could get the position of the mouse with [arg.mouse_x, arg.mouse_y].

Note also that Anura has a built-in, opt-out concept of active/inactive objects as a major aid to performance-optimization. The general idea is that when you're playing on a game level, only a small region of the level near the player will actually be executing the scripts/physics of objects on the level, whilst everything else will be paused in a sort of suspended-animation, waiting until the player gets near them. By default, being active means an object needs to be on-screen (the engine checks, at the time of this writing, to see if the object's graphical edges are within 100 pixels of the edge of the screen bounds), but it's also possible to set flags that make an object always_active, regardless of its position, or that make objects activate within a wide margin of the screen (these are possible both individually, or for all objects of a given type). The vast majority of events will only be fired once their target object is active, although there are exceptions to this rule (such as on_start_level) - this means most events fired at a sleeping/inactive object will wait in a queue and be executed once the object is activated again.

Events

Periodic Events

  • on_process: One of the two most-useful events - this is triggered every "cycle" (1/50 of a second), if an object is active. Unlike the similar on_draw event, this will fire even if the game skips frames for performance (that is, when the game starts skipping frames, it only skips drawing - it still calculates object behavior during those skipped frames. If you have stuff that's purely visual, putting it in on_draw would be a manual optimization hint.).
  • on_process_(xxx)``: Triggered at the start of an active object's processing cycle if its current animation is 'xxx'
  • on_timer: Triggered at the start of every n processing cycles for an active object. n is given by the object's timer_frequency attribute.
  • on_draw: Triggered during every cycle, nominally when an object is drawn. Naively, one might expect this to be identical to on_process; the key difference is that this is not fired during skipped frames (which would get skipped during heavy processing), whereas on_process is always fired even for skipped frames. Another big usage of this is that unlike on_process, the level.camera_position variable is updated to be correct by the time on_draw is fired; if you need to keep an object's position in synch with the camera, this is the event to do it in - on_process would have a frame's worth of lag.

Creation events

  • on_start_level: Triggered when the level starts its first cycle. Triggered for all objects in the level whether they are active or not. Not executed when loading a save. Not executed on spawned objects. This should generally be avoided for object setup, in favor of on_create, because if for some reason an object is not present on the first cycle of a level (perhaps being spawned a frame late for some reason), it will not receive this event.
  • on_create: Triggered the cycle an object is created; for objects that are pre-placed on a level this works rather like on_start_level, except without the problematic need to be present on the level's first cycle. For objects that would be inactive, this will fire, execute its behavior, and then the object will go inactive. This event is generally ideal for object setup. This fires both for new levels, and spawning an object, but not for existing objects when loading a save.
  • on_first_cycle: Differs from on_create in that it fires the moment an object is first active. If an object is on the other side of a level, this will not execute until you walk over and cause the object to activate by it coming on-screen.
  • on_load: Differs from on_first_cycle in that whilst it similarly fires on new levels and being spawned, unlike them it does fire when an object is loaded from a save file (but not when reloading at checkpoints). This is useful for re-creating things that are not saved like ambient sound loops. (Typically such behavior should only be in on_load.)
  • on_done_create: Triggered immediately after create.
  • on_become_active: Triggered when an object becomes active after a period of inactivity.
  • on_child_spawned: Args: parent, child. Triggered when an object successfully spawns a child object. Note: this is only fired in response to spawn(), not in response to add_object() or other means of creating an object.
  • on_spawned: Args: spawner, child. Triggered when an object is successfully spawned. Note: this is only fired in response to spawn(), not in response to add_object() or other means of creating an object. Unlike on_create, this has the special property of being fired immediately on the same frame as the spawn() function is called, and this will work recursively if it, in turn, spawns another child - all of them will immediately appear on the same frame. Thus, this is the event to use to set up 'trees' of related objects, such as platforms and ropes. Also unlike on_create, this offers convenient access to the parent.

Destruction events

  • on_die: Triggered when an object's hitpoints are reduced to 0 or less, or when die() is called on the object. Not called when an object is removed using remove_object().

Usage: a major issue with on_die that we've had to work around in Frogatto is that whilst it will reliably fire whenever an object's hitpoints reach zero, it won't give you any information about why that happened. If you're making a game where all deaths are functionally the same (i.e. the dying enemy/shot/etc always plays the same animation regardless of the cause), then it's fine to use this to handle death.

In Frogatto, we wanted to have a sophisticated system where enemies could not merely respond to different kinds of damage in different cosmetic ways, but also in different behavioral ways (such as transforming into a different object if attacked by certain damage types, or dying in an explosive, area-of-effect damage treatment if attacked with the right kind of damage.) Because of this, we opted to make our own function to wrap any calls that would trigger death - we did this by hand-calculating any circumstances where a creature would die inside our own code, rather than only relying on the engine's own hitpoint calculations - and then within this code, triggering our custom death function whenever a creature would die. Because you, the modder, have to hand-apply any hitpoint-losses anyways (generally inside on_collide_object_body or whatever collision responder you chose to use), we had full control over how many hitpoints would get removed at any point in time.

  • on_being_removed: Triggered when an object is being removed from the level, not just by death as with on_die.

Usage: this should basically be considered what most languages call a 'destructor'; we've set up the game to always trigger this; if you're running into a situation where this doesn't fire, file a bug. I don't think, at the time of this writing, that this will trigger when leaving a level (at which point all objects on the level get deallocated), but in all other situations where an object gets removed (whether by dying, being removed via remove_object(object), or via dies_on_inactive: true,) this will get triggered. Thus, it's typically most useful for cleanup code.

Animation Change Events

  • on_enter_anim: Triggered when the object enters a new animation
  • on_enter(xxx)anim: Triggered when the object enters the animation 'xxx'
  • on_end_anim: Triggered when the object reaches the end of an animation. Only triggered if the object naturally reaches the end of an animation rather than terminating it early.
  • on_end(xxx)anim: Triggered when the object reaches the end of 'xxx' animation.
  • on_leave_anim: Triggered when the object leaves an animation for any reason.
  • on_leave(xxx)anim: Triggered when the objects leaves animation 'xxx' for any reason.

Collision/Damage Events

  • on_collide_object_(xxx): Triggered when the named area (xxx) collides with another area of the same name.
  • on_collide_level: Triggered when an object's opaque pixels collide with any solid pixels specifically provided by tiles (Anura provides solid areas that come from both objects, and the tiles on the level, this event applies only to those provided by tiles). This offers no integration with modder-named rectangles of opaque pixels (i.e. body_area, etc), it just indiscriminately applies to all opaque pixels.

Usage: This is generally a function used for damage, or the self-destruction of shots - it has no positional info, so it's difficult to use for movement and such, since you effectively have to guess which side of your object is making contact. Because of the limitation where this is indiscriminate about which modder-named collision-rect hits, we feel like this is basically a convenience function. You don't get to discriminate about which part of your object triggers this - it's all or nothing.

This convenience can work, because it's pretty common to have games where the full visual/sprite size of an object is equal to its hitbox - things like shots, for example. In quite a few games, though, it's common for the sprite to be much larger than the actual hit-box, and that's where this event stops being useful. In those cases, the best bet for detecting whether you're hitting the level is to actually define a solid area, and use the on_collide_side, on_collide_feet, on_collide_head functions - typically writing a single function, and calling the same function in all three.

  • on_collide_head: Args: area, collide_with, collide_with_area. Triggered when the top of an object's solid area collides with a solid part of the level or another solid object.
  • on_collide_feet: Args: area, collide_with, collide_with_area. Triggered when the bottom of an object's solid area collides with a solid part of the level or another solid object.
  • on_collide_side: Args: area, collide_with, collide_with_area. Triggered when the side of an object's solid area collides with a solid part of the level or another solid object.
Miscellaneous Collisions
  • on_collide_damage: Args: surface_damage. Triggered when an object collides with a tile that does damage. **TODO: why is this present when surface_damage also is?

  • on_surface_damage: A legacy feature in our engine is the ability to flag a tile with a damage value. The tile has to be solid (this cannot be applied to tiles the player is able to pass through). When the player's solid_area touches one of these, it will trigger this collision.

  • on_stuck: Triggered when an object is 'stuck' in a small pit without its feet able to touch the ground. Objects-with-feet in frogatto have a solid_area that's essentially an upside-down "isosceles right pentagon" (aka the shape of a baseball home plate). The peak of this pentagon is right in the middle, at an object's bottom edge. This allows them to ascend 45° angles, and clip into the terrain, correctly. It is possible for an object to drop into some obstruction where both sides of this pentagon are touching solid terrain, but the peak representing the "feet" are not. When this happens an object is assumed to have gotten "stuck" somehow, and this event is fired to allow it to execute some recovery behavior. In Frogatto, we typically make them bounce upwards and slightly to the side.
  • on_jumped_on: Args: jumped_on_by. Triggered when the "feet" portion of another object collide with the upper surface of this object's solid_area.
Water Collisions

Water is an engine-level feature of Anura, used heavily in Frogatto that marks off certain rectangular areas as submerged. Within these, you automatically get (radically) different object physics, but you also get a few events you can use to further alter behavior. Most enemies in Frogatto will die when submerged, and a few of them (such as the player) will switch to a major alternate behavior mode, with different animations, when they enter it.

  • on_enter_water: Triggered when an active object that wasn't previously in water enters water.
  • on_exit_water: Triggered when an active object that was in water exists the water.
Other
  • on_interact: This is a special event which was built into the engine for performance reasons, even though for most intents-and-purposes it's just a basic collision-area event. This will trigger any time the player 'interacts' (stands in front of and presses up) with an object which has an interact_area specified in one of its animations (this means the player's interact_area has to be overlapping the other object's interact_area). We put this in the engine a long time ago to make it easier to update an old C++ based HUD with information about whether the player was currently overlapping such an object. We left it in because it wasn't broken.

Keyboard/Input Events

  • on_ctrl_(xxx)``: Triggered when a control key is pressed. xxx = (left/right/up/down/jump/attack/tongue)
  • on_end_ctrl_(xxx)``: Triggered when a control key stops being pressed.
Other
  • on_begin_dialog: Triggered whenever a dialog begins. Dialogs are a special, engine-level behavior built for Frogatto, which lock player input, and take over the main processing loop of an object. We use these in most of our cutscenes, and they're able to provide the player with a series of written dialog lines coming from multiple (i.e. >2) characters. You can also nest them, have multiple branching options for player-responses, and execute almost any arbitrary commands at the start of a given line of dialogue.

Mouse Events

  • on_mouse_(xxx): Args: mouse_x, mouse_y, mouse_index, mouse_button. Triggered by mouse buttons being pressed/released or mouse motion over an active object at the mouse location. mouse_x, mouse_y are the current level x and y locations. mouse_index is a number, most useful in the case of touchscreen devices where every finger touch is a different mouse. mouse_button is a the mouse button being pressed, left=1, middle=2, right=3, mouse wheel up=4, mouse wheel down=5, 6&7 are side buttons if the mouse supports them. mouse_button is not passed to the on_mouse_move event. Note that all args need to be accessed through the arg variable in strict mode, eg, mouse_index is referenced by arg.mouse_index.
  • on_click: Args: mouse_x, mouse_y, mouse_index, mouse_button. Click event passed to the item at the top of the z-order when items are stacked together. The order of testing is z-order, then sub-z-order, then the item with the highest midpoint value of the z-order and sub-z-order are the same for multiple items. See on_mouse_(xxx) for a discussion regarding the arguments passed to the handler.
  • on_mouse_(xxx): Args: mouse_x, mouse_y, mouse_index, mouse_button, handled. Triggered by mouse buttons being pressed/released and mouse motion. The full set of names is on_mouse_up, on_mouse_down, on_mouse_move. These events are given to *every object on a level, though the expectation is that the number of objects actually implementing handlers would be small. The arguments to the function are interpreted in the same way as for the on_mouse_(xxx) handlers. The exception being the handled parameter which indicates that there was an active object under the mouse position that got the message first.
  • on_drag: as in on_mouse_* Fired when the object is dragged.
  • on_drag_end: as in on_mouse_* See on_drag.
  • on_drag_start: as in on_mouse_* See on_drag.

Editor events

Before having a live editor, we didn't have any of these events; it complicated things considerably for a worthwhile payoff.

Setup/Destruction

Quite a few pieces of code in our objects depend on prior setup, and fully assume that certain things about the object (such as position) will not change. For a simple example, some objects (which are assumed to be permanent, non-moving scenery) spawn extra objects that appear to be attached to them. Since they position these just once, during on_create, any subsequent movement of the object in the editor would not update the position of the child objects. What we do to fix this, is we provide a bunch of events that will trigger anytime an object is altered. Typically, it's a good idea to wrap any of your setup commands for an object into a single property, and call it not only in on_create, but also in one of these editor events.

We do actually handle a few trivial cases automatically - if there are child objects spawned by a another object, we automatically destroy them if the user deletes the creator. As a result of this, there aren't a lot of obvious use-cases for the on_editor_added/…removed events, but they're included nonetheless if you've got an odd use-case you need to cover.

  • on_editor_added: Fired when you use the "Add Objects..." mouse-driven tool in the editor to click on the level and place a new object.
  • on_editor_removed: Fired any time you select an object in the editor, and delete it.
  • on_level_tiles_refreshed: Triggered in the editor, when level tiles and their solidity are recalculated. This should be used by objects that use set_solid() to modify the normal solidity of a level to re-apply whatever changes they want to make.
  • on_type_updated: Triggered when an object is reloaded in the live editor, or triggered in the regular, non-editor execution of Anura if you've set the command-line option --reload-modified-objects. This is useful for instantiated classes, where the reloading of the base class normally does not reload the instantiated class. For example, a monster in Frogatto called gazer_grey does some stretch-and-squish animation by attaching a secondary object to itself (a class object called motion_distort), which does the animation procedurally by tracking the object's movement and stretching the sprite accordingly. It instantiates this motion_distort class with construct('motion_distort', {obj:me, squash:false}). If we wanted the motion distortion to reset to 'no distort' when the base class was reloaded, we could use on_type_updated to do so.
Changing written-to-disk variables on objects

Our live editor makes certain very complicated realtime puzzles (regardless of genre) easy to lay out, because you can see the execution of them in realtime, and adjust very complicated timings to match up correctly, rather than having to reboot the whole game and hope that the values you plugged in were correct. However, this throws a bunch of complications which an object writer needs to be aware of into the matter of "How is our editor supposed to draw things?". In a non-live editor, this is simple; any object that gets moved from point A to point B is destroyed, and a new copy is instantiated at point B (the same holds true for any other settings manually applied to an object). Whatever new value you change the variable to, is exactly what is displayed.

This rule goes out the window the moment any "written to disk" value gets changed, by game-execution, to something different than what you would prefer to have written to the disk. To illustrate this, we'll consider an example of a platform in a platformer game like Frogatto - for a platform, many puzzles built on it are built around the phase that not just one platform, but several platforms in a group, are set to in their back-and-forth oscillation. It's very common to write a puzzle where you have to wait for a brief moment when two physically separate platforms move close to each other, to make a difficult jump - to make this possible, it's crucial that these two move out of phase, rather than following each other in sync.

If we drag around an object in the editor, we cannot just "reboot" the object with a time-elapsed value (aka cycle) of zero (which would be a wonderfully simple, clean approach), because doing so would destroy this "phase" information. But at the same time, we're in a bind, because we really do want to be able to update the position of whatever the modder changed whilst editing - if you drag one end of the path downwards so that a horizontal path becomes diagonal, you do want to see that. You not only want to see the path change, but you want the live position of the object to be in the correct, phase-adjusted position as though it had started with the initial endpoints of the path where they are now.

To accomodate this, we have a distinction between events that will apply temporary, runtime changes to something, and permanent, written-to-disk changes to something. There are two events for this - they both receive a "what would be written to disk" value for the variable, but the meaning of the set() command is different in each of these! It's temporary/runtime in …changing… and permanent/serialized in …changed…. Since the value in the function that handles writing it to disk (…changed…) is already "what would be written to disk", you usually don't need to do anything to it - you can just set() it (or skip writing this event entirely). However, within the function to update the runtime appearance (…changing…), you'll need to pass it through whatever function you have to get the new value of the variable as a function of time, before setting it. If you don't have such a thing (if the position of the object is non-deterministic), then you'll need to just live with 'live' positions of the object which may fall out of sync with the actual game if you sufficiently modify the values in the editor.

  • on_editor_changed_variable: Ran after on_editor_changing_variable, to allow you to read the new, live value, derive a new on-disk value from it, and write it to disk.
  • on_editor_changing_variable: Ran prior to on_editor_changed_variable to update the live/realtime value of the variable in the editor.

Error-handling Events

Most of these errors happen when an object changes something about itself, or is added to the level (including when a level first starts up, at load-time), and ends up having its solid_area overlap with solid terrain, or some other object's solid_area. The engine doesn't do anything to try to recover from these issues, although opening a level in the editor will at least apply move_to_standing() to all objects that are failing this criterion.

Instead, it provides you with a chance to resolve this problem if such a collision happens. If you do not provide the corresponding event for an object, the game will immediately assert upon reaching the trigger condition. Within these events, you typically will want to 1] first set a tracking variable that you've tried something to fix it, and try moving the object's position around to resolve the problem. 2] check a frame later, and if the tracking variable is set but the problem isn't solved, either assert, or remove the object.

  • on_change_solid_dimensions_fail: Args: collide_with. Triggered when an attempt is made to change an object's solid dimensions, but this would cause a collision with other objects. Since terrain is exempt from solid_dimensions, this will only affect objects.
  • on_add_object_fail: Triggered when an attempt is made to add an object but the position it's currently set to (typically a position being read from disk at level-load) collides with solid terrain (or another object).
  • on_change_animation_failure: Args: previous_animation. Triggered when an attempt is made to change an object's animation but the new animation has different solid area and would cause a collision.
Something went wrong with that request. Please try again.