IntroductionSince September of 2018, I have been the lead programmer of the small indie development team named Drillbit Studios. Our game is called THIS IS NOT A DRILL, a free-to-play endless jumper on mobile devices where the player climbs upwards through a barrage of falling blocks before their mineshaft is consumed by lava. For reference, here are links to our Google Play and App Store pages, but they aren't required to follow along.
My primary task for this project was to implement and refine all gameplay mechanics, with particular emphasis on the player's various forms of movement and interactions, block behavior, and procedural generation. The topic of this article, as suggested by the title, pertains to the stack of blocks itself -- and how surprisingly difficult it can be to make simulated objects stack convincingly. THIS IS NOT A DRILL was created with the Unity engine, but the lessons here should be broadly applicable to any game development environment. |
The ProblemRight about now you may be thinking to yourself, "Why Jesse, how difficult could it possibly be to stack game objects? All you have to do is spawn them, and have Unity's built-in physics simulation take care of the rest!" That is exactly what I thought before starting this project. And indeed, relying on built-in physics simulation gets the job done... until you try to scale it up. And the nature of TINAD requires a lot of objects to be stacked on top of each other. Take a look at the gif to the left from an early version of TINAD. Do you see anything wrong with it? That's right; as you can see, Unity's built-in physics simulation bugs out and causes the objects to squish together in ways that should be physically impossible.
But why does that happen? This problem occurred because I tried to create a large stack of game objects with Dynamic Rigidbodies. A dynamic rigidbody will take into account Mass, Drag, and Gravity and try its darndest to simulate your game objects. Just like how large stacks of heavy objects in the real world push downward with considerable force, each rigidbody tries to push on the next with the cumulative force of all the objects above it. If you're familiar with how Unity's collision works, you'll know that the engine only recognizes that a collision occurs when the two objects are already overlapping, only then to push the objects away from each other. But because each block is pressing down with such force, and the static ground at the bottom can't move out of the way, objects begin to clip into each other and compound the problem. |
The Solution
The problem of the squishing rigidbodies haunted much of TINAD's development. I spent months trying to iron it out, and along the way I stumbled upon a variety of quick-and-dirty hotfixes that could be used to help mitigate the problem. I have shared these hotfix solutions below, but none were the silver bullet needed to stop the stack from collapsing in on itself. Thinking it over, I concluded that the ideal solution to this problem would be to not use dynamic rigidbodies at all. Static rigidbodies wouldn't move or compress, no matter the force that's pushing on them. But it's not as simple as just making the blocks static. After all, they still need to fall, land, and fall again if the block they land on is destroyed.
Instead, you can set a block to static when it lands. A static block won't push on blocks below it, nor be pushed by blocks above. This fix completely solves the compression problem, but it comes with two new problems of its own. 1) How do you detect when a block has landed, and 2) What happens to a static block when you break the block it sits on? Luckily, these problems have solutions of their own.
Instead, you can set a block to static when it lands. A static block won't push on blocks below it, nor be pushed by blocks above. This fix completely solves the compression problem, but it comes with two new problems of its own. 1) How do you detect when a block has landed, and 2) What happens to a static block when you break the block it sits on? Luckily, these problems have solutions of their own.
1) Landing Detection
The first method for landing detection which I implemented, where I simply detect whether the object is moving vertically, was insufficient. It may take a moment for a block's vertical movement to reach 0, so if another block lands on it in that time, it will be pushed downwards and we're back to square one. This was actually the method in place when the gif from above was recorded. Notice how blocks at the bottom are static, but blocks at the top are still affected by the "squishing" problem. This is because they never slow enough to become static. We need a solution that works instantaneously, so that we can freeze blocks before they have the chance to begin compressing. The method used to achieve this was to use collision detection to freeze the block when it comes into contact with a static block. This goes into effect the same tick which it first collides, preventing the stack compression problem almost completely. The only exception is when many blocks that are already touching land at the same time, which may take several ticks as blocks become static from the bottom up. This problem is unavoidable, however, because a block must distinguish between static and dynamic objects in order to prevent unintentional freezing by mid-air collisions. For this reason, it is useful to use both of these landing detection methods simultaneously. |
2) Deal with floating blocks
So, now we have a stack of static blocks that won't collapse in on itself. That's all fine and dandy, but what about when we want the tower to come tumbling down? How do we put the blocks back to normal, without running into the same "squishing" problem? Ideally, you would only want to reset blocks to dynamic that no longer have support. I attempted to achieve this by creating a tree of children that each block supports, but I found it difficult to receive reliable results. There were many edge cases, such as when two colliding blocks do not support each other, where my algorithm broke down. Add on top of that the increased memory usage, and I found it necessary to use a much simpler solution. Upon breaking a block, I reset to dynamic all blocks in the stack that are above the broken block. This method will never leave a static block suspended in the air, although it may reset some blocks that do not need to be dynamic that form a separate branch. Unfortunately, using any method to solve this problem will trigger the edge case for my solution to the previous problem -- where a large chunk of blocks falling at once may take several ticks to fully freeze. The squish effect in this edge case is fairly minimal and fixes itself quickly, but can still be noticeable when done repeatedly at the bottom of a large stack. Luckily, the last remnants of the effect can still be mitigated by the tips below! |
Various Quick-and-Dirty Tricks
Results may vary. Many of these options will help mask the problem, but won't prevent it.
- Destroy blocks that overlap. While this doesn't solve the problem, it prevents it from getting out of hand by preventing the worst-case scenario. If using the method above, the problem should sort itself out fairly quickly.
- Don't spawn more blocks than you need. This is fairly obvious, but a short stack will squish less.
- Put the rigidbodies to sleep while not moving. Implementing this trick requires many of the same steps as the method above, so you'd be better off setting them to static as well.
- Try using Continuous collision detection. Continuous detection is much more resource-intensive than discrete detection, but continuous colliders won't push into each other as much.
- Try using Interpolation. Very similar to the previous tip, you may be able to prevent an overlap in the first place. In my experience, however, this could cause more problems than it solves.
- Test different options available in the Physics section of Project Settings. There are many options to refine the physics simulation available to you, play around with these variables and you may be able to increase performance.
- Increase Linear Drag. This makes objects fall slower, and therefore push down with less force.
- Increase Friction. This makes objects fall slower, and therefore push down with less force.
- Lower Gravity. This makes objects fall slower, and therefore push down with less force.
Code Samples - C#
The following is a code sample demonstrating my solution to the "Landing Detection" problem in section 1. This function is part of a script attatched to the Block object that activates when the block is collides with another object, and is designed to lock itself into place by setting its rigidbody to static.
Section 1: Landing Detection
The following is a code sample demonstrating my solution to the "Deal with floating blocks" problem in section 2. This function is part of a script attatched to the Block object that activates when the block is destroyed, and is designed to reset the rigidbodies of all unsupported blocks to Dynamic.
Section 2: Deal with Floating Blocks