Search code examples
pythonpygamecollisionblit

In pygame, how do I blit an image when I collide with something and make it stay?


So I have been using a slightly modified version of the python script from the first answer here and I added a class called "SpeechBlock" by adding it here:

    level = [
        "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP",
        "P                                                             P",
        "P                                                             P",
        "P                                                             P",
        "P                                                             P",
        "P                                                             P",
        "P                                                             P",
        "P                                                             P",
        "PPPPPPPPSPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP",]
    # build the level
    for row in level:
        for col in row:
            if col == "P":
                p = Platform(x, y)
                platforms.append(p)
                entities.add(p)
            if col == "E":
                e = ExitBlock(x, y)
                platforms.append(e)
                entities.add(e)
            if col == "S":
                s = SpeechBlock(x, y)
                platforms.append(s)
                entities.add(s

and making it a class:

class SpeechBlock(Platform):
    def __init__(self, x, y):
        Platform.__init__(self, x, y)
        self.image.fill(Color("#0033FF"))
        self.x=x
        self.y=y

    def speak(self):
        self.events = [
            "test",
            ]
        for row in self.events:
            image=pygame.image.load(row+".png")
            screen.blit(image, (self.x,self.y))

The script already had a method for collisions and I added the last 3-lines here:

ef collide(self, xvel, yvel, platforms):
        for p in platforms:
            if pygame.sprite.collide_rect(self, p):
                if isinstance(p, ExitBlock):
                    pygame.event.post(pygame.event.Event(QUIT))
                if xvel > 0:
                    self.rect.right = p.rect.left
                    print("collide right")
                if xvel < 0:
                    self.rect.left = p.rect.right
                    print ("collide left")
                if yvel > 0:
                    self.rect.bottom = p.rect.top
                    self.onGround = True
                    self.yvel = 0
                if yvel < 0:
                    self.rect.top = p.rect.bottom
                if isinstance(p, SpeechBlock):
                    SpeechBlock.speak(self)
                    pygame.display.update()

What I wanted and still want to achieve is that when the player is stands over a SpeechBlock an image of a speech bubble is blitted onto the screen. Instead what happens is that when the player stands on the edge of a SpeechBlock the image will appear for a short moment and then disappear and then reappear for a moment and so on... What have I done wrong? I'm pretty new to pygame so I don't really know much about how blitting and display updating/flipping works. Any help would be greatly appreciated.


Solution

  • You have to learn that your game runs in a loop. Each iteration of this loop is called a frame. And in each frame you clean everything from the screen, handle any events, update your game world, and draw everything again.

    So when you call SpeechBlock.speak(self) and pygame.display.update() you draw the images (from the speak method) to the screen, but they will get erased in the next frame.

    You should not call pygame.display.update() more than once per frame. What you should do is to introduce some new state that changes how SpeechBlock behaves when you touch it.

    tl;dr Example below:

    import pygame
    from pygame import *
    import sys
    
    SCREEN_SIZE = pygame.Rect((0, 0, 800, 640))
    TILE_SIZE = 32 
    GRAVITY = pygame.Vector2((0, 0.3))
    
    class CameraAwareLayeredUpdates(pygame.sprite.LayeredUpdates):
        def __init__(self, target, world_size):
            super().__init__()
            self.target = target
            self.cam = pygame.Vector2(0, 0)
            self.world_size = world_size
            if self.target:
                self.add(target)
    
        def update(self, *args):
            super().update(*args)
            if self.target:
                x = -self.target.rect.center[0] + SCREEN_SIZE.width/2
                y = -self.target.rect.center[1] + SCREEN_SIZE.height/2
                self.cam += (pygame.Vector2((x, y)) - self.cam) * 0.05
                self.cam.x = max(-(self.world_size.width-SCREEN_SIZE.width), min(0, self.cam.x))
                self.cam.y = max(-(self.world_size.height-SCREEN_SIZE.height), min(0, self.cam.y))
    
        def draw(self, surface):
            spritedict = self.spritedict
            surface_blit = surface.blit
            dirty = self.lostsprites
            self.lostsprites = []
            dirty_append = dirty.append
            init_rect = self._init_rect
            for spr in self.sprites():
                rec = spritedict[spr]
                newrect = surface_blit(spr.image, spr.rect.move(self.cam))
                if rec is init_rect:
                    dirty_append(newrect)
                else:
                    if newrect.colliderect(rec):
                        dirty_append(newrect.union(rec))
                    else:
                        dirty_append(newrect)
                        dirty_append(rec)
                spritedict[spr] = newrect
            return dirty            
    
    def main():
        pygame.init()
        screen = pygame.display.set_mode(SCREEN_SIZE.size)
        pygame.display.set_caption("Use arrows to move!")
        timer = pygame.time.Clock()
    
        level = [
            "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP",
            "P                                          P",
            "P                                          P",
            "P                                          P",
            "P                    PPPPPPPPPPP           P",
            "P                                          P",
            "P                                          P",
            "P                                          P",
            "P    PPPPPPPP                              P",
            "P                                          P",
            "P                          PPPPPPP         P",
            "P                 PPPPPP                   P",
            "P                                          P",
            "P         PPPPPPP                          P",
            "P                                          P",
            "P                     PPPPPP               P",
            "P                                          P",
            "P   PPPPPPPPPPP                            P",
            "P                                          P",
            "P                 PPPPPPPPPPP              P",
            "P                                          P",
            "P                                          P",
            "P                                          P",
            "P                                          P",
            "PPPPPPPPPSPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP",]
    
    
        platforms = pygame.sprite.Group()
        player = Player(platforms, (TILE_SIZE, TILE_SIZE))
        level_width  = len(level[0])*TILE_SIZE
        level_height = len(level)*TILE_SIZE
        entities = CameraAwareLayeredUpdates(player, pygame.Rect(0, 0, level_width, level_height))
    
        # build the level
        x = y = 0
        for row in level:
            for col in row:
                if col == "P":
                    Platform((x, y), entities, platforms)
                if col == "S":
                    SpeechBlock((x, y), entities, platforms)
                x += TILE_SIZE
            y += TILE_SIZE
            x = 0
    
        dt = 0
    
        while 1:
            events = pygame.event.get()
            for e in events:
                if e.type == QUIT: 
                    return
                if e.type == KEYDOWN and e.key == K_ESCAPE:
                    return
    
            entities.update(dt, events)
            screen.fill((0, 0, 0))
            entities.draw(screen)
            pygame.display.update()
            dt = timer.tick(60)
    
    class Entity(pygame.sprite.Sprite):
        def __init__(self, color, pos, *groups):
            super().__init__(*groups)
            self.image = Surface((TILE_SIZE, TILE_SIZE))
            self.image.fill(color)
            self.rect = self.image.get_rect(topleft=pos)
    
        def update(self, dt, events):
            pass
    
    class Player(Entity):
        def __init__(self, platforms, pos, *groups):
            super().__init__(Color("#0000FF"), pos)
            self.vel = pygame.Vector2((0, 0))
            self.onGround = False
            self.platforms = platforms
            self.speed = 8
            self.jump_strength = 10
    
        def update(self, dt, events):
            pressed = pygame.key.get_pressed()
            up = pressed[K_UP]
            left = pressed[K_LEFT]
            right = pressed[K_RIGHT]
            running = pressed[K_SPACE]
    
            if up:
                # only jump if on the ground
                if self.onGround: self.vel.y = -self.jump_strength
            if left:
                self.vel.x = -self.speed
            if right:
                self.vel.x = self.speed
            if running:
                self.vel.x *= 1.5
            if not self.onGround:
                # only accelerate with gravity if in the air
                self.vel += GRAVITY
                # max falling speed
                if self.vel.y > 100: self.vel.y = 100
    
            if not(left or right):
                self.vel.x = 0
            # increment in x direction
            self.rect.left += self.vel.x * dt/10.
            # do x-axis collisions
            self.collide(self.vel.x, 0, self.platforms)
            # increment in y direction
            self.rect.top += self.vel.y * dt/10.
            # assuming we're in the air
            self.onGround = False;
            # do y-axis collisions
            self.collide(0, self.vel.y, self.platforms)
    
        def collide(self, xvel, yvel, platforms):
            for p in platforms:
                if pygame.sprite.collide_rect(self, p):
                    if isinstance(p, SpeechBlock):
                        p.trigger()
                    if xvel > 0:
                        self.rect.right = p.rect.left
                    if xvel < 0:
                        self.rect.left = p.rect.right
                    if yvel > 0:
                        self.rect.bottom = p.rect.top
                        self.onGround = True
                        self.yvel = 0
                    if yvel < 0:
                        self.rect.top = p.rect.bottom
    
    class Platform(Entity):
        def __init__(self, pos, *groups):
            super().__init__(Color("#DDDDDD"), pos, *groups)
    
    class TextBlock(pygame.sprite.Sprite):
        def __init__(self, pos, text, *groups):
            super().__init__(*groups)
            self.image = Surface((200, 100))
            self.rect = self.image.get_rect(center=pos)
            self.image.fill(Color("white"))
            # todo: don't load font everytime
            self.image.blit(pygame.font.SysFont(None, 32).render(text, True, Color("Black")), (10, 10))
    
    class SpeechBlock(Entity):
        def __init__(self, pos, *groups):
            super().__init__(Color("orange"), pos, *groups)
            self.cooldown = 0
            self.text = None
    
        def trigger(self):
            # do nothing if already triggered
            if self.cooldown: return
    
            self.cooldown = 2000
            # first group is the entity group
            self.text = TextBlock(self.rect.move(0, -150).center, 'Ouch!', self.groups()[0])
    
        def update(self, dt, events):
    
            if self.cooldown:
                self.cooldown -= dt
                if self.cooldown <= 0:
                    self.text.kill()
                    self.text = None
                    self.cooldown = 0
    
    if __name__ == "__main__":
        main()
    

    Here you see that we create a new Entity that displays the text when the player steps in the SpeechBlock. After two seconds, the SpeechBlock calls kill to remove it from the game.

    I also updated the code a little bit to use delta time so we can easily check if 2 seconds have passed.

    enter image description here