Вступление:
Пока что игра работает хорошо, и меня больше всего беспокоит система пуль (включая функцию, в которой вы щелкаете мышью, чтобы стрелять пулями) и система столкновений между зомби и пулями, поскольку я пробовал много разных способов решить эти две проблемы, но безуспешно. В любом случае, если бы кто-то не мог объяснить, как реализовать эти две концепции с предоставленным кодом, это было бы замечательно.
Итерационный обзор моего предыдущего вопроса.
Репозиторий 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 ответа
Столкновение
А пока предположим, что ваши пули летят по прямой слева направо или справа налево.
У ваших зомби есть позиция, 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()


