Пересмотренная ролевая игра в подземелье с видом сверху

Вступление:

Пока что игра работает хорошо, и меня больше всего беспокоит система пуль (включая функцию, в которой вы щелкаете мышью, чтобы стрелять пулями) и система столкновений между зомби и пулями, поскольку я пробовал много разных способов решить эти две проблемы, но безуспешно. В любом случае, если бы кто-то не мог объяснить, как реализовать эти две концепции с предоставленным кодом, это было бы замечательно.

Итерационный обзор моего предыдущего вопроса.

Репозиторий GitHub: https://github.com/Zelda-DM/Dungeon-Minigame

# --- Imports ---
import pygame
import os
import random
# --- Screen Dimensions ---
SCR_WIDTH = 1020
SCR_HEIGHT = 510
# --- Colors ---
WHITE = [240, 240, 240]
# --- Game Constants ---
FPS = 60
score = 0
lives = 3
# --- Fonts ---
pygame.font.init()
TNR_FONT = pygame.font.SysFont('Times_New_Roman', 27)
# --- Player Variables ---
playerX = 120
playerY = 100
# --- Dictionaries/Lists ---
images = {}
enemy_list = []
bullets = []
# --- Classes ---
class Enemy:
    def __init__(self):
        self.x = random.randint(600, 1000)
        self.y = random.randint(8, 440)
        self.moveX = 0
        self.moveY = 0

    def move(self):
        if self.x > playerX:
            self.x -= 0.7
        
        if self.x <= 215:
            self.x = 215
            enemy_list.remove(enemy)
            for i in range(1):
                new_enemy = Enemy()
                enemy_list.append(new_enemy)

    def draw(self):
        screen.blit(images['l_zombie'], (self.x, self.y))
# --- Functions --
def load_zombies():
    for i in range(8):
        new_enemy = Enemy()
        enemy_list.append(new_enemy)
def clip(value, lower, upper):
    return min(upper, max(value, lower))
def load_images():
    path="Desktop/Files/Dungeon Minigame/"
    filenames = [f for f in os.listdir(path) if f.endswith('.png')]
    for name in filenames:
        imagename = os.path.splitext(name)[0]
        images[imagename] = pygame.image.load(os.path.join(path, name))
def main_menu():
    screen.blit(images['background'], (0, 0))
    start_button = screen.blit(images['button'], (420, 320))
    onclick = False
    while True:
        mx, my = pygame.mouse.get_pos()
        if start_button.collidepoint((mx, my)):
            if onclick:
                game()
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
            if event.type == pygame.MOUSEBUTTONDOWN:
                onclick = True
        clock.tick(FPS)
        pygame.display.update()
def game(): 

    load_zombies()

    while True:

        global playerX, playerY, score, lives, enemy

        screen.blit(images['background2'], (0, 0))
        score_text = TNR_FONT.render('Score: ' + str(score), True, WHITE)
        lives_text = TNR_FONT.render('Lives: ', True, WHITE)
        screen.blit(score_text, (20, 20))
        screen.blit(lives_text, (840, 20))
        screen.blit(images['r_knight'], (playerX, playerY))

        heart_images = ["triple_empty_heart", "single_heart", "double_heart", "triple_heart"]
        lives = clip(lives, 0, 3)
        screen.blit(images[heart_images[lives]], (920, 0))

        if lives == 0:
            main_menu()

        for enemy in enemy_list:
            enemy.move()
            enemy.draw()
            if enemy.x == 215:
                lives -= 1
    
        onpress = pygame.key.get_pressed()

        Y_change = 0
        if onpress[pygame.K_w]:
            Y_change -= 5
        if onpress[pygame.K_s]:
            Y_change += 5
        playerY += Y_change

        X_change = 0
        if onpress[pygame.K_a]:
            X_change -= 5
        if onpress[pygame.K_d]:
            X_change += 5
        playerX += X_change

        playerX = clip(playerX, -12, 100)
        playerY = clip(playerY, -15, 405)

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                quit()
            
        clock.tick(FPS)
        pygame.display.update()
# --- Main ---
pygame.init()
clock = pygame.time.Clock()
screen = pygame.display.set_mode((SCR_WIDTH, SCR_HEIGHT))
pygame.display.set_caption('Dungeon Minigame')
load_images()
main_menu()

Выход

2 ответа
2

Столкновение

А пока предположим, что ваши пули летят по прямой слева направо или справа налево.

У ваших зомби есть позиция, self.x а также self.y. Кроме того, у них также есть изображение, которое вы рисуете на экране (images['l_zombie'] в Enemy.draw() метод).

Чего я не вижу, так это упоминания об оставшихся пикселях на изображении. Может случиться так, что все ваши изображения зомби имеют одинаковый размер, но есть граница в 1, 2 или 50 пикселей «мертвого пространства» вокруг изображения.

А пока предположим, что мертвого пространства нет. Если мертвое пространство существует, вам просто нужно сделать вычитание.

Вы рисуете своих зомби с помощью Enemy.draw() метод, который использует (self.x, self.y) чтобы установить положение изображения.

Вам нужно обнаружить столкновение. Поскольку пули (мы предполагаем) движутся горизонтально, мы можем сделать несколько радикально упрощающих предположений.

  • Мы предполагаем, что есть некоторая «позиция пули».
  • Мы предполагаем, что вы можете вычислить «переднее положение пули» на основе положения пули. (Примечание: это может немного отличаться, так как положение пули всегда может быть с одной стороны от пули, но передняя часть будет меняться в зависимости от направления огня.)
  • Мы предполагаем, что есть какая-то «позиция зомби» (self.x, self.y)
  • Мы предполагаем, что вы можете вычислить «переднюю позицию зомби» на основе позиции зомби (Примечание: те же проблемы, что и выше).

Поскольку вы путешествуете горизонтально, вы можете вычислить dx по данному обновлению.

Сравните «позицию спереди зомби» для каждого зомби со старым / новым «положением спереди пули» для каждой пули. Если пуля перемещается с одной стороны фронта зомби на другую (или находится в контакте), то происходит попадание.

Пример

Позволять zed быть зомби.

zed = Enemy()

Предположим, ваше изображение зомби имеет 5 пикселей мертвого пространства вверху и 13 пикселей внизу.

ABOVE = 0
BELOW = 1

Enemy.dead_space = (5, 13)

Теперь предположим, что zed находится в позиции 200, 350.

zed.x, zed.y = 200, 350

Мы можем вычислить зону попадания на основе этой информации:

zed.image = images['l_zombie']
hit_zone = (zed.y + zed.dead_space[ABOVE],
            zed.y + zed.image.get_height() - zed.dead_space[BELOW])

Зона попадания — это полоса по оси Y, где горизонтальная пуля может поразить зомби. Если пуля находится выше или ниже этой полосы, она промахнется.

Конечно, ваши пули могут быть высотой более одного пикселя. В этом случае нам нужно проверить перекрытие.

Подобно зомби, пули могут иметь мертвое пространство на изображении. Так что будьте уверены и проверьте это:

Bullet.dead_space = (10, 12)
bullet = Bullet()

bullet.image = images['r_bullet']  # left-facing zed gets shot at with right-facing bullets

bullet_zone = (bullet.y + bullet.dead_space[ABOVE], 
               bullet.y + bullet.image.get_height() - bullet.dead_space[BELOW])

Итак, у нас есть bullet_zone и impact_zone, и мы хотим проверить, не перекрываются ли они:

if bullet_zone[BELOW] > impact_zone[ABOVE] or bullet_zone[ABOVE] > impact_zone[BELOW]:
    return False  # no impact

Если пуля и зед находятся в правильном диапазоне для удара, теперь мы должны спросить, происходит ли удар прямо в этот момент. Предположим, что мы делаем эту проверку после все зомби и пули переместились:

bullet_oldpos_x = bullet.x - bullet.speed_x
bullet_newpos_x = bullet.x

zed_oldpos_x = zed.x - zed.speed_x
zed_newpos_x = zed.x

Теперь спросите, на чьей мы стороне. Изображения располагаются в соответствии с их левым верхним углом. Но мы можем захотеть позаботиться о правом крае, а не о левом. Разберись:

if bullet.speed > 0:   # shooting left->right
    img_width = bullet.image.get_width()
    bullet_oldpos_x += img_width
    bullet_newpos_x += img_width
        
if zed.speed > 0:      # walking left->right
    img_width = zed.image.get_width()
    zed_oldpos_x += img_width
    zed_newpos_x += img_width

Теперь проверьте, прошла ли пуля через край зеда:

if bullet_oldpos_x < zed_oldpos_x and bullet_newpos_x >= zed_newpos_x:
    return True

Для коллизий есть три случая:

  • Пуля пересекла линию ньюпо.
  • Пуля существовала в шаге зеда.
  • Пуля пересекла линию олдпоса.

В первых двух случаях oldpos пули был «before» zed, но newpos пули был «внутри или вне» zed.

Во вторых двух случаях новая позиция пули находится «за пределами» старой позиции Зеда.

Вам нужно будет сделать несколько аналогичных проверок в обратном направлении для направления справа налево.

Когда вы начнете двигаться в двух измерениях, у вас будет прямоугольник, о котором стоит побеспокоиться. На этом этапе «правильным» является вычисление пересечения линии пули с прямоугольником или какой-либо другой формой зеда. Но если вы можете упростить «край» zed до отрезка линии, это будет проще.

    • Ваши константы (ширина экрана и т. Д.) В порядке, но, например, lives не является константой — он не должен находиться в глобальном пространстве имен
    • Избегайте вызова методов инициализации pygame в глобальном пространстве имен
    • moveX а также moveY не используются, поэтому удалите их
    • Вам нужно отделить свою логику от презентации; они перепутались прямо сейчас
    • Подумайте о модульности вашего проекта, что упростит тестирование и упаковку.
    • Делать нет предположить, что есть Desktop каталог относительно текущего, в котором находятся ваши изображения; вместо этого ищите пути к изображениям через pathlib а также glob
    • Ваш main_menu глубоко обеспокоен. В настоящее время он выполняет цикл кадра, хотя в нем нет необходимости, поскольку нет обновлений пользовательского интерфейса; поэтому вместо этого вам нужен цикл блокирующих событий. Кроме того, ваш onclick а также get_pos неверно представлять, как должна выполняться правильная обработка событий с помощью pygame — не вызывайте get_pos вообще; и вместо этого полагаться на позиционную информацию из самого события.
    • Я не думаю pygame.quit делает то, что вы думаете. Честно говоря, не очень удачно назвали. Не выходит. Он освобождает все ресурсы pygame. Вы должны делать это как часть очистки игры, а не запускать выход.
    • Рассмотрите возможность использования f-строки для 'Score: ' + str(score)
    • heart_images должна быть глобальной константой
    • В настоящее время у вас есть цикл взрыва стека — main_menu звонки game, но game звонки main_menu. Не делайте этого.

    Следующий пример кода не пытается реализовать вашу функцию маркера, но сохраняет ваши существующие функциональные возможности и исправляет неработающую до сих пор функцию, которая пыталась переключиться между игрой и главным меню.

    Структура каталогов

    структура каталогов

    game/resources/__init__.py

    from pathlib import Path
    from typing import Iterable, Tuple
    from pygame import image, Surface
    
    
    def load_images() -> Iterable[Tuple[str, Surface]]:
        root = Path(__file__).parent
        for path in root.glob('*.png'):
            yield path.stem, image.load(path)
    

    game/__main__.py

    Это поддерживает вызов

    python -m game
    
    from .ui import main
    main()
    

    игра / logic.py

    from numbers import Number
    from random import randint
    from typing import TypeVar, Tuple
    
    N_ENEMIES = 8
    
    ClipT = TypeVar('ClipT', bound=Number)
    
    
    def clip(value: ClipT, lower: ClipT, upper: ClipT) -> ClipT:
        return min(upper, max(value, lower))
    
    
    class HasPosition:
        def __init__(self, x: float, y: float):
            self.x, self.y = x, y
    
        @property
        def pos(self) -> Tuple[float, float]:
            return self.x, self.y
    
    
    class Player(HasPosition):
        def __init__(self):
            super().__init__(120, 100)
    
        def up(self):
            self.y -= 5
    
        def down(self):
            self.y += 5
    
        def left(self):
            self.x -= 5
    
        def right(self):
            self.x += 5
    
        def clip(self):
            self.x = clip(self.x, -12, 100)
            self.y = clip(self.y, -15, 405)
    
    
    class Enemy(HasPosition):
        def __init__(self):
            super().__init__(randint(600, 1000), randint(8, 440))
    
        def collide(
            self, left_limit: float,
        ) -> bool:  # Whether the enemy collided
            if self.x > left_limit:
                self.x -= 0.7
    
            if self.x <= 215:
                self.x = 215
                return True
            return False
    
    
    class Game:
        def __init__(self):
            self.score = 0
            self.lives = 3
            self.player = Player()
            self.enemies = [Enemy() for _ in range(N_ENEMIES)]
            self.bullets = []
    
        @property
        def alive(self) -> bool:
            return self.lives > 0
    
        def update(self) -> None:
            self.player.clip()
    
            to_remove, to_add = [], []
            for enemy in self.enemies:
                if enemy.collide(left_limit=self.player.x):
                    self.lives -= 1
                    to_remove.append(enemy)
                    enemy = Enemy()
                    to_add.append(enemy)
    
            for enemy in to_remove:
                self.enemies.remove(enemy)
            self.enemies.extend(to_add)
    

    игра / ui.py

    import pygame
    from pygame import display, font, key
    
    from .logic import Game
    from .resources import load_images
    
    
    SCR_WIDTH = 1020
    SCR_HEIGHT = 510
    WHITE = [240, 240, 240]
    FPS = 60
    
    HEART_IMAGES = ["triple_empty_heart", "single_heart", "double_heart",
                    "triple_heart"]
    
    
    class Window:
        def __init__(self):
            self.tnr_font = font.SysFont('Times_New_Roman', 27)
            self.clock = pygame.time.Clock()
            self.screen = display.set_mode((SCR_WIDTH, SCR_HEIGHT))
            display.set_caption('Dungeon Minigame')
            self.images = dict(load_images())
    
        def main_menu(self) -> None:
            self.screen.blit(self.images['background'], (0, 0))
            start_button = self.screen.blit(self.images['button'], (420, 320))
            display.update()
    
            while True:
                event = pygame.event.wait()
                if event.type == pygame.QUIT:
                    exit()
                if event.type == pygame.MOUSEBUTTONDOWN and start_button.collidepoint(event.pos):
                    break
    
        def draw(self, game: Game) -> None:
            self.screen.blit(self.images['background2'], (0, 0))
    
            antialias = True
            score_text = self.tnr_font.render(f'Score: {game.score}', antialias, WHITE)
            lives_text = self.tnr_font.render('Lives: ', antialias, WHITE)
            self.screen.blit(score_text, (20, 20))
            self.screen.blit(lives_text, (840, 20))
    
            self.screen.blit(self.images[HEART_IMAGES[game.lives]], (920, 0))
            self.screen.blit(self.images['r_knight'], game.player.pos)
    
            for enemy in game.enemies:
                self.screen.blit(self.images['l_zombie'], enemy.pos)
    
            display.update()
    
        @staticmethod
        def handle_keys(game: Game) -> None:
            onpress = key.get_pressed()
            if onpress[pygame.K_w]:
                game.player.up()
            if onpress[pygame.K_s]:
                game.player.down()
            if onpress[pygame.K_a]:
                game.player.left()
            if onpress[pygame.K_d]:
                game.player.right()
    
        @staticmethod
        def maybe_exit() -> None:
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    exit()
    
        def game(self) -> None:
            game = Game()
    
            while True:
                self.handle_keys(game)
                game.update()
                if not game.alive:
                    break
    
                self.maybe_exit()
                self.draw(game)
                self.clock.tick(FPS)
    
    
    def main():
        pygame.init()
        font.init()
    
        window = Window()
        try:
            while True:
                window.main_menu()
                window.game()
        finally:
            pygame.quit()
    

    • Почему в этом.py использовался дважды?

      — Зельда

    • Потому что есть два разных модуля.

      — Райндериен

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *