Let's Talk Collisions
So, if I were to start this whole project over again I'd probably think of a better way to handle collisions. It's not that the current way is
bad, and the underlying logic was ... on the right path, it's just that the logic is little
widespread. Now, when I'm talking collisions here I'm not talking about level geometry, mostly. That's a whole other topic, instead I'm talking about things like whip detection, picking up items and enemies damaging the player on touch. If you read my earlier posts you know collision detection is part of every obj_entity, but the way it's implemented is a little more ambiguous than that. The interesting thing about parenting is that you can use it to define behavioral patterns, but more explicitly you can define things that are true about a particular instance. In the case of obj_entity, they are inherited by almost everything: enemies, items, weapons, candles, etc... and behavior just
isn't the same between them. For example, candles
are entities, and they can be collided
with, but they don't actually
collide with anything. As part of an entity, I can define the type of collisions they should have:
Code:
if collisionBehavior != noone { script_execute( collisionBehavior ) }
Rather than explicitly tying collision detection to obj_entity, instead the
potential for collision detection is assigned. The primary use is to remove it from objects that don't need it, but it also allows me to write special logic if the need arises. Not to dig into another topic altogether, but that's largely how my trigger objects work, they are a shell of behaviors, and I simply plug the behaviors in they need. Back to collisions however, the simplest types look like this:
Code:
/// @desc Collision Behavior
var _id = id;
var _collideMax = collisionMax;
var _collideWith = collisionWith;
var _collideGroup = collisionGroup;
with ( collisionType ) {
if _collideWith & collisionGroup == 0 || _id == id { continue }
if rectangle_in_rectangle( bbox_left, bbox_top, bbox_right, bbox_bottom, _id.bbox_left, _id.bbox_top, _id.bbox_right, _id.bbox_bottom ) > 0 {
_id.meeting = id;
meeting = _id;
with ( _id ) { event_user( 2 ) }
event_user( 3 );
if _collideMax - 1 == 0 { break } else { _collideMax-- }
}
}
Let's break down what we're talking about here: each object has a domain of objects it can collide with, this is used to simplify collision further for things like enemies that can only collide with the player. Why have them search for collisions with candles when they can't do anything with them? This also allows this script to also be further tailored based on how it's called. Next up is the _collideWith and collisionGroup. Sometimes the net that needs to be cast is a bit wide, for example weapons have to be able to collide with all sorts of things. However, only the whip can collide with the secret blocks you can break, and enemy attacks don't collide with enemies any more than player attacks collide with Simon. The whip has the same collision group as the destructible blocks it can destroy, along with enemies and candles. This is done through the use of a simple bitmask allowing groups to overlap as needed. It also, sometimes, might have to check itself because it belongs to a group it's checking, and this is a simple way to also allow it to ignore itself. Then we get into the meat and potatoes: we perform a check if the bounding boxes of the two objects overlap, and if so we set the 'meeting' variable for both objects then run some events. The
collision event for the the initiator, and the
collidee event for what was hit. This allows objects to have special behaviors depending on what they hit or are hit by, and when I said above that I'd do this system differently, this is a good example. It's a nice robust system, but it's barely used. Most collision effects are handled by the initiator, and my original intention was the opposite of that. It's not a bad system, I just haven't implemented it quite as consistently or strongly as I might like. Last, but not least, we have the max collisions. For reasons some things can only handle a limited number of collisions, whether for performance or mechanical reasons and once that number has been reached the checks are terminated. Though this may not work quite as intended, the
with loop is a little odd, I'm not entirely sure
break performs entirely as expected.
Now, to extrapolate on this, you might say, "That's all well and good, but I don't see anything about damage or anything else in there! How does this accomplish anything?" Well, that's where those collision events that are attached to each object come into play. If you've noticed nothing else, hopefully you've picked up on the fact that this code is all very generic. It really doesn't have much to do with anything except checking if two objects can, and have, collided. There's no mention of power-ups, invincibility frames, health loss or anything else in here. So, let's take a look at the collision event on the whip:
Code:
/// @desc On Collision
if attack {
if meeting.object_index == obj_secret_block {
if brick = false { brick = true } else { exit }
}
event_inherited();
}
Now, not to dig too far into how the whip actually works the generic idea here is that the whip is only 'active' during certain frames, which is determined by the 'attack' variable. When true it will check if the object being hit in this case is a secret block, and if we haven't yet hit a secret block, set it to true, otherwise ignore the collision. Again, you might be frustrated:
where is the code where you actually attack things??!? DID YOU EVEN WRITE CODE TO DO THAT?!? Again, this is all behavior that is
exclusive to the whip. The axe doesn't need to perform these types of checks, and so it doesn't. The 'generic' attacking logic is one step up, in obj_weapon:
Code:
/// @desc On Collision
if object_is_ancestor( meeting.object_index, obj_destructable ) && meeting.iFrame == 0 {
if meeting.hitPoints > 0 {
var _overlap_x = ( min( bbox_right, meeting.bbox_right ) + max( bbox_left, meeting.bbox_left ) ) div 2;
var _overlap_y = ( min( bbox_bottom, meeting.bbox_bottom ) + max( bbox_top, meeting.bbox_top ) ) div 2;
instance_create_for( sprite_render, _overlap_x, _overlap_y, layer_instance, effect_hit );
meeting.hitPoints -= damage;
meeting.iFrame = 24;
with ( meeting ) { event_user( 4 ) }
}
}
And there we have it, after
all of the other code has been run, this is the code that finally does two things: checks if the object hit is destructable, checking if the object is invincible, spawning the 'hit' effect, subtracting the damage from the unfortunate recipient's health and finally running the 'On Damage' event for the object that was just struck. This is where that 'meeting' variable I set earlier comes in, it allows the striker, and strikee, to have knowledge of what they hit/were hit by, and make determinations. In this case it uses the position of the two objects to spawn the hit effect, as well as call back to the thing that was just hit. Again, this is another place I might go a slightly different route, but it is effective as is. It allows each object to deal with death and destruction in it's own way, if needed. And to borrow the whip as a specific example, even though it uses the same generic collision detection as everything else, it's been highly tailored to provide a very specific output. It can hit blocks other weapons cannot, it has special attacking rules, and will even produce unique results. If I wanted, I could even make the enemies respond to the exact weapon they are hit with since they have some knowledge of that. The through-line in all of this is what I've been talking about in the other posts: each object only has the logic that it needs, it shares all of it's code with all the other objects like it, then extends it out in places where it happens to be unique. Sometimes that's as simple as the graphic. All items Simon can pick up share 99% of the same code, they have different graphics and different code on pickup. Everything else about them is identical.
That's probably enough harping on that for one day, hopefully you found this interesting or illuminating and until next time!
P.S. I did upload a playable version, it simply contains everything I posted in the previous video but if you want to give it a try, feel free. And if you have and feedback or find any bugs, please let me know!