Menu
CoddyTech

Chốt

Bài học 10 trên 25 trong khóa học Dự án Pygame: Trấn giữ thành trì của Coddy.

Vũ khí đầu tiên của hiệp sĩ tự bắn: cứ sau 30 khung hình, một tia sét ma thuật bay về phía quái vật gần nhất. Để tìm con gần nhất, hãy giữ lại con gần nhất đã thấy cho đến lúc đó rồi so sánh từng con quái vật với nó:

def nearest(self):
    point = pygame.Vector2(self.knight.rect.center)
    best = None
    for monster in self.monsters:
        if best is None or point.distance_to(monster.pos) < point.distance_to(best.pos):
            best = monster
    return best

Tia sét là một sprite bay theo đường thẳng. Nó tính hướng bay một lần khi được tạo ra, giống như bước di chuyển của quái vật, rồi giữ nguyên hướng đó. Khi rời khỏi màn hình, nó tự xóa bằng self.kill(), thao tác này xóa sprite khỏi mọi Group mà nó thuộc về:

class Bolt(pygame.sprite.Sprite):
    def __init__(self, start, target, damage):
        super().__init__()
        self.pos = pygame.Vector2(start)
        step = pygame.Vector2(target) - self.pos
        if step.length() == 0:
            step = pygame.Vector2(1, 0)
        self.velocity = step.normalize() * 7
        self.damage = damage
        self.rect = pygame.Rect(0, 0, 10, 10)
        self.rect.center = self.pos

    def update(self):
        self.pos += self.velocity
        self.rect.center = self.pos
        if not screen.get_rect().colliderect(self.rect):
            self.kill()

Bộ hẹn giờ đếm số khung hình kể từ lần bắn tia sét trước. Khi đạt đến bolt_wait của hiệp sĩ và có mục tiêu, hãy bắn rồi đặt lại bộ hẹn giờ về 0. Thời gian chờ giữa các lần sử dụng như vậy được gọi là thời gian hồi chiêu.

challenge icon

Thử thách

Trung bình

Cứ mỗi 30 khung hình, hiệp sĩ bắn một tia vào con quái vật gần nhất.

  1. Thêm BOLT = (120, 200, 255); đặt bolt_wait = 30 và bolt_damage = 1 cho hiệp sĩ.
  2. Viết lớp Bolt như trên.
  3. Trong Night: thêm bolts (một Group), bolt_timer = 0, nearest() như trên và shoot(). update di chuyển các tia sau khi cập nhật quái vật, rồi bắn.
  4. Vẽ các tia: pygame.draw.circle(screen, BOLT, bolt.rect.center, 5).

Nhấn Chơi: các tia bay về phía con quái vật gần nhất, tạm thời bay xuyên qua nó. Sau đó nhấn Kiểm tra.

Tự làm thử

import random
import pygame

WIDTH, HEIGHT = 720, 480
TILE = 48
NIGHT = 60 * 60 * 4
# Các loại quái vật xuất hiện trong từng đợt, từ đợt đầu đến đợt cuối
WAVES = [
    ["slime"],
    ["slime", "bat"],
    ["slime", "bat", "spider"],
    ["bat", "spider"],
    ["slime", "spider", "ghost"],
    ["bat", "spider", "ghost"],
    ["spider", "ghost"],
    ["bat", "ghost"],
]
WHITE = (240, 240, 240)

pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Hold the Keep")
clock = pygame.time.Clock()
font = pygame.font.Font("assets/fonts/pixel.ttf", 24)
big_font = pygame.font.Font("assets/fonts/pixel.ttf", 48)


def load(name):
    return pygame.image.load("assets/images/" + name).convert_alpha()


floor_img = load("floor.png")
wall_img = load("wall.png")
knight_img = load("knight.png")
heart_img = load("heart.png")
empty_img = load("heart_empty.png")
MONSTERS = {
    "slime": {"image": load("slime.png"), "speed": 1.0},
    "bat": {"image": load("dungeon_bat.png"), "speed": 1.6},
    "spider": {"image": load("spider.png"), "speed": 1.2},
    "ghost": {"image": load("ghost.png"), "speed": 0.8},
}

courtyard = pygame.Rect(TILE, TILE, WIDTH - 2 * TILE, HEIGHT - 2 * TILE)
background = pygame.Surface((WIDTH, HEIGHT))
for row in range(HEIGHT // TILE):
    for col in range(WIDTH // TILE):
        x = col * TILE
        y = row * TILE
        if courtyard.collidepoint(x, y):
            background.blit(floor_img, (x, y))
        else:
            background.blit(wall_img, (x, y))


def draw(image, center):
    screen.blit(image, image.get_rect(center=center))


def clock_text(frames):
    seconds = frames // 60
    return f"{seconds // 60}:{seconds % 60:02d}"


def edge_point():
    side = random.randint(0, 3)
    if side == 0:
        return (random.randint(0, WIDTH), -24)
    if side == 1:
        return (random.randint(0, WIDTH), HEIGHT + 24)
    if side == 2:
        return (-24, random.randint(0, HEIGHT))
    return (WIDTH + 24, random.randint(0, HEIGHT))


class Knight(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = knight_img
        self.rect = pygame.Rect(0, 0, 24, 24)
        self.rect.center = courtyard.center
        self.speed = 3
        self.hearts = 5
        self.max_hearts = 5
        self.hurt = 0

    def move(self, keys):
        if keys[pygame.K_LEFT]:
            self.rect.x -= self.speed
        if keys[pygame.K_RIGHT]:
            self.rect.x += self.speed
        if keys[pygame.K_UP]:
            self.rect.y -= self.speed
        if keys[pygame.K_DOWN]:
            self.rect.y += self.speed
        self.rect.clamp_ip(courtyard)


class Monster(pygame.sprite.Sprite):
    def __init__(self, kind, pos):
        super().__init__()
        self.kind = kind
        self.image = MONSTERS[kind]["image"]
        self.speed = MONSTERS[kind]["speed"]
        self.pos = pygame.Vector2(pos)
        self.rect = pygame.Rect(0, 0, 28, 28)
        self.rect.center = self.pos

    def update(self, target):
        step = pygame.Vector2(target) - self.pos
        if step.length() > 0:
            self.pos += step.normalize() * self.speed
        self.rect.center = self.pos


class Night:
    def __init__(self):
        self.knight = Knight()
        self.time = 0
        self.monsters = pygame.sprite.Group()
        self.state = "playing"

    def wave(self):
        return min(len(WAVES), self.time * len(WAVES) // NIGHT + 1)

    def spawn(self):
        wave = self.wave()
        if self.time % max(16, 46 - wave * 5) == 0:
            kind = random.choice(WAVES[wave - 1])
            self.monsters.add(Monster(kind, edge_point()))

    def take_hits(self):
        knight = self.knight
        if knight.hurt > 0:
            knight.hurt -= 1
        elif pygame.sprite.spritecollideany(knight, self.monsters):
            knight.hearts -= 1
            knight.hurt = 75
            if knight.hearts <= 0:
                self.state = "lost"

    def update(self, keys):
        self.time += 1
        self.knight.move(keys)
        self.spawn()
        self.monsters.update(self.knight.rect.center)
        self.take_hits()


night = Night()

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    if night.state == "playing":
        night.update(pygame.key.get_pressed())

    screen.blit(background, (0, 0))
    knight = night.knight
    for monster in night.monsters:
        draw(monster.image, monster.rect.center)
    if knight.hurt % 10 < 5:
        draw(knight.image, knight.rect.center)
    for i in range(knight.max_hearts):
        if i < knight.hearts:
            screen.blit(heart_img, (8 + i * 34, 6))
        else:
            screen.blit(empty_img, (8 + i * 34, 6))
    left = font.render(clock_text(max(0, NIGHT - night.time)) + " until dawn", True, WHITE)
    screen.blit(left, left.get_rect(center=(WIDTH // 2, 24)))
    if night.state == "lost":
        message = big_font.render("The keep has fallen", True, WHITE)
        screen.blit(message, message.get_rect(center=(WIDTH // 2, 200)))
    pygame.display.flip()
    clock.tick(60)

pygame.quit()

Tất cả bài học trong Dự án Pygame: Trấn giữ thành trì

Tự luyện tập: Playground Pygame