Physics of Robot Shooter


Recently, I shared some of the work I did on Robot Shooter on my Mastodon. In the comments someone asked me how I did the physics interactions. This is actually a really good question since gameplay like Robot Shooter's doesn't work very well with the physics engine's out of the box behaviour. The focus of this dev log is on how I got stacking of robot pieces to work. I will not cover how I did ragdolls that can have their limbs removed in this. That's a topic for another day.

First, Godot's built in physics engine is, well, a bit janky for the kind of stacking of irregular objects that I'm trying to do. It works fine for small stacks of regular but when a game involves piling odd shapes higher and deeper the physics engine starts to get a bit unhappy. Switching to Jolt physics helped a lot but it still has issues. At first things seemed to be working well. I can take pieces of enemies and pile them up and the piles will stabilize and stop moving. But, as soon as the player touches any of the enemies on the pile all the objects in the pile wake up and move again. This results in the player either sinking into the pile or shoving everything to the side. This is not good since I want the player to climb on TOP of the pile.

The solution to this was actually surprisingly simple. Physics objects in Godot have a setting that allows them to be completely frozen in space. Once frozen an object will not move at all and anything colliding with it will bounce off. With that in mind I wrote this function

        // NOTE: freezing is to make climbing on wreckage easier for the player
        //       so that instead of shoving piles of enemies around they act like
        //       rigid climbable props instead of ragdolls
        if (rigidBody.Sleeping && !rigidBody.Freeze)
        {
            //GD.Print("Limb ", collisionShape.Name, " is now ", rigidBody.Sleeping ? "asleep and freezing" : "awake");
            rigidBody.LinearVelocity = Vector3.Zero;
            rigidBody.AngularVelocity = Vector3.Zero;
            rigidBody.Freeze = true;
        }

Which mostly worked. Except when an enemy had some of its limbs attached and was ragdolling. For reasons that I didn't care to look too far into when multiple pieces of a robot were attached to each other all pieces would stay awake. That called for adding this lovely hack in the _PhysicsProcess function

        if(!gotPushed &&
           rigidBody.AngularVelocity.Length() < Mathf.Pi * 8.0f / 180.0f && 
           rigidBody.LinearVelocity.Length() < 0.1f)
        {
            // if no movement is going on even if there are active collisions force
            // the limb to sleep (and freeze) so enemies and probs pile nicely
            if (!rigidBody.Sleeping)
            {
                forcedSleepTimer -= delta;
                if (forcedSleepTimer < 0)
                {
                    rigidBody.Sleeping = true;
                    rigidBody.EmitSignal("sleeping_state_changed");
                }
            }
        }
        else
        {
            forcedSleepTimer = 0.1f;
        }

Now, when a piece of a robot sits still for a long enough time it will freeze even if the physics engine does not normally let it fall asleep. Problem solved. Robot wreckage now stacks up nicely and does not collapse when the player walks on it. But this introduces a new problem. Now there needs to be a way to allow the robot pieces to move again. Otherwise the player can't reposition destroyed robots to make proper use of them.

I can't simply let the pieces start moving again on any collision since that would eliminate the entire point of freezing them in the first place. So, instead, enemy pieces only unfreeze if they're hit by specific interactions. Namely, anything that would do damage to them were they still alive. This unfreezes and wakes them up and applies a force proportional to the damage being done. That way a powerful weapon will push an enemy harder than a weak one.

    GD.Print("Pushed by damage ", e.DamageAmount, " ", e.DamageDirection, " ", e.DamageSource.DamageType);
    Unfreeze(this);
    if (e.DamageSource.DamageType != DamageTypes.Yank)
    {
        // TODO: use Push method?
        // TODO: Supply damage location with DamageEventArgs at contact point of collision
        gotPushed = true;

        // TODO: * 60 is from physics tick rate, get tick rate from ??? somewhere
        // F = m*a
        // a = (endv - startv) / time
        // F = m*((endv - startv) / time)
        // F = Mass * ((PushVelocity - LinearVelocity) / Delta)
        Vector3 pushedVelocity = e.DamageDirection * (pushOnShotSpeed + e.DamageSource.DamageType);
        pushForce = rigidBody.Mass * ((pushedVelocity - rigidBody.LinearVelocity) * 60);
        pushPosition = Vector3.Zero;
    }

One thing you might have noticed is that the push isn't applied to the rigid body for the robot part immediately. That is because of a small quirk of the physics engine. There's a one frame window after a physics object has been created where trying to apply a force will cause an error. This is not a problem with freezing, I think, it's been a while. Instead it's a problem for an enemy that's killed while being hit by another shot on the same frame. This setup allows pushing to be put off for one tick to ensure no errors occur due to bad timing.

This still isn't perfect. There are a few improvements I'd like to make in the future, maybe some will make it into Alpha 5, who knows.

  • Robot pieces that are frozen can float in the air if what they're on is pull out from under them
  • I want really hard impacts to unfreeze robot parts.
  • Maybe have really heavy stuff still unfreeze and collapse part piles.

And that's it. Later folks.

Get Robot Shooter (alpha)

Leave a comment

Log in with itch.io to leave a comment.