Creating the Solar System - Part 1
- Max Clark
- May 25, 2017
- 7 min read

In this series, I will explain how to create realistic orbital physics in a 2-dimensional solar system. We will use the key formula:

Where F is the force of gravity between two object, G is the gravitational constant (6.67x10^-11), M and m are the masses of the objects and r is the distance between their centers of mass.
First, we'll import some libraries.
import pygame import time import os from random import randint from math import sqrt, atan, atan2, sin as sine, cos as cosine, radians, degrees
Now, we create some basic math functions so we can deal in degrees.
def sin(deg): return sine(radians(deg)) def cos(deg): return cosine(radians(deg))
def pythag(a,b): return sqrt(a*a + b*b)
This function will calculate the horizontal and vertical resultant force given a force and its angle.
def apply_force(magnitude,angle,hf,vf): hf += magnitude*sin(angle) vf += -magnitude*cos(angle) return hf, vf
This next function will add celestial bodies to our system. The bodies are stored in a dictionary where 'num' is the number of bodies and 'hv' and 'vv' are horizontal and vertical velocities. In dictionary notation, '"variable{0}".format(x)' will read '"variable5"' if 'x' is equal to 5.
def add_body(name,col,mass,pos,hv,vv): bodies["name{0}".format(bodies["num"])] = name bodies["mass{0}".format(bodies["num"])] = mass bodies["color{0}".format(bodies["num"])] = col bodies["pos{0}".format(bodies["num"])] = pos bodies["hv{0}".format(bodies["num"])] = hv bodies["vv{0}".format(bodies["num"])] = vv bodies["num"] += 1
This function will calculate the coordinates of a body from a top down perspective, taking zoom and panning into consideration.
def conv_z(pos,panx,pany,zoom): return (int((pos[0] + panx - xSize/2)*zoom + xSize/2),int((pos[1] + pany - ySize/2)*zoom + ySize/2))
Now we initiate pygame and make it full screen.
pygame.init() infoObject = pygame.display.Info() xSize, ySize = infoObject.current_w, infoObject.current_h screen = pygame.display.set_mode((xSize, ySize), pygame.NOFRAME | pygame.FULLSCREEN) pygame.display.set_caption("Gravity")
Here are the variables. The variable 'zoom_d' is the direct zoom value that can be changed by the user. Next, 'focus' is the number of the body that the camera is centered on. Then, 'view_mode' is the viewing mode, which for part one of this series will only be top-down. In the next part of the series, we will introduce a first person viewing mode. I have reduced the gravitational constant 'G' so everything doesn't have to be so astronomically far apart. Then we define 'bodies' and set 'num' to zero.
#----------------------Main Loop----------------------#
zoom_d = 3*sqrt(xSize) #zoom focus = 0 view_mode = 0
G = pow(10,-21)
bodies = {} bodies["num"] = 0
Next, we add the bodies. The first line shows a template. I have given Earth an initial horizontal velocity such that it has a circular orbit around the Sun. The formula for the speed of a circular orbit is v = G*M/r. I have done the same for the Moon but then added Earth's velocity meaning the Moon will orbit the Earth. None of the values are to scale.
#add_body(NAME, COLOUR, MASS, POSITION, HV, VV) add_body("Spaceship",[255,255,255],100,[2000,2000],0,0) add_body("The Sun",[254,229,117],pow(10,25),[0,0],0,0) add_body("Earth",[92,135,45],pow(10,22),[0,500],sqrt(G*bodies["mass1"]/500),0) add_body("The Moon",[155,155,155],pow(10,20),[0,520],-sqrt(G*bodies["mass2"]/20) + bodies["hv2"],0) add_body("Mercury",[189,122,33],pow(10,19),[0,50],sqrt(G*bodies["mass1"]/50),0)
Then we begin our main loop.
fps = 60 clock = pygame.time.Clock() frameCount = 0 done = False while not done: frameCount += 1 keys = pygame.key.get_pressed() for event in pygame.event.get(): if event.type == pygame.QUIT: done = True
Next, we allow the user to change the camera focus using the number keys. The last line prevents the error of having a focus number higher than the number of bodies.
if keys[pygame.K_0]: focus = 0 if keys[pygame.K_1]: focus = 1 if keys[pygame.K_2]: focus = 2 if keys[pygame.K_3]: focus = 3 if keys[pygame.K_4]: focus = 4 if keys[pygame.K_5]: focus = 5 if keys[pygame.K_6]: focus = 6 if keys[pygame.K_7]: focus = 7 if keys[pygame.K_8]: focus = 8 if keys[pygame.K_9]: focus = 9 focus = min(bodies["num"] - 1,focus)
Then, we allow the user to adjust the zoom and fill the screen black, using the page up and page down buttons.
if keys[pygame.K_PAGEUP]: zoom_d -= 1 if keys[pygame.K_PAGEDOWN]: zoom_d += 1 zoom_d = max(zoom_d,1) zoom = xSize/(zoom_d*zoom_d) screen.fill([0,0,0])
Each frame we recalculate the spaceship's velocity, hence needing to set it to zero. These variables are used in representing the force on screen with lines.
phf, pvf = 0, 0 #player horizontal/vertical velocity
Now we introduce the physics. Since every body feels the gravity of every other body, we need two for loops, where 'a' is the body we are calculating the physics for and 'b' is the body that is affecting 'a'. We do not want the mass of a body affecting itself, hence 'if b != a:'
for a in range(bodies["num"]): for b in range(bodies["num"]): if b != a:
Next we give shorter names to the positions and calculate the angle between the two bodies in question. Then we get the distance between them and we use the main formula to get the force between them.
px1, py1 = bodies["pos{0}".format(b)] px2, py2 = bodies["pos{0}".format(a)] th = degrees(atan2((py2 - py1),(px2 - px1))) - 90 #theta, in degrees d = pythag(py1 - py2,px1 - px2) + 0.00000001 #true distance
f = G*bodies["mass{0}".format(a)]*bodies["mass{0}".format(b)]/(d*d) #gravitational force
Now, we set the horizontal and vertical force to zero. When 'a' is zero, we are calculating the physics of the spaceship. The user can use WASD to control the spaceship. If shift is held, the force will be greater and if space is held, the force will be smaller but more precise. I've used 'if b == 1:' just so it isn't repeated many times each frame.
hf = 0 #horizontal force vf = 0 #vertical force
if a == 0: if keys[pygame.K_LSHIFT]: thrust = 2 elif keys[pygame.K_SPACE]: thrust = 0.3 else: thrust = 1
if b == 1: if keys[pygame.K_w]: hf, vf = apply_force(15*pow(10,thrust),0,hf,vf) if keys[pygame.K_a]: hf, vf = apply_force(15*pow(10,thrust),270,hf,vf) if keys[pygame.K_s]: hf, vf = apply_force(15*pow(10,thrust),180,hf,vf) if keys[pygame.K_d]: hf, vf = apply_force(15*pow(10,thrust),90,hf,vf)
This next part will detect a collision between the spaceship and another body.
if d < int(pow(bodies["mass{0}".format(b)]/pow(10,22),1/3.5)) + 2: print("You crashed into",bodies["name{0}".format(b)] + ".") done = True
Now, we apply the force of gravity to the body and add to the spaceship's force.
hf, vf = apply_force(f,th,hf,vf)
if a == 0: phf += hf pvf += vf
Every frame the acceleration increases by force divided by mass from the equation F = ma.
ha = hf/bodies["mass{0}".format(a)] #horizontal acceleration va = vf/bodies["mass{0}".format(a)]
Every frame the velocity increases by the acceleration times the time. '5/fps' is a constant that I've made to have the time progress at a reasonable rate.
bodies["hv{0}".format(a)] += ha*5/fps #horizontal velocity bodies["vv{0}".format(a)] += va*5/fps
Now we change the positions of the bodies given their velocities. I have excluded this from our double for loop above so that the change in position does not affect the physics of bodies calculated afterwards.
#Move for a in range(bodies["num"]): bodies["pos{0}".format(a)][0] += bodies["hv{0}".format(a)]*5/fps bodies["pos{0}".format(a)][1] += bodies["vv{0}".format(a)]*5/fps
Finally, we draw the bodies on the screen, taking into account the panning around the focus body and the zoom. The lines drawn represent the forces acting on the spaceship. The value of the radius is the same value used for the collision detection.
#Draw if view_mode == 0: px, py = bodies["pos0"][0], bodies["pos0"][1] panx = xSize/2 - bodies["pos{0}".format(focus)][0] pany = ySize/2 - bodies["pos{0}".format(focus)][1] for a in range(1,bodies["num"]): rad = int(pow(bodies["mass{0}".format(a)]/pow(10,22),1/3.5)) + 2
x, y = conv_z((bodies["pos{0}".format(a)][0],bodies["pos{0}".format(a)][1]),panx,pany,zoom) pygame.draw.circle(screen, [255,255,255], (x,y), int(rad*zoom)) pygame.draw.circle(screen, [255,255,255], conv_z((px,py),panx,pany,zoom), 1) pygame.draw.line(screen, [255,0,0], conv_z((px + phf*pow(10,-1),py),panx,pany,zoom),conv_z((px,py),panx,pany,zoom),1) pygame.draw.line(screen, [255,0,0], conv_z((px,py + pvf*pow(10,-1)),panx,pany,zoom),conv_z((px,py),panx,pany,zoom),1) pygame.display.flip() clock.tick(fps)
pygame.quit()
This concludes the basic physics and top down viewing of the solar system. Here's all the code so far:
import pygame import time import os from random import randint from math import sqrt, atan, atan2, sin as sine, cos as cosine, radians, degrees
def sin(deg): return sine(radians(deg)) def cos(deg): return cosine(radians(deg))
def pythag(a,b): return sqrt(a*a + b*b)
def apply_force(magnitude,angle,hf,vf): hf += magnitude*sin(angle) vf += -magnitude*cos(angle) return hf, vf
def add_body(name,col,mass,pos,hv,vv): bodies["name{0}".format(bodies["num"])] = name bodies["mass{0}".format(bodies["num"])] = mass bodies["color{0}".format(bodies["num"])] = col bodies["pos{0}".format(bodies["num"])] = pos bodies["hv{0}".format(bodies["num"])] = hv bodies["vv{0}".format(bodies["num"])] = vv bodies["num"] += 1
def conv_z(pos,panx,pany,zoom): return (int((pos[0] + panx - xSize/2)*zoom + xSize/2),int((pos[1] + pany - ySize/2)*zoom + ySize/2))
dir_path = os.path.dirname(os.path.realpath(__file__))
pygame.init() infoObject = pygame.display.Info() xSize, ySize = infoObject.current_w, infoObject.current_h screen = pygame.display.set_mode((xSize, ySize), pygame.NOFRAME | pygame.FULLSCREEN) pygame.display.set_caption("Gravity")
#----------------------Main Loop----------------------#
zoom_d = 3*sqrt(xSize) #zoom focus = 0 view_mode = 0
G = pow(10,-21)
bodies = {} bodies["num"] = 0
#add_body(NAME, COLOUR, MASS, POSITION, HV, VV) add_body("Spaceship",[255,255,255],100,[2000,2000],0,0) add_body("The Sun",[254,229,117],pow(10,25),[0,0],0,0) add_body("Earth",[92,135,45],pow(10,22),[0,500],sqrt(G*bodies["mass1"]/500),0) add_body("The Moon",[155,155,155],pow(10,20),[0,520],-sqrt(G*bodies["mass2"]/20) + bodies["hv2"],0) add_body("Mercury",[189,122,33],pow(10,19),[0,50],sqrt(G*bodies["mass1"]/50),0)
fps = 60 clock = pygame.time.Clock() frameCount = 0 done = False while not done: frameCount += 1 keys = pygame.key.get_pressed() for event in pygame.event.get(): if event.type == pygame.QUIT: done = True
if keys[pygame.K_0]: focus = 0 if keys[pygame.K_1]: focus = 1 if keys[pygame.K_2]: focus = 2 if keys[pygame.K_3]: focus = 3 if keys[pygame.K_4]: focus = 4 if keys[pygame.K_5]: focus = 5 if keys[pygame.K_6]: focus = 6 if keys[pygame.K_7]: focus = 7 if keys[pygame.K_8]: focus = 8 if keys[pygame.K_9]: focus = 9 focus = min(bodies["num"] - 1,focus)
if keys[pygame.K_PAGEUP]: zoom_d -= 1 if keys[pygame.K_PAGEDOWN]: zoom_d += 1 zoom_d = max(zoom_d,1) zoom = xSize/(zoom_d*zoom_d) screen.fill([0,0,0])
phf, pvf = 0, 0 #player horizontal/vertical velocity
for a in range(bodies["num"]): for b in range(bodies["num"]): if b != a: px1, py1 = bodies["pos{0}".format(b)] px2, py2 = bodies["pos{0}".format(a)] th = degrees(atan2((py2 - py1),(px2 - px1))) - 90 #theta, in degrees d = pythag(py1 - py2,px1 - px2) + 0.00000001 #true distance
f = G*bodies["mass{0}".format(a)]*bodies["mass{0}".format(b)]/(d*d) #gravitational force hf = 0 #horizontal force vf = 0 #vertical force
if a == 0: if keys[pygame.K_LSHIFT]: thrust = 2 elif keys[pygame.K_SPACE]: thrust = 0.3 else: thrust = 1
if b == 1: if keys[pygame.K_w]: hf, vf = apply_force(15*pow(10,thrust),0,hf,vf) if keys[pygame.K_a]: hf, vf = apply_force(15*pow(10,thrust),270,hf,vf) if keys[pygame.K_s]: hf, vf = apply_force(15*pow(10,thrust),180,hf,vf) if keys[pygame.K_d]: hf, vf = apply_force(15*pow(10,thrust),90,hf,vf)
if d < int(pow(bodies["mass{0}".format(b)]/pow(10,22),1/3.5)) + 2: print("You crashed into",bodies["name{0}".format(b)] + ".") done = True hf, vf = apply_force(f,th,hf,vf)
if a == 0: phf += hf pvf += vf
ha = hf/bodies["mass{0}".format(a)] #horizontal acceleration va = vf/bodies["mass{0}".format(a)]
bodies["hv{0}".format(a)] += ha*5/fps #horizontal velocity bodies["vv{0}".format(a)] += va*5/fps #Move for a in range(bodies["num"]): bodies["pos{0}".format(a)][0] += bodies["hv{0}".format(a)]*5/fps bodies["pos{0}".format(a)][1] += bodies["vv{0}".format(a)]*5/fps #Draw if view_mode == 0: px, py = bodies["pos0"][0], bodies["pos0"][1] panx = xSize/2 - bodies["pos{0}".format(focus)][0] pany = ySize/2 - bodies["pos{0}".format(focus)][1] for a in range(1,bodies["num"]): rad = int(pow(bodies["mass{0}".format(a)]/pow(10,22),1/3.5)) + 2
x, y = conv_z((bodies["pos{0}".format(a)][0],bodies["pos{0}".format(a)][1]),panx,pany,zoom) pygame.draw.circle(screen, [255,255,255], (x,y), int(rad*zoom)) pygame.draw.circle(screen, [255,255,255], conv_z((px,py),panx,pany,zoom), 1) pygame.draw.line(screen, [255,0,0], conv_z((px + phf*pow(10,-1),py),panx,pany,zoom),conv_z((px,py),panx,pany,zoom),1) pygame.draw.line(screen, [255,0,0], conv_z((px,py + pvf*pow(10,-1)),panx,pany,zoom),conv_z((px,py),panx,pany,zoom),1) pygame.display.flip() clock.tick(fps)
pygame.quit()
Comments