Search code examples
pythonpygamesprite

Updates to elements of list are applied to the whole list python


I am trying to write just a simple program to simulate an N-Body system using python (and pygame for GUI just because I have more experience than tk). The issue I am running into is I made a body class that is a subclass from a sprite. I have an adaptable list size of n that appends that many bodies into the list. To start I just decided to have updates be performed in a double for loop with a final for loop that passes the updates after they have been calculated (I know this is not efficient, not the point). My issue is that when I pass an update to an element of this list, the update is applied to every thing in that list. What is going on here? Why are these completely separate elements tied together? Necessary code below

Body class

class Body(Sprite):
    vel = Vector2(0,0)
    acc = Vector2(0,0)
    def __init__(self, x, y, size, mass, init_val = True, color=(255,255,0)):
        super().__init__()
        self.rect = Rect(x-size[0]/2,y-size[1]/2,size[0],size[1])
        self.mass = mass
        self.pos = Vector2(x,y)
        #self.vel = Vector2(random(),random())
        if init_val:
            self.acc = Vector2(random(),random())
        self.image = Surface(size)
        self.image.fill((255,255,255))
        self.image.set_colorkey((255,255,255))
        draw_circle(self.image,color,(size[0]/2,size[1]/2),size[0]/2)

    def update(self, force):
        self.acc += force
        self.vel += self.acc
        self.rect.move_ip(self.vel)

Sim loop

class nbody:
    def __init__(self, n = 3, screensize=(1366,768)):
        self.win = pg.display.set_mode(screensize)
        self.clock = pg.time.Clock()
        self.bodies = Group()
        sareax = [int(screensize[0]/4), int(screensize[0]*3/4)]
        sareay = [int(screensize[1]/4), int(screensize[1]*3/4)]
        c = [(255,0,0), (0,255,0), (0,0,255)]
        self.bodies = []
        for i in range(n):
            mass = 5 + random()*5
            size = [mass*2]*2
            self.bodies.append(Body(randint(sareax[0], sareax[1]),randint(sareay[0],sareay[1]),size,mass, init_val=False, color=c[i]))
    
    def run_sim(self, time=None, fps = 0.5):
        run = True
        while run:
            self.clock.tick(fps)
            self.win.fill((0,0,0))
            for e in pg.event.get():
                if e.type == pg.QUIT:
                    pg.quit()
                    quit()
                elif e.type == pg.KEYDOWN:
                    if e.key == pg.K_ESCAPE:
                        run = False
                elif e.type == pg.KEYUP:
                    NotImplemented
            updates = []
            for b1,i in zip(self.bodies,range(len(self.bodies))):
                df = Vector2(0,0)
                for b2,j in zip(self.bodies,range(len(self.bodies))):
                    if i != j:
                        temp = newton(GRAV_CONSTANT, b1, b2)
                        mag = Vector2(b2.rect.centerx-b1.rect.centerx, b2.rect.centery-b1.rect.centery)
                        mag.scale_to_length(temp)
                        df += temp*mag
                        #print("Body {} has force {} to body {}".format(i,temp*mag,j))
                        if pg.sprite.collide_mask(b1,b2):
                            b1.kill()
                            b2.kill()
                updates.append(df)
            print(updates)
            for bnum,up in zip(range(len(updates)),updates):
                print("Before:", self.bodies[bnum].rect.center, self.bodies[bnum].vel, self.bodies[bnum].acc)
                self.bodies[bnum].update(up)
                print("Change:", up)
                print("After:", self.bodies[bnum].rect.center, self.bodies[bnum].vel, self.bodies[bnum].acc)
                pg.draw.line(self.win, (255,255,0), self.bodies[bnum].rect.center, (20*up.normalize())+self.bodies[bnum].rect.center, width=2)
                self.win.blit(self.bodies[bnum].image,self.bodies[bnum].rect)
                
            pg.display.flip()
        
        return

Solution

  • See Class and Instance Variables:

    Generally speaking, instance variables are for data unique to each instance and class variables are for attributes and methods shared by all instances of the class.

    Since val and acc are class attributes they are shared by all Body objects and as you mentioned in your question: "these completely separate elements tied together".

    vel and acc have to be instance attribute instead of class attributes:

    class Body(Sprite):
        def __init__(self, x, y, size, mass, init_val = True, color=(255,255,0)):
            super().__init__()
            self.rect = Rect(x-size[0]/2,y-size[1]/2,size[0],size[1])
            self.mass = mass
            self.pos = Vector2(x,y)
           
            self.vel = Vector2(0,0)
            self.acc = Vector2(0,0)
            if init_val:
                self.acc = Vector2(random(),random())
    
            self.image = Surface(size)
            self.image.fill((255,255,255))
            self.image.set_colorkey((255,255,255))
            draw_circle(self.image,color,(size[0]/2,size[1]/2),size[0]/2)