車のステアリング操作
課題
2D見下ろし方式カーコントローラーを作成したい。
解決策
この問題に取り組む際、初心者はしばしば実際の車とはかけ離れた挙動のゲームを作ってしまいがちです。アマチュアが作ったカーゲームでよく見られる失敗例をご紹介します。
- 自動車は中心軸を中心に旋回するものではありません。別の言い方をすれば、後輪が左右に滑ることはありません。(ドリフト走行時はこの限りではないが、その点については後述します)
- 自動車は動いている時にのみ方向転換が可能で、その場で回転することはできません。
- 自動車は列車ではありません。レールに縛られていません。高速で曲がる際にはある程度の横滑り(ドリフト)が伴うべきです。
2Dカー物理の実装には様々なアプローチ方法があり、主に「リアル志向」か「アーケードスタイル」かによって選択が分かれます。ここでは、リアリティよりもアクション性を優先する「アーケードレベル」の現実感を追求していきます。
以下の方法は、こちらのアルゴリズムに基づいています。 https://engineeringdotnet.blogspot.com/2010/04/simple-2d-car-physics-in-games.html
以下のレシピは5つのセクションに分かれており、それぞれが車の異なる動作要素を追加してください。必要に応じて自由に組み合わせてご使用ください。
シーン設定
以下が車シーンの設定内容です。
CharacterBody2D
Sprite2D
CollisionShape2D
Camera2D
このデモでは、Kenneyのレーシングパックのアートワークを使用します。CapsuleShape2Dは衝突判定に最適な形状です。これにより、車が障害物に引っかかるような鋭い角を防ぐことができます。
以下の4つの入力操作も使用します。「右旋回」「左旋回」「加速」「ブレーキ」。お好みのキー割り当てを設定してください。
その1:動き
最初のステップは、前述のアルゴリズムに基づいて動作をコーディングすることです。
以下の変数から始めてください。
extends CharacterBody2D
var wheel_base = 70 # Distance from front to rear wheel
var steering_angle = 15 # Amount that front wheel turns, in degrees
var steer_direction
wheelbase をスプライトに適した値に設定してください。
steer_direction には車輪の回転量が設定されます。
キーボード操作を使用している場合、ターンはオン/オフの二択になります。アナログジョイスティックを使用する場合は、代わりにスティックの移動距離に応じてこの値を調整できます。
func _physics_process(delta):
get_input()
calculate_steering(delta)
move_and_slide()
各フレームでは、入力のチェックとステアリング計算が必要です。その後、算出された速度を move_and_slide() 関数に渡します。これら2つの関数については次に定義します。
func get_input():
var turn = Input.get_axis("steer_left", "steer_right")
steer_direction = turn * deg_to_rad(steering_angle)
velocity = Vector2.ZERO
if Input.is_action_pressed("accelerate"):
velocity = transform.x * 500
ここではユーザー入力を確認し、移動速度を設定します。注意:速度500は一時的なもので、動きのテスト用です。これについては次のセクションで対処します。
以下に、リンク先のアルゴリズムを実装します。
func calculate_steering(delta):
# 1. Find the wheel positions
var rear_wheel = position - transform.x * wheel_base / 2.0
var front_wheel = position + transform.x * wheel_base / 2.0
# 2. Move the wheels forward
rear_wheel += velocity * delta
front_wheel += velocity.rotated(steer_direction) * delta
# 3. Find the new direction vector
var new_heading = rear_wheel.direction_to(front_wheel)
# 4. Set the velocity and rotation to the new direction
velocity = new_heading * velocity.length()
rotation = new_heading.angle()
プロジェクトを実行すると、車が動き始め、方向転換するはずです。ただし現在の動作はまだ不自然で、車の動きが瞬時に開始・停止します。これを改善するため、計算に加速度を追加してください。
その2:加速について
以下の設定変数と、車全体の加速を追跡するための変数が必要になります。
var engine_power = 900 # Forward acceleration force.
var acceleration = Vector2.ZERO
入力コードを変更して、車の velocity を直接変更するのではなく、加速度を適用するようにします。
func get_input():
var turn = Input.get_axis("steer_left", "steer_right")
steer_direction = turn * deg_to_rad(steering_angle)
if Input.is_action_pressed("accelerate"):
acceleration = transform.x * engine_power
一旦加速度が得られたら、以下のように速度に適用できます。
func _physics_process(delta):
acceleration = Vector2.ZERO
get_input()
calculate_steering(delta)
velocity += acceleration * delta
move_and_slide()
今から車を走らせると、徐々に速度が上がっていきます。注意:まだ減速する方法はないんですよ!
その3:摩擦・抵抗について
自動車には2種類の異なる減速力が作用します。摩擦力と空気抵抗です。
摩擦力は路面が車に及ぼす抵抗力です。砂地では非常に強いですが、氷上では極めて小さくなります。この力は速度に比例し、スピードが速いほど大きくなります。
ドラッグは空気抵抗によって生じる力です。車両の断面積が大きいほど影響が大きくなります。例えば、流線型のレースカーに比べて大型トラックやバンの方がより大きな抗力を受けます。ドラッグの値は速度の二乗に比例します。
これにより、低速走行時には摩擦抵抗がより顕著になる一方、高速域では空気抵抗が支配的になります。これら両方の力を計算に含めます。さらに、これらの量の値は、車の最高速度――エンジン出力がもはや空気抵抗に打ち勝てなくなる点――を示す指標にもなります。
以下にこれらの数量に対する初期値を示します。
var friction = -55
var drag = -0.06
このグラフから分かるように、これらの値は、速度が600に達した時点で、抗力が摩擦力を上回ることを示しています。
こちらのツールで値を変更してその影響を確認できます。 https://www.desmos.com/calculator/e4ayu3xkip
関数 _physics_process() では、現在の摩擦係数を計算する関数を呼び出し、それを加速度力に適用します。
func _physics_process(delta):
acceleration = Vector2.ZERO
get_input()
apply_friction(delta)
calculate_steering(delta)
velocity += acceleration * delta
velocity = move_and_slide(velocity)
func apply_friction(delta):
if acceleration == Vector2.ZERO and velocity.length() < 50:
velocity = Vector2.ZERO
var friction_force = velocity * friction * delta
var drag_force = velocity * velocity.length() * drag * delta
acceleration += drag_force + friction_force
まず、最低速度を設定してください。これにより、摩擦力が完全に車の速度をゼロにさせない場合でも、車両が極端に低速で前進し続けるのを防ぐことができます。
その後、これら2つの力を計算し、合計加速度に加算します。どちらも負の値なので、車には逆方向に作用になります。
その4:リバース/ブレーキ操作
以下の 2 つの設定変数も必要です。
var braking = -450
var max_speed_reverse = 250
get_input()で入力できるようにしましょう。
if Input.is_action_pressed("brake"):
acceleration = transform.x * braking
これは停止時には問題ありませんが、リバースギアに入れたときに正常に動作しない問題があります。現在、加速は常に「進行方向」(前進)に対して適用されているため、バック走行時に逆方向に加速することができません。リバース時は後方への加速度を加える必要があります。
func calculate_steering(delta):
var rear_wheel = position - transform.x * wheel_base / 2.0
var front_wheel = position + transform.x * wheel_base / 2.0
rear_wheel += velocity * delta
front_wheel += velocity.rotated(steer_angle) * delta
var new_heading = (front_wheel - rear_wheel).normalized()
var d = new_heading.dot(velocity.normalized())
if d > 0:
velocity = new_heading * velocity.length()
if d < 0:
velocity = -new_heading * min(velocity.length(), max_speed_reverse)
rotation = new_heading.angle()
ドット積を使用することで、前進または後退の加速状態を判定できます。2つのベクトルが一致していれば、結果は 0 より大きくなります。移動方向が車の向いている方向と逆向きの場合、ドット積は 0 未満となり、後退中であることが分かります。
その5:ドリフト/スライド操作
ここで止めても十分満足のいく運転体験が得られるでしょう。ただ、車がまだ「レールに乗っている」ような感覚です。最高速で走行していても、カーブは完璧にクリアされ、まるでタイヤが完全に路面を掴んでいるみたいです。
高速時(あるいは低速でもお好みで)には、駆動力によってタイヤが滑り、車体が「魚の尾のように」左右に滑る動きが生じるはずです。
var slip_speed = 400 # Speed where traction is reduced
var traction_fast = 2.5 # High-speed traction
var traction_slow = 10 # Low-speed traction
これらの値は、ステアリング計算時に適用します。現在の設定では、速度が瞬時に新しい進行方向に切り替わるようになっています。代わりに、補間処理(lerp())を使用して、車両が新しい方向に完全に向きを変えることなく、部分的にしか旋回しないようにします。その際、「トラクション」値がタイヤの「粘着性」を決定します。
func calculate_steering(delta):
var rear_wheel = position - transform.x * wheel_base / 2.0
var front_wheel = position + transform.x * wheel_base / 2.0
rear_wheel += velocity * delta
front_wheel += velocity.rotated(steer_angle) * delta
var new_heading = (front_wheel - rear_wheel).normalized()
# choose which traction value to use - at lower speeds, slip should be low
var traction = traction_slow
if velocity.length() > slip_speed:
traction = traction_fast
var d = new_heading.dot(velocity.normalized())
if d > 0:
velocity = lerp(velocity, new_heading * velocity.length(), traction * delta)
if d < 0:
velocity = -new_heading * min(velocity.length(), max_speed_reverse)
rotation = new_heading.angle()
ここでは、使用する牽引値を選択し、velocityにlerp()を適用します。
調整項目
この時点で、車両の挙動を制御する多数の設定項目があります。これらを調整することで、車の運転特性を大きく変更できます。さまざまな値を試す作業をより簡単にするため、以下にレシピ用プロジェクトをダウンロードしてください。ゲームを起動すると、走行中に車の挙動を変更可能なスライダーパネルが表示されます(<Tab>キーでスライダーパネルの表示/非表示を切り替え可能)。
関連レシピ
プロジェクトのダウンロード
プロジェクトコードはこちらからダウンロードできます。https://github.com/godotrecipes/2d_car_estrada