Pygame显示屏上的矩形碰撞;在碰撞后按下第二个键会导致矩形跳跃。

3
我在解决这个碰撞问题时遇到了一些困难。如果我只按一个键,碰撞似乎能够正常工作,但是如果我按下一个键并继续按着,同时发生碰撞,然后再按下另一个键,碰撞似乎会同时考虑两个键。从研究中得知,我需要进行分别的轴计算,但是我不确定如何使用我目前的碰撞算法来实现。如果可能的话,我希望这个解决方案以程序化方式呈现。如果有人可以修改我的代码,提供一个可行的程序化解决方案,我将非常感激。谢谢。
import pygame as pg
import sys
from math import fabs

pg.init()

width = 600
height = 600

gameDisplay = pg.display.set_mode((width, height))
pg.display.set_caption('Block')


white = (255, 255, 255)
red = (255, 0, 0)

clock = pg.time.Clock()
closed = False
FPS = 60
Player_Speed = 200
x, y = 270, 0
vx = 0
vy = 0
collision = False

def Collision(hero, enemy):
    global vx, vy, x, y, collision
    deltay = fabs(block.centery - ENEMY.centery)
    deltax = fabs(block.centerx - ENEMY.centerx)
    if deltay < ENEMY.height and deltax < ENEMY.width:
        collision = True
        if vx > 0:
            vx = 0
            x = ENEMY[0] - block[2] 
        if vx < 0:
            vx = 0
            x = ENEMY[0] + 30
        if vy > 0:
            vy = 0
            y = ENEMY[1] - block[3] 
        if vy < 0:
            vy = 0
            y = ENEMY[1] + 30
    else:
        collision = False


def xy_Text(x, y):
    font = pg.font.SysFont("Courier", 16, True)
    text = font.render("X: " + str(round(x)), True, (0,150,0))
    text1 = font.render("Y: " + str(round(y)), True, (0,150,0))
    gameDisplay.blit(text, (0,0))
    gameDisplay.blit(text1, (0,14))

while not closed:
    for event in pg.event.get():
        if event.type == pg.QUIT:
            closed = True

        dt = clock.tick(FPS)/1000
        vx, vy = 0, 0

        keys = pg.key.get_pressed()

        if keys[pg.K_ESCAPE]:
            closed = True
        if keys[pg.K_LEFT] or keys[pg.K_a]:
            vx = -Player_Speed
        if keys[pg.K_RIGHT] or keys[pg.K_d]:
            vx = Player_Speed
        if keys[pg.K_UP] or keys[pg.K_w]:
            vy = -Player_Speed
        if keys[pg.K_DOWN] or keys[pg.K_s]:
            vy = Player_Speed
        if vx != 0 and vy != 0:
            vx *= 0.7071
            vy *= 0.7071


    gameDisplay.fill(white)
    ENEMY = pg.draw.rect(gameDisplay, red, (270, 270, 30, 30))
    block = pg.draw.rect(gameDisplay, (0, 150, 0), (x, y, 30, 30))
    xy_Text(x, y)
    x += vx * dt
    y += vy * dt
    Collision(block, ENEMY)
    pg.display.update()
    clock.tick(FPS)

pg.quit()
sys.exit()

碰撞处理很困难。我建议您不要试图重新发明轮子,而是使用现有的库,如Box2d或Pymunk。 - Omni
我知道我可以使用现有的库,甚至pygame也有碰撞处理功能。但出于教育目的,我正在"重新发明轮子"。我想要理解一切是如何工作的。不过还是谢谢你的建议。 - Tophiero
这是一个很好的动机。例如,在基于力的方法中,您可以通过创建辅助力来将碰撞的对象移开来处理碰撞。按下的键数可能对这种模型无关紧要。另一方面,这在您的问题中扮演了重要角色,从我的角度来看,这意味着您需要更多地研究此类模拟如何工作。 - Omni
使用Python和Pygame编写游戏 - 实验16:Pygame平台游戏示例。其中一个示例中,您可以看到:1.仅垂直移动,2.检查所有碰撞,3.仅水平移动,4.再次检查所有碰撞。 - furas
我猜你想处理与墙壁的碰撞,但我不确定,因为你把一个矩形称为“ENEMY”。 - skrx
2个回答

1

经过许多更改,碰撞已经生效。

我移动了

x += vx * dt
y += vy * dt

碰撞检测。

我在代码中更改了组织结构,并始终使用Rect()来保持玩家和敌人的位置和大小。

我还添加了第二个敌人来测试如何检查与多个元素的碰撞。

import pygame as pg
import sys
from math import fabs

# - functions --- (lower_case_names)

def check_collision(player, enemy1, enemy2):
    global player_vx, player_vy

    # --- X ---

    player.x += player_vx * dt

    # enemy 1

    deltay = fabs(player.centery - enemy1.centery)
    deltax = fabs(player.centerx - enemy1.centerx)

    if deltay < enemy1.height and deltax < enemy1.width:
        if player_vx > 0:
            player_vx = 0
            player.x = enemy1.x - player.w
        elif player_vx < 0:
            player_vx = 0
            player.x = enemy1.x + player.w

    # enemy 2

    deltay = fabs(player.centery - enemy2.centery)
    deltax = fabs(player.centerx - enemy2.centerx)

    if deltay < enemy2.height and deltax < enemy2.width:
        if player_vx > 0:
            player_vx = 0
            player.x = enemy2.x - player.w
        elif player_vx < 0:
            player_vx = 0
            player.x = enemy2.x + player.w

    # --- Y ---

    player.y += player_vy * dt

    # enemy 1

    deltay = fabs(player.centery - enemy1.centery)
    deltax = fabs(player.centerx - enemy1.centerx)

    if deltay < enemy1.height and deltax < enemy1.width:
        if player_vy > 0:
            player_vy = 0
            player.y = enemy1.y - player.h
        elif player_vy < 0:
            player_vy = 0
            player.y = enemy1.y + player.w

    # enemy 2

    deltay = fabs(player.centery - enemy2.centery)
    deltax = fabs(player.centerx - enemy2.centerx)

    if deltay < enemy2.height and deltax < enemy2.width:
        if player_vy > 0:
            player_vy = 0
            player.y = enemy2.y - player.h
        elif player_vy < 0:
            player_vy = 0
            player.y = enemy2.y + player.w


def xy_text(screen, x, y):
    font = pg.font.SysFont("Courier", 16, True)

    text = font.render("X: " + str(round(x)), True, (0,150,0))
    screen.blit(text, (0,0))

    text = font.render("Y: " + str(round(y)), True, (0,150,0))
    screen.blit(text, (0,14))

# --- constants --- (UPPER_CASE_NAMES)

WIDTH = 600
HEIGHT = 600

WHITE = (255, 255, 255)
RED = (255, 0, 0)

FPS = 60

# --- main --- (lower_case_names)

player_speed = 200
player_vx = 0
player_vy = 0

player = pg.Rect(270, 0, 30, 30)
enemy1 = pg.Rect(270, 270, 30, 30)
enemy2 = pg.Rect(240, 300, 30, 30)

# - init -
pg.init()

game_display = pg.display.set_mode((WIDTH, HEIGHT))
pg.display.set_caption('Block')

# - mainloop -

clock = pg.time.Clock()

closed = False
while not closed:

    dt = clock.tick(FPS)/1000

    # - events -

    for event in pg.event.get():
        if event.type == pg.QUIT:
            closed = True

    keys = pg.key.get_pressed()

    player_vx = 0
    player_vy = 0

    if keys[pg.K_ESCAPE]:
        closed = True
    if keys[pg.K_LEFT] or keys[pg.K_a]:
        player_vx = -player_speed
    if keys[pg.K_RIGHT] or keys[pg.K_d]:
        player_vx = player_speed
    if keys[pg.K_UP] or keys[pg.K_w]:
        player_vy = -player_speed
    if keys[pg.K_DOWN] or keys[pg.K_s]:
        player_vy = player_speed
    if player_vx != 0 and player_vy != 0:
        player_vx *= 0.7071
        player_vy *= 0.7071

    # - updates -

    check_collision(player, enemy1, enemy2)

    # - draws -

    game_display.fill(WHITE)
    pg.draw.rect(game_display, RED, enemy1)
    pg.draw.rect(game_display, RED, enemy2)
    pg.draw.rect(game_display, (0, 150, 0), player)
    xy_text(game_display, player.x, player.y)
    pg.display.update()
    clock.tick(FPS)

# - end -

pg.quit()
sys.exit()

感谢您的修改和建议。我喜欢您用注释将所有内容分开的方式。我会开始这样做。我发现这段代码唯一的问题是它减慢了移动速度,不够流畅。但碰撞确实有效。 - Tophiero
有什么办法可以让这个代码与平滑移动一起工作吗? - Tophiero
问题是两个“帧率(FPS)”非常接近 - 但不是用于绘制元素的时间,因此“dt”提供了两个“帧率(FPS)”之间的时间。现在代码运行更快。 - furas
如果你有很多对象需要检查碰撞,那么这可能会成为一个问题。有一种方法叫做“空间划分”(“Spatial Partition”),它可以让你只检查最近的对象是否发生碰撞。你可以参考以下链接了解更多:youtube: 2D grid spatial partitioning basics (speed up collision broad-phase),或者 Game Programming Patterns: Spatial Partition - furas

1
如果您想处理与墙壁的碰撞,请首先沿着x或y轴移动,如果玩家与墙壁发生碰撞,则将其设置回来,然后对另一个轴执行相同操作。 如果使用pygame.Rect,则可以使用其leftrighttopbottom属性轻松将玩家设置回块的相应侧面。
因此,检查玩家是否与块发生碰撞,如果向右移动(vx > 0),则将hero.right设置为block.left,并对其他方向执行相同操作。 如果更新了玩家矩形,则还必须返回新的x和y坐标。
我建议将块(矩形)放入blocks列表中,您可以将其传递给墙壁碰撞函数,然后只需使用for循环即可。
from math import fabs
import pygame as pg


pg.init()

width = 600
height = 600

gameDisplay = pg.display.set_mode((width, height))

white = (255, 255, 255)
red = (255, 0, 0)

clock = pg.time.Clock()
closed = False
FPS = 60
Player_Speed = 200
x, y = 270, 0
vx = 0
vy = 0
# Define the font in the global scope.
FONT = pg.font.SysFont("Courier", 16, True)


# Better don't use global variables.
def handle_horizontal_collisions(hero, blocks, x, vx):
    """Sets the player back to the left or right side."""
    for block in blocks:
        if hero.colliderect(block):
            if vx > 0:
                hero.right = block.left
            elif vx < 0:
                hero.left = block.right
            return hero.x  # Need to update the actual `x` position.
    return x


def handle_vertical_collisions(hero, blocks, y, vy):
    """Sets the player back to the top or bottom side."""
    for block in blocks:
        if hero.colliderect(block):
            if vy > 0:
                hero.bottom = block.top
            elif vy < 0:
                hero.top = block.bottom
            return hero.y  # Need to update the actual `y` position.
    return y


def xy_Text(x, y):
    text = FONT.render("X: " + str(round(x)), True, (0,150,0))
    text1 = FONT.render("Y: " + str(round(y)), True, (0,150,0))
    gameDisplay.blit(text, (0,0))
    gameDisplay.blit(text1, (0,14))


# Use pygame.Rects for the player and blocks.
player = pg.Rect(x, y, 30, 30)
# Put the blocks into a list.
blocks = []
for rect in (pg.Rect(200, 200, 60, 30), pg.Rect(230, 230, 60, 30)):
    blocks.append(rect)

dt = 0

while not closed:
    for event in pg.event.get():
        if event.type == pg.QUIT:
            closed = True
        # clock.tick should not be called in the event loop.

    vx, vy = 0, 0
    # key.get_pressed should also not be in the event loop.
    keys = pg.key.get_pressed()

    if keys[pg.K_ESCAPE]:
        closed = True
    if keys[pg.K_LEFT] or keys[pg.K_a]:
        vx = -Player_Speed
    if keys[pg.K_RIGHT] or keys[pg.K_d]:
        vx = Player_Speed
    if keys[pg.K_UP] or keys[pg.K_w]:
        vy = -Player_Speed
    if keys[pg.K_DOWN] or keys[pg.K_s]:
        vy = Player_Speed
    if vx != 0 and vy != 0:
        vx *= 0.7071
        vy *= 0.7071

    # Game logic.
    x += vx * dt
    player.x = x
    x = handle_horizontal_collisions(player, blocks, x, vx)

    y += vy * dt
    player.y = y
    y = handle_vertical_collisions(player, blocks, y, vy)

    # Rendering.
    gameDisplay.fill(white)
    for block in blocks:
        pg.draw.rect(gameDisplay, red, block)
    pg.draw.rect(gameDisplay, (0, 150, 0), player)

    xy_Text(x, y)
    pg.display.update()
    dt = clock.tick(FPS)/1000

pg.quit()

这是最好的答案。它完全符合我的要求,甚至超出了一些,并且没有牺牲流畅运动。像上面的那个人一样,您也可以使用注释来分隔不同的代码部分。我也会开始这样做;它使代码看起来更整洁和有组织。感谢您的时间、努力和建议。 - Tophiero

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接