Pygame: How to map png texture 1-1 to rectangle

87 Views Asked by At

I'm new to Pygame and I'm trying to create a flappy bird clone for my first Pygame project. I have a decent starting point but I'm running into an issue when I try to add a texture to a rectangle. I have two rectangles representing the top and bottom pipes. When I leave them as just rectangles the size of the rectangles is properly fitted how I want them. However, when I try to add the pipe texture to the rectangle, they texture doesn't properly map to the rectangle. My code is:

import pygame
import random

pygame.init()

screen = pygame.display.set_mode((250,500))
clock = pygame.time.Clock()
run = True

background = pygame.image.load('graphics/background-day.png')
ground = pygame.image.load('graphics/base.png')

birdRec = pygame.Rect(0, 0, 30, 24)
birdRec.center = (100,300)


bottomPipeHeight = random.randrange(50, 350)

pipeRecBottom = pygame.Rect(0,0, 50, bottomPipeHeight)
pipeRecBottom.midbottom = (150, 450)


pipeRecTop = pygame.Rect(0,0,50,500)
pipeRecTop.midbottom = pipeRecBottom.midtop
pipeRecTop.y -= 100

gravity = 0


pipeBottomTexture = pygame.image.load("graphics/pipe-green.png").convert_alpha()



while run:
    #poll for events
    #pygame.QUIT event means the user clicked x
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False

    gravity += .1
    birdRec.y += gravity

    keys = pygame.key.get_pressed()
    if keys[pygame.K_w]:
        birdRec.y -= 7
        gravity = 0
    if keys[pygame.K_s]:
        birdRec.y += 2
    if keys[pygame.K_d]:
        birdRec.x += 2
    if keys[pygame.K_a]:
        birdRec.x -= 2

    pipeRecBottom.x -= 1
    pipeRecTop.x -= 1

    if(birdRec.colliderect(pipeRecTop) or birdRec.colliderect(pipeRecBottom)):
        print("Collision")
        

    # RENDER GAME
    screen.blit(background,(0,0))
    screen.blit(ground,(0,450))

    #PROBLOMATIC AREA BEGIN

    pygame.draw.rect(screen, "Black", pipeRecBottom)  
    #screen.blit(pipeBottomTexture, pipeRecBottom)

    #PROBLOMATIC AREA END

    pygame.draw.rect(screen, "Green", pipeRecTop)     
    pygame.draw.rect(screen, "Red", birdRec)

    if pipeRecBottom.right < -30: 
        pipeRecBottom.height = random.randrange(50,350)
        pipeRecBottom.bottomleft = (280,450)
    
    if pipeRecTop.right < -30: 
        pipeRecTop.midbottom = pipeRecBottom.midtop
        pipeRecTop.y -= 100



    pygame.display.flip()
    

    clock.tick(60)

pygame.quit()

I tried to initially comment out the first line in the problematic area and simply write the second line. My thought process was that I have the rectangle location calculated so I just want to slap the texture on top of it.

But instead, the texture is offset from the rectangle, usually resulting in the bottom of the pipe going beneath the floor. I think the top of the bottom pipe is being mapped to the correct point, I would just like to be able to stop the texture from going into the floor.

Here's a screenshot of the correct behavior:

Here's a screenshot of the incorrect behavior, notice the pipe clipping into the floor:

The difference in height between the two is because the height is being randomly generated. If I fix the height value, the clipping issue is still there.

2

There are 2 best solutions below

0
furas On

blit has third argument to define displayed area from original image.

blit(source, dest, area=None, special_flags=0)

Doc: blit

It can crop any part of image so it needs own rect or tuple (x,y, width, height)

screen.blit(pipeBottomTexture, pipeRecBottom, (0, 0, 50, pipeRecBottom.height))

You can't use pipeRecBottom as third value because it needs x=0, y=0 to keep top of image.


BTW:

In your code other solution is to blit ground after pipes.
And this doesn't need to change height in pipe.


Full working example:

enter image description here

I use Surface to create image - so everyone can simply copy and run it.

import pygame

pygame.init()

screen = pygame.display.set_mode((550, 200))

# create image 
#pipeBottomTexture = pygame.image.load("graphics/pipe-green.png").convert_alpha()
pipeBottomTexture = pygame.Surface((50, 100))
pipeBottomTexture.fill('green')
pygame.draw.rect(pipeBottomTexture, 'black', (3, 3, 44, 94), 3)

#pipeRecBottom = pygame.Rect(0,0, 50, 100)
pipeRecBottom = pipeBottomTexture.get_rect()
pipeRecBottom.topleft = (50, 50)

# --- loop ---

clock = pygame.time.Clock()
run = True

while run:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                run = False
   
    screen.blit(pipeBottomTexture, pipeRecBottom)
    
    pos_1 = pipeRecBottom.copy()
    pos_1.x += 100    
    screen.blit(pipeBottomTexture, pos_1, (0, 0, 50, pipeRecBottom.height-50))
    
    pos_2 = pipeRecBottom.copy()
    pos_2.x += 200    
    screen.blit(pipeBottomTexture, pos_2, (0, 50, 50, pipeRecBottom.height-50))

    pos_3 = pipeRecBottom.copy()
    pos_3.x += 300    
    pos_3.y += 50
    screen.blit(pipeBottomTexture, pos_3, (0, 0, 50, pipeRecBottom.height-50))
    
    pos_4 = pipeRecBottom.copy()
    pos_4.x += 400    
    pos_4.y += 50
    screen.blit(pipeBottomTexture, pos_4, (0, 50, 50, pipeRecBottom.height-50))

    pygame.display.flip()

    clock.tick(60)

pygame.quit()
0
oBrstisf8o On

The simplest solution is to cut-out the ground from the background and save it to another image, then when rendering your pipes, do as you have in your code, but after drawing the pipes, draw the ground - the pipe will technically still be clipping through the ground, but no one will see it - the pipe image is longer than you want it to show. You randomize the length of pipes, so you could either have a set of predefined pipes with varying lengths, or one longer pipe image that you could partially cut out on runtime to mach selected pipe size. The other answer proposes exactly the second solution - the area value of pygame.Surface.blit defines the rectangle area of surface which will be drawn to the destination (a mask). This is definitely a good solution, but I have a different proposition - the image that sticks out of the window is invisible and if you draw another image onto the first one, that part of the fist image will be covered.

    # RENDER GAME
    screen.blit(background,(0,0))

    # (NO LONGER) PROBLEMATIC AREA BEGIN
    screen.blit(pipeBottomTexture, pipeRecBottom)
    # (NO LONGER) PROBLEMATIC AREA END
    
    screen.blit(ground,(0,450)) # Just move below the pipe-drawing logic

That's a lot of text for a simple solution of just moving one line of code below the other one. I hope that I didn't over-complicate the explanation.