Writing object behaviors
An instance of a class implementing jjBEHAVIORINTERFACE lets you redefine nearly every aspect of how an individual jjOBJ, or set of them, behaves and interacts with the game around it. You could almost think of them as extending the jjOBJ class directly, in fact, but the API isn't quite set up right to make that possible. Still, they're very closely linked.
To begin with, it is important to have some understanding of how JJ2 handles its objects. Every active object contains a pointer to a function that defines its behavior. To take a simple example: the Pulze Light object sits in place, constantly adjusting its light property, and removes itself from memory when it goes too far offscreen or when the player dies in single player. Most objects have significantly more complicated behaviors than that, but they all come down to one thing: a function that is called by the object, every single tick. What AngelScript does is allow you to write your own object behaviors, either based on JJ2's native ones or else totally from scratch.
The starting point for any object customization is the jjOBJ property behavior. JJ2 (and by extension AngelScript) has a massive inventory of possible values, all grouped together for you in the BEHAVIOR namespace. Most of the behaviors correspond to individual JJ2 objects—BEHAVIOR::QUEEN for OBJECT::QUEEN, BEHAVIOR::CHESHIRE1 for OBJECT::CHESHIRE1, and so on—but there are also a lot of more generic behaviors that get recycled for multiple objects, such as BEHAVIOR::PICKUP (food, gems, ammo, coins, and so on), BEHAVIOR::WALKINGENEMY (lizards, hatters, doggy doggs, and several more), and BEHAVIOR::SHARD (various particle effects). To make the Norm Turtle enemy behave like its JJ1 counterpart, i.e. walk back and forth and never do anything else, we need only change its behavior from BEHAVIOR::NORMTURTLE to BEHAVIOR::WALKINGENEMY. (And by giving it a generic enemy-type playerHandling value, we can also remove its behavior of creating turtle shells when defeated.)
void onLevelLoad() {
jjObjectPresets[OBJECT::NORMTURTLE].behavior = BEHAVIOR::WALKINGENEMY;
jjObjectPresets[OBJECT::NORMTURTLE].playerHandling = HANDLING::ENEMY;
}
Still, that's not very exciting. How about a Norm Turtle that walks back and forth and changes direction every second, regardless of whether it's about to hit a wall? For this, instead of setting behavior to a BEHAVIOR::Behavior constant, we use an instance of a script-defined class that implements jjBEHAVIORINTERFACE:
void onLevelLoad() {
jjObjectPresets[OBJECT::NORMTURTLE].behavior = TurnAround(); //jjOBJ::behavior must be set to an instance of a class, not to a class itself. This means that instead of BEHAVIOR::WALKINGENEMY or BEHAVIOR::NORMTURTLE, Norm Turtles will call the method named "onBehave" on this class instance.
jjObjectPresets[OBJECT::NORMTURTLE].playerHandling = HANDLING::ENEMY;
}
class TurnAround : jjBEHAVIORINTERFACE { //The full list of methods for jjBEHAVIORINTERFACE is given below, but the only required one is "void onBehave(jjOBJ@)"
void onBehave(jjOBJ@ obj) { //called once per tick, like any other behavior
if ((jjGameTicks % 70) == 0) //a second is 70 ticks long, so this means "once per second"
obj.xSpeed *= -1; //reverses xSpeed and therefore direction
obj.behave(BEHAVIOR::WALKINGENEMY); //jjOBJ::behave tells the object to spend a tick as if its jjOBJ::behavior equalled the method's first argument, in this case, BEHAVIOR::WALKINGENEMY. Without the xSpeed code, therefore, this would be functionally identical to the previous ".behavior = BEHAVIOR::WALKINGENEMY;" example.
}
}
Another fun trick is that behave takes an optional boolean parameter (defaults true), to specify whether JJ2 should actually follow any instructions within the native behavior function to draw the object. By setting this parameter to false, we gain the opportunity to draw the object however we like using AngelScript's various drawing functions, for example, tinted red:
void onLevelLoad() {
jjObjectPresets[OBJECT::NORMTURTLE].behavior = TurnAroundTintedRed();
jjObjectPresets[OBJECT::NORMTURTLE].playerHandling = HANDLING::ENEMY;
}
class TurnAroundTintedRed : jjBEHAVIORINTERFACE {
void onBehave(jjOBJ@ obj) {
if ((jjGameTicks % 70) == 0)
obj.xSpeed *= -1;
obj.behave(BEHAVIOR::WALKINGENEMY, false);
}
void onDraw(jjOBJ@ obj) { //usually also called once per tick
jjDrawSpriteFromCurFrame(obj.xPos, obj.yPos, obj.curFrame, obj.direction, SPRITE::TINTED, 24);
}
}
In the above examples, we've been using the default argumentless constructor for our jjBEHAVIORINTERFACE classes, but that's only scratching the surface. You have the power to define your own classes, and with that comes the power to define any number of properties or methods not included in the fairly limited jjOBJ class. A jjOBJ has no string properties, for example, but you can add one to a jjBEHAVIORINTERFACE:
void onLevelLoad() {
jjObjectPresets[OBJECT::NORMTURTLE].behavior = DescribedWalker("turtle");
jjObjectPresets[OBJECT::NORMTURTLE].playerHandling = HANDLING::ENEMY;
jjObjectPresets[OBJECT::LIZARD].behavior = DescribedWalker("lizard");//note that both turtles and lizards use the exact same class, DescribedWalker, but pass different strings to its constructor
}
class DescribedWalker : jjBEHAVIORINTERFACE {
private string enemyType; //jjOBJs don't have string properties, but we can put one in here
DescribedWalker(const string &in et) {
enemyType = et;
}
void onBehave(jjOBJ@ obj) {
obj.behave(BEHAVIOR::WALKINGENEMY, true);
jjDrawString(obj.xPos, obj.yPos - 40, "I'm a " + enemyType + "!");
}
}
Or, taking the power of constructors to their extreme, you can give each object its own individual class instance. The most convenient way to do this is often using anonymous functions. The following example once again uses a string property on the jjBEHAVIORINTERFACE class, but you can use any other types too, from uints and floats all the way to arrays or dictionaries.
uint NumberOfTurtles = 0;
void onLevelLoad() {
jjObjectPresets[OBJECT::NORMTURTLE].behavior = function(obj) { obj.behavior = CountTurtles(); };
}
class CountTurtles : jjBEHAVIORINTERFACE {
string description;
CountTurtles() {
description = "Turtle #" + (++NumberOfTurtles);
}
void onBehave(jjOBJ@ obj) {
obj.behave(BEHAVIOR::NORMTURTLE, true);
jjDrawString(obj.xPos, obj.yPos - 40, description);
}
}
(In addition to constructors, you may also give your class a destructor; it should however be noted that the AngelScript library does not guarantee that an object's destructor will be called the exact moment there are no more references to it, only that it will be called eventually. Therefore, you should not rely on a destructor to notify you instantly when a jjOBJ's behavior changes due to e.g. jjOBJ::delete.)
At some point, though, you'll want to break away from the crutch that is jjOBJ::behave and write your own object behavior from start (literally) to finish. The most important jjOBJ property to consider when defining a custom object behavior is state, since JJ2 objects are basically state machines. When an object is first created—barring bizarre jjObjectPresets fiddling—its state will equal STATE::START. Traditionally, objects take this opportunity to initialize a few properties not already set in jjObjectPresets, perhaps read some parameters from the event map, and then change their state to something else, e.g. STATE::IDLE or STATE::STILL or STATE::DELAYEDSTART. A red spring, for instance, learns during STATE::START whether it's supposed to be a ceiling spring or a floor spring, and never bothers checking to find out ever again. If you're defining a bullet object, changing state to something else (usually STATE::FLY) is mandatory, since bullets of STATE::START are not checked for collision with other objects/players; otherwise there's nothing in the game code that forces you to do it, but it certainly seems like it should be a good idea.
On the opposite side of an object's lifespan is STATE::KILL. Not all objects need to have this state, but anything that uses JJ2's normal shootable-object code will be set to STATE::KILL when the energy property reaches 0. In most cases, STATE::KILL is a sign to call jjOBJ::delete, although you may want to do other things as well, e.g. add an explosion. Bullets are a bit different, using STATE::EXPLODE instead, but the outcome is pretty much the same.
Somewhat related is STATE::DEACTIVATE, the only state besides STATE::START that is essentially guaranteed to apply to every single object, albeit only in Single Player. (In multiplayer, it can of course be invoked manually, but will never be triggered from the game itself.) STATE::DEACTIVATE occurs under one of two circumstances: the player dies, causing every object to deactivate, or the object was active but is now more than about thirty tiles distant from the player and thus no longer belongs in active memory. Objects whose deactivates property is set to false, e.g. Rotating Rock, are immune from the latter case, but all objects get STATE::DEACTIVATE when the player dies in SP. Like STATE::KILL and jjOBJ::delete, you can usually just get away with calling jjOBJ::deactivate, which deletes the object and (if it was created directly from the level map) makes a note that the object is no longer active and can be recreated later.
Finally, many objects will need some code for STATE::FREEZE. The usual pattern is to decrease the freeze property by one every tick, and once it hits 0, restore state to oldState. This is an also an opportunity to use the SPRITE::FROZEN mode for drawing sprites, although jjOBJ::draw will take care of that for you automatically, along with flashing the object white if it's recently been shot. Here, then, is some sample code for a very basic enemy that sits in place and does absolutely nothing but animate:
void onLevelLoad() {
jjOBJ@ presetObject = jjObjectPresets[OBJECT::NORMTURTLE]; //at this point, it doesn't matter so much which jjObjectPresets you choose, since you'll be editing most or all of its relevant properties
presetObject.behavior = StationaryEnemy();
presetObject.determineCurAnim(ANIM::SUCKER, 4);
presetObject.playerHandling = HANDLING::ENEMY;
presetObject.bulletHandling = HANDLING::HURTBYBULLET; //some of these values will already be used by whichever jjObjectPresets slot you choose, but it can't hurt to make sure
presetObject.isTarget = true;
presetObject.isFreezable = true;
presetObject.triggersTNT = true;
presetObject.deactivates = true;
presetObject.energy = 1;
presetObject.points = 300;
}
class StationaryEnemy : jjBEHAVIORINTERFACE {
void onBehave(jjOBJ@ obj) {
switch (obj.state) {
case STATE::START: //always used
obj.state = STATE::IDLE;
case STATE::IDLE: //arbitarily chosen state
obj.frameID = (jjGameTicks/5) & 7;
obj.determineCurFrame(); //remember to do this after changing frameID, since by the time you're writing your own behavior, JJ2 won't do it for you anymore
break;
case STATE::FREEZE: //can be left out if object can't be shot, or if isFreezable equals false, or if there's no ice in the level, or if you don't mind object never unfreezing
if (--obj.freeze == 0) obj.state = obj.oldState;
//consider calling jjOBJ::unfreeze() here
break;
case STATE::DEACTIVATE: //can be left out if level is MP-only
obj.deactivate();
break;
case STATE::KILL: //can be left out if not using normal object energy handling
obj.delete();
break;
}
}
void onDraw(jjOBJ@ obj) {
obj.draw();
}
}
Naturally there's a lot more that an enemy can do—move around, change animations, fire bullets—but that's the basic structure right there. Draw the sprite to the screen somehow, remember to delete the object when it gets killed or deactivated, and pretty much everything else is optional or bonus. You don't even need to worry about its energy, since the HANDLING::ENEMY and HANDLING::HURTBYBULLET settings make JJ2 take care of all that stuff for you. Then there's the basic form of a bullet, which might look something like this:
void onLevelLoad() {
jjObjectPresets[OBJECT::BLASTERBULLET].behavior = DullBullet(); //for the sake of example, let's just use blaster's existing values for curAnim and xSpeed and so on
}
class DullBullet : jjBEHAVIORINTERFACE {
void onBehave(jjOBJ@ obj) {
if (obj.state == STATE::START) {
obj.state = STATE::FLY;
if (obj.creatorType == CREATOR::PLAYER) obj.xSpeed += obj.var[7] / 65536.0; //xSpeed of the player when firing the bullet
} else if (obj.state == STATE::DEACTIVATE) {
obj.delete();
} else if (obj.state == STATE::EXPLODE) {
obj.behavior = BEHAVIOR::EXPLOSION2;
obj.frameID = 0; //display the full .killAnim animation
} else {
obj.xSpeed += obj.xAcc;
obj.ySpeed += obj.yAcc;
if ((--obj.counterEnd == 0) || (jjMaskedPixel(obj.xPos + obj.xSpeed, obj.yPos + obj.ySpeed))) {
obj.state = STATE::EXPLODE;
} else {
obj.xPos += obj.xSpeed;
obj.yPos += obj.ySpeed;
obj.draw();
}
}
}
}
The most important things about defining a bullet are a) changing the state from STATE::START and b) changing the state to STATE::EXPLODE, because those states are referenced by various bits of external code. Bullets with either of those two states will not be checked for collision with other objects or players. Moreover, setting the state to STATE::EXPLODE in an online server may potentially tell other clients that their own copies of that bullet object need to be destroyed.
To illustrate one way in which bullet behaviors may be made more complicated, using the following code instead will cause the bullet to recognize ricochet events:
if (--obj.counterEnd == 0) {
obj.state = STATE::EXPLODE;
} else if (
jjMaskedPixel(obj.xPos + obj.xSpeed, obj.yPos + obj.ySpeed) && ((jjEventAtLastMaskedPixel != AREA::RICOCHET) || !obj.ricochet())) {
obj.state = STATE::EXPLODE;
} else {
obj.xPos += obj.xSpeed;
obj.yPos += obj.ySpeed;
obj.draw();
}
Not all objects are enemies and bullets, of course, but you can get by for quite a while by pretending they are while making ever more inventive use of the various sprite-drawing functions. Still, what if you want to add a new pickup instead? For example, say you want the Fast Feet pickup to increase how high a player can jump. For that you'll need the most complicated jjBEHAVIORINTERFACE method of them all: onObjectHit.
void onLevelLoad() {
jjObjectPresets[OBJECT::FASTFEET].points = 100;
jjObjectPresets[OBJECT::FASTFEET].scriptedCollisions = true;
jjObjectPresets[OBJECT::FASTFEET].behavior = FastFeet();
}
class FastFeet : jjBEHAVIORINTERFACE {
void onBehave(jjOBJ@ obj) {
obj.behave(BEHAVIOR::PICKUP);
}
bool onObjectHit(jjOBJ@ obj, jjOBJ@, jjPLAYER@ player, int) {
player.jumpStrength -= 1;
obj.behavior = BEHAVIOR::EXPLOSION2; //this is _essential_. just like enemies die by getting their states set to STATE::KILL, and bullets die by getting their states set to STATE::EXPLODE, pickups die by getting their behavior set to BEHAVIOR::EXPLOSION2. yes, sometimes a little consistency is in fact too much to ask for.
obj.scriptedCollisions = false; //or obj.playerHandling = HANDLING::EXPLOSION; or something like that
obj.frameID = 0;
//you should probably play a sound here too, using jjSample. pick one! it'll be an adventure!
return true; //for details, see discussion in the onObjectHit item in the method list below, but basically you should almost always return "true" here
}
}
The short version of the story is that onObjectHit is what gets called for objects for which scriptedCollisions equals true, if their playerHandling value is HANDLING::PICKUP (called for collisions with players only), or HANDLING::SPECIAL (called for collisions with either players or bullets). The above case, HANDLING::PICKUP, is pretty straightforward: the third argument points to the jjPLAYER who collided with the pickup object, and the second and fourth arguments may be totally ignored. For the details of working with HANDLING::SPECIAL, which is rather more complicated, refer to the onObjectHit method description.