Pygame/Python: Can't create deepcopy of surfaces for room loading system (tl;dr at bottom)

Viewed 183

tl;dr at the bottom. Most of this post is to give context to my problem. If that isn't needed, my main question is down below.

I am currently working on a pygame project (action adventure game) and I currently am working on stuff relating to object persistence and room transitioning. Basically, I want to create a system where I have a dictionary that contains all of the information about a particular room including the images it uses and the objects that are in that room.

My game has a dictionary that contains lists of the current objects that are updating and doing stuff in my game. Let's call this instances. Anytime I add an object to instances, it will appear in game. With that in mind, lets consider how loading the objects in a particular room actually works. The way my system works is that I have a variable that is called room which contains a string that represents what room I am currently in. I have another dictionary that contains all of the objects within a room. Lets call this room_dict. room_dict would have the keys "objects":[obj1,obj2]. So based on the current value of room, it can access certain objects (i.e,room_dict[room]["objects"] would return a list of all of the objects in the current room).

Now that I've explained the basics of how it works, I have a method that actually knows when I have triggered a room transition (or rather, when the value of room is changed). When this happens, all of the objects existing in the room (that I was just in) are cleared from the instances dictionary. All of the objects from room_dict[room]["objects"] are added to instances so that they now appear in the game. Makes sense so far, right?

The main problem with this is that when objects in the instances dictionary (objects that are currently loaded) are updating, the objects that are in room_dict[room]["objects"] are also updated as well. This means that if I change the position of an enemy in one room and then leave the room and return, the object will be created at that position instead of the original position. So I tried doing instances[list_of_enemies].append(copy.copy(enemy_object)) to add a copy of the object as well. This still didn't work though, and when I tried doing a copy.deepcopy(), the interpreter said that it was unable to serialize the object because one of its attibutes was a pygame.Surface object.

So in other words my main issue is that I want to make a copy of an object that contains a pygame.Surface as its attribute that doesn't reference the original object at all. How would I go about making a deepcopy with an object that has a pygame.Surface type attribute?

tl;dr: I want to make a copy of an object that has an attribute that is a pygame.Surface object but copy.deepcopy() doesn't work. Is there any other way to copy without referencing the original object?

EDIT: Forgot to mention that the project is rather hefty, so it would be quite difficult to give code for context. I personally don't think it is needed, but I thought I'd put this out anyways. Thanks everyone!

1 Answers

One way to solve this is to create your objects from your json file everytime you need a new copy instead of creating the objects beforehand.

Or you could change your copy method: implement __deepcopy__ and/or __copy__ and copy the attributes of your objects without the image attribute, maybe just create a new instance.

Simple example:

import pygame
from copy import deepcopy
from itertools import cycle

# an implementation of something in our game
class Cloud(pygame.sprite.Sprite):
    def __init__(self, pos, speed):
        super().__init__()
        self.pos = pos
        self.speed = speed
        self.image = pygame.Surface((50, 20))
        self.image.set_colorkey((11, 12, 13))
        self.image.fill((11, 12, 13))
        pygame.draw.ellipse(self.image, 'white', self.image.get_rect())
        self.rect = self.image.get_rect(topleft=self.pos)

    def update(self):
        super().update()
        self.rect.move_ip(self.speed, 0)
        if not pygame.display.get_surface().get_rect().colliderect(self.rect):
            self.rect.right = 0

    def __deepcopy__(self, memo):
        # just create a new instance 
        return Cloud(self.pos, self.speed)

# the definition of our game world
game_data = {
    'WORLD_A': {
        'color': 'lightblue',
        'objects': pygame.sprite.Group(Cloud((50, 50), 1))
    },
    'WORLD_B': {
        'color': 'red',
        'objects': pygame.sprite.Group(Cloud((100, 100), 2), Cloud((80, 30), 3))
    },

}

screen = pygame.display.set_mode((300, 300))
clock = pygame.time.Clock()

keys = cycle(game_data.keys())

# happy deepcopying
current = deepcopy(game_data[next(keys)])

while True:
    for e in pygame.event.get():
        if e.type == pygame.QUIT:
            quit()
        if e.type == pygame.KEYDOWN:
            # happy deepcopying
            current = deepcopy(game_data[next(keys)])
            
    screen.fill(current['color'])
    current['objects'].update()
    current['objects'].draw(screen)
    pygame.display.flip()
    clock.tick(30)

Another solution is to lazy load the images and look them up only when needed so you don't need to copy them. Here's a simple example:

... see example above ...

# load/create all images once and store them in a dict
def create_cloud_image():
    image = pygame.Surface((50, 20))
    image.set_colorkey((11, 12, 13))
    image.fill((11, 12, 13))
    pygame.draw.ellipse(image, 'white', image.get_rect())
    return image

images = {
    'cloud': create_cloud_image()
}

# a simple sprite that lazy loads its image
class CopyableActor(pygame.sprite.Sprite):
    def __init__(self, image_key, pos):
        super().__init__()
        self.pos = pos
        self.image_key = image_key

    def init_image(self):
        self.image = images['cloud']
        self.rect = self.image.get_rect(topleft=self.pos)

    def update(self):
        if not hasattr(self, 'image'):
            self.init_image()

# an implementation of something in our game
class Cloud(CopyableActor):
    def __init__(self, pos, speed):
        super().__init__('cloud', pos)
        self.speed = speed

    def update(self):
        super().update()
        self.rect.move_ip(self.speed, 0)
        if not pygame.display.get_surface().get_rect().colliderect(self.rect):
            self.rect.right = 0


... see example above ...

enter image description here

Related