It is my hope that generosity will attract a person who knows something or two about PyGame's internal work (I tried to take a look at the source code .. there are tons) and can tell me if this is really related to the presence of threading.py, or if there is some practice I can generally avoid this from happening in other PyGame projects.
I created a wrapper for objects that explode when calling obj.kill() .
def make_explosion(obj, func): def inner(*args, **kwargs): newX, newY = obj.x, obj.y newBoom = Explosion(newX, newY) allqueue.add(newBoom)
It takes the x and y coordinates of the object, then creates a new Explosion instance at these coordinates, adds it to allqueue , which I use to make sure that everything is update d during the game, and finally returns the function that it wraps - in this case obj.kill . obj.kill() is a pygame.sprite.Sprite method that removes an object from all sprite.Group instances to which it belongs.
Then I would simply wrap the method as follows: at the time of creating the Enemy instance.
newEnemy = Enemy()
When I started the game, explosions appeared in random places, and not somewhere near the actual coordinates of the object and y. Anecdotally, it didn’t even seem that it happened on the object (x, y) of origin or even on the roads that they traveled during their short existence on the screen (I am pretty good in my game not to brag), so I felt that should have excluded that x and y were assigned during method wrapping.
A little aimlessly, I futzed around and changed the code to this:
def make_explosion(obj, func): def inner(*args, **kwargs): allqueue.add(Explosion(obj.x, obj.y)) return func(*args, **kwargs) return inner
This change made it work "as intended" - when the self.kill() method is self.kill() , the explosion appears in the correct coordinates.
I do not understand why this works! Especially considering the PyGame documentation that says kill() does not necessarily delete the object; he simply removes him from all the groups to which he belongs.
from https://www.pygame.org/docs/ref/sprite.html#pygame.sprite.Sprite.kill -
kill ()
remove Sprite from all groups kill () -> None
The sprite is removed from all groups that contain it. This will not change anything about the state of the sprite. You can continue to use Sprite after calling this method, including adding it to groups.
So, although I accidentally solved my problem, I don’t understand this at all. Why does he behave this way?
EDIT: Based on some early comments, I tried to reproduce a similar condition without using the PyGame library, and I cannot do this.
Example:
>>> class A(object): ... def __init__(self, x, y): ... self.x = x ... self.y = y ... def go(self): ... self.x += 1 ... self.y += 2 ... print "-go-" ... def stop(self): ... print "-stop-" ... print "(%d, %d)" % (self.x, self.y) ... def update(self): ... self.go() ... if self.x + self.y > 200: ... self.stop() >>> Stick = A(15, 15) >>> Stick.go() -go- >>> Stick.update() -go- >>> Stick.x 17 >>> Stick.y 19 >>> def whereat(x, y): ... print "I find myself at (%d, %d)." % (x, y) ... >>> def wrap(obj, func): ... def inner(*args, **kwargs): ... newX, newY = obj.x, obj.y ... whereat(newX, newY) ... return func(*args, **kwargs) ... return inner ... >>> Stick.update = wrap(Stick, Stick.update) >>> Stick.update() I find myself at (17, 19). -go- >>> Stick.update() I find myself at (18, 21). -go- >>> Stick.update() I find myself at (19, 23). -go-
So, I immediately notice that before changing the coordinates in Stick.go() is whereat() , so it uses x and y immediately before incrementing, but this is fine; I could easily change the shell to wait until go() called. The problem is missing here, as it was in my PyGame project; the explosions were not even “enemy neighbors”, they appeared in all kinds of random places, and not in the sprite of the previous (x, y) coordinates (if it were, I might not have noticed the problem!).
UPDATE: After a user comment made me think, I cProfile Internet a bit, ran cProfile in the project in question, and here it is - PyGame definitely uses threading.py . Unfortunately, the same Internet was not good enough to tell me how PyGame uses threading.py , so apparently I need to read how threads work. Oi. :)
After reading PyGame (bleh) source code several times, I discovered one module that seems to decide when and why PyGame spawns a stream. From what I know about streams, this can lead to something like a race condition; x is not defined "in time" to execute the shell, and therefore (the Explosion object) ends in the wrong place. I saw other errors occurring in another project, which are a little harder in mathematics; they are more related to core dumps, but they usually include a pulse inability to receive an event or an event wait timeout or something similar (I have a long stack trace v lying around in case someone has a sleep problem).
At the end of the day, I suspect that this is something that I can avoid by adhering to certain practices, but I don’t know what these practices really are, and I don’t know how to prevent PyGame from arbitration, the processes get their own threads, and when it's worse than NOT. If you have a unified theory, I would love to know about it.