Menu
CoddyTech

Vung kiếm

Thuộc phần Dự án trong hành trình Pygame của Coddy. Bài học 28 trên 33.

Phím cách vung kiếm của hiệp sĩ trong 12 khung hình, về phía hiệp sĩ đang quay mặt. Slime nào chạm vào lưỡi kiếm sẽ biến mất.

Hiệp sĩ ghi nhớ hướng đi gần nhất trong knight.facing: (1, 0) là phải, (0, -1) là lên. Lưỡi kiếm là một hình chữ nhật 36 x 36, cách tâm hiệp sĩ 34 pixel theo hướng đó:

def sword(self):
    box = pygame.Rect(0, 0, 36, 36)
    box.centerx = self.rect.centerx + self.facing[0] * 34
    box.centery = self.rect.centery + self.facing[1] * 34
    return box

knight.swing đếm ngược số khung hình của cú vung kiếm về 0. Phím cách chỉ bắt đầu cú vung mới khi giá trị này bằng 0, nên giữ phím cách không làm lưỡi kiếm tiếp tục vung. Khi cú vung đang diễn ra, mọi kẻ địch chạm vào hình chữ nhật đều bị xóa bằng enemy.kill(), thao tác này xóa sprite khỏi mọi nhóm mà nó thuộc về.

sword.png hướng lên trên. pygame.transform.rotate xoay ảnh ngược chiều kim đồng hồ theo góc tương ứng với từng hướng:

ANGLES = {(0, -1): 0, (-1, 0): 90, (0, 1): 180, (1, 0): -90}
challenge icon

Thử thách

Khó

Vung kiếm bằng phím Space.

  1. Thêm ANGLES; thiết lập cho hiệp sĩ self.facing = (1, 0), self.swing = 0 và sword().
  2. Các phím mũi tên cũng thiết lập knight.facing: phím trái (-1, 0).
  3. Phím Space thiết lập knight.swing = 12 nếu giá trị đang là 0. Trước khi kiểm tra trạng thái bị thương, khi giá trị lớn hơn 0, hãy giảm giá trị đó và gọi kill() với những kẻ địch chạm vào knight.sword().
  4. Khi vung kiếm, vẽ lưỡi kiếm đã xoay tại vị trí knight.sword().

Nhấn Chơi rồi đánh một con slime: nó sẽ biến mất. Sau đó nhấn Kiểm tra.

Tự làm thử

import random
import pygame

TILE = 48
MAP = [
    "##############",
    "#............#",
    "#.P......K...#",
    "#............#",
    "#....##......D",
    "#....##......#",
    "##.#######.###",
    "#............#",
    "#...S....H...#",
    "##############",
]

pygame.init()
screen = pygame.display.set_mode((672, 480))
pygame.display.set_caption("Rune Keep")
clock = pygame.time.Clock()
font = pygame.font.Font("assets/fonts/pixel.ttf", 32)


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")
door_img = load("dungeon_door.png")
key_img = load("key.png")
slime_img = load("slime.png")
spider_img = load("spider.png")
heart_img = load("heart.png")
empty_img = load("heart_empty.png")
sword_img = load("sword.png")
potion_img = load("potion_red.png")


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


def cell_rect(col, row):
    rect = pygame.Rect(0, 0, 32, 32)
    rect.center = (col * TILE + 24, row * TILE + 24)
    return rect


class Enemy(pygame.sprite.Sprite):
    def __init__(self, image, rect, dx, dy):
        super().__init__()
        self.image = image
        self.rect = rect
        self.dx = dx
        self.dy = dy

    def update(self, blocks):
        self.rect.x += self.dx
        self.rect.y += self.dy
        if self.rect.collidelist(blocks) != -1:
            self.rect.x -= self.dx
            self.rect.y -= self.dy
            self.dx = -self.dx
            self.dy = -self.dy


class Room:
    def __init__(self, lines):
        self.walls = []
        self.enemies = pygame.sprite.Group()
        self.start = (72, 72)
        self.has_key = False
        self.door_open = False
        for row, line in enumerate(lines):
            for col, char in enumerate(line):
                if char == "#":
                    self.walls.append(pygame.Rect(col * TILE, row * TILE, TILE, TILE))
                elif char == "P":
                    self.start = cell_rect(col, row).center
                elif char == "K":
                    self.key = cell_rect(col, row)
                elif char == "D":
                    self.door = pygame.Rect(col * TILE, row * TILE, TILE, TILE)
                elif char == "S":
                    speed = random.choice([-2, 2])
                    self.enemies.add(Enemy(slime_img, cell_rect(col, row), speed, 0))

    def blocks(self):
        if self.door_open:
            return self.walls
        return self.walls + [self.door]


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

    def move(self, dx, dy, blocks):
        self.rect.x += dx
        for block in blocks:
            if self.rect.colliderect(block):
                if dx > 0:
                    self.rect.right = block.left
                elif dx < 0:
                    self.rect.left = block.right
        self.rect.y += dy
        for block in blocks:
            if self.rect.colliderect(block):
                if dy > 0:
                    self.rect.bottom = block.top
                elif dy < 0:
                    self.rect.top = block.bottom


room = Room(MAP)
knight = Knight(room.start)
state = "playing"

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

    if state == "playing":
        keys = pygame.key.get_pressed()
        dx = 0
        dy = 0
        if keys[pygame.K_LEFT]:
            dx = -3
        if keys[pygame.K_RIGHT]:
            dx = 3
        if keys[pygame.K_UP]:
            dy = -3
        if keys[pygame.K_DOWN]:
            dy = 3
        knight.move(dx, dy, room.blocks())
        knight.rect.clamp_ip(screen.get_rect())

        if not room.has_key and knight.rect.colliderect(room.key):
            room.has_key = True
        if room.has_key and knight.rect.inflate(8, 8).colliderect(room.door):
            room.door_open = True

        room.enemies.update(room.walls + [room.door])

        if knight.hurt > 0:
            knight.hurt -= 1
        elif pygame.sprite.spritecollide(knight, room.enemies, False):
            knight.hearts -= 1
            knight.hurt = 60
            if knight.hearts == 0:
                state = "lost"

    for row in range(10):
        for col in range(14):
            screen.blit(floor_img, (col * TILE, row * TILE))
    for wall in room.walls:
        screen.blit(wall_img, wall)
    if not room.door_open:
        screen.blit(door_img, room.door)
    if not room.has_key:
        draw(key_img, room.key)
    for enemy in room.enemies:
        draw(enemy.image, enemy.rect)
    if knight.hurt % 10 < 5:
        draw(knight.image, knight.rect)
    for i in range(3):
        if i < knight.hearts:
            screen.blit(heart_img, (8 + i * 40, 6))
        else:
            screen.blit(empty_img, (8 + i * 40, 6))
    if room.has_key:
        screen.blit(key_img, (136, 6))
    if state == "lost":
        message = font.render("Game over", True, (240, 240, 240))
        screen.blit(message, message.get_rect(center=(336, 240)))
    pygame.display.flip()
    clock.tick(60)

pygame.quit()

Tất cả bài học trong Dự án

Tự luyện tập: Playground Pygame