Simple space simulation using Python and Box2D

Hello, Habr.

This article was inspired by the recent publication of Modeling the Universe , where the author showed a very interesting simulation of various cosmic phenomena. However, the code presented there is not easy for beginners. I’ll show you how to do physical simulations using the Box2D engine by writing just a few lines of code.

I’ll take a chance to make a mistake, but this is the first description of Box2D for Python on Habré, we fill this gap.



For those who are interested in how this works, details are under the cut.

Box2D is a free cross-platform library created by Blizzard Erin Catto. The library was introduced in 2007, and today it has been ported to almost all platforms. There is a port for Python as well, its description is rather confusing, but I hope that with the help of this article everything will become clearer.

Introduction


The pybox2d library consists of two components - Box2D itself, a cross-platform library for physical modeling, and a separate rendering module called Framework. Rendering is needed if we want to see the created objects on the screen, which is convenient enough for our simulation. The Framework class can use various output methods (more here ), we will use pygame. If the pygame library is installed, it is "picked up" automatically, and nothing else needs to be done. To install, just enter the pip install Box2D pygame command .

The smallest running program using Box2D is shown below. The code is cross-platform, and will work everywhere, both on Linux and Windows.

from Box2D.examples.framework import Framework
from Box2D import *

class Simulation(Framework):
    def __init__(self):
        super(Simulation, self).__init__()

        # Ground body
        self.world.CreateBody(shapes=b2LoopShape(vertices=[(20, 0), (20, 40), (-20, 40), (-20, 0)]))
        # Dynamic body
        circle = b2FixtureDef(shape=b2CircleShape(radius=2), density=1, friction=1.0, restitution=0.5)
        self.world.CreateBody(type=b2_dynamicBody, position=b2Vec2(0,30), fixtures=circle, linearVelocity=(5, 0))

    def Step(self, settings):
        super(Simulation, self).Step(settings)

if __name__ == "__main__":
    Simulation().run()

As you can see, we are creating a Simulation class that inherits from the already mentioned Framework. Next, we create two objects by calling the CreateBody method . The first is a static object that defines the boundaries of our world. The second object is of type b2_dynamicBody, the remaining parameters (shape, size, density, friction coefficient, initial speed) are obvious from the code. The Step function is called every time during the simulation, we will use this in the future. If the UI is not needed, for example, we do a backend for the server, then of course, the Framework class can be omitted, but for us it is quite convenient.

That's all, run the program and see the finished simulation:



As you can see, we just created two objects and specified their parameters. Everything works “out of the box” - gravity, friction, elasticity, interaction of bodies, etc. Based on this, we can proceed with our “space” simulation.

We launch the "satellite"


Unfortunately, there is no built-in support for Newtonian gravity in Box2D, you will have to add it yourself by adding the Step function. For the first test, we will create two bodies - a planet, and a satellite rotating around it.

Source code as a whole:

from Box2D import *
from Box2D.examples.framework import Framework


class Simulation(Framework):
    def __init__(self):
        super(Simulation, self).__init__()

        # Default gravity disable
        self.world.gravity = (0.0, 0.0)
        # Gravity constant
        self.G = 100

        # Planet
        circle = b2FixtureDef(shape=b2CircleShape(radius=5), density=1, friction=0.5, restitution=0.5)
        self.world.CreateBody(type=b2_dynamicBody, position=b2Vec2(0,0), fixtures=circle)

        # Satellite
        circle_small = b2FixtureDef(shape=b2CircleShape(radius=0.2), density=1, friction=0.5, restitution=0.2)
        self.world.CreateBody(type=b2_dynamicBody, position=b2Vec2(0, 10), fixtures=circle_small, linearVelocity=(20, 0))

    def Step(self, settings):
        super(Simulation, self).Step(settings)

        # Simulate the Newton's gravity
        for bi in self.world.bodies:
            for bk in self.world.bodies:
                if bi == bk:
                    continue

                pi, pk = bi.worldCenter, bk.worldCenter
                mi, mk = bi.mass, bk.mass
                delta = pk - pi
                r = delta.length
                if abs(r) < 1.0:
                    r = 1.0

                force = self.G * mi * mk / (r * r)
                delta.Normalize()
                bi.ApplyForce(force * delta, pi, True)

if __name__ == "__main__":
    Simulation().run()

As you can see, we “turn off” the standard gravity by setting the self.world.gravity parameter to 0. We also add the parameter G, this is the “gravitational constant” of our virtual world, which is used in the calculation of the Step method. We also created two objects - a satellite and a planet. It is important to note the density and radius parameters. According to these parameters, the Box2D library itself calculates the mass that is used in the calculation. To calculate the interaction force, the usual "school" formula of Newton's law of gravity is used :



Now we start the simulation. We did not reach the first cosmic speed, and although the satellite still circled the entire planet, it’s difficult to call it “flight”: We



increase the speed by changing the line of code to linearVelocity = (28, 0):



Our "satellite" has successfully entered orbit around the "planet"! If the speed is further increased, the orbit will become elliptical:



Finally, we will depict something more similar to our “solar system”, adding three planets of different sizes in different orbits:

circle_small = b2FixtureDef(shape=b2CircleShape(radius=0.2), density=1, friction=0.5, restitution=0.2)
circle_medium = b2FixtureDef(shape=b2CircleShape(radius=0.3), density=1, friction=1.0, restitution=0.5)
self.world.CreateBody(type=b2_dynamicBody, position=b2Vec2(0, 6), fixtures=circle_small, linearVelocity=(37, 0))
self.world.CreateBody(type=b2_dynamicBody, position=b2Vec2(0, 10), fixtures=circle_small, linearVelocity=(28, 0))
self.world.CreateBody(type=b2_dynamicBody, position=b2Vec2(0, 15), fixtures=circle_medium, linearVelocity=(22, 0))

Result:



We see that the farther the planet is from the “sun”, the longer its period of revolution (Kepler’s 3rd law). Unfortunately, the Box2D engine does not allow drawing motion tracks on the screen, so it’s difficult to “see” Keppler’s 1st and 2nd laws, but you can be sure that they are also implemented.

Conclusion


As you can see, with Box2D, simple simulations can be done with minimal effort. Of course, this engine is still a game engine, not a scientific one, so you should not expect from it a correct simulation of a collision of galaxies or the expansion of matter during the Big Bang. But some patterns are quite interesting to watch.

All conceived in one piece did not fit. If the estimates are positive, in the second part it will be possible to consider more non-trivial examples.

All Articles