運動學角色(2D)

前言

是的,這個名字聽起來很奇怪。「運動學角色」到底是什麼?會有這個名稱,是因為早期物理引擎問世時,被稱為「動力學(Dynamics)」引擎(因為它們主要處理碰撞回應)。當時有許多嘗試想用動力學引擎做角色控制器,但實際上沒那麼簡單。Godot 擁有目前最好的動力學角色控制器實作之一(可以在 2D 平台範例中看到),但要使用它需要對物理引擎有相當深的理解與技巧(或者你要有很多耐心來摸索)。

有些物理引擎(像 Havok)認為動力學角色控制器是最佳選擇,但也有其他(例如 PhysX)則更偏好推廣運動學角色控制器。

那麼,兩者到底有什麼區別?:

  • 動力學角色控制器 是用一個具有無限慣性張量的剛體(RigidBody),這種剛體是不能旋轉的。物理引擎會讓物體移動與碰撞,再一起解決碰撞反應。這讓動力學角色控制器可以像平台遊戲範例那樣,和其他物理物件無縫互動。不過,這些互動並非總是可預測的。有時碰撞會需要超過一個影格才能解決,因此碰撞時可能會有輕微的偏移。這些問題雖然能解決,但需要一定程度的技巧。

  • 運動學角色控制器 則假設一開始總是在非碰撞狀態,並且移動時也會保持非碰撞狀態。如果它一開始就處於碰撞狀態,會像剛體一樣嘗試自我脫離,但這是例外狀況。這讓它的控制和移動更加可預測,也更容易寫程式。不過缺點是:不能直接和其他物理物件互動,除非你在程式裡手動處理。

這個簡短教學主要聚焦在運動學角色控制器。它採用傳統的碰撞處理方式(底層未必比較簡單,只是都被很好地包裝在 API 裡了)。

物理處理流程

要管理 Kinematic 角色或物體的邏輯,建議都放在物理處理流程(_physics_process)裡,因為它會在物理步驟前呼叫,並且和物理伺服器同步,而且每秒呼叫次數固定。這樣做,物理和移動的計算會比用一般的 process 更可預測,不會因為影格率太高或太低而產生誤差或抖動。

extends CharacterBody2D

func _physics_process(delta):
    pass

場景設定

要方便測試,這裡有一個場景(來自 TileMap 教學):kinematic_character_2d_starter.zip。我們會為角色建立一個新場景。請使用機器人角色圖,並建立如下圖的場景:

../../_images/kbscene.webp

你會注意到 CollisionShape2D 節點旁邊有個警告圖示,因為還沒設定形狀。請在 CollisionShape2D 的 shape 屬性裡建立一個新的 CircleShape2D,然後點擊 <CircleShape2D> 進入選項,把半徑(radius)設為 30:

../../_images/kbradius.webp

注意:如同之前物理教學提到,物理引擎無法正確處理大多數形狀的縮放(只有碰撞多邊形、平面和線段除外),所以請直接修改形狀的參數(例如半徑),而不是去縮放它。對於 Kinematic/Rigid/Static 這些物理節點本身也是一樣,因為它們的 scale 會影響碰撞形狀。

現在,請為這個角色建立腳本,可以用上面範例作為基礎。

最後,將角色場景實例化到 TileMap 場景中,並把地圖場景設為主場景,這樣按下執行時就會直接運作。

../../_images/kbinstance.webp

移動運動學角色

回到角色場景並打開腳本,魔法即將開始!Kinematic 物件預設什麼都不會做,但它有一個很實用的方法 CharacterBody2D.move_and_collide()。這個方法會接收一個 Vector2 參數,並嘗試讓角色移動這個向量的距離。如果過程中發生碰撞,角色就會停在碰撞點。

所以,讓我們把角色往下移動,直到碰到地板為止:

extends CharacterBody2D

func _physics_process(delta):
    move_and_collide(Vector2(0, 1)) # Move down 1 pixel per physics frame

結果會發現角色會移動,但在碰到地板時就會停下來。很酷吧?

下一步是加上重力,這樣角色行為會更像一般遊戲角色:

extends CharacterBody2D

const GRAVITY = 200.0

func _physics_process(delta):
    velocity.y += delta * GRAVITY

    var motion = velocity * delta
    move_and_collide(motion)

現在角色會順暢地落下了。接下來讓它在按下左右方向鍵時往左右移動。記得這裡的速度單位是像素/秒。

這樣就支援按下左右鍵時角色能左右走動了:

extends CharacterBody2D

const GRAVITY = 200.0
const WALK_SPEED = 200

func _physics_process(delta):
    velocity.y += delta * GRAVITY

    if Input.is_action_pressed("ui_left"):
        velocity.x = -WALK_SPEED
    elif Input.is_action_pressed("ui_right"):
        velocity.x =  WALK_SPEED
    else:
        velocity.x = 0

    # "move_and_slide" already takes delta time into account.
    move_and_slide()

可以試試看效果。

這就是平台遊戲設計的好起點。更完整的範例可以在 Godot 官方提供的 demo zip 或 https://github.com/godotengine/godot-demo-projects/tree/master/2d/kinematic_character 找到。