I do not like my current collision system. When dynamic stuff interacts, everything just stops dead in their tracks. I expect the player to bump into a lot of things — enemies, chests, walls, etc. But it doesn’t feel good and that’s a big no-no for a game like this that is supposed to be gameplay-centric. I mean, the system has to be pretty much “perfect” to feel good. And the current wall checking logic is somewhat horrible too. I guess, in hindsight, I should not have done it the way I did. Oh well.

So I have 2 major requirements:

  • Walls must never allow any entity to overlap them
  • Entities can bump each other but should not overlap more than a little

Let’s redo the walls collision logic first. It will still have to be a separate algorithm due to its unique square nature. I’ll describe my result first and then mention all the caveats and important details. And this time, with pretty in-scene debug:

Here I’m simply marking every tile around the player (and every other entity) that I am checking for tiles. If the player’s coordinate moves into a wall (red sphere), then I know I have to push the player back:

This is also true when they move into two walls at the same time:

Note that the two cases above of running into walls do not have the red/green debug sphere on the neighbouring tiles. This is deliberate, because the corner cases are only applicable when there aren’t any directly blocking tiles. In other words, a corner can only block like this (otherwise the player would have collided with either left or bottom tile):


I’m glossing over a lot of details and iterations that I had to go through before I arrived at this solution. For example, I have to push the player back only in 1 direction (sound obvious, but it does mean that each of the 8 directions gets its own case handling):

Here, technically, the player is colliding in both X and Y directions. But I am only going to use the “least colliding” direction to push the player back. In fact, if I were to just move the player in both X and Y directions, they would completely spaz out. Similarly, I cannot use the corner tile for collisions (also sounds semi-obvious, but that means conditionally deciding when to check corners or not):

If I did collide, that tile would want to push the player back into the bottom tile and then even more to the right. Not only do the calculations become a mess, but the whole thing spazzes out even worse. In short, there’s a lot of ways to make it go haywire and only a few very specific solutions that don’t conflict.

Finally, I can optimize the tile checking by ignoring tiles that are too far away to ever collide with the player (assuming, they are not bigger than a tile):

Now onto entity collisions. Firstly, I changed the entire approach to moving and colliding entities. The main pipeline is now as follows:

  • Perform Requested Movement
  • Perform Entity Collisions
  • Perform Forced Movement
  • Perform Tile Collision

First, I let every entity move as it wants to. The player can follow player’s input, enemies can follow paths, projectiles can fly, etc. There aren’t any restrictions here, but the movement used proper acceleration and deceleration as applicable to what they are doing. This is what makes the player move smoothly.

Then I collide every entity with every other entity. Two entities can be overlapping because one or both moved into the other’s space or because last frame they were forced to. If they overlap, then I push the apart by storing a “force” for the next step.

Next, every entity is forced to move according to all the forces that were applied to it. Currently, the only forces are from overlapping two or more entities. In the future, this can be other things that make the entities move, like sliding floor or wind or whatever. So this step “separates” the entities from one another.

Finally, I check that the entities don’t collide with tiles. If they do, I move them away. There are no forces or speeds here — they get literally teleported out of the walls without compromise. This can result in entities overlapping after this, but that’s allowed. While moving into walls and warping through or something is definitely not allowed.

This approach lets me control exactly what happens to entity movement. Actual movement and force from collision do not interfere. The tiles always take precedence.

Now, to actually have the entities collide:

These are basically circle collisions — straight distances between the entities. When they are closer than the sum of their collision distance, then they both get pushed away from each other by that amount (step 2). If they happen to be moving as well, that’s an extra movement (step 1). The result is as above. The player can bump into and move enemies around. The player can squeeze between diagonally placed chests, but not two chests next to each other. Of course, all entities have their collision sizes, so the actual distance/interaction depends on that.

Now, doing all of this I broke the actual detection of who hits what, like projectiles hitting enemies. I also managed to inadvertently add stuff like projectiles pushing the enemies as they touch. So that’s another thing I’m fixing and adjusting.

And things moving too fast can pass through walls. This is a typical discrete vs continuous collision detection issue that physics engines have to deal with. And apparently, so do I now:

At least I know what the problem is and how to solve it. Essentially, I can never move things more than the smallest collider size that I have. In other words, if projectiles are 0.1 units wide, then they can move at most in 0.1 units steps or they can pass through things. Breaking movement into steps is much less efficient, but I have to do it if I don’t want things noclipping. Mobs may need some 2-3 steps per frame, while fast projectiles could go to 7-10. From the four tasks I mentioned above, both 1 and 3 now do these stepped movements.

I can further optimize performance a bunch, like cache various queries and requests and such. I’ve already taken steps to make it all run smoothly. So this shouldn’t be too bad, even with dozens if a hundred moving objects.

A new issue with having movement done in steps and potentially cancelled early is that when the player runs into a wall, they slide very slowly (that is, only the distance of the first step). This may not be too bad, but this can happen often when moving through doors or running in narrow corridors or mazes. It looks bad:

To solve this, whenever I first encounter a tile collision I can modify my movement (step) to only move (slide) along the tile/wall and not into it. So I can continue doing steps and if there is no other tile, the player will “slide” along the wall. It’s almost as fast as not sliding (since only the first step gets partly “cancelled”):

And this works great now. In fact, pretty much all collisions work great now. Looking back at the start of this, I can’t imagine how I could live with the old collisions. And I can’t imagine how I would force Unity physics to do some of the stuff that I want to do (I could get may be 80% there but last 20% would be nigh-impossible).

And, while enemies may still be dumb, the whole movement and collision system now works in their favour. They can get caught on each other, the player or props and the collisions will resolve themselves without the AIs needing complicated pathing checks:

This is really important if I ever want to do unexpected things to AIs, like push them around, have explosions scatter them, etc. And things like enemy swarms can navigate much better if they don’t have to do complicated crowd avoidance. I can make small enemies and large enemies and it doesn’t matter that they don’t fit exactly in grid squares or know how to avoid each other perfectly.

Final small issue is that, occasionally, the player experiences abrupt movement changes, such as when running into walls. So I will be making my camera follow them smoothly, so that there are never any jarring changes like this. But that’s for later.

MicroRogue DevDiary #12 – Better collisions
Tagged on:     

Leave a Reply

Your email address will not be published. Required fields are marked *