Question

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 updated 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.

Was it helpful?

Solution 2

Unfortunately, I have to say that for this particular issue, the error was mine.

In the Explosion class, whose code was not included here, I was not correctly updating its self.x and self.y values during its creation or its self.update() method. Changing that code, in concert with the shortening of the code I do have displayed here, caused that issue and others to vanish.

I know it's a bit gauche to accept your own answer and close the question summarily, especially after the problem was actually in a different part of the code, especially after putting up a bounty. My apologies.

OTHER TIPS

I'm going to assume that you use pygame.sprite.Group() to group sprites and then render them to the screen. Correct me if I'm wrong on that. Then you create a collison which creates using the attributes of the sprite, but then 'destroys' the sprite, meaning that any groups you created using pygame.sprite.Group that then contains that sprite will no longer contain that sprite. Now considering that you put a link in your question I'm going to assume that you already have read the docs.

So here is the code that you posted with comments of what each does.

#so here is your wrapper which will call make explosion before it kills the sprite.
def make_explosion(obj, func):
    #you call kill and so here it goes.
    def inner(*args, **kwargs):
        #you add the explosion to your queue
        allqueue.add(Explosion(obj.x, obj.y)) #create an instance of explosion inside.
        # return to the outside function `make_explosion`
        return func(*args, **kwargs)
    #you call inner second and then go to the inner.
    return inner
    #after it returns inner kill is called

So this means that it calls make_explosion, and then calls kill on the sprite which removes it from any group. Please tell me if you need any clarification on this or if you need a better, or more deep answer. Or tell me if you have any other things that you are in need of.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top