I have recently been making a orbit simulator using this equation:
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.
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.