Search code examples
pythonpygamephysicsgame-physicsorbital-mechanics

Pygame orbit simulation issue


I have recently been making a orbit simulator using this equation: Picture of Equation I am using

Here is my code:

import pygame, math
from pygame.locals import *
from random import randint
pygame.init()
screen = pygame.display.set_mode([500,500])
clock = pygame.time.Clock()

class Planet():
    def __init__(self, vel = [1, 1], mass = 100000, pos = [100, 100], pathLength = 100000):
        self.v = vel
        self.m = mass
        self.size = mass/1000000
        self.pos = pos
        self.pL = pathLength
        self.path = [[pos[0], pos[1]]]

    def update(self):
        self.pos[0] += self.v[0]
        self.pos[1] += self.v[1]
        self.path.append([self.pos[0], self.pos[1]])
        if len(self.path) == self.pL:
            self.path.pop(0)

class World():
    def __init__(self, planetList, iterations, mass = 10000000, gravityConstant = (6 * 10 ** -9)):
        self.plnt = planetList
        self.iter = iterations
        self.mass = mass
        self.size = int(mass/1000000)
        self.gC = gravityConstant
    def draw(self):
        pygame.draw.circle(screen, [0, 0, 0], [250, 250], self.size)
        for p in self.plnt:
            pygame.draw.rect(screen, [0, 0, 0], [p.pos[0], p.pos[1], p.size, p.size])
            pygame.draw.lines(screen, [0, 0, 0], False, p.path)
    def update(self):
        for i in range(self.iter):
            for p in self.plnt:
                d = math.sqrt((p.pos[0] - 250) ** 2 + (p.pos[1] - 250) ** 2)
                f = (self.gC * self.mass * p.m)/(d ** 2)
                vect = [((250 - p.pos[0]) / d) * f, ((250 - p.pos[1]) / d) * f]
                p.v[0] += vect[0]
                p.v[1] += vect[1]
                p.update()
        self.draw()


a = Planet([4,0])
b = Planet([4, 0])
w = World([b], 100)
while 1:
    screen.fill([255, 255, 255])

    w.update()

    for event in pygame.event.get():
        if event.type == QUIT:
            pygame.quit()

    pygame.display.update()
    clock.tick(60)

If i just have 1 planet in the simulation it works as expected, but with this it has issues

a = Planet([4,0])
b = Planet([4, 0])
w = World([a, b], 100)

The planets fly off the screen and continue on forever, I cannot see where i have made a mistake.


Solution

  • You fell for the age-old Python trap of declaring mutable default arguments. :)

    To cut to the chase so you can get your code working, copy the replacements I've made below into your own code:

    class Planet():
        def __init__(self, vel = [1, 1], mass = 100000, pos = [100, 100], pathLength = 100000):
            self.v = vel[:]  # Added [:] to ensure the list is copied
            self.m = mass
            self.size = mass/1000000
            self.pos = pos[:]  # Added [:] here for the same reason
            self.pL = pathLength
            self.path = [[pos[0], pos[1]]]
    

    Explanation

    In Python, lists are mutable - you can modify the same instance of a list. One common mistake that people make when using Python is to declare mutable arguments as default values within function signatures.

    The problem is that Python will assign that default value once to the parameter at the time that the function definition is processed, and then reuse that assigned value each time the function is invoked and the default argument called upon.

    In your Planet class constructor, you're declaring two mutable default arguments:

    • vel = [1, 1]
    • pos = [100, 100]

    Every instance of Planet you create will store a reference to these lists, but note that because of what I said above, every planet will share the same vel list and the same pos list. This means each instance will interfere with the velocity and position data of the others.

    You can read more about this gotcha here.

    An alternative and preferred way of handling situations like this would be to set the default value as None and then assign the "real" default value if the caller doesn't provide an explicit value for it:

    class Planet():
        def __init__(self, vel = None, mass = 100000, pos = None, pathLength = 100000):
            self.v = vel or [1, 1]
            self.m = mass
            self.size = mass/1000000
            self.pos = pos or [100, 100]
            self.pL = pathLength
            self.path = [[self.pos[0], self.pos[1]]]
    

    You would then be expected to document this behaviour of the function, since it would not be apparent to the caller otherwise.