2D platform game - Phần 2: Ghép hoạt ảnh cho nhân vật
Xin chào mọi người! Mình đã trở lại với phần 2 của serie 2D platform game. Trong phần này mình sẽ hướng dẫn các bạn thêm hoạt ảnh nhân vật vào bộ khung game ta đã làm trong phần trước nha!
1. Chuẩn bị hình ảnh
Để tiện cho việc đọc các hình ảnh của nhân vật, mình tạo file utils.py
với hàm
getSurfaceList
như sau:
import os
from pathlib import Path
import pygame
def getSurfaceList(path: Path) -> None:
surfaceList = []
for imageFilename in os.listdir(path):
pathToImage = path / imageFilename
surface = pygame.image.load(pathToImage).convert_alpha()
surfaceList.append(surface)
return surfaceList
Các hình ảnh mô tả một hành động của nhân vật sẽ được đánh số và được lưu vào trong một thư mục. Ví dụ, nhân vật hiện tại có 7 hình ảnh mô tả hành động chạy, cấu trúc thư mục chứa các hình ảnh này có dạng như sau:
run ____1.png
|\___2.png
|____3.png
|____4.png
|____5.png
|____6.png
|____7.png
Hàm getSurfaceList
sẽ nhận vào đường dẫn đến thư mục run
và chuyển tất cả
các file ảnh trong đó thành Surface và lưu vào một list.
2. Thêm hoạt ảnh cho nhân vật
Ý tưởng chính của việc này là ta vẽ liên tục các hình ảnh của nhân vật sao cho ăn khớp với hoạt động (trạng thái) hiện tại của nhân vật.
Cụ thể, tại mỗi vòng lặp game ta thực hiện các công việc sau:
- Nhận tín hiệu đầu vào từ người chơi
- Cập nhật trạng thái của nhân vật
- Cập nhật hình ảnh phù hợp với trạng thái
- Cập nhật tọa độ của nhân vật
- Vẽ lại nhân vật với hình ảnh mới và tọa độ mới
Với những công việc trên mình thay đổi class Player
như sau:
from pathlib import Path
import pygame
from utils import getSurfaceList
class Player(pygame.sprite.Sprite):
def __init__(self, position: tuple) -> None:
super().__init__()
# Player status
self.status = 'idle'
self.facingRight = True
self.onGround = False
# Player image
self.getAnimations()
self.frameIndex = 0
self.animationSpeed = 0.25
self.image = self.animations[self.status][self.frameIndex]
self.rect = self.image.get_rect(topleft=position)
self.hitbox = pygame.Rect(position, (40, 50))
self.hitbox.midbottom = self.rect.midbottom
# Player movement
self.direction = pygame.math.Vector2(0, 0)
self.speed = 5
self.gravity = 0.8
self.smoothSpeed = 0.4
self.jumpSpeed = -16
def getAnimations(self) -> None:
currentDirPath = Path(__file__).absolute().parents[1]
characterFolder = 'Assets/Image/MainCharacters/PinkMan'
characterAnimationsPath = currentDirPath / characterFolder
self.animations = {'idle':[], 'run':[], 'jump': [], 'fall': []}
for animation in self.animations.keys():
characterAnimationPath = characterAnimationsPath / animation
self.animations[animation] = getSurfaceList(path=characterAnimationPath)
def getEvent(self) -> None:
# Get event
keys = pygame.key.get_pressed()
# Move event
if keys[pygame.K_d]:
self.direction.x = 1
self.facingRight = True
elif keys[pygame.K_a]:
self.direction.x = -1
self.facingRight = False
else:
self.direction.x = 0
# Jump event
if keys[pygame.K_w] and self.onGround:
self.jump()
def getStatus(self) -> None:
if self.direction.y < 0:
self.status = 'jump'
elif self.direction.y > self.gravity:
self.status = 'fall'
else:
if self.direction.x != 0:
self.status = 'run'
else:
self.status = 'idle'
def animate(self):
animation = self.animations[self.status]
self.frameIndex += self.animationSpeed
if self.frameIndex >= len(animation):
self.frameIndex = 0
image = animation[int(self.frameIndex)]
if self.facingRight:
self.image = image
else:
self.image = pygame.transform.flip(surface=image,
flip_x=True,
flip_y=False)
def move(self) -> None:
self.rect.x += self.direction.x * self.speed
self.hitbox.x += self.direction.x * self.speed
def jump(self) -> None:
self.direction.y = self.jumpSpeed
def applyGravity(self) -> None:
self.direction.y += self.gravity
self.rect.y += self.direction.y
self.hitbox.y += self.direction.y
def update(self) -> None:
self.getEvent()
self.getStatus()
self.animate()
Các thuộc tính mới:
animations
: lưu trữ tất cả các hình ảnh tương ứng với các hành động của nhân vậtframeIndex
: số thứ tự của hình ảnh hiện tạiimage
: hình ảnh hiện tại của nhân vậtanimationSpeed
: tốc độ chuyển hình ảnh của nhân vậtstatus
: trạng thái hiện tại của nhân vậtfacingRight
: dùng để xác định có lật hình ảnh hay không? Vì khi nhân vật hoạt động bên trái hoặc phải thì các hình ảnh là đối xứng nhau, nên ta chỉ lưu các hình ảnh ở một phía và sử dụng tham số này để lật ảnh khi nhân vật hoạt động ở phía còn lạionGround
: xác định xem nhân vật có đang ở trên mặt đất không? (giới hạn số lần nhảy của nhân vật)hitbox
: hitbox của nhân vật (dùng cho va chạm)
Các phương thức mới:
getAnimations
: lấy tất cả hoạt ảnh của nhân vậtgetStatus
: cập nhật của nhân vậtanimate
: cập nhật hình ảnh của nhân vật (được gọi tại mỗi vòng lặp game, để cập nhật hình ảnh của nhân vật liên tục, từ đó tạo thành hoạt ảnh)
3. Một số thay đổi khác
Thay đổi class Level
để hiện thị hình ảnh của nhân vật
import pygame
from player import Player
from tiles import Tile
class Level:
def __init__(self, map: list, tileSize: tuple) -> None:
self.map = self.setupMap(map, tileSize)
def setupMap(self, map: list, tileSize: tuple) -> None:
self.tiles = pygame.sprite.Group()
self.player = pygame.sprite.GroupSingle()
for rowIndex, row in enumerate(map):
for colIndex, tile in enumerate(row):
position = (colIndex * tileSize[0], rowIndex * tileSize[1])
if tile == 'X':
tile = Tile(position, tileSize)
self.tiles.add(tile)
if tile == 'P':
player = Player(position)
self.player.add(player)
def horizontalMovementCollision(self) -> None:
player = self.player.sprite
player.move()
for tile in self.tiles.sprites():
if tile.rect.colliderect(player.hitbox):
if player.direction.x == -1:
player.hitbox.left = tile.rect.right
elif player.direction.x == 1:
player.hitbox.right = tile.rect.left
player.rect.midbottom = player.hitbox.midbottom
def verticalMovementCollision(self) -> None:
player = self.player.sprite
player.applyGravity()
for tile in self.tiles.sprites():
if tile.rect.colliderect(player.hitbox):
if player.direction.y > 0:
player.hitbox.bottom = tile.rect.top
player.direction.y = 0
player.onGround = True
elif player.direction.y < 0:
player.hitbox.top = tile.rect.bottom
player.direction.y = 0
player.rect.midbottom = player.hitbox.midbottom
if (player.onGround and player.direction.y < 0) or player.direction.y > 1:
player.onGround = False
def update(self, screen) -> None:
self.tiles.draw(screen)
self.player.update()
self.horizontalMovementCollision()
self.verticalMovementCollision()
self.player.draw(screen)
Thay đổi hàm thực thi
import sys
import pygame
from level import Level
map = [
' ',
' ',
' ',
' XX XXX ',
' XX ',
' XXXX ',
' XXXX XX',
' XX X XXXX',
' X XXXX',
' P XXXXXXXXXX',
'XXXXXXXXXXXXXX']
tileSize = (64, 64)
screenWidth = len(map[0]) * tileSize[0]
screenHeight = len(map) * tileSize[1]
# Pygame setup
pygame.init()
screen = pygame.display.set_mode((screenWidth,screenHeight))
clock = pygame.time.Clock()
level = Level(map, tileSize)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill('black')
level.update(screen)
pygame.display.update()
clock.tick(60)
Sau khi thực hiện thì đây là kết quả
4. Lời kết
Như vậy, chúng ta đã xong phần 2, phần này có hơi khó hiểu vì mình không dành nhiều thời gian để trau chuốt câu từ, mong các bạn thông cảm. Mình sẽ cố gắng diễn đạt tốt hơn cho những phần sau, khi có nhiều thơi gian để làm hơn.
Code chi tiết các bạn có thể tham khảo tại repository.