Pygame Platformer

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Henri.py
User
Beiträge: 20
Registriert: Sonntag 28. März 2021, 15:33

HI, ich wollte nach langer Zeit mal endlich einen vernünftigen Platformer in Pygame programmieren. Stoße da jetzt aber auf ein Problem. Der PLayer ist nähmlich in der Lage irgendwie in Blöcke zu clippen. Das passiert, wenn dieser von unten oder oben in eine der Ecken hinein springt. Das ganze zu reproduzieren ist ein bisschen kniffelig, aber vielleicht weiß jemand ja wie man das fixen kann.

HIer einmal ein Video davon (am besten ranzoomen und langsamer abspielen, damit mans sieht)
https://easyupload.io/d1sjsq

Und hier der Code:

Code: Alles auswählen

import pygame
import sys

pygame.init()
Clock = pygame.time.Clock()
FPS = 60
size = [1000, 800]
bg = [255, 255, 255]
screen = pygame.display.set_mode(size)
pygame.display.set_caption('classes in pygame')


class Obstacle:
    def __init__(self, x, y, width, height):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.rect = pygame.Rect(self.x, self.y, self.width, self.height)


class Player:
    def __init__(self, x, y, width, height, speed, level):
        self.speed = speed
        self.jumping_speed = 16
        self.vel = 0  # velocity
        self.vel_y = 0  # jumping velocity
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.IsJump = False
        self.falling_speed = 0

        self.collision = [False, False]  # x, y
        self.on_bottom = False

        self.level = level  # getting all data about obstacles

    def HandleInput(self):
        k = pygame.key.get_pressed()

        if k[pygame.K_LEFT]:
            self.vel = -self.speed
        elif k[pygame.K_RIGHT]:
            self.vel = +self.speed
        if k[pygame.K_UP] and self.jumping_speed == 16 and self.on_bottom:
            self.IsJump = True

    def jump(self):
        if self.IsJump:  # jump
            self.falling_speed = 0
            self.on_bottom = False
            if self.jumping_speed >= 0:
                self.vel_y = -self.jumping_speed
                self.jumping_speed -= 1
            else:
                self.IsJump = False
                self.jumping_speed = 16
    
    def gravity(self):
        # applying gravity
        if self.falling_speed <= 16 and not self.IsJump and not self.on_bottom:
            self.falling_speed += 0.5
        if self.on_bottom:
            self.falling_speed = 0
        self.vel_y += self.falling_speed
    
    def DetectCollision(self):
        # collision detection
        test_rect_x = pygame.Rect(self.x + self.vel, self.y, self.width, self.height)
        test_rect_y = pygame.Rect(self.x, self.y + self.vel_y, self.width, self.height)
        self.collision = [False, False]  # x, y
        self.on_bottom = False

        for block in level:
            if test_rect_x.colliderect(block.rect):  # checke ob es eine self.collision auf der x-achse gibt
                self.collision[0] = True
                if self.vel > 0:  # setzte player an block den er berührt um abstand zu verhindern
                    self.x = block.rect.left - self.width
                elif self.vel < 0:
                    self.x = block.rect.right

            if test_rect_y.colliderect(block.rect):  # checke ob es eine self.collision auf der y-achse gibt
                self.collision[1] = True
                if self.vel_y < 0:
                    self.y = block.rect.bottom
                    self.IsJump = False  # resete jump um an der decke fliegen zu verhindern
                    self.jumping_speed = 16
                elif self.vel_y > 0:
                    self.on_bottom = True
                    self.y = block.rect.top - self.height

    def update(self):
        self.HandleInput()
        self.jump()
        self.gravity()
        self.DetectCollision()

        # wenn es eine colision gibt:
        if self.collision[0]:
            # verhindere movement
            self.vel = 0
        if self.collision[1]:
            self.vel_y = 0

        # verändere player cords je nach velocity
        self.x += self.vel
        self.y += self.vel_y
        self.vel = 0
        self.vel_y = 0

    def draw(self):
        for block in level:
            pygame.draw.rect(screen, (0, 0, 0), block.rect)
        pygame.draw.rect(screen, (231, 111, 81), (self.x, self.y, self.width, self.height))


level = []
for i in range(int(size[0] / 10)):
    level.append(Obstacle(i * 10, size[1] - 50, 10, 10))
level.append(Obstacle(100,size[1] - 100, 100, 20))
level.append(Obstacle(300,size[1] - 150, 70, 20))
level.append(Obstacle(500,size[1] - 150, 100, 20))
level.append(Obstacle(700,size[1] - 200, 70, 20))
level.append(Obstacle(900,size[1] - 250, 100, 20))

player = Player(500, 600, 20, 40, 4, level)


while True:

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()

    player.update()
    player.draw()

    Clock.tick(FPS)
    pygame.display.update()
    screen.fill(bg)
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Da sind diverse Namenskonventionen verletzt, und das der Player fast alles weiß und macht, ist ein Codesmell.

Aber zu deinem eigentlichen Problem: das Verhalten ist ja zu erwarten, weil du nun mal Geschwindigkeiten größer 1 zulässt. Damit kann natürlich der Fall eintreten, das zwischen zwei Zeitschritten ein Objekt in das andere tiefer eindringt. Das problem haben alle Physik-basierten Ansätze auch.

Es gibt grundsätzlich zwei Weg, das zu lösen: Einzelschrittsimulation, und Backtracking. Ersteres besteht darin, statt einer zeitlich festen Auflösung eine räumlich feste Auflösung zu nutzen. Oder wenigstens eine so kleine zeitliche, dass man nie mehr als ein Pixel Unterschied hat. Du bewegst dich also immer nur in Pixelschritten. Das ist relativ einfach zu programmieren, aber teuer, weil du ja im Grunde eine viel höhere Frame Rate (zumindest in der Physik) berechnest. Beim Backtracking hingegen berechnet man bei einer Kollision rückwärts den Punkt, an dem man tatsächlich zuerst kollidiert ist. Das ist preiswerter, aber der mathematische Aufwand etwas höher, vor allem, wenn es präzise sein soll. Zum einen, weil man dabei ggf die nicht-lineare Flugbahn berücksichtigen will. Aber auch, weil ja die Konsequenz der Kollision - das abprallen - schon innerhalb des Zeitschrittes begonnen hat. Vorteil ist aber, dass man mittels binärer Suche recht schnell zum Kollsionszeitpunkt findet.

Musst dir also überlegen, was es sein soll. Am Schnellsten ist sicher einfach eine 5-10 fache Physik Frame Rate umzusetzen.

Noch spannender wird’s, wenn man Objekte hat, die in einem Zeitschritt ein anderes Objekt komplett durchfliegen können - Schüsse, zb. Da gibt’s dann noch wieder andere Ansätze.
Henri.py
User
Beiträge: 20
Registriert: Sonntag 28. März 2021, 15:33

mit dem Verletzten der Namenskoventionen meinst du wahrscheinlich DetectCollision() und HandleInput(), hab das kurs umbenannt.
Du hast natürlich auch mit dem Codesmell recht. In der Player Class wird sogar das Level gezeichnet.
Da ich die Geschwindigkeit/FPS so variabel wie möglich halten möchte, glaube ich das Backtracking am klugsten wäre. Hast du da einen Ansatz? Und müsste nicht eigentlich in der detect_collision() der Zusammenprall vorzeitig erkannt und dann auch verhindert werden?
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Der simple Ansatz ist lineares bisecten. Also bei Kollision den Punkt zwischen der letzen Position und der jetzigen trivial mit Vektoren berechnen. Den Testen. Und je nach dem in der entsprechenden Halblinie weiter testen. Wenn man keine neue Koordinate mehr berechnet dabei (weil gerundet immer das gleiche Pixel dabei rumkommt), ist man am Ziel.
Henri.py
User
Beiträge: 20
Registriert: Sonntag 28. März 2021, 15:33

ich habs jetzt gefixt. Ich habe für die collision erkennen zwei test-player erstellt. 1 für x und 1 für y richtung. wenn jetzt beispielsweise bei test_rect_x die x position verändert wird, bleibt diese jedoch in test_rect_y wie sie zuvor war. und test_rect_y wird deswegen auf stand des alten rects angepasst. wodurch es zu dem glitchen kommt. einfach das hier:

Code: Alles auswählen

if self.vel > 0:  # setzte player an block den er berührt um abstand zu verhindern
    self.x = block.rect.left - self.width
elif self.vel < 0:
    self.x = block.rect.right
zu dem hier ändern:

Code: Alles auswählen

if self.vel > 0:  # setzte player an block den er berührt um abstand zu verhindern
    self.x = block.rect.left - self.width
    test_rect_y.x = block.rect.left - self.width
elif self.vel < 0:
    self.x = block.rect.right
    test_rect_y.x = block.rect.right
Antworten