It's my hope that the bounty will draw an individual who knows a thing
or two about the inner workings of PyGame (I tried looking at the
source code.. there's a bunch of it) and can tell me if this is truly
related to the presence of threading.py, or if there is some practice
I can avoid in general to prevent this from happening in other
PyGame projects.
I created a wrapper for objects that explode when obj.kill()
is called.
def make_explosion(obj, func):
def inner(*args, **kwargs):
newX, newY = obj.x, obj.y
newBoom = Explosion(newX, newY)
allqueue.add(newBoom) #an instance of pygame.sprite.Group
return func(*args, **kwargs)
return inner
It gets the object's x and y coordinates, then creates a new instance of an Explosion
at those coordinates, adds it to the allqueue
which I use to make sure everything is update
d during the game, and finally returns the function it's wrapping - in this case, obj.kill
. obj.kill()
is a pygame.sprite.Sprite
method that removes the object from all instances of sprite.Group
that it belongs to.
I would then simply wrap the method in the following way, during the creation of an instance of Enemy
.
newEnemy = Enemy()
##other code to add new AI, point values…eventually, the following happens:
newEnemy.kill = make_explosion(newEnemy, newEnemy.kill)
When I ran the game, the explosions appeared in random places, not anywhere near the object's actual x and y coordinates. Anecdotally it didn't even seem like it was occurring at the object's (x, y) of origin or even on or near the paths they traveled during their brief existence onscreen (I'm pretty good at my own game, not to brag), so I felt I had to rule that out that x and y were being assigned at the time the method was wrapped.
A bit 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 object's self.kill()
method is called, the explosion appears at the correct coordinates.
What I do not understand is why this works! Especially considering PyGame's documentation, which states that kill()
doesn't necessarily delete the object; it just removes it from all Groups that it belongs to.
from https://www.pygame.org/docs/ref/sprite.html#pygame.sprite.Sprite.kill --
kill()
remove the Sprite from all Groups kill() -> None
The Sprite is removed from all the Groups that contain it. This won’t change
anything about the state of the Sprite. It is possible to continue to
use the Sprite after this method has been called, including adding it
to Groups.
So even though I accidentally the solution to my problem, I don't actually understand it at all. Why does it behave in this way?
EDIT: Based on some early comments, I've tried to reproduce a similar condition without the use of the PyGame library, and I am unable to do so.
Par 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, right away I notice that whereat()
is being called before the change in coordinates under Stick.go()
so it's using x
and y
just prior to the incrementation, but that's fine; I could easily change the wrapper to wait until after go()
is called. The issue is not present here as it was in my PyGame project; the explosions were not even "enemy adjacent", they would appear in all kinds of random places, not at the sprite's previous (x, y) coordinates(if it had, I may not have even noticed the problem!).
UPDATE: After a user comment got me wondering, I dug around the Internet a bit, ran cProfile
on the project in question, and lo and behold -- PyGame definitely makes use of threading.py
. Unfortunately the same Internet wasn't good enough to tell me exactly how PyGame uses threading.py
, so apparently I need to read up on how threads even work. Oi. :)
After reading the PyGame source code a little bit (bleh) I found one module which appears to arbitrate when and why PyGame spawns a thread. From what little I know about threads, this can cause something like a race condition; x is not defined 'in time' for the wrapper to execute, and so it (the Explosion
object) ends up at the wrong location. I've been seeing other errors popping up in a different project that's a little heavier on the maths; these are more along the lines of core dumps, but they typically involve pulse
being unable to get an event, or timing out waiting for an event, or something similar (I do have a v. long stack trace lying around in case anyone's having trouble sleeping).
At the end of the day I suspect that this is something I can avoid by sticking to certain practices, but I don't know what those practices would actually be, and I don't know how to prevent PyGame from arbitrating which processes get their own threads and when that is worse than NOT doing so. If there's a unified theory for all this I'd sure love to know about it.