├── LICENSE ├── README.md ├── main.py └── requirements.txt /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 000Nobody 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Orbit Simulator 2 | An intuitive simulation of gravitational pull between planets. 3 | * View a demo video [here](https://www.youtube.com/watch?v=AYotPcp_fOI) 4 | 5 | 6 | ## Using the application 7 | * Clone GitHub repository 8 | * Download required dependencies: ```$ pip install -r requirements.txt``` 9 | * ```$ python main.py``` 10 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import sys 3 | import random 4 | import math 5 | import numpy as np 6 | from pygame.locals import KEYDOWN, KEYUP, MOUSEBUTTONDOWN, MOUSEBUTTONUP, QUIT 7 | 8 | pygame.init() 9 | clock = pygame.time.Clock() 10 | pygame.display.set_caption("Orbital Simulator") 11 | 12 | WINDOW_SIZE = (1920, 1080) 13 | screen = pygame.display.set_mode(WINDOW_SIZE) 14 | screen_rect = screen.get_rect() 15 | display = pygame.Surface(WINDOW_SIZE) 16 | display_rect = display.get_rect() 17 | 18 | G = 6.67408 * (10 ** -11) # Gravitational Constant 19 | MASS_AREA_RATIO = 2 * (10 ** 9) # mass in kilograms to area in pixels 20 | 21 | planets = [] 22 | particles = [] 23 | planet_id = 0 24 | mouse_down = False 25 | ctrl_down = False 26 | 27 | 28 | class Planet: 29 | def __init__(self, x, y, radius, mass, id): 30 | self.x = x 31 | self.y = y 32 | self.radius = radius 33 | self.volume = radius ** 3 34 | self.mass = mass 35 | self.id = id 36 | self.rect = pygame.Rect( 37 | self.x - self.radius / 1.5, 38 | self.y - self.radius / 1.5, 39 | self.radius * 1.5, 40 | self.radius * 1.5, 41 | ) 42 | self.last_pos = (x, y) 43 | self.velocity = (0, 0) 44 | self.doneCreating = False 45 | self.color = random.choice( 46 | [ 47 | (236, 37, 37), 48 | (236, 151, 37), 49 | (247, 219, 41), 50 | (41, 247, 72), 51 | (46, 231, 208), 52 | (46, 63, 231), 53 | (221, 53, 232), 54 | (255, 84, 180), 55 | ] 56 | ) 57 | 58 | def update(self): 59 | self.getVelocity() 60 | self.collision() 61 | self.rect = pygame.Rect( 62 | self.x - self.radius / 1.5, 63 | self.y - self.radius / 1.5, 64 | self.radius * 1.5, 65 | self.radius * 1.5, 66 | ) 67 | self.x += self.velocity[0] 68 | self.y += self.velocity[1] 69 | self.mass = math.pi * (self.radius ** 2) * MASS_AREA_RATIO 70 | if not mouse_down: 71 | self.doneCreating = True 72 | if not self.doneCreating: 73 | self.create() 74 | if self.doneCreating and self.velocity[0] != 0 and self.velocity[1] != 0: 75 | particles.append( 76 | Particle( 77 | self.x, 78 | self.y, 79 | [np.subtract(self.color, (35, 35, 35))], 80 | 1, 81 | int(-self.velocity[0]), 82 | 1, 83 | int(-self.velocity[1]), 84 | 1, 85 | int(self.radius / 1.5), 86 | 0.4, 87 | 0, 88 | ) 89 | ) 90 | 91 | def create(self): 92 | if self.radius <= 200: 93 | self.radius += 0.35 94 | self.mass = math.pi * (self.radius ** 2) * MASS_AREA_RATIO 95 | self.volume = self.radius ** 3 96 | self.x, self.y = mx, my 97 | 98 | def getVelocity(self): 99 | if not self.doneCreating: 100 | self.current_pos = [mx, my] 101 | self.dpos = [ 102 | (self.current_pos[0] - self.last_pos[0]) / 2, 103 | (self.current_pos[1] - self.last_pos[1]) / 2, 104 | ] # dividing by two to make it less sensitive 105 | self.last_pos = [ 106 | mx, 107 | my, 108 | ] # Comparing mouse pos from one frame ago to current frame to get the velocity of the mouse movement 109 | self.velocity = self.dpos 110 | 111 | if self.doneCreating: 112 | for planet in planets: 113 | if self.id != planet.id and planet.doneCreating: 114 | dx = planet.x - self.x 115 | dy = planet.y - self.y 116 | angle = math.atan2(dy, dx) # Calculate angle between planets 117 | d = math.sqrt((dx ** 2) + (dy ** 2)) # Calculate distance 118 | if d == 0: 119 | d = 0.000001 # Prevent division by zero error 120 | f = ( 121 | G * self.mass * planet.mass / (d ** 2) 122 | ) # Calculate gravitational force 123 | 124 | self.velocity[0] += (math.cos(angle) * f) / self.mass 125 | self.velocity[1] += (math.sin(angle) * f) / self.mass 126 | 127 | def collision(self): 128 | if self.doneCreating: 129 | for planet in planets: 130 | if ( 131 | self.id != planet.id 132 | and planet.doneCreating 133 | and self.rect.colliderect(planet.rect) 134 | and self.mass > planet.mass 135 | ): 136 | planets.remove(planet) 137 | if self.radius <= 200: 138 | self.volume += planet.volume 139 | self.radius = self.volume ** (1. /3.) 140 | 141 | def draw(self): 142 | pygame.draw.circle( 143 | display, self.color, (int(self.x), int(self.y)), int(self.radius) 144 | ) 145 | # pygame.draw.rect(display, (255, 255, 255), self.rect) 146 | 147 | 148 | class Particle: 149 | def __init__( 150 | self, 151 | x, 152 | y, 153 | colors, 154 | min_xvel, 155 | max_xvel, 156 | min_yvel, 157 | max_yvel, 158 | min_radius, 159 | max_radius, 160 | shrink_rate, 161 | gravity, 162 | ): 163 | self.x = x 164 | self.y = y 165 | self.color = random.choice(colors) 166 | if min_xvel < max_xvel: 167 | self.xvel = random.randint(min_xvel, max_xvel) / 10 168 | else: 169 | self.xvel = random.randint(max_xvel, min_xvel) / 10 170 | if min_yvel < max_yvel: 171 | self.yvel = random.randint(min_yvel, max_yvel) / 10 172 | else: 173 | self.yvel = random.randint(max_yvel, min_yvel) / 10 174 | self.radius = random.randint(min_radius, max_radius) 175 | self.shrink_rate = shrink_rate 176 | self.gravity = gravity 177 | 178 | def update(self): 179 | self.x += self.xvel 180 | self.y += self.yvel 181 | self.radius -= self.shrink_rate 182 | self.yvel += self.gravity 183 | 184 | def draw(self): 185 | pygame.draw.circle( 186 | display, self.color, (int(self.x), int(self.y)), int(self.radius) 187 | ) 188 | 189 | 190 | def draw(): 191 | display.fill((25, 25, 25)) 192 | 193 | for particle in particles: 194 | particle.draw() 195 | 196 | for planet in planets: 197 | planet.draw() 198 | 199 | display_rect.center = screen_rect.center 200 | screen.blit(display, (0, 0)) 201 | 202 | pygame.display.update() 203 | 204 | 205 | while True: 206 | clock.tick(60) 207 | mx, my = pygame.mouse.get_pos() 208 | for event in pygame.event.get(): 209 | if event.type == QUIT: 210 | sys.exit() 211 | pygame.quit() 212 | 213 | if event.type == MOUSEBUTTONDOWN and event.button == 1: 214 | mouse_down = True 215 | if not ctrl_down: 216 | planets.append(Planet(mx, my, 0, 0, planet_id)) 217 | planet_id += 1 218 | 219 | if event.type == MOUSEBUTTONUP and event.button == 1: 220 | mouse_down = False 221 | 222 | if event.type == KEYDOWN and event.key == pygame.K_LCTRL: 223 | ctrl_down = True 224 | 225 | if event.type == KEYUP and event.key == pygame.K_LCTRL: 226 | ctrl_down = False 227 | 228 | if event.type == KEYDOWN and event.key == pygame.K_TAB: 229 | planets.clear() 230 | 231 | for planet in planets: 232 | if len(planets) >= 50: 233 | planets.remove(planet) 234 | if planet.x > 50000 or planet.x < -50000: 235 | planets.remove(planet) 236 | if planet.y > 50000 or planet.y < -50000: 237 | planets.remove(planet) 238 | if planet.velocity[0] > 1000 or planet.velocity[0] < -1000: 239 | try: 240 | planets.remove(planet) 241 | except ValueError: 242 | pass 243 | if planet.velocity[1] > 1000 or planet.velocity[1] < -1000: 244 | planets.remove(planet) 245 | 246 | for particle in particles: 247 | particle.update() 248 | if particle.radius <= 0: 249 | particles.remove(particle) 250 | for planet in planets: 251 | planet.update() 252 | draw() 253 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy==1.22.0 2 | pygame==2.0.0.dev10 3 | --------------------------------------------------------------------------------