Creating Objects


#1

Creating Objects in Frogatto:

Objects in frogatto are one of only two main elements that make up the entire game, the other being tiles. Objects are even sometimes used for set-pieces that blend in with, look like, and can be walked on, like tiles. Objects have very-little built-in behavior, and almost everything about them is described with small scripts that are triggered when some in-game event happens to them (such as bumping into the player). These scripts can also fire at regular time intervals.

Objects are defined in files placed in ~/data/objects/ There are no restrictions on which subfolder they go in, inside data/objects/ and we typically group them into categories. Because objects are referred to only by their own name, and not their path, they can be freely moved between folders. However, this means care must be taken that names never collide. The id of an object must match its filename.

Note: One thing that helps with this is that we have a namespace-like mechanism to create objects inside other objects (which we call utility objects) - if we need to create several sub-objects for a larger compound object, or if a certain enemy has some unique shots that it fires, they can be created inside the parent that uses them, and then referred to as “parentname.theirname” from any object. These are all created inside a single file, and are done simply by putting one [object_type] tag inside another.

There’s very little hard-coded behavior for objects. Hitpoints are one example; all objects default to one hitpoint unless you specify a new value, and if an object drops below one hitpoint, it’s “die” event will be fired, and it will be removed from the game. Losing hitpoints is NOT hardcoded behavior; in order to lose hitpoints, an object has to manually subtract them in response to something happening. This is important because different kinds of objects have very different reasons for losing hitpoints - some objects should be hurt when hit by the player; some objects are the player, and should be hurt only by enemies, other objects are scenery that should never be damaged.

Objects have a few hard-coded behaviors related to leaving the screen, or the level bounds; most of these can be customized with a few flags. All objects will die if they fall off the bottom of the level. By default, whenever any object travels more than 1/3 screen off the area directly visible to the camera, it will be “paused”. This saves on processing time; we can have enormous, cavernous levels, but the game will never need to crunch the calculations for more than what’s on one screen. This can optionally be turned off by setting “always_active=yes”; some rare objects are used for level-wide effects, such as rain, and always need to be on - be aware though that the performance cost of these can quickly add up, so don’t use this for objects that will be spawned in large numbers and which would have complex calculations. For many transient and decorative objects, the behavior of pausing can either just be a waste of processing power, or can even seem like a bug. To the players eye, it would seem very strange if a shot (that’s only supposed to have a tiny lifetime) got paused after it left the side of the screen, and then was unpaused when the player ran in that direction. Thus, it’s also possible to make some objects get deleted when the leave the screen, by setting “dies_on_inactive”.

The bulk of object’s behavior is specified in events. Events come in the form on_eventname=“FFL code that does something in response”. Some frequent uses are:

  • when an animation ends, the object will typically start another animation, or repeat the current one. For example, if we wanted to repeat the walking animation every time it finished, we could write: on_end_walk_anim=“animation(‘walk’)”
  • events can be triggered on regular time intervals; often, an object might respond to this by tracking the player’s movement (e.g. for an aiming turrent), or adjusting it’s trajectory (for, say, a guided missile).
  • events are triggered by objects bumping into each other, or bumping into the ground. This can selectively check if you’re bumping into different parts of the player or an enemy, allowing you to render only small parts of a larger enemy as harmful, or to render parts of an enemy as the only vulnerable part.

The other bits of hard-coded behavior are solidity, ‘named areas of a sprite’, gravity/motion, and animations. I mention these together because these can all be, unlike other stuff, specified on a per-animation basis. Animations are created inside an object with [animation] tags. These tags contain a reference to the image used by a sprite, as well as timings and arrangement of frames within that image. To save space, all of these can specified in a special [base:animation] tag, which spares you from having to repeat one value which stays the same for all of 10 or so different animations (for example, it’s common for them to all be in the same image, and to use a similar padding value of pixels between each frame). Each animation can have acceleration and velocity values specified inside it, and these will automatically be applied to the object during every frame it’s in that animation. These are used, for example, to make our ants move forward whilst in their walk animation. These are also used to give our creatures any gravity at all. These will remain in place unless another animation specifies a new value - thus, a common idiom is to specify accel_x=0 and accel_y=80, so that all animations will revert to those when they change away from the single animation that’s intended to represent locomotion.

An object can be given a “solid_area”, and the game will prevent this from intersecting with other solid-areas on the level. To allow the player to walk through certain friendly NPCs, and to allow friendly-fire shots from enemies to pass through each other, we have a system called solid_dimensions; objects will only be prevented from colliding with each other if they’re in the same solid dimension. Individual animations can be given solid areas in order to re-center the sprite when the animation changes; they must not change the size of their solid area, however, because this can lead to them overlapping other objects when they change an animation. The game has a rudimentary ability to resolve two objects bumping into each other, but it will also fire an event when this happens, and even provides an event for the case where it’s unable to resolve the overlap, which you can provide a more graceful resolution for (such as killing the enemy that’s colliding like that).

Individual animations (or frequently, all animations through the [base:animation] tag), can specify special rectangular areas of their sprite which are given a name. In response to some other object’s sprite touching these areas, the game will fire events. In these events, you can even query if some special area of the other object is touching yours - most commonly, an “attack_area” touching your “body_area”, in response to which you can decrement hitpoints. Conventionally, we use attack, body, and thrown areas to implement fighting, and being grabbed by frogatto’s tongue, but any arbitrary areas can be used to implement almost any kind of behavior.

Because many objects in the game, such as the walking ants, have similar behavior to one another, we’ve developed a system loosely like “superclasses”. Objects can be based on a “prototype”, and will inherit all of the behavior defined in that prototype, unless they manually override it. This allows us to quickly make new enemies that are “exactly like an ant, except for doing one thing differently”. In order to override the prototype’s behavior, you simply re-specify the same value or event in the object using the prototype. If you actually still want the prototype’s behavior, but with an additional twist, you can state “%PROTO%” in your event and the contents of the prototype’s event will be copied in. For example, if a prototype had some behavior like on_hurt=“add(hitpoints,-1)”, and you wanted to base a new object on it that displayed an animation of being hurt, you could base in on the prototype, and specify: on_hurt="[animation(‘hurt’),%PROTO%]"