Pastebin Link
I am working with Pygame on a beat-em-up game and I'm running into an issue where the enemy will grab the player, forever and not let him go. A timer is supposed to run and when it reaches zero the enemy is supposed to push the player away, but it never reaches zero.
The variable contact is group of enemies who are actually touching the player. It's using the pygame.sprite.Group() function. If the sprites overlap (of the player and any enemy in the list of all enemies), then they are added to the group. If the sprites stop overlapping (enemy walks away, or the player walks away), then the enemies are removed from that group.
contact = pygame.sprite.spritecollide(heroBoonrit, enemy_list, False)
For every tick of the clock, I have it set up to check if there are any enemies touching the player, and if there are, then go through every one of them and see if any of the enemies are in a grabbing state (villan.grabbing_cooldown). It's just an on/off switch that means that the enemy is currently attacking with a grab move. I could probably come up with a more logical variable name.
If those conditions are met, then a few things happen, such as snapping the player's position up (or down) in order to be on the same y coordinate as the enemy. The variable heroBoonrit.held_cooldown is another on/off switch that means that he is currently being held. Both the player and the enemies have their own variable called heroBoonrit.held_countdown and villan.grabbing_countdown respectively. The problem I'm seeing (by running print() for diagnostics is that the enemy countdown decrements by 1 and then it stop decreasing, whereas the countdown timer of my hero decrements until 0. So therefore the enemy never lets go.
I have a feeling that there's probably a much more elegant and clean way to go about handling player and enemy behavior rather than putting on/off switches for the player that relate to whether or not he's being stunned, grabbed, etc (and additionally, corresponding countdown timers for those), but I've only been doing programming for a month and would appreciate any feedback. Maybe after every tick of the clock, the enemy holding timer is reset to 60 again...I have read in other posts that when you are using the Pygame Group() function, you can't easily iterate over the group. Still learning many of the ins and outs...
I ran this command in my main game loop to find out that enemy's grabbing countdown timer only goes from 60 to 59 and then stops counting down:
print("||Shit Clown|| Grabbing = ", enemyShit_Clown.grabbing_cooldown, " Countdown = ", enemyShit_Clown.grabbing_countdown, "||Boonrit|| Grabbed = ", heroBoonrit.held_cooldown, " Countdown = ", heroBoonrit.held_countdown)
Here is the block of code where I'm running into the problem.
for villan in contact:
for villan in enemy_list:
if villan.grabbing_cooldown:
heroBoonrit.held_cooldown = True
heroBoonrit.rect.y = villan.rect.y
heroBoonrit.rect.x = (villan.rect.x + 30)
heroBoonrit.held_countdown = villan.grabbing_countdown
villan.grabbing_countdown -= 1
heroBoonrit.held_countdown -= 1
if villan.grabbing_countdown <= 0:
villan.grabbing_countdown = 0
heroBoonrit.held_countdown = 0
villan.grabbing_cooldown = False
heroBoonrit.held_cooldown = False
heroBoonrit.rect.x += 30
elif villan.attacking_cooldown:
if heroBoonrit.blocking != True:
heroBoonrit.hp -= 100
else:
heroBoonrit.hp -= 10
Here is the Enemy class:
class Enemy(pygame.sprite.Sprite):
def __init__(self, name, level, speed, hp, stamina, fear, blocking_cooldown, jumping_cooldown, attacking_cooldown, held_cooldown, knockdown_cooldown, stun_cooldown, jumping_countdown, attacking_countdown, held_countdown, knockdown_countdown, stun_countdown, grabbing_cooldown, grabbing_countdown, blocking_countdown):
super().__init__()
self.image = pygame.image.load(os.path.join('res','img','chars','shit_clown-3.png'))
#self.image = pygame.Surface([30,20])
#self.image.fill(color)
self.rect = self.image.get_rect()
self.surf = pygame.Surface((30,20))
self.name = name
self.speed = speed
self.level = level
self.hp = hp
self.stamina = stamina
self.fear = fear
self.blocking_cooldown = blocking_cooldown
self.jumping_cooldown = jumping_cooldown
self.jumping_countdown = jumping_countdown
self.attacking_cooldown = attacking_cooldown
self.attacking_countdown = attacking_countdown
self.held_cooldown = held_cooldown
self.held_countdown = held_countdown
self.knockdown_cooldown = knockdown_cooldown
self.knockdown_countdown = knockdown_countdown
self.stun_cooldown = stun_cooldown
self.stun_countdown = stun_countdown
self.grabbing_cooldown = grabbing_cooldown
self.grabbing_countdown = grabbing_countdown
self.blocking_countdown = blocking_countdown
blocking_cooldown = False
jumping_cooldown = False
jumping_coutndown = 0
attacking_cooldown = False
attacking_countdown = 0
held_cooldown = False
held_countdown = 0
knockdown_cooldown = False
knockdown_countdown = 0
stun_cooldown = False
stun_countdown = 0
grabbing_cooldown = False
grabbing_countdown = 0
blocking_countdown = 0
And to spawn the enemy:
enemyShit_Clown = Enemy("Shit Clown", 1, 4, 1000, 10, 90, False, False, False, False, False, False, 0, 0, 0, 0, 0, True, 60, 0)
enemyShit_Clown.image = pygame.image.load(os.path.join('res','img','chars','shit_clown-3.png')).convert()
enemyShit_Clown.rect.x = 300 #random.randrange(300, 400)
enemyShit_Clown.rect.y = 300 #random.randrange(200, 400)
enemy_list.add(enemyShit_Clown)
all_sprites_list.add(enemyShit_Clown)
Thanks very much for your help
I don't see any obvious reason for the villain to stop counting down. That said, I do see that you are decrementing the countdown both in the hero.update code and in the main loop. I'd expect your hero to count down twice as fast as the villain, but not 60 times as fast.
I'd like to suggest some code changes. First, get rid of most of the parameters in your __init__ code. Just set the default values to 0 or False or whatever.
Next, I see you creating a character, and then assigning an image to it. That should be in the constructor, with maybe a defaulted parameter to select a different starting image.
Next, you have countdown code in the hero's update method, but not in the villains. I think this is a mistake. Move the countdowns into the update routine, and don't worry about searching for it in the main loop.
Finally, there is a rule of thumb for OO programming you are missing: "Tell, don't ask."
Basically, this says, among other things, that you shouldn't be accessing the attributes of another object, and you definitely shouldn't be modifying the attributes of another object.
I suggest you write methods for this stuff, like so:
class Villain:
def grab(self, hero):
"""Start a grabbing attack on hero, if not blocked/busy"""
if self.grabbing:
# Already grabbing someone. Fail.
return False
ticks = 60
if hero.grabbed(self, ticks):
self.grabbing = hero
self.grabbing_countdown = ticks
hero.moveto(self.rect.y, self.rect.x + 30)
return True
else:
return False
def update(self, *args):
:
blah blah blah
:
if self.grabbing:
self.grabbing_countdown -= 1
if self.grabbing_countdown == 0:
self.grabbing.release(self)
self.grabbing.push_away(self.rect.x, self.rect.y, 30)
self.grabbing = None
Related
I am trying to filter collisions occurring in my Box2D world by reproducing this example: https://github.com/pybox2d/pybox2d/blob/master/examples/collision_filtering.py
I have four classes in my world, Car, Wheel, Building, and Pedestrian, I want to filter which instance collided with which and one of the possible outputs is (pseudo-code)
if contact.FixtureA.isinstance(Pedestrian) and contact.FixtureB.isinstance(Car):
print("You have caused a traffic accident")
I have this set of categories
CAR_CATEGORY = 2
PEDESTRIAN_CATEGORY = 4
BUILDING_CATEGORY = 8
box2world = world(gravity =(0.0, 0.0), doSleep =True)
I also tried this: but it doesn't work (it does nothing)
class myContactListener(b2ContactListener):
def __init__(self):
b2ContactListener.__init__(self)
def BeginContact(self, contact):
fixture_a = contact.fixtureA
fixture_b = contact.fixtureB
body_a, body_b = fixture_a.body, fixture_b.body
ud_a, ud_b = body_a.userData, body_b.userData
pedestrian = None
car = None
for ud in (body_a, body_b):
if isinstance(ud, Pedestrian):
pedestrian = ud
elif isinstance(ud, Car):
car = ud
if car is not None and pedestrian is not None:
if began:
print("It does stuff")
else:
print("It does something")
def EndContact(self, contact):
pass
def PreSolve(self, contact, oldManifold):
pass
def PostSolve(self, contact, impulse):
pass
box2world = world(contactListener=myContactListener(),gravity =(0.0, 0.0), doSleep =True)
and I apply this in given classes (only class Pedestrian shown as example for simplicity):
class Pedestrian():
def __init__(self,box2world, ped_velocity =25, position =None,):
if position == None:
position = [5,5]
self.ped_velocity = ped_velocity
self.position = position
self.box2world = box2world
self.nearest_building = 0
self.body = self.box2world.CreateDynamicBody(position = position,
angle = 0.0,
fixtures = b2FixtureDef(
shape = b2CircleShape(radius = 1),
density = 2,
friction = 0.3,
filter = b2Filter(
categoryBits=PEDESTRIAN_CATEGORY,
maskBits=BUILDING_CATEGORY + CAR_CATEGORY,
groupIndex=0,
)))
self.current_position = [self.body.position]
self.body.userData = {'obj': self}
And then I draw the bodies and run the world using pygame
But I am confused about how to continue, how could I use the information from the collisionfilter to be able to for example print the sentence about the accident from above?
Thank you very much
EDIT: I have found a link which solves exactly what I want to do, but it is written in C++ and I do not understand it
http://www.iforce2d.net/b2dtut/collision-callbacks
hey I just answered your question on stackexchange :-)
For collisions it's easy:
local filterData = {
categoryBits = player,
maskBits = wall + nme + platform,
groupIndex = 0
}
fixture:setFilterData(filterData)
player, wall, nme, ... are integers variables (must be power of 2 numbers):
player = 1
wall = 2
nme = 4
... = 16, 32, 64, 128, 256, ...
categoryBits = main object you want to test collisions on
maskBits = you add (with +) all the numbers the main object can collide with.
It's better to store the numbers as variables otherwise it would look like:
local filterData = {
categoryBits = 1,
maskBits = 2 + 4 + 8 + 16 ...,
groupIndex = 0
}
fixture:setFilterData(filterData)
:-)
to answer your second comment that is more complex so I add it as another answer.
You don't handle collisions yourself, Box2D does it for you but you need to set things up.
- create your world
world = b2.World.new(0, 24, true)
- attach listeners to your world (for collisions handling)
world:addEventListener(Event.BEGIN_CONTACT, self.onBeginContact, self)
world:addEventListener(Event.END_CONTACT, self.onEndContact, self)
world:addEventListener(Event.PRE_SOLVE, self.onPreSolveContact, self)
world:addEventListener(Event.POST_SOLVE, self.onPostSolveContact, self)
- in your game loop you need to call Box2D update
self.world:step(1/60, 1, 1)
- then ANSWER IS HERE you check collisions for each objects in those box2d functions listeners
-- collisions handler
function LF_Bullet:onBeginContact(e)
local bodyA = e.fixtureA:getBody()
local bodyB = e.fixtureB:getBody()
if bodyA.type == 100 or bodyB.type == 100 then
self.removebullet = true
end
if bodyA.type == 200 and bodyB.type == 100 then
bodyA.isdead = true
end
if bodyA.type == 100 and bodyB.type == 200 then
bodyB.isdead = true
end
if bodyA.type == 201 and bodyB.type == 100 then
bodyA.isdead = true
end
if bodyA.type == 100 and bodyB.type == 201 then
bodyB.isdead = true
end
end
function LF_Bullet:onPreSolveContact(e)
end
function LF_Bullet:onPostSolveContact(e)
end
function LF_Bullet:onEndContact(e)
end
This is a quick example written in LUA using gideros mobile http://giderosmobile.com/.
A must website regarding box2d is of course:
https://www.iforce2d.net/b2dtut/
It is a vast subject and you may want to follow some youtube tuts. Even if it is not written in py, box2d works the same so you just have to adapt to py.
Some links that may help:
https://www.youtube.com/playlist?list=PLZm85UZQLd2SXQzsF-a0-pPF6IWDDdrXt
That's how I learned using box2d. Hope that helps?
Really new to Python and I'm stuck. I can't figure out how to get HP and DMG to randomize when it's called when the button I've created is clicked.
Here's currently what I have:
# find your fav character images and pass it here
Char1 = Character('Snart.png','CAPTAIN COLD',DISPLAYSURF,(100,300),200)
Char2 = Character('Flash.png','FLASH',DISPLAYSURF,(700,300),200)
def displayButtons(bList):
for x in bList:
x.display()
def main():
B1.active = True
clickCount = 1
B2.active = False
while True:
DISPLAYSURF.fill(BGCOLOR)
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
## MOUSE EVENTS
elif event.type == MOUSEBUTTONDOWN:
mouse = pygame.mouse.get_pos()
if B1.clicked(mouse):
B1.highlight = True
print("Hello") ## Just to see if it actually get's pressed
clickCount = 2
elif B2.clicked(mouse):
B2.highlight = True
print("Bye") ## Just to see if it actually get's pressed
clickCount = 1
elif event.type == MOUSEBUTTONUP:
if B1.clicked(mouse):
Char1.Randomize() ## Randomize the HP DMG
B1.highlight = False
B1.active = False
B2.active = True
elif B2.clicked(mouse):
Char2.Randomize() ## Randomize the HP DMG
B2.highlight = False
B2.active = False
B1.active = True
Char1.display()
Char2.display()
displayButtons(BUTTONLIST)
pygame.display.update()
main()
And for the class that it's creating:
class Character(object):
def __init__(self, imagefile,charname,surf,pos,scalesize):
self.SURF = surf
self.POS = pos
self.IMAGESURF = pygame.image.load(imagefile)
self.IMAGESURF = pygame.transform.scale(self.IMAGESURF, (scalesize,scalesize))
self.HP = (0, 300) # should range from (0 - 300) ## randint: return a random integer(start,stop)
self.DMG = (0, 100) # should range from (0 - 100)
self.GameFont = pygame.font.SysFont("Sylfaen", 50)
# this text has a black background. Can you make it transparent ?. DONE
self.NAME = self.GameFont.render(charname, True,(255,255,255),None)
self.Randomize()
self.__drawText()
self.__displayText()
# complete this function
# this function should randomize HP, DMG and should display on the screen
# this function should be called on a button press
def Randomize(self):
#pass
self.HP = randint(0, 300)
self.DMG = randint(0, 300)
## DON'T UNCOMMENT UNLESS YOU WANT IT TO RANDOMLY GENERATE NON-STOP
## self.HPText = self.GameFont.render('HP : ' +str(self.HPrand), True,(255,255,255),None)
## self.DMGText = self.GameFont.render('DMG: ' +str(self.DMGrand), True,(255,255,255),None)
def __displayText(self):
self.SURF.blit(self.HPText,(self.POS[0]+200,self.POS[1]+50))
self.SURF.blit(self.DMGText,(self.POS[0]+200,self.POS[1]+150))
self.SURF.blit(self.NAME,(self.POS[0]+20,self.POS[1]-100))
# fix the error in this function, DONE
def __drawText(self):
# this text has a black background. Can you make it transparent ?.
self.HPText = self.GameFont.render('HP : ' +str(self.HP), True,(255,255,255),None)
self.DMGText = self.GameFont.render('DMG: ' +str(self.DMG), True,(255,255,255),None)
# fix the errors in this function, DONE
def display(self):
self.Randomize()
self.__displayText()
self.SURF.blit(self.IMAGESURF,self.POS)
After you randomize the HP and DMG values, you need to re-render the text values for each of them. You have a function to do that, named __drawText, but you're not calling it when the button is pressed. This is why you keep drawing the old values even after Randomize has been called.
I'm not sure exactly how you want your class to work, but perhaps __drawText should be called from Randomize? You can't rely upon the external code that that runs Randomize to also call __drawText since you've given it a name starting with two underscores (which invokes Python's name mangling system). If it's supposed to be part of the class API, you certainly don't want to be doing that. External code can still call __drawText, but only by manually doing the mangling (and calling e.g. Char1._Character__drawText).
One final thing, which is unrelated to your current issues. Your variables are being named in a way that is a bit unusual for Python code. The more usual Python style is to use lowercase_with_underscores names for most variables and functions (including methods), and reserve TitleCase names for classes. ALL_CAPS is used occasionally for variables that are notionally constant, but the regular variable style is also pretty common even for constants (e.g. math.pi).
Using a different naming convention doesn't make your code wrong, but it may be harder for other programmers to follow than if you followed the standard conventions. See PEP 8 for the style used for the official Python interpreter. A lot of other Python code follows that guide (with perhaps a little more leeway given on line lengths). Google also has a Python style guide, which is (as far as I can tell) pretty much compatible with PEP 8.
I to make a hidden button, that will show, when you hoer over it, then lights up upon pressing it for a short time (2 secs in my script atm) and then runs a function. I implemented a mechanism that appears logical to me to pull this off. Bit I get a variable reference error.
This is my button class:
class Button:
def __init__(self, imgInAc, imgAc, posX, posY, width, height, function, gameInstance, hiddenButton):
self.imgInAc = pygame.image.load(imgInAc)
self.imgAc = pygame.image.load(imgAc)
self.posX = posX
self.posY = posY
self.w = width
self.h = height
self.function = function
self.gameInstance = gameInstance
self.hiddenButton = hiddenButton
# Make clickDump false, to make sure, refTiem gets measured only once after the button press,
# because otherweise it would get overwritten over and over again while the game loop is running
self.clickDump = False
def button(self):
# Measure the current time to later run the button function while displaying the active image before doing so.
curTime = time.clock()
mouse = pygame.mouse.get_pos()
click = pygame.mouse.get_pressed()
if self.posX+self.w > mouse[0] > self.posX and self.posY+self.h > mouse[1] > self.posY:
if self.hiddenButton == False:
self.gameInstance.gameDisplay.blit(self.imgAc,(self.posX,self.posY))
if click[0] == 1:
buttonFunctions.buttonFunctions(self.function)
# If the button is a ahidden one, go through teh following protocol
elif self.hiddenButton == True:
# Display the off version if the mouse hovers over it
self.gameInstance.gameDisplay.blit(self.imgInAc,(self.posX,self.posY))
if click[0] == 1:
# if you click on it, display teh active version
self.gameInstance.gameDisplay.blit(self.imgAc,(self.posX,self.posY))
###print curTime
# If clickDump is false, defien teh reference time relative to teh current time.
# Then make it true, so the ref time doesn't get changed again, while the game loop is running
if self.clickDump == False:
refTime = time.clock()+2
###print refTime
self.clickDump = True
# If ClickDump is true, check if teh time condition is satiesfied. Note, that this check is run outside the "if click[0] == 1"-scope,
# to make sure, it also gets checked if the click happened in a previoues cycle.
if self.clickDump == True:
# If the time interval between the click event and the current time has elapsed
# set clickDUmp back to flase, so it can be used for teh next button press protocol
# and finally run the button function.
if curTime > refTime:
self.clickDump = False
buttonFunctions.buttonFunctions(self.function)
else:
if self.hiddenButton == False:
self.gameInstance.gameDisplay.blit(self.imgInAc,(self.posX,self.posY))
the error is:
"...line 296, in button
if curTime > refTime:
UnboundLocalError: local variable 'refTime' referenced before assignment"
I really don't understand this, as I thought I only execute the part where the time variables are compared IF refTime has been initialized, because the condition or running the comparison onyl gets fulfilled in the part where refTime gets assigned its value.
Sorry for potential loads of typos in my commets... I only wrote them for myself.
So... me confused.
it is due to the scope of refTime.
reftime is assigned in some if loop while used in another if loop.
assign it like this.
class Button(object):
...
...
def button(self):
refTime = 0
I am currently trying to implement an MVC pattern using Python and Pygame, but I can't figure out how to properly handle animations. Let's say we have a model object that can attack:
class ModelObject:
def attack():
if not self.attacking:
self.attacking = True
# Then compute some stuff
def isattacking():
return self.attacking
And a view object that renders the model object by displaying an attack animation:
class ViewObject(pygame.Sprite):
attacking_ressource = [] # List of surfaces for animation
default_ressource = None
def update():
# Detect the model object is attacking
if self.model.isattacking():
# Create animation if needed
if self.attacking_animation is None:
self.attacking_animation = iter(self.attacking_ressource)
# Set image
self.image = next(self.attacking_animation, self.default_ressource)
else:
# Reset animation
self.attacking_animation = None
self.image = self.default_ressource
The question is, how do the model know that it's no longer attacking?
The view could notify the model when the animation is over, but it guess it's not how the MVC pattern is supposed to work. Or, an animation counter could be set in the model, but it doesn't seem right either.
I think you got it the wrong way round.
The attack should not last as long as the animation, but the animation should last as long as the attack.
So the ViewObject is fine as it already asks the model if it is still attacking.
As for the ModelObject, it's up to you how long the attack should last and how to keep track of time. You could for example call pygame.time.get_ticks() to get the number of millisconds since you started your game once in attack, and then periodically check it again, like:
class ModelObject:
def attack():
if not self.attacking:
self.attacking = True
self.started = pygame.time.get_ticks()
# Then compute some stuff
def isattacking():
return self.attacking
def update():
# attack lasts 1000ms
self.attacking &= pygame.time.get_ticks() - self.started < 1000
I'm using Livewires and pygame and one of my objects in the game that gives you extra lives is being mistaken as an asteroid object, and when the extra lives objects collides with the player it returns the 'Extra lives object has no attribute handle_caught' error message, so can I please have some help.
class Extralives(games.Sprite):
global lives
image = games.load_image('lives.png', transparent = True)
speed = 2
def __init__(self,x,y = 10):
""" Initialize a asteroid object. """
super(Extralives, self).__init__(image = Extralives.image,
x = x, y = y,
dy = Extralives.speed)
def update(self):
""" Check if bottom edge has reached screen bottom. """
if self.bottom>games.screen.height:
self.destroy()
self.add_extralives
def add_extralives(self):
lives+=1
The asteroid class:
class Asteroid(games.Sprite):
global lives
global score
"""
A asteroid which falls through space.
"""
image = games.load_image("asteroid_med.bmp")
speed = 1.7
def __init__(self, x,image, y = 10):
""" Initialize a asteroid object. """
super(Asteroid, self).__init__(image = image,
x = x, y = y,
dy = Asteroid.speed)
def update(self):
""" Check if bottom edge has reached screen bottom. """
if self.bottom>games.screen.height:
self.destroy()
score.value+=10
def handle_caught(self):
if lives.value>0:
lives.value-=1
self.destroy_asteroid()
if lives.value <= 0:
self.destroy_asteroid()
self.end_game()
def destroy_asteroid(self):
self.destroy()
part of the player class which handles the collisions:
def update(self):
""" uses A and D keys to move the ship """
if games.keyboard.is_pressed(games.K_a):
self.x-=4
if games.keyboard.is_pressed(games.K_d):
self.x+=4
if self.left < 0:
self.left = 0
if self.right > games.screen.width:
self.right = games.screen.width
self.check_collison()
def ship_destroy(self):
self.destroy()
def check_collison(self):
""" Check if catch pizzas. """
global lives
for asteroid in self.overlapping_sprites:
asteroid.handle_caught()
if lives.value <=0:
self.ship_destroy()
for extralives in self.overlapping_sprites:
extralives.add_extralives()
Here is your problem:
for asteroid in self.overlapping_sprites:
asteroid.handle_caught()
if lives.value <=0:
self.ship_destroy()
The fact that you call your loop variable asteroid does not mean that it's magically only going to ever be an asteroid. Not if you have other kinds of objects you can collide with! overlapping_sprites is all overlapping sprites, not just asteroids. At some point asteroid is an ExtraLives object. When you try to call handle_caught() on it, this obviously fails because ExtraLives doesn't have a handle_caught() method.
The simplest solution here is to rename add_extralives to handle_caught on your ExtraLives class. After all, you're doing the same thing: handling the situation where you collide with (or "catch") the object, it's just a different kind of object so the result needs to be different, which you specify by providing different code. Being able to implement entirely different kinds of behavior by calling the same methods (called "polymorphism") is kinda the whole point of object-oriented programming.
The following loop has a similar problem, in that you're calling add_extralives() on objects that might not be of type ExtraLives. Fortunately you can remove this code since you're already handling this situation by renaming add_extralives to handle_caught.
for extralives in self.overlapping_sprites:
extralives.add_extralives()