Discoveries of a Gravitational Nature

I’ve made two small discoveries in some games I’ve been writing that I want to share.

Coyote Jumps

The first is the idea of coyote time. I saw this initially in a Reddit post about a teaching aid for game developers. The name derives from Wile E. Coyote cartoons, where gravity doesn’t really start to work until after he’s left the edge of the cliff.

Coyote time comes into play most visibly with platformer games. Normally, when a player needs to jump, they need to be standing on a surface to do so. This can lead to some tricky timing situations, where a player has to time the jump perfectly to make a long jump or hit a moving platform. Players who can’t do this get frustrated easily, and leave your game behind.

Coyote time allows the player to jump even after they have left the platform. The player is given a few frames "grace" after they have left the platform to start the jump. This makes tricky jumps less tricky, and improves playability.

I recently implemented coyote time in an Arcade platformer. Arcade provides a method called .can_jump() which returns True if the player is standing on the ground and is allowed to jump:

if self.physics_engine.can_jump():
    self.player.change_y = game.PLAYER_JUMP_SPEED

To implement the coyote jump, I needed a way to check if the player had been able to jump within a few previous frames. I track this in a list called self.can_jump, which was initialized to all True values. The length was set to three, but could be made larger or smaller to change the amount of time for the jump. Then, every frame, it was updated as follows:

self.can_jump.append(self.physics_engine.can_jump())
self.can_jump = self.can_jump[1:]

If the player walks off a platform, then False will be appended every frame. However, at least one the values before it will remain True for at least three frames (or whatever the length of self.can_jump is).

Now, when the player wants to jump, I check the state of the can_jump list using functools.reduce():

if functools.reduce(lambda x,y: x or y, self.can_jump):
    self.player.change_y = game.PLAYER_JUMP_SPEED

Now, if the player could jump in any of the three previous frames, .reduce() will return True, and the player can jump. Otherwise, coyote time is up and it returns False.

There are more improvements to be had here, but this was the easiest to implement.

EDIT: A friend read this blog post, and made some suggestions to improve the code. I tested the changes, and they work brilliantly. Thanks Geir Arne!

First, self.can_jump was changed to a deque using the following:

from collections import deque
...
self.can_jump = deque([True]*3, maxlen=3)

A deque is basically a double-ended queue structure. It has a number of uses, but I’m taking advantage of the maxlen setting. This allows the deque to manage it’s length automatically, so I don’t have to trim it in .on_update():

self.can_jump.append(self.physics_engine.can_jump())

I’m also simplifying the checking code. Rather than using reduce() with a lambda, I use the built-in function any(), which returns True if anything in the iterable passed in is True. Since deques are iterable, this works perfectly:

if any(self.can_jump):
    self.player.change_y = game.PLAYER_JUMP_SPEED

Simpler, more Pythonic, and steering away from things which scare some programmers (I’m looking at you, lambda). What could be better?

Gravity

The second discovery I made was how to handle gravity with a mix of dynamic and static bodies using Arcade and Pymunk.

Arcade provides Pymunk to handle real world physics applications. This adds some interesting capabilities to many games, and is something I wanted to explore more fully.

I’ve been working on a game where the player launches an orbiting body around a planet to get to a goal on the other side. I need to build up to actual game play, so I wanted to start making the gravity simulations work and figure out how to make it both playable and workable.

To start, I set up two sprites on the screen. One represented a planet, and the other the player’s orbiting body, shown here with hit boxes enabled:

Testing the gravity game

For my game mechanics, I don’t want the planet to move, just the player’s rock.

Everything in the Pymunk physics engine is a body of some type.. Pymunk let’s you define the type of the body be static, dynamic, or kinematic. The differences are important:

  • Static bodies don’t move.
  • Kinematic bodies are moved deliberately in code, not by the physics engine.
  • Dynamic bodies are moved only by the physics engine.

Because the planet needed to be stationary and the orbiting body fly around it, I set the planet to be static and the orbiting body to be dynamic.

To make dynamic objects move in Pymunk, you apply forces and impulses to them, and the physics engine handles the rest. To make the orbiting body move, I first applied an initial impluse to get it moving to the left, then started calculating gravity between it and the planet:

def on_update(self, delta_time):
""" Movement and game logic """
        if self.initial_impulse:
            self.physics_engine.apply_impulse(self.player, self.initial_impulse)
            self.physics_engine.set_friction(self.player, 0)
            self.initial_impulse = None

        # To figure out gravity, I need the mass and position
        player_pos = self.physics_engine.get_physics_object(self.player).body.position
        player_mass = self.physics_engine.get_physics_object(self.player).body.mass

        # Gravity is 0 to start
        grav = pymunk.Vec2d(0,0)

        for planet in self.planets:
            # I need the mass and position of each planet as well
            planet_pos = self.physics_engine.get_physics_object(planet).body.position
            planet_mass = self.physics_engine.get_physics_object(planet).body.mass

            # Gravity per Newton
            grav_force = (planet_mass * player_mass) / player_pos.get_dist_sqrd(planet_pos)

            # Turn it into a directional vector
            grav += grav_force * (planet_pos - player_pos).normalized()

        # Now I can make the player move
        self.physics_engine.apply_force(self.player, grav)
        self.physics_engine.step()

The first few lines give me the initial impulse. Then I get the player’s mass and position, and use that in a loop to calculate a gravitational pull between it and the planet, using Newton’s Law of Gravitation:

G = \frac{m_1 \times m_2}{d^2}

Then I apply that force to the orbiting body, and let the physics engine figure out where to move the rock.

When I did this though, there was a problem — the orbiting body wasn’t on the screen! I wasn’t sure why, so I did some print debugging to figure out the values for the player and planet position and mass during the update:

Player pos: (600.0, 600.0), Player mass: 10.0
Gravity: (0, 0)
  Planet pos: (400.0, 400.0), Planet mass: inf
  Distance: 80000.0
  Grav Force: inf

Gravity was being set to ∞, because the mass of the planet was also set to ∞. This was wrong — I set the planet mass to 10,000. I needed to find out why the mass was so large, when I set it to something more reasonable.

After digging into the docs a little, I found the reason — in order to ensure that static and kinematic bodies don’t move automatically, they are never part of the physics engines calculations. Their mass is always set to ∞ so they will be ignored. Obviously, a static body wasn’t going to work for me as is.

My first try at a fix was to change the type of the planet from static to dynamic. This returned the mass of the planet to something reasonable, but it also moved the planet when the orbiting body collided with it. I could just move it back, but messing with the position of a dynamic body can seriously screw up a physics simulation.

After thinking about it for a minute, I found my final solution. I changed the planet back to a static body so it wouldn’t move. Then, instead of using Pymunk to track the mass of the planet, I stored the mass of the planet in the Arcade sprite instead:

for planet in self.planets:
    planet_pos = self.physics_engine.get_physics_object(planet).body.position
    planet_mass = planet.mass
    grav_force = (planet_mass * player_mass) / player_pos.get_dist_sqrd(planet_pos)
    grav += grav_force * (planet_pos - player_pos).normalized()

Now I had a planet that would stay put, but still be able to attract another body with built in gravity:

Slow orbit, but an orbit

Of course, this is just proof of concept. There’s still a lot more that needs to be done, like tweaking the forces and masses, speeding things up to a game play speed, fixing the silly hitboxes, and adding some real collision handling, but it’s a start. And it’s better than what I had before, which was nothing.

Next time, a deeper dive into the Arcade implementation of Pymunk, and an explanation of the gravity calculations I’m using.

1 thought on “Discoveries of a Gravitational Nature

Comments are closed.