Resolving collisions in real life - not as easy as in Crafty.

Crafty is an incredible little game framework. I had the pleasure of getting introduced to it during my first Ludum Dare last month, and I’m working with it again this month for #1GAM. I hope to have a small platformer completed by the end of this month, and I’m well on my way.

However, I did run into one roadblock; how do I resolve collisions in Crafty? It’s got normal SAT collision detection built-in, but it seemed like most of the code samples I ran into simply moved entities back to where they were before the collision occured. That’s not the right way to do things, and if Crafty is already doing SAT, there should be more information provided. Turns out, there is - you just have to find it.

Typically, when using SAT, you’d just move your colliding entity out of whatever it collided with using something called the Minimum Translation Vector, or MTV. The name explains it all, really - it’s a vector representing the minimum amount of distance to translate (move) the entity so that it’s no longer colliding. Basically, it’s the arrow in this picture:

I’ll leave out the details of how you find it; the normal recommended article is this one from CodeZealot.

In Crafty, finding the MTV is indeed done for you, but it’s not documented very well. If you look in the API documentation, you can see that the .hit() function on the Collision component returns an object with an overlap attribute. However, it’s not entirely clear what this number is supposed to be used for. It says that it signifies “the overlap percentage between the colliding entities”; as far as I can tell, this is entirely wrong. It’s seems that it’s actually the opposite of the magnitude component of the MTV.

Unfortunately, that’s almost completely useless without the directional component of the vector, which Crafty’s documentation says nothing about. However, if you simply print out an example of the collision data returned by the Collision component’s .hit(), you’ll find that it contains an undocumented field: normal. Yup, you guessed it - it’s the normal of the entity’s collision, or the direction of the MTV.

The magnitude and direction of the MTV are enough data for us to resolve any collisions. The following short code sample would move an entity out of any one entity with the “walls” component that it’s colliding with. It would go into an entity’s _moved(). Note that we subtract rather than add in the code below; while the normal vector points in the right direction, overlap seems to always be negative.

var collisions = this.hit("walls");
if(collisions) {
var collisionData = collisions[0];
this.attr({x: this._x - collisionData.normal.x * collisionData.overlap,
y: this._y - collisionData.normal.y * collisionData.overlap});
}


There’s three things you should be aware of before blindly copy-pasting the code:

• I haven’t tested this code with anything other than axis-aligned rectangles, so I’m not completely sure that overlap is indeed the magnitude of the MTV, and that normal is always a unit vector. Edit: I and a commenter have tried it out on angled obstacles, and the technique above does seem to work.

• The above code moves the entity out of only one of the entities it’s colliding with; therefore, make sure you put this in your _moved() and not in _enterFrame(). When you do that, it’s safer (though still not entirely safe) to check only one entity, since the Moved event triggers for each axis the entity is moving on.

• If you’re using this for a platformer and adding gravity (as I am), you may want to note that this will cause the player not to be colliding with the platform on the next frame. That means the very next frame, it will seem like he’s in the air, possibly causing him not to be able to jump. I took this into account by simply adding 1 to the character’s y on a collision, so that he’d be “embedded” in the platform.

Despite these caveats, for my simple platformer, this code seems to work fairly well. Hopefully this is stuff that’s going to stay in Crafty, and the maintainers didn’t deliberately leave out of the documentation.