Phát triển game với Pygame – Part 3: Va chạm và chuyển động

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 tuc 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: đnag đứ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.

gravity_mario.gif

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:

wall.png

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.

platform.png

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ác blocker, 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

with.gif

và khi không hiển thị lớp trigger:

no_trigger.gif

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

https://github.com/vigov5/mario_game/tree/tech_blog