Pygame How Do I Make Slopes?

Viewed 348

VIDEO I have a basic player moving across my screen and I have a displayed image that I want to use for the slope when ever my player is on the image it is able to move the way the image is but I am having trouble with that because I am using a rectangle and I am not sure how that will work. is there a way I could make my player move like the slope image instead of just moving straight

my code right now I just made it when the player colides with my slope rects to move forward but how could I improve it to look like its moving with the image slope??

import pygame
pygame.init()


# make our screen for our game
window = pygame.display.set_mode((700,800))

# name our game
pygame.display.set_caption("Our Space Game")


class player:
    def __init__(self,x,y,height,width,color):
        self.x = x
        self.y = y
        self.height = height
        self.width = width
        self.color = color

        # make our player jump
        self.isJump = False
        self.JumpCount = 10

        self.fall = 0


        self.rect = pygame.Rect(x,y,height,width)

                
    def draw(self):
        self.rect.topleft = (self.x,self.y)
        pygame.draw.rect(window,self.color,self.rect)

white = (255,255,255)
playerman = player(200,200,10,10,white)







class slope():
    def __init__(self,x,y,height,width,color):
        self.x = x
        self.y = y
        self.height = height
        self.width = width
        self.color  = color
        self.image = pygame.image.load("hills1.png")
        self.rect = pygame.Rect(x,y,height,width)
    def draw(self):
        self.rect.topleft = (self.x,self.y)
        pygame.draw.rect(window,self.color,self.rect)
        window.blit(self.image,self.rect)




slopes = []

platformGroup = pygame.sprite.Group

level = [
"                                                                  ",
"                                                                  ",
"                                                                  ",
"                                                                  ",
"                                                                  ",
"                                                                  ",
"                                                                  ",
"   c                                                              "





]
                                                                                    
for iy, row in enumerate(level):
    for ix, col in enumerate(row):
        if col == "c":
            new_platforms = slope(ix*9.8, iy*50, 520,220,(23, 32, 42))
            slopes.append(new_platforms)
        
        


# our game fps
fps = 40
clock = pygame.time.Clock()


def redraw():
    window.fill((0,0,0))
    playerman.draw()

    for slope in slopes:
        slope.draw()


# our game main loop
runninggame = True
while runninggame:
    clock.tick(fps)
    for event in pygame.event.get():
        if event.type == pygame.quit:
            runninggame = False



    keys = pygame.key.get_pressed()
    for slope in slopes:
        if playerman.rect.colliderect(slope.rect):
            playerman.x += 7


    collide = False
    if not playerman.isJump:                    
        playerman.y += playerman.fall
        playerman.fall += 1
        playerman.isJump = False
        collide = False


            # collisions
        for slope in slopes:
            if playerman.rect.colliderect(slope.rect):
                collide = True
                playerman.isJump = False
                playerman.y = slope.rect.top - playerman.height + 1
                if playerman.rect.right > slope.rect.left and playerman.rect.left < slope.rect.left - playerman.width:
                    playerman.x = slope.rect.left - playerman.width
                if playerman.rect.left < slope.rect.right and playerman.rect.right > slope.rect.right + playerman.width:
                    playerman.x = slope.rect.right


        if playerman.rect.bottom >= 890:
            collide = True
            playerman.isJump = False
            playerman.JumpCount = 10
            playerman.y = 890 - playerman.height



        if collide:
            if keys[pygame.K_SPACE]:
                playerman.isJump = True
                        
            playerman.fall = 0




    else:
        if playerman.JumpCount > 0:
            playerman.y -= (playerman.JumpCount*abs(playerman.JumpCount)) * 0.3
            playerman.JumpCount -= 1
        else:
            playerman.JumpCount = 10
            playerman.isJump = False


    

    

            

    redraw()
    pygame.display.update()

pygame.quit()

my sprites

enter image description here

1 Answers

It's totally possible, easy even.

I loaded the "slope" or ground image and converted it into a numpy array. After that, I found the surface pixels. I used a simple condition to determine if a given pixel should be solid ground of 'air': if any of the 4 channels of the pixel (= Red, Green, Blue or Alpha) is greater than zero, that pixel is considered solid ground. If the alpha channel (the last one) is above some threshold, for example 0 or 20, the pixel is considered as a block of solid ground. To test it, I made a super simple sample "game" using pygame. The game loads a png image (for example, the one you showed) and converts it.

This solution uses the same image for both collision detection and visuals. It might be a good idea to add a separate mask layer for collisions instead of using the same file. If we wanted to use the same file, we could change the contrast and whatnot to make the picture black and white, then apply some sort of edge detection to the picture.

Demo

Here is a gif of it in action

Here is the source

How does it work?

Let's say we have an image like this one and we want to use that as a slope. It is 8 pixels wide and 5 pixels high. I left a white pixel there on purpose since it doesn't really matter what is below the surface.

Our demo bitmap

After we have loaded the bitmap, it looks like this (we have 2 channels for demonstration, it doesn't really matter what they are - could be, for example, intensity and alpha. In reality there would be 4, for RGBA but for the sake of demonstration, I'll go with 2):

>>> from PIL import Image
>>> import numpy as np

>>> bitmap_original = Image.open("hills1.png")
>>> bitmap = np.array(bitmap_original)

array([[ [0,0], [0,0], [0,0], [0,0], [0,0], [0,0], [0,0], [0,0] ],
       [ [0,0], [0,0], [0,0], [0,0], [0,0], [0,0], [0,0], [0,1] ],
       [ [0,0], [0,1], [0,1], [0,0], [0,0], [0,1], [0,1], [0,1] ],
       [ [0,1], [0,1], [0,1], [0,1], [0,1], [0,1], [0,1], [0,1] ],
       [ [0,1], [0,1], [0,1], [0,1], [0,1], [0,1], [0,0], [0,1] ]])

I used Image from PIL to load the image, it should be possible to do with pygame as well.

Let's turn it into 2d array containing only truth values. Here we are only concerned of the last value (which is the alpha channel in pngs).

>>> bitmap = bitmap[:,:,-1] > 0

array([[False, False, False, False, False, False, False, False],
       [False, False, False, False, False, False, False,  True],
       [False,  True,  True, False, False,  True,  True,  True],
       [ True,  True,  True,  True,  True,  True,  True,  True],
       [ True,  True,  True,  True,  True,  True, False,  True]])

Then, to find first "True" from each column, we can transpose the array and go through each vector in for loop (there are also faster ways to do this...) and when we find the first True, we save the location to the heatmap and go to the next column.

>>> bitmap_transposed = bitmap_2d.T


array([[False, False, False,  True,  True],
       [False, False,  True,  True,  True],
       [False, False,  True,  True,  True],
       [False, False, False,  True,  True],
       [False, False, False,  True,  True],
       [False, False,  True,  True,  True],
       [False, False,  True,  True, False],
       [False,  True,  True,  True,  True]])

Now the array is ready and we can easily pick the y values for each x.

for x, col in enumerate(bitmap_transposed):
    for y, is_ground in enumerate(col): # 
    if is_ground:
        self.height_map[x] = y

I implemented it like this in the sample game:

from PIL import Image
import pygame as pg
import numpy as np

# Other code...


# This is called once during startup, possibly within a 'Game' class.
def img_to_map(self):

    bitmap = np.array(self.bitmap)

    # For each pixel: [0,0,0,0] => False, otherwise True
    # bitmap = np.any(bitmap, axis=2)

    # last element of each pixel value (= alpha) > 20 => True
    bitmap = bitmap[:,:,-1] > 20


    # initialize an empty array for the final height map
    self.height_map = np.zeros(bitmap.shape[1], dtype=int)
    for x, col in enumerate(bitmap.T):      # bitmap's transpose
        for y, is_ground in enumerate(col):

            # First ground pixel found (=surface)! Save the location
            # to the 'height map' and move to the next column.
            if is_ground:
                self.height_map[x] = int(y)
                break

# Other code...

# This method can be used to get the surface height in given x coordinate,
# which can then be used to detect collisions with
# the ground, move a player vertically or whatever...
def height_at(self, x):
    """
        Returns the ground height at given x coordinate.
        Principle:
            1. Check that the given coordinate lays somewhere within the ground.
            2. The corresponding height value is available in the height map
               that is generated during startup.
    """
    # self.position is an object that holds the ground offset position
    x_idx = x - self.position.x

    # object is outside the slope, cannot collide
    if x_idx < 0 or x_idx > (len(self.height_map) - 1):
        return 0

    # get the height value from the height map and add the ground offset value
    return self.position.y + self.height_map[x_idx]

# Other code...

Related