├── .demo ├── Solar System.gif └── Triple Star System.gif ├── .gitattributes ├── README.md ├── classes ├── object.py └── vector.py ├── constants.py ├── functions.py ├── main.py ├── presets ├── set0.py ├── set1.py └── set2.py └── requirements.txt /.demo/Solar System.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroKhet/Orbit-Simulator-2D/82c04d0a49145f46c76b7d15889707ea37c42d7a/.demo/Solar System.gif -------------------------------------------------------------------------------- /.demo/Triple Star System.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AstroKhet/Orbit-Simulator-2D/82c04d0a49145f46c76b7d15889707ea37c42d7a/.demo/Triple Star System.gif -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 2D Multi Object Simulator 2 | 3 | A simple Multi-Object gravitational motion simulator made while learning about linear algebra & orbital mechanics 4 | 5 | ## How it works 6 | 7 | Create a file in the folder `presets/` with the following variables: 8 | * **NAME** : Name of simulation 9 | * **TIME_DELTA** : Time passed per iteration 10 | * **SPACE_SCALE** : Meters per pixel 11 | * **BASE_SPACE_VECTOR** : Position of bottom-left coordinate of window (in real scale) 12 | * **object_list** : List of objects 13 | 14 | This simulation uses [Newton's law of universal gravitation](https://en.wikipedia.org/wiki/Newton%27s_law_of_universal_gravitation) to calculate the acceleration of each object, then uses kinematics to simulate their corresponding velocity & displacement. 15 | 16 | ## Demo 17 | Triple Star System 18 | ![Alt Text](https://github.com/AstroKhet/2D-Multi-Object-Simulator/blob/main/.demo/Triple%20Star%20System.gif) 19 | 20 | Solar System 21 | ![Alt Text](https://github.com/AstroKhet/2D-Multi-Object-Simulator/blob/main/.demo/Solar%20System.gif) 22 | 23 | ## What can be improved/implemented 24 | * Collision between objects 25 | * Displaying vectors for each object's velocity and gravitational acceleration 26 | * Displaying an orbit path 27 | * Ability to add & drag objects using mouse button 28 | * Optimization in general 29 | -------------------------------------------------------------------------------- /classes/object.py: -------------------------------------------------------------------------------- 1 | from classes.vector import Vector2D 2 | from constants import WIN_HEIGHT 3 | 4 | import pygame 5 | 6 | 7 | class Object2D: 8 | # objects are all circular 9 | def __init__(self, name, mass, radius, scale, colour, position, velocity=Vector2D(0, 0), label=True): 10 | # Assumed to be constant 11 | self.name = name 12 | self.mass = mass 13 | self.radius = radius 14 | self.scale = scale 15 | self.colour = colour 16 | 17 | self.Position = position 18 | self.Velocity = velocity 19 | 20 | self.trajectory = [] 21 | self.label = label 22 | 23 | # dynamic variables that cannot be set when instantiated 24 | self.SPACE_SCALE = None 25 | self.display_radius = None 26 | self.display_pos = None 27 | 28 | def initialize(self, SPACE_SCALE, SPACE_VECTOR): 29 | self.SPACE_SCALE = SPACE_SCALE 30 | self.display_radius = self.radius * self.scale / self.SPACE_SCALE 31 | self.display_pos = (1/SPACE_SCALE) * (SPACE_VECTOR + Vector2D(self.Position.x, WIN_HEIGHT*SPACE_SCALE - self.Position.y)) 32 | 33 | if len(self.trajectory) > 12: 34 | del self.trajectory[0] 35 | 36 | def draw(self, window): 37 | x, y = self.display_pos.Args 38 | 39 | self.trajectory.append((x, y)) 40 | 41 | for t in range(len(self.trajectory)): 42 | pygame.draw.circle(window, self.colour, self.trajectory[t], self.display_radius * (t+1)/12) 43 | 44 | pygame.draw.circle(window, self.colour, (x, y), self.display_radius) 45 | 46 | -------------------------------------------------------------------------------- /classes/vector.py: -------------------------------------------------------------------------------- 1 | from math import sqrt 2 | 3 | 4 | class Vector2D: 5 | def __init__(self, x, y): 6 | self.x = x 7 | self.y = y 8 | 9 | @property 10 | def Magnitude(self): 11 | return sqrt(self.x**2 + self.y**2) 12 | 13 | @property 14 | def Args(self): 15 | return self.x, self.y 16 | 17 | def dot(self, other): 18 | return self.x * other.x + self.y - other.y 19 | 20 | def __add__(self, other): 21 | x = self.x + other.x 22 | y = self.y + other.y 23 | return self.__class__(x, y) 24 | 25 | def __sub__(self, other): 26 | x = self.x - other.x 27 | y = self.y - other.y 28 | return self.__class__(x, y) 29 | 30 | def __mul__(self, scale): 31 | x = self.x * scale 32 | y = self.y * scale 33 | return self.__class__(x, y) 34 | 35 | def __rmul__(self, scale): 36 | return self * scale 37 | 38 | def __str__(self): 39 | return f"(x={self.x}, y={self.y})" 40 | -------------------------------------------------------------------------------- /constants.py: -------------------------------------------------------------------------------- 1 | WIN_HEIGHT = 1000 2 | WIN_WIDTH = 1000 3 | 4 | # Gravitational Constant 5 | G = 6.6743015 * 10**-11 6 | -------------------------------------------------------------------------------- /functions.py: -------------------------------------------------------------------------------- 1 | from math import sin, cos, atan2 2 | from classes.vector import Vector2D 3 | from constants import G 4 | from copy import deepcopy 5 | 6 | 7 | def update_objects(objects_list, TIME_DELTA): 8 | new_obj_list = [] 9 | 10 | for i in range(len(objects_list)): 11 | obj_i = deepcopy(objects_list[i]) 12 | acc_i = Vector2D(0, 0) 13 | 14 | for j in range(len(objects_list)): 15 | # obj_j refers to all objects that isnt obj_i 16 | if i == j: 17 | continue 18 | 19 | # Getting position values 20 | obj_j = deepcopy(objects_list[j]) 21 | obj_i_pos = obj_i.Position 22 | obj_j_pos = obj_j.Position 23 | 24 | # Calculating accelaration using Newton's law of gravity 25 | dis_i_j = obj_j_pos - obj_i_pos 26 | acc_i_j_mag = (G * obj_j.mass) / (dis_i_j.Magnitude**2) 27 | acc_theta = atan2(dis_i_j.y, dis_i_j.x) 28 | 29 | # Calculating & adding acceleration vector in 2D space to total acceleration of obj_i 30 | acc_i_j = Vector2D(acc_i_j_mag*cos(acc_theta), acc_i_j_mag*sin(acc_theta)) 31 | acc_i += acc_i_j 32 | 33 | # Kinematics 34 | obj_i.Velocity += TIME_DELTA * acc_i 35 | obj_i.Position += TIME_DELTA * obj_i.Velocity 36 | 37 | new_obj_list.append(obj_i) 38 | 39 | return new_obj_list 40 | 41 | 42 | def sim_time(seconds): 43 | years, seconds = divmod(seconds, 31536000) 44 | days, seconds = divmod(seconds, 86400) 45 | hours, seconds = divmod(seconds, 3600) 46 | minutes, seconds = divmod(seconds, 60) 47 | return f"Time Elapsed: {years:,} Years {days:<3} Days {hours:<2} Hours {minutes:<2} Minutes {seconds:<2} Seconds" 48 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from classes.vector import Vector2D 2 | from constants import WIN_WIDTH, WIN_HEIGHT 3 | from functions import update_objects, sim_time 4 | import pygame 5 | 6 | # choose name of preset by changing the line below witth presets. 7 | from presets.set0 import NAME, TIME_DELTA, SPACE_SCALE, BASE_SPACE_VECTOR, object_list 8 | 9 | 10 | pygame.init() 11 | pygame.font.init() 12 | FONT = pygame.font.SysFont('Courier', 12) 13 | 14 | window = pygame.display.set_mode((WIN_WIDTH, WIN_HEIGHT)) 15 | pygame.display.set_caption(NAME) 16 | 17 | 18 | time_elapsed = 0 19 | 20 | # For moving simulation space 21 | mouse = None 22 | SPACE_VECTOR = Vector2D(*BASE_SPACE_VECTOR.Args) 23 | 24 | clock = pygame.time.Clock() 25 | # Simulation :) 26 | running = True 27 | while running: 28 | clock.tick(240) 29 | window.fill((0, 0, 0)) 30 | for event in pygame.event.get(): 31 | # x-button 32 | if event.type == pygame.QUIT: 33 | running = False 34 | continue 35 | 36 | # Zooming in and out 37 | if event.type == pygame.MOUSEBUTTONDOWN: 38 | pos = pygame.mouse.get_pos() 39 | delta_space = (0.05 * SPACE_SCALE) * Vector2D(pos[0], -pos[1]) 40 | 41 | if event.button == 4: 42 | SPACE_SCALE *= 0.95 43 | SPACE_VECTOR -= delta_space 44 | 45 | if event.button == 5: 46 | SPACE_SCALE *= 1.0526 47 | SPACE_VECTOR += delta_space 48 | 49 | # Moving simulation space 50 | if pygame.mouse.get_pressed(3)[0]: 51 | pos = pygame.mouse.get_pos() 52 | moving = True 53 | if mouse is None: 54 | mouse = pos 55 | else: 56 | SPACE_CHANGE = SPACE_SCALE * (Vector2D(*pos) - Vector2D(*mouse)) 57 | SPACE_VECTOR = BASE_SPACE_VECTOR + SPACE_CHANGE 58 | 59 | else: 60 | BASE_SPACE_VECTOR = Vector2D(*SPACE_VECTOR.Args) 61 | mouse = None 62 | 63 | # Displaying objects 64 | for obj in object_list: 65 | obj.initialize(SPACE_SCALE, SPACE_VECTOR) 66 | obj.draw(window) 67 | if obj.label: 68 | label_x, label_y = obj.display_pos.Args 69 | label = FONT.render(obj.name, False, obj.colour) 70 | label_rect = label.get_rect(center=(label_x, label_y+obj.display_radius+10)) 71 | window.blit(label, label_rect) 72 | 73 | object_list = update_objects(object_list, TIME_DELTA) 74 | 75 | # Time elapsed since beginning 76 | time_elapsed += TIME_DELTA 77 | time_elapsed_text = FONT.render(sim_time(time_elapsed), False, (255, 255, 255)) 78 | 79 | window.blit(time_elapsed_text, (10, 960)) 80 | 81 | # Displaying map scales 82 | map_scale = FONT.render(f"{int(SPACE_SCALE/10):,} KM", False, (255, 255, 255)) 83 | window.blit(map_scale, (120, 925)) 84 | 85 | pygame.draw.line(window, (255, 255, 255), (10, 925), (10, 935)) 86 | pygame.draw.line(window, (255, 255, 255), (110, 925), (110, 935)) 87 | pygame.draw.line(window, (255, 255, 255), (10, 930), (110, 930)) 88 | 89 | pygame.display.update() 90 | -------------------------------------------------------------------------------- /presets/set0.py: -------------------------------------------------------------------------------- 1 | from classes.object import Object2D 2 | from classes.vector import Vector2D 3 | 4 | # Name of preset 5 | NAME = "Earth & Moon" 6 | 7 | # time interval used in calculations 8 | TIME_DELTA = 10 9 | 10 | # Each pixel represents SCALE meters 11 | SPACE_SCALE = 1 * 10**6 12 | 13 | # Initial coordinate of bottom-left (0, 0) pixel 14 | BASE_SPACE_VECTOR = Vector2D(0, 0) 15 | 16 | 17 | object_list = [ 18 | Object2D( 19 | name="Earth", 20 | mass=5.972 * 10 ** 24, 21 | radius=6.371 * 10 ** 6, 22 | scale=3, 23 | colour=(68, 167, 196), 24 | position=Vector2D(5 * 10**8, 5 * 10**8), 25 | velocity=Vector2D(0, 0) 26 | ), 27 | 28 | Object2D( 29 | name="Moon", 30 | mass=7.342 * 10 ** 22, 31 | radius=1.737 * 10 ** 6, 32 | scale=3, 33 | colour=(180, 180, 180), 34 | position=Vector2D(8.992 * 10 ** 8, 5 * 10 ** 8), 35 | velocity=Vector2D(0, 1022) 36 | ), 37 | ] 38 | -------------------------------------------------------------------------------- /presets/set1.py: -------------------------------------------------------------------------------- 1 | from classes.object import Object2D 2 | from classes.vector import Vector2D 3 | 4 | NAME = "Solar System" 5 | TIME_DELTA = 43210 6 | SPACE_SCALE = 12 * 10 ** 9 7 | BASE_SPACE_VECTOR = Vector2D(0, 0) 8 | 9 | 10 | object_list = [ 11 | Object2D( 12 | name="Sun", 13 | mass=1.989 * 10**30, 14 | radius=6.957 * 10**8, 15 | scale=25, 16 | colour=(252, 186, 3), 17 | position=Vector2D(6 * 10**12, 6 * 10**12), 18 | velocity=Vector2D(0, 0) 19 | ), 20 | 21 | Object2D( 22 | name="Mecury", 23 | mass=3.285 * 10**23, 24 | radius=2.4397 * 10**6, 25 | scale=1000, 26 | colour=(180, 180, 180), 27 | position=Vector2D(6 * 10**12 + 57.9 * 10**9, 6 * 10**12), 28 | velocity=Vector2D(0, 47.36 * 10**3) 29 | ), 30 | 31 | Object2D( 32 | name="Venus", 33 | mass=4.867 * 10**24, 34 | radius=6.0518 * 10**6, 35 | scale=1000, 36 | colour=(187, 183, 171), 37 | position=Vector2D(6 * 10**12 - 107.48 * 10**9, 6 * 10**12), 38 | velocity=Vector2D(0, -35.02 * 10**3) 39 | ), 40 | 41 | Object2D( 42 | name="Earth", 43 | mass=5.9724 * 10**24, 44 | radius=6.356 * 10**6, 45 | scale=1000, 46 | colour=(11, 227, 195), 47 | position=Vector2D(6 * 10**12 + 151.96 * 10**9, 6 * 10**12), 48 | velocity=Vector2D(0, 29.78 * 10**3) 49 | ), 50 | 51 | Object2D( 52 | name="Mars", 53 | mass=64171 * 10**23, 54 | radius=3.3895 * 10**6, 55 | scale=1000, 56 | colour=(193, 68, 14), 57 | position=Vector2D(6 * 10**12 - 250.17 * 10**9, 6 * 10**12), 58 | velocity=Vector2D(0, -24.07 * 10**3) 59 | ), 60 | 61 | Object2D( 62 | name="Jupiter", 63 | mass=1.89819 * 10**27, 64 | radius=6.9911 * 10**7, 65 | scale=200, 66 | colour=(211, 156, 126), 67 | position=Vector2D(6 * 10**12 + 754.87 * 10**9, 6 * 10**12), 68 | velocity=Vector2D(0, 13.06 * 10**3) 69 | ), 70 | 71 | Object2D( 72 | name="Saturn", 73 | mass=5.6834 * 10**26, 74 | radius=5.4364 * 10**7, 75 | scale=200, 76 | colour=(197, 171, 110), 77 | position=Vector2D(6 * 10 ** 12 - 1.4872 * 10**12, 6 * 10 ** 12), 78 | velocity=Vector2D(0, -9.68 * 10**3) 79 | ), 80 | 81 | Object2D( 82 | name="Uranus", 83 | mass=8.6813 * 10**25, 84 | radius=2.4973 * 10**7, 85 | scale=600, 86 | colour=(187, 225, 228), 87 | position=Vector2D(6 * 10 ** 12 + 2.9541 * 10**12, 6 * 10 ** 12), 88 | velocity=Vector2D(0, 6.80 * 10**3) 89 | ), 90 | 91 | Object2D( 92 | name="Neptune", 93 | mass=1.02413 * 10**26, 94 | radius=2.4341 * 10**7, 95 | scale=600, 96 | colour=(62, 84, 232), 97 | position=Vector2D(6 * 10 ** 12 - 4.495 * 10**12, 6 * 10 ** 12), 98 | velocity=Vector2D(0, -5.43 * 10**3) 99 | ), 100 | 101 | Object2D( 102 | name="Pluto", 103 | mass=1.303 * 10**22, 104 | radius=1.188 * 10**6, 105 | scale=1000, 106 | colour=(150, 133, 112), 107 | position=Vector2D(6 * 10 ** 12 + 5.90538 * 10**12, 6 * 10 ** 12), 108 | velocity=Vector2D(0, 4.67 * 10**3) 109 | ), 110 | ] 111 | -------------------------------------------------------------------------------- /presets/set2.py: -------------------------------------------------------------------------------- 1 | from classes.object import Object2D 2 | from classes.vector import Vector2D 3 | 4 | NAME = "Triple Star System" 5 | TIME_DELTA = 1800 6 | SPACE_SCALE = 5 * 10**6 7 | BASE_SPACE_VECTOR = Vector2D(1.5 * 10**9, -2 * 10 ** 9) 8 | 9 | 10 | object_list = [ 11 | Object2D( 12 | name="Binary 0", 13 | mass=5.972 * 10 ** 24, 14 | radius=6.371 * 10 ** 6, 15 | scale=5, 16 | colour=(255, 255, 255), 17 | position=Vector2D(3 * 10 ** 8, 5 * 10 ** 8), 18 | velocity=Vector2D(0, 600) 19 | ), 20 | 21 | Object2D( 22 | name="Binary 1", 23 | mass=5.972 * 10 ** 24, 24 | radius=6.371 * 10 ** 6, 25 | scale=5, 26 | colour=(255, 255, 255), 27 | position=Vector2D(7 * 10 ** 8, 5 * 10 ** 8), 28 | velocity=Vector2D(0, -600) 29 | ), 30 | 31 | Object2D( 32 | name="Triplet", 33 | mass=5.972 * 10 ** 24, 34 | radius=6.371 * 10 ** 6, 35 | scale=5, 36 | colour=(255, 0, 0), 37 | position=Vector2D(18 * 10 ** 8, 18 * 10 ** 8), 38 | velocity=Vector2D(-450, 450) 39 | ) 40 | ] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pip==21.1.1 2 | pygame==2.0.1 3 | setuptools==56.1.0 --------------------------------------------------------------------------------