Menu
CoddyTech

Rìu trúng đích

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

Cây rìu đi xuyên qua một quái vật trong nhiều khung hình, khoảng 13 khung hình. Nếu làm nó bị thương trong mỗi khung hình đó, nó sẽ tan chảy bất cứ thứ gì nó chạm vào. Để chỉ đánh trúng một lần mỗi lượt đi qua, quái vật bị đánh trúng sẽ nhận thời gian chờ, đếm ngược thời gian đó trong update của chính nó, còn cây rìu chỉ làm bị thương những quái vật có thời gian chờ bằng 0. Một cái bẫy kích hoạt mỗi giây cũng hoạt động theo cách tương tự:

if player.trap_wait == 0 and spikes.colliderect(player.rect):
    player.hearts -= 1
    player.trap_wait = 60

Lưỡi rìu có một Rect riêng, kích thước 36 x 36, nằm ở tâm của mỗi vị trí từ axe_spots(). rect.colliderect(other) trả về True khi hai Rect chồng lên nhau.

challenge icon

Thử thách

Trung bình

Cho rìu đánh trúng mỗi quái vật một lần trong mỗi lượt.

  1. Thêm axe_wait = 0 cho Monster; update giảm giá trị này đi 1 khi nó lớn hơn 0.
  2. Trong hurt_monsters, sau các tia đạn: với mỗi vị trí, tạo một Rect blade kích thước 36 x 36, đặt tâm tại vị trí đó. Rect này đánh trúng mỗi quái vật mà nó chạm vào nếu axe_wait của quái vật bằng 0, bằng cách gọi monster.hit(1) và đặt monster.axe_wait = 20.

Nhấn “Chơi”: các slime bị rìu chạm vào sẽ gục xuống, còn một ghost cần bốn lượt đánh. 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)
BOLT = (120, 200, 255)

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")
axe_img = load("axe.png")
heart_img = load("heart.png")
empty_img = load("heart_empty.png")
MONSTERS = {
    "slime": {"image": load("slime.png"), "speed": 1.0, "hp": 1},
    "bat": {"image": load("dungeon_bat.png"), "speed": 1.6, "hp": 1},
    "spider": {"image": load("spider.png"), "speed": 1.2, "hp": 3},
    "ghost": {"image": load("ghost.png"), "speed": 0.8, "hp": 4},
}

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
        self.bolt_wait = 30
        self.bolt_damage = 1
        self.axes = 1

    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.hp = MONSTERS[kind]["hp"]
        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

    def hit(self, damage):
        self.hp -= damage


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()


class Night:
    def __init__(self):
        self.knight = Knight()
        self.time = 0
        self.monsters = pygame.sprite.Group()
        self.bolts = pygame.sprite.Group()
        self.bolt_timer = 0
        self.spin = 0
        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 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

    def shoot(self):
        self.bolt_timer += 1
        target = self.nearest()
        if self.bolt_timer >= self.knight.bolt_wait and target is not None:
            self.bolt_timer = 0
            bolt = Bolt(self.knight.rect.center, target.pos, self.knight.bolt_damage)
            self.bolts.add(bolt)

    def axe_spots(self):
        spots = []
        for i in range(self.knight.axes):
            angle = self.spin + i * 360 / self.knight.axes
            offset = pygame.Vector2(70, 0).rotate(angle)
            spots.append(pygame.Vector2(self.knight.rect.center) + offset)
        return spots

    def hurt_monsters(self):
        for bolt in self.bolts:
            target = pygame.sprite.spritecollideany(bolt, self.monsters)
            if target is not None:
                target.hit(bolt.damage)
                bolt.kill()
        for monster in list(self.monsters):
            if monster.hp <= 0:
                monster.kill()

    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.spin = (self.spin + 4) % 360
        self.knight.move(keys)
        self.spawn()
        self.monsters.update(self.knight.rect.center)
        self.bolts.update()
        self.shoot()
        self.hurt_monsters()
        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 spot in night.axe_spots():
        draw(pygame.transform.rotate(axe_img, -night.spin * 3), spot)
    for bolt in night.bolts:
        pygame.draw.circle(screen, BOLT, bolt.rect.center, 5)
    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