06. Nhảy và đè bẹp quái vật (Jumping and squashing monsters)


Trong bài học này, chúng ta sẽ thêm khả năng nhảy cho người chơi và cơ chế đè bẹp quái vật khi nhảy dẫm lên đầu chúng. Ở bài tiếp theo, chúng ta sẽ xử lý trường hợp ngược lại: người chơi sẽ thua khi bị quái vật húc trúng trên mặt đất.

Trước tiên, chúng ta cần tìm hiểu và cấu hình các lớp tương tác vật lý (Physics Layers).


Kiểm soát tương tác vật lý: Layers và Masks

Mọi thân vật lý trong Godot đều có hai thuộc tính bổ trợ nhau: - Layer (Lớp): Xác định đối tượng đang nằm ở lớp nào. - Mask (Mặt nạ): Xác định đối tượng sẽ lắng nghe và va chạm với những lớp nào.

Khi bạn muốn hai vật thể va chạm với nhau, ít nhất một trong hai vật thể phải có Mask khớp với Layer của vật thể kia.

Mặc định, mọi node vật lý đều được đặt ở cả Layer 1 và Mask 1, nghĩa là tất cả đều va chạm với tất cả. Bằng cách phân chia Layer, chúng ta có thể tối ưu hóa hiệu năng và tránh việc quái vật va chạm lẫn nhau hay sàn đấu phải lắng nghe sự kiện va chạm không cần thiết.

Đặt tên cho các Physics Layers

  1. Mở menu Project -> **Project Settings...

** 2. Tìm đến mục Layer Names -> 3D Physics ở danh mục bên trái. 3. Đặt tên cho 3 layer đầu tiên: - Layer 1: player (Người chơi) - Layer 2: enemies (Kẻ địch / Quái vật) - Layer 3: world (Thế giới / Môi trường / Sàn đấu)

  1. Nhấn Close.

Gán Layer và Mask cho từng đối tượng

1. Sàn đấu (Ground trong main.tscn)

  • Mở main.tscn, chọn node Ground.
  • Trong Inspector, mở rộng mục Collision:
  • Layer: Tắt số 1, bật số 3 (world).
  • Mask: Tắt toàn bộ (bỏ chọn số 1).
  • Giải thích: Sàn đấu chỉ đóng vai trò làm giá đỡ cho thế giới game, nó không cần phải lắng nghe hay phản hồi lại bất kỳ vật thể nào khác.

2. Người chơi (Player trong player.tscn)

  • Mở player.tscn, chọn node Player.
  • Trong Inspector, mục Collision:
  • Layer: Giữ nguyên số 1 (player).
  • Mask: Bật số 2 (enemies) và số 3 (world).
  • Giải thích: Người chơi cần va chạm với sàn nhà (world) để không bị rơi và va chạm với quái vật (enemies) để dẫm bẹp chúng.

3. Quái vật (Mob trong mob.tscn)

  • Mở mob.tscn, chọn node Mob.
  • Trong Inspector, mục Collision:
  • Layer: Tắt số 1, bật số 2 (enemies).
  • Mask: Tắt toàn bộ (để trống).
  • Giải thích: Quái vật chỉ di chuyển ngang trên mặt phẳng XZ mà không chịu trọng lực nên không cần mask world, và việc tắt mask giúp các con quái có thể bay xuyên qua nhau mà không bị kẹt hay đẩy nhau.


Lập trình cơ chế nhảy (Jumping)

Cơ chế nhảy trong Godot thực chất chỉ cần 2 dòng code!

Mở script player.gd: 1. Khai báo lực nhảy jump_impulse: gdscript # Lực đẩy thẳng đứng khi nhảy (mét/giây). @export var jump_impulse = 20 2. Trong hàm _physics_process(), thêm khối lệnh sau ngay trước dòng tính move_and_slide(): gdscript # Xử lý Nhảy if is_on_floor() and Input.is_action_just_pressed("jump"): target_velocity.y = jump_impulse

Hàm is_on_floor() đảm bảo người chơi chỉ có thể nhảy khi đang tiếp xúc với mặt đất. Khi bấm phím cách (jump), ta lập tức gán một vận tốc dương lớn theo trục Y hướng lên trên.


Đè bẹp quái vật (Squashing monsters)

Chúng ta muốn khi người chơi nhảy dẫm lên đầu quái vật, con quái sẽ bị tiêu diệt và người chơi sẽ nảy nhẹ lên không trung (bounce).

Đánh dấu nhóm (Group Tagging) cho Mob

Để phân biệt va chạm với quái vật và va chạm với mặt đất, ta gắn tag nhóm cho Mob: 1. Mở scene mob.tscn, chọn node gốc Mob. 2. Ở bảng bên phải, chuyển sang tab Node -> chọn mục con Groups.

  1. Nhấn vào dấu + (Create new Group), nhập tên nhóm là mob và nhấn OK.

  1. Lúc này node Mob đã thuộc nhóm "mob".

Bổ sung lực nảy và phát hiện va chạm trong player.gd

    # Lặp qua tất cả các va chạm xảy ra trong khung hình này
    for index in range(get_slide_collision_count()):
        var collision = get_slide_collision(index)

        # Bỏ qua nếu đối tượng va chạm đã bị xóa trong khung hình
        if collision.get_collider() == null:
            continue

        # Kiểm tra xem có va chạm với quái vật thuộc nhóm "mob" không
        if collision.get_collider().is_in_group("mob"):
            var mob = collision.get_collider()
            # Kiểm tra xem người chơi có đang tiếp đất từ phía trên đầu quái không
            if Vector3.UP.dot(collision.get_normal()) > 0.1:
                # Đè bẹp quái vật và nảy lên
                mob.squash()
                target_velocity.y = bounce_impulse
                # Thoát vòng lặp để tránh gọi nhiều lần cho 1 lần dẫm
                break
📌 Lưu ý
**Giải thích về Tích vô hướng (Dot Product):** `collision.get_normal()` trả về vector pháp tuyến vuông góc với mặt tiếp xúc va chạm. `Vector3.UP.dot(collision.get_normal()) > 0.1` tính góc giữa vector hướng thẳng đứng lên trời (`Vector3.UP`) và pháp tuyến va chạm. Nếu giá trị lớn hơn `0.1`, nghĩa là bề mặt tiếp xúc hướng lên trên (tức người chơi đang dẫm từ trên xuống dưới đỉnh đầu quái vật).

Thêm hàm squash() vào script mob.gd

Mở script mob.gd: 1. Khai báo một tín hiệu mới squashed ở đầu script. 2. Thêm hàm squash() ở cuối script:

# Phát ra khi người chơi dẫm bẹp quái vật
signal squashed

func squash():
    squashed.emit()
    queue_free()

Mã nguồn cập nhật đầy đủ

Player (player.gd)

extends CharacterBody3D

@export var speed = 14
@export var fall_acceleration = 75
@export var jump_impulse = 20
@export var bounce_impulse = 16

var target_velocity = Vector3.ZERO


func _physics_process(delta):
    var direction = Vector3.ZERO

    if Input.is_action_pressed("move_right"):
        direction.x += 1
    if Input.is_action_pressed("move_left"):
        direction.x -= 1
    if Input.is_action_pressed("move_back"):
        direction.z += 1
    if Input.is_action_pressed("move_forward"):
        direction.z -= 1

    if direction != Vector3.ZERO:
        direction = direction.normalized()
        $Pivot.basis = Basis.looking_at(direction)

    # Vận tốc mặt đất
    target_velocity.x = direction.x * speed
    target_velocity.z = direction.z * speed

    # Trọng lực
    if not is_on_floor():
        target_velocity.y = target_velocity.y - (fall_acceleration * delta)

    # Xử lý Nhảy
    if is_on_floor() and Input.is_action_just_pressed("jump"):
        target_velocity.y = jump_impulse

    # Lặp qua tất cả va chạm trong khung hình
    for index in range(get_slide_collision_count()):
        var collision = get_slide_collision(index)

        if collision.get_collider() == null:
            continue

        if collision.get_collider().is_in_group("mob"):
            var mob = collision.get_collider()
            # Dẫm từ trên xuống
            if Vector3.UP.dot(collision.get_normal()) > 0.1:
                mob.squash()
                target_velocity.y = bounce_impulse
                break

    velocity = target_velocity
    move_and_slide()

Mob (mob.gd)

extends CharacterBody3D

signal squashed

@export var min_speed = 10
@export var max_speed = 18


func _physics_process(_delta):
    move_and_slide()


func initialize(start_position, player_position):
    look_at_from_position(start_position, player_position, Vector3.UP)
    rotate_y(randf_range(-PI / 4, PI / 4))

    var random_speed = randi_range(min_speed, max_speed)
    velocity = Vector3.FORWARD * random_speed
    velocity = velocity.rotated(Vector3.UP, rotation.y)


func squash():
    squashed.emit()
    queue_free()


func _on_visible_on_screen_notifier_3d_screen_exited():
    queue_free()

Kiểm thử

Nhấn F5 để chạy toàn bộ game (nếu Godot hỏi chọn main scene, hãy chọn main.tscn): - Nhấn phím Space để nhảy lên không trung. - Căn thời điểm đáp xuống trúng đỉnh đầu của một con quái vật: con quái vật sẽ lập tức biến mất và nhân vật của bạn sẽ bật nảy lên cao!