How to join two objects to share values but maintain the original name? Aka N-body merger

Question:

As the title says, I am working on an N-body simulation I found on YouTube. The current script has gravity turned off when radius is small enough. However, I’d like to join the two objects (body and otherbody) into a new one which behaves according to the same rules, it just has the properties of both added. How do I do this? Here is the code in question.

import pygame
from sys import exit
import math as m
import itertools
import random

pygame.init()
WINDOW_SIZE = (1500,1500)
WINDOW = pygame.display.set_mode(WINDOW_SIZE)
CLOCK = pygame.time.Clock()
G=1

BACKGROUND = pygame.Surface(WINDOW_SIZE)
BACKGROUND.fill("Black")
BACKGROUND_RECT = BACKGROUND.get_rect(center=(500,500))

class Body(pygame.sprite.Sprite):
    def __init__(self, mass, radius, init_position, color, vx, vy):
        super().__init__()
        
        self.image = pygame.Surface([(radius*2),(radius*2)])
        self.image.fill("Black")
        self.rect = self.image.get_rect(center=init_position)
        pygame.draw.circle(self.image, color, (radius,radius), radius, 0)
        
        self.mass = mass
        self.radius = radius
        self.x_pos = init_position[0]
        self.y_pos = init_position[1]
        
        self.vx = vx
        self.ax = 0
        self.vy = vy
        self.ay = 0
    
    def set_vy(self,value):
        self.vy = value
    def set_xv(self,value):
        self.vx = value
    def set_ay(self,value):
        self.ay = value
    def set_ax(self,value):
        self.ax=value
        
    def change_x_pos(self,value):
        self.x_pos+=value
    def change_y_pos(self,value):
        self.y_pos+=value
    def change_vx(self,value):
        self.vx+=value
    def change_vy(self,value):
        self.vy+=value
        
    def update_pos(self):
        self.rect.center = (round(self.x_pos), round(self.y_pos))
        
    def animate(self):
        self.change_vx(self.ax)
        self.change_vy(self.ay)
        
        self.change_x_pos(self.vx)
        self.change_y_pos(self.vy)
        
        self.update_pos()

    def gravity(self, otherbody):
        dx = self.x_pos - otherbody.x_pos
        dy = self.y_pos - otherbody.y_pos
        
        if dx < self.radius*2 and dy < self.radius*2:
            pass

        else:
            try:
                r = m.sqrt(dx**2 + dy**2)
                a = G*otherbody.mass/r**2
                theta = m.asin(dy/r)
        
                if self.y_pos > otherbody.y_pos:
                    self.set_ay(-m.sin(theta)*a)
                else:
                    self.set_ay(m.sin(theta)*a)
                
                if self.x_pos > otherbody.x_pos:
                    self.set_ax(-m.cos(theta)*a)
                else:
                    self.set_ax(m.cos(theta)*a)
            except ZeroDivisionError:
                pass


body_group = pygame.sprite.Group()
BODYCOUNT=3
for i in range(BODYCOUNT):
    body_group.add(Body(10, 3, (random.randrange(100,900), random.randrange(100,900)), "White", 0,0))
    
body_list = list(body_group)
body_pairs = list(itertools.combinations(body_list,2))

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            exit()
    WINDOW.blit(BACKGROUND, BACKGROUND_RECT)
    
    body_group.draw(WINDOW)
    
    for body, otherbody in body_pairs:
        body.gravity(otherbody)
        otherbody.gravity(body)
        body.animate()
        otherbody.animate()
    
    pygame.display.update()
    CLOCK.tick(60)

The part in question is if dx < self.radius*2 and dy < self.radius*2: pass

How do I join the objects body and otherbody into a new body?

Also the current version of the script only has one object being attracted to the other, not equal attraction. If you see why, can you point it out?

Asked By: docyftw

||

Answers:

The condition is wrong. You need to calculate and compare the square of the Euclidean distance. Use pygame.sprite.Sprite.kill to remove the otherbody from all groups and increase the size and mass of the body by the size and mass of the otherbody. Additionally there are some problems in the function gravity, which can be simplified anyway.

class Body(pygame.sprite.Sprite):
    # [...]

    def join(self, otherbody):
        dx = self.x_pos - otherbody.x_pos
        dy = self.y_pos - otherbody.y_pos  
        if (dx*dx + dy*dy) < self.radius*self.radius:
            otherbody.kill()
            self.mass += otherbody.mass
            self.radius = m.sqrt(self.radius**2 + otherbody.radius**2)
            self.image = pygame.Surface([(self.radius*2),(self.radius*2)])
            self.image.fill("Black")
            self.rect = self.image.get_rect(center=self.rect.center)
            pygame.draw.circle(self.image, self.color, (self.radius,self.radius), self.radius, 0)

    def gravity(self, otherbody): 
        dx = self.x_pos - otherbody.x_pos
        dy = self.y_pos - otherbody.y_pos
        r = m.sqrt(dx*dx + dy*dy)  
        a = G*otherbody.mass/r**2
        self.set_ax(a * -dx / r)
        self.set_ay(a * -dy / r)

Since the objects in body_group can change, body_pairs must be regenerated in each frame. animate each body only once and join the bodies after moving.

while True:
    # [...]

    body_list = list(body_group)
    body_pairs = list(itertools.combinations(body_list,2))
    for body, otherbody in body_pairs:
        body.gravity(otherbody)
        otherbody.gravity(body)
    for body in body_list:
        body.animate()
    for body, otherbody in body_pairs:
        body.join(otherbody)
Answered By: Rabbid76