3

I am developing a game with python 3.6, I want in its multiplayer version to send to the server objects modified by the client ( the player) I thought to serialize them for transfer. I use pygame and thus pygame.Surface in my objects

I have objects with this structure:

class Cargo(Bateau):
  dictCargos = dict()
  def __init__(self, map, nom, pos, armateur=None):
    Bateau.__init__(self, map, nom, armateur, pos)
    self.surface = pygame.image.load(f"images/{self.nom}.png").convert_alpha()
    self.rect = self.map.blit(self.surface, self.pos)
    ...
    Cargo.dictCargos[self.nom] = self

When I serialize another object without pygame instance it's ok But with the object described above I get this error message:

import pickle as pickle
pickle.dump(Cargo.dictCargos, open('file2.pkl', 'wb'), protocol=pickle.HIGHEST_PROTOCOL)

Traceback (most recent call last):
  File "./pytransit.py", line 182, in <module>
    encreG(joueur, event)
  File "/home/patrick/Bureau/PyTransit/modulesJeu/tests.py", line 25, in encreG
    pickle.dump(Cargo.dictCargos, open('file2.pkl', 'wb'), protocol=pickle.HIGHEST_PROTOCOL)
TypeError: can't pickle pygame.Surface objects

Do you have any idea how to transport these items to the server. Or bypass this pickle restriction?
The same problem would arise if I wanted to save a part, so save these objects

patol
  • 136
  • 2
  • 10
  • 1
    Possible duplicate of [pickle saving pygame Surface (python)](https://stackoverflow.com/questions/18413963/pickle-saving-pygame-surface-python) – IonicSolutions May 03 '18 at 09:01
  • Thanks, But i don't see how use these functions in my objects. I can't delete pygame.surface of my object for transport and recreate this on other client or on server ??? – patol May 03 '18 at 09:13
  • Can't you use the suggested [`pygame.image.tostring()`](http://www.pygame.org/docs/ref/image.html#pygame.image.tostring) to transfer the surface? – IonicSolutions May 03 '18 at 09:18
  • That means I have to "get out" of all my Cargo instances the surfaces? To put them where? I don't see how I can organize my data under these conditions! – patol May 03 '18 at 09:25
  • 1
    Since you cannot pickle the Surface objects, you need to find a way to remove them before pickling and to re-create them afterwards. For this, I suggest you look at [Handling Stateful Objects](https://docs.python.org/3/library/pickle.html#pickle-state) in the `pickle` documentation. I've used this in the past to [deal with file logging handlers](https://github.com/ionicsolutions/pyfecs/blob/master/objects/_object.py), which is very similar to your problem. – IonicSolutions May 03 '18 at 11:07
  • Ok I'll study this and see how I can adapt it to my problem and especially to my classes :) Thank you – patol May 03 '18 at 12:00
  • 1
    Could you tell us why you want to send surfaces? What is actually going on in your game? The question sounds a bit like an [XY problem](http://xyproblem.info) and there could be a better way to achieve what you want. Also, note that unpickling data from untrusted sources is a huge security risk, so pickle shouldn't be used in a multiplayer game. – skrx May 03 '18 at 19:32
  • Well I give you a summary: this game plays per turn, from 2 to 6 players. Each player is an object, he can modify these characteristics (attributes) during the game, or other players can modify them by their actions. It handles different tools that also change during the game, these "tools" are obviously objects. So I thought I would use the objects to communicate all the information of a player or one of his tools to the server at the end of a player's game turn. Player A does not know what Player B does in the same turn. – patol May 03 '18 at 21:14
  • Indeed it is not the pygame.surface that changes during the game but the rectangle and its position, but the surface is an attribute of the object player and the objects he handles – patol May 03 '18 at 21:20
  • Then you could send only the relevant data (the rect position and the size if it changes as well) instead of the whole player objects with their surfaces/images and all the other attributes. BTW, write @skrx if you want to send me a notification. – skrx May 05 '18 at 02:24
  • Yes I see very well, but I was looking to "simplify" the code I have indeed 5 different objects each with more than a dozen instance attributes, all necessary to update the game. And I thought it would be faster and easier to pass the data with pickle while passing the object... But that's not necessarily the case! I am not English speaking and I do not yet know all the subtleties of stackoverflow, what do you mean by "BTW" and by " write @skrx if you want to send me a notification"! :( What is "notification" – patol May 06 '18 at 12:16
  • 1
    "BTW" means "by the way" and if you write @username in a comment, the user will see the little red new comment icon in the top bar. I only saw your previous comments because I still had this tab open. – skrx May 07 '18 at 00:29
  • @skrx Thank you for that clarification. BTW what do you think of my approach, does it seem absurd to you? I also thought to put all surfaces as attributes of an identical code on the server and the client. For example MySurfaces.player1. And refer to it in the rest of the code. It's an idea I haven't tested yet. – patol May 07 '18 at 09:50
  • @ skrx You said, "Also, note that unpickling data from untrusted sources is a huge security risk, so pickle shouldn't be used in a multiplayer game". But then what do you suggest to transfer that kind of data? Maybe I should open a new post? – patol May 07 '18 at 14:45
  • 1
    You could use the [JSON](https://docs.python.org/3/library/json.html) format if you only send the position and other simple data types that can be serialized with the json module. I'd avoid sending surfaces, since they can be pretty large. – skrx May 10 '18 at 09:30
  • @ skrx Ok the surface are all the time same, but I will need to list all the attributes of the objects... I don't think it's possible to send an object as such with Json!. Thank you anyway for your suggestions. – patol May 11 '18 at 18:27

2 Answers2

6

Here is an example of what @IonicSolutions pointed to in the comments:

import pickle
import pygame


class Test:
    def __init__(self, surface):
        self.surface = surface
        self.name = "Test"

    def __getstate__(self):
        state = self.__dict__.copy()
        surface = state.pop("surface")
        state["surface_string"] = (pygame.image.tostring(surface, "RGB"), surface.get_size())
        return state

    def __setstate__(self, state):
        surface_string, size = state.pop("surface_string")
        state["surface"] = pygame.image.fromstring(surface_string, size, "RGB")
        self.__dict__.update(state)


t = Test(pygame.Surface((100, 100)))
b = pickle.dumps(t)
t = pickle.loads(b)

print(t.surface)

To see what modes you can use to store the data as a string (here "RGB") look into the documentation

MegaIng
  • 7,361
  • 1
  • 22
  • 35
  • It's clearer with a concrete example... Just a question: I use .convert_alpha(), in your example you write that (surface_string, size, "RGB") do I have to replace RGB with alpha or something else? – patol May 03 '18 at 12:18
0

Based on the answer by @MegaIng, I develop his/her answer, so that you can just use the pygame.Surface normally, but with the pickle feature added. It shouldn't disturb any of your code. I have tested it on python 3.7, 64 bit, and it works properly. Also have tried/implement it on my project, nothing had been disturbed.

import pygame as pg

pgSurf = pg.surface.Surface

class PickleableSurface(pgSurf):
    def __init__(self, *arg,**kwarg):
        size = arg[0]

        # size given is not an iterable,  but the object of pgSurf itself
        if (isinstance(size, pgSurf)):
            pgSurf.__init__(self, size=size.get_size(), flags=size.get_flags())
            self.surface = self
            self.name='test'
            self.blit(size, (0, 0))

        else:
            pgSurf.__init__(self, *arg, **kwarg)
            self.surface = self
            self.name = 'test'

    def __getstate__(self):
        state = self.__dict__.copy()
        surface = state["surface"]

        _1 = pg.image.tostring(surface.copy(), "RGBA")
        _2 = surface.get_size()
        _3 = surface.get_flags()
        state["surface_string"] = (_1, _2, _3)
        return state

    def __setstate__(self, state):
        surface_string, size, flags = state["surface_string"]

        pgSurf.__init__(self, size=size, flags=flags)

        s=pg.image.fromstring(surface_string, size, "RGBA")
        state["surface"] =s;
        self.blit(s,(0,0));self.surface=self;
        self.__dict__.update(state)

And here's an example

pg.Surface = PickleableSurface
pg.surface.Surface = PickleableSurface

surf = pg.Surface((300, 400), pg.SRCALPHA|pg.HWSURFACE)
# Surface, color, start pos, end pos, width
pg.draw.line(surf, (0,0,0), (0,100), (200, 300), 2)  

from pickle import loads, dumps

dump = dumps(surf)
loaded = loads(dump)
pg.init()
screen = pg.display.set_mode((300, 400))
screen.fill((255, 255, 255))
screen.blit(loaded, (0,0))
pg.display.update()

Then on my screen:

The result of the script on my screen

Thank you @MegaIng

P.s: I also added feature to convert unpickle-able pygame surface into a pickle-able surface, by doing newSurface = PickleableSurface(PygameSurface) However, it just tested once, so there are maybe some bugs. Just feel free to let me know if you find one! I hope it would help you! :D

Hzzkygcs
  • 1,532
  • 2
  • 16
  • 24