Phát triển game với Pygame – Part 3: Va chạm và chuyển động
This post hasn't been updated for 3 years
Long time no see, hôm nay mình xin phép được đào mộ lại một series về làm game với pygame. Đây là một series tưởng chừng đã bị mình vùi trong quên lãng nhưng lương tâm cắn rứt, tác giả quyết định hoàn thành nốt series này . Hãy cùng tiếp tục phần 3 của serie với chủ đề : Va chạm và chuyển động
Chuyển động trong pygame
Ở phần trước ta đã biết cách đổi sprite của Mario thông qua các state: v_state
là trạng thái theo chiều dọc: đang nhảy (jumping
) và theo chiều ngang h_state
: đang đứng yên (standing
) hay là đang chạy (running
) cùng với đó là các trạng thái đang quay mặt về phía nào (facing
): trái hay phải. Tuy nhiên, ta chưa thể tương tác với Mario. Ta sẽ thực hiện việc tương tác này thông qua các các phím di chuyển : lên (nhảy lên), trái (di chuyển sang trái), phải (di chuyển sang phải) kèm theo là quay mặt nếu cần thiết.
Để xử lý các sự kiện phím ấn mà người dùng nhập vào, ta định nghĩa thêm hàm handle trong class Mario để xử lý các xự kiện. Như ở bài trước, tại hàm run
, hàm lặp chính của game, sau mỗi clock tick ta lấy ra các sự kiện diễn ra trong game và đưa cho từng đối tượng trong game để xử lý một cách phù hợp
def run(self):
# main game loop
while True:
# hold frame rate at 60 fps
dt = self.clock.tick(60)
self.time_step += 1
# enumerate event
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit(0)
# sprite handle event
self.handle(event)
self.update(dt / 1000.)
# re-draw screen
self.draw(self.screen)
Tại hàm handle
của vòng lặp của game, ta tiến hành gọi đến hàm handle
của mario để xử lý:
def handle(self, event):
self.my_mario.handle(event)
pass
Trong phần này ta sẽ tiến hành implement hàm handle cho Mario.
def handle(self, event):
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_UP:
if self.v_state == "resting":
self.jump()
elif event.key == pygame.K_RIGHT:
self.move_right()
elif event.key == pygame.K_LEFT:
self.move_left()
elif event.type == pygame.KEYUP:
if event.key == pygame.K_RIGHT or \
event.key == pygame.K_LEFT:
self.vx = 0
self.h_state = "standing"
Như trong phần code ở phía trên, tham số truyền vào là biến event
, từ biến này ta có thể kiểm tra xem sự kiện đó là nhấn 1 phím xuống hay không (event.type == pygame.KEYDOWN
), tại đây ta sẽ thực hiện các hành động: nếu là nhấn phím sang phải sẽ thực hiện hành động di chuyển sang phải. Dưới đây là chi tiết của hàn move_right
. Đối với hàm move_left
, chúng ta làm hoàn toàn tương tự. Hàm jump
chúng ta sẽ nói kỹ hơn khi thực hiện mô phỏng trọng lượng.
def move_right(self):
self.vx = 2.5
self.h_state = "running"
self.h_facing = "right"
Đó là logic khi chúng ta nhấn một phím xuống. Việc cần làm tiếp theo đó là xử lý khi ta nhả phím ra, lúc này ta sẽ cập nhật lại các trạng thái của Mario về trạng thái đứng yên như ở đoạn code phía trên.
Việc di chuyển sang bên phải, đơn giản là thiết lập vận tốc theo chiều x, thay đổi các trạng thái tương ứng với hướng di chuyển. sau khi thực hiện các bước này, chạy thử ta thấy mario đã di chuyển theo đúng các hướng trái phải mà ta chỉ định.
Mô phỏng trọng lực
Chúng ta cũng xem qua một chút logic về phần chuyển động của Mario. Thứ luôn kéo chúng ta về với mặt đầt, khiến ta không thể bay lượn như chim đó chính là trọng lực. Hãy mô phỏng trọng lượng trong game như sau. Đầu tiên là định nghĩa một số thông số:
GRAVITY = 0.4
MAX_VX = 3
MAX_VY = 20
def update(self, dt, game):
last = self.rect.copy()
if abs(self.vx) > self.MAX_VX:
self.vx = math.copysign(self.MAX_VX, self.vx)
if abs(self.vy) > self.MAX_VY:
self.vy = math.copysign(self.MAX_VY, self.vy)
dy = self.vy
dx = self.vx
self.vy += self.GRAVITY
self.rect = self.rect.move(dx, dy)
new = self.rect4
Ở đây ta cần có một chút kiến thức về vật lý. Ta giả lập trọng lực như sau:
- cứ mỗi game step ta tính toán lại vận tốc của Mario, cộng thêm vào vận tốc theo chiều y một lượng là GRAVITY
- Li độ mà vật di chuyển được sau mỗi bước game sẽ chính là vận tốc của vật theo các chiều x, y
- Tính toán lại các vận tốc tối đa để tránh việc nhân vật của chúng ta rơi quá nhanh.
Khi đó việc thực hiện nhảy lên chỉ đơn giản là di chuyển với vận tốc âm theo phương y:
def jump(self):
self.vy = -9
self.v_state = "jumping"
Ta init vị trí của Mario như sau tại hàm init
của main:
self.tilemap = tmx.load("map.tmx", self.screen.get_size())
self.sprites = tmx.SpriteLayer()
self.my_mario = mario.Mario(self.sprites)
self.my_mario.set_position(100, 100)
self.tilemap.layers.append(self.sprites)
Như vậy vật sẽ càng ngày càng di chuyển xuống dưới và càng ngày càng nhanh theo đúng mô phỏng trọng lực.
Va chạm với mặt đất
Để có thể thực hiện phần va chạm với platformer ta cần chỉnh sửa lại Map trong Tiled một chút. Như đã nó sơ qua trong bài đầu tiên của series. Map được chia thành các lớp background layer, midground layer, foreground layer, các layer này sẽ là phần hiển thị. Nhưng phần sẽ tương tác với đối tượng của game sẽ là trigger layer, phần này chứa các đối tượng nhưng lại không hiển thị, làm ta có cảm giác là đối tượng đang tương tác với lớp tile.
Trong lớp trigger của file map, ta thêm các đối tượng hình chữ nhật với các thuộc tính như sau:
Ta đặt giá trị cho thuộc tính là "tlbr" nghĩa là block theo tất cả các hướng trên (t) trái (l) dưới (b) phải (r) cho platform
. Và tương tự cho đối tượng wall
dùng để ngăn căn người chơi đi ra khỏi màn hình. Tất cả các đối tượng này đều nằm trong lớp trigger của map.
Tiếp đó ta cần phải xử lý va chạm giữa người chơi với các đối tượng thuộc kiểu blocker
này. Khi đó toàn bộ code của hàm update
sẽ như sau:
def update(self, dt, game):
last = self.rect.copy()
if abs(self.vx) > self.MAX_VX:
self.vx = math.copysign(self.MAX_VX, self.vx)
if abs(self.vy) > self.MAX_VY:
self.vy = math.copysign(self.MAX_VY, self.vy)
dy = self.vy
dx = self.vx
self.vy += self.GRAVITY
self.rect = self.rect.move(dx, dy)
new = self.rect
for cell in game.tilemap.layers['triggers'].collide(new, 'blockers'):
if last.right <= cell.left and new.right > cell.left:
new.right = cell.left
if last.left >= cell.right and new.left < cell.right:
new.left = cell.right
if last.bottom <= cell.top and new.bottom > cell.top:
new.bottom = cell.top
self.v_state = "resting"
self.vy = 0
if last.top >= cell.bottom and new.top < cell.bottom:
new.top = cell.bottom
self.vy = 0
game.tilemap.set_focus(new.x, new.y)
- Đầu tiên ta tính toán vị trí của Mario theo lực tác động của trọng lực.
- Tiếp đến là tính toán va chạm với
blocker
. Nguyên lý rất đơn giản: lấy ra cácblocker
, kiểm tra xem phần bao ngoài của chúng có giao với phần bao ngoài của Mario hay không. Nếu có giao nhau ta sẽ thiết lập lại bao ngoài mới (new
) cho Mario để chúng ko còn giao nhau nữa.
Hình demo khi hiển thị lớp trigger
và khi không hiển thị lớp trigger
:
End
Vậy là ta đã có một nhân vật Mario có thể di chuyển trên một địa hình cơ bản rồi. Phần tiếp theo sẽ là các cách sử dụng map nâng cao cùng với Tiled
Source Code
All Rights Reserved