【Godot】move_and_slideとmove_towardの使い分け:ノックバック処理の実装

作成: 2025-06-20最終更新: 2025-12-16

Godot EngineのCharacterBodyで使われるmove_and_slideと、汎用的なmove_towardの違いを解説。ノックバック、敵の追従、UIアニメーションなど、具体的なコード例を交えて実践的な使い方を紹介します。

概要

Godotでキャラクターを動かそうとすると、move_and_slide()move_toward()という、よく似た名前の2つの関数に出会います。どちらもオブジェクトを移動させるためのものですが、その背景にある仕組みと最適な用途は全く異なります。

この2つの違いを理解することは、物理法則に従ったスムーズな動きや、意図した通りのゲームロジック(例えばノックバック)を実装する上で非常に重要です。

Godotのノックバック処理

move_and_slide(): 物理ワールドの住人

move_and_slide()は、CharacterBody2DCharacterBody3Dといった、物理エンジンによって制御されるノード専用の高機能な移動メソッドです。これを呼び出すだけで、Godotの物理エンジンが裏側で複雑な計算を行い、ごく自然な移動を実現してくれます。

主な特徴:

  • 物理ベースの衝突処理: Godotの物理エンジンと密接に連携し、他の物理ボディやタイルマップとの衝突を自動的に検出し、応答します。オブジェクトがめり込むのを防ぎます。
  • 壁や坂道での滑らかな移動: 壁に衝突した際、オブジェクトは停止するのではなく、壁に沿って「滑る」ように動きます。これにより、特に狭い通路などでの操作性が格段に向上します。また、up_directionプロパティと組み合わせることで、坂道をスムーズに上り下りできます。
  • 動く床への対応: move_and_slide()は動くプラットフォームを自動的に検出し、キャラクターがその上に乗っている間、プラットフォームと一緒に移動するようにvelocityを調整します。
  • velocityプロパティとの連携: この関数はノードのvelocityプロパティを引数として受け取り、移動を実行します。そして重要なことに、衝突によって変化した後の速度を戻り値として返します

基本的な使い方:

move_and_slide()は、物理フレームごとに呼び出される_physics_process(delta)内で使用するのが基本です。

extends CharacterBody2D

const SPEED = 300.0
const JUMP_VELOCITY = -400.0

# プロジェクト設定から重力を取得(デフォルト値: 980)
var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")

func _physics_process(delta):
    # 重力加速度を適用
    if not is_on_floor():
        velocity.y += gravity * delta

    # ジャンプ処理
    if Input.is_action_just_pressed("ui_accept") and is_on_floor():
        velocity.y = JUMP_VELOCITY

    # 左右の入力方向を取得
    var direction = Input.get_axis("ui_left", "ui_right")
    velocity.x = direction * SPEED

    # 物理エンジンに移動と衝突処理を任せる
    move_and_slide()

move_toward(): シンプルな数学的補間

move_toward()は、Godotに組み込まれている汎用的な数学関数です。物理エンジンとは一切関係なく、ある値(数値、ベクトルなど)を、目標値に向かって、指定した分だけ直線的に変化させます。

主な特徴:

  • 物理を完全に無視: この関数は衝突を検知しません。あくまで値の計算を行うだけです。オブジェクトの位置をこの関数で直接変更した場合、壁をすり抜ける可能性があります。
  • 一定速度の変化: 現在地や目標値との距離に関わらず、常に指定された一定のステップ(delta引数)で値を変化させます。これにより、等速での移動や変化が簡単に実現できます。
  • 高い汎用性: Vector2Vector3の位置座標だけでなく、float(HP、経験値)、Color(フェードイン・アウト)、Transform2Dなど、様々なデータ型に対して使用できます。

主な用途:

  1. 非物理オブジェクトの移動: UI要素、背景オブジェクト、カメラなど、コリジョンを必要としないオブジェクトの移動。
  2. 値の平滑化: 急激に変化する値を滑らかにする。例えば、カメラの追従を少し遅らせてスムーズに見せるなど。
  3. 状態変化の演出: HPバーが滑らかに減少する、キャラクターの色が徐々に変わるなど。
  4. 物理演算の補助: velocityのような物理プロパティを直接操作する際に、その値を目標値(例: Vector2.ZERO)に向かって滑らかに変化させるために使われます。ノックバックからの復帰処理はその典型例です。
# 例1: UI要素をターゲット位置へ移動させる
func _process(delta):
    var target_position = get_viewport_rect().size / 2
    # 1秒あたり200ピクセルの速度で中央へ
    position = position.move_toward(target_position, 200 * delta)

# 例2: 敵キャラクターがプレイヤーを追従する
func _physics_process(delta):
    # player_nodeはプレイヤーへの参照
    if player_node:
        var direction = global_position.direction_to(player_node.global_position)
        velocity = direction * ENEMY_SPEED
        move_and_slide()

実践例:状態管理を取り入れたノックバック処理

move_and_slidemove_towardの連携が最も輝く例が「ノックバック」です。ここでは、より堅牢な実装のために簡単なステートマシン(状態管理)の考え方を取り入れます。

extends CharacterBody2D

const SPEED = 300.0
const JUMP_VELOCITY = -400.0
const KNOCKBACK_FRICTION = 500.0

var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")

enum State { MOVE, KNOCKBACK }
var current_state = State.MOVE

var knockback_vector = Vector2.ZERO

func _physics_process(delta):
    match current_state:
        State.MOVE:
            move_state(delta)
        State.KNOCKBACK:
            knockback_state(delta)

# 通常の移動状態
func move_state(delta):
    if not is_on_floor():
        velocity.y += gravity * delta

    if Input.is_action_just_pressed("ui_accept") and is_on_floor():
        velocity.y = JUMP_VELOCITY

    var direction = Input.get_axis("ui_left", "ui_right")
    velocity.x = direction * SPEED

    move_and_slide()

# ノックバック状態
func knockback_state(delta):
    # velocityをノックバックのベクトルで上書き
    velocity = knockback_vector
    move_and_slide() # 衝突検出は物理エンジンに任せる

    # move_towardを使って、ノックバックの力を徐々に0に近づける(減速)
    knockback_vector = knockback_vector.move_toward(Vector2.ZERO, KNOCKBACK_FRICTION * delta)

    # ノックバックの力がほぼ0になったら、通常状態に復帰
    if knockback_vector.is_equal_approx(Vector2.ZERO):
        current_state = State.MOVE

# 外部(例: 敵の攻撃判定エリア)からこの関数を呼び出す
func apply_knockback(direction: Vector2, power: float):
    current_state = State.KNOCKBACK
    knockback_vector = direction.normalized() * power

この実装では、current_stateによってキャラクターの振る舞いを明確に分離しています。State.MOVEではプレイヤーの入力を受け付け、State.KNOCKBACKでは入力を無視して強制的に体を動かします。そして、knockback_state内でmove_towardを使い、knockback_vectorを滑らかに減衰させているのがポイントです。これにより、自然なノックバックからの復帰が実現できます。


よくある間違いとベストプラクティス

これらの関数を使う上で陥りがちな罠と、それを避けるためのベストプラクティスをまとめました。

関数よくある間違いベストプラクティス
move_and_slide()_process内で呼び出している(物理演算と同期しない)必ず_physics_process内で呼び出す。 物理演算は固定フレームレートで実行されるため、同期が不可欠です。
velocityを直接設定せず、positionを変更しようとするCharacterBodyではpositionを直接操作せず、常にvelocityプロパティを介して動きを制御するのが原則です。
move_toward()CharacterBodyの移動に直接使ってしまい、壁をすり抜ける物理的な衝突が必要なオブジェクトには使わない。velocityの値を変化させる補助として使うか、UIなど非物理オブジェクトに限定します。
deltaを乗算し忘れる(フレームレート依存の動きになる)move_toward(target, speed * delta) のように、変化量にdeltaを乗算することで、フレームレートに関わらず一定の速度を保てます。
lerp関数との違いを理解していないmove_towardは等速、lerpは距離に応じて変化量が小さくなる(イージング)。**急停止させたいならmove_toward、滑らかに停止させたいならlerp**が適しています。

パフォーマンスと代替案

  • move_and_slide()のコスト: この関数は内部で複数のレイキャストや衝突チェックを行っており、決して軽い処理ではありません。シーン内に多数のCharacterBodyを配置する場合は、パフォーマンスに影響を与える可能性があります。遠くで動く必要のない敵などは、_physics_process自体を無効化(set_physics_process(false))するなどの最適化が有効です。

  • move_toward() vs Tween: UIのアニメーションや単純なオブジェクトの移動には、Tweenノードも非常に強力な選択肢です。move_towardがコードベースで直線的な動きを制御するのに対し、Tweenはインスペクターから多彩なイージング(緩急)をつけたアニメーションを非同期に設定できます。複雑な演出やシーケンスにはTween、単純な補間や物理プロパティの制御にはmove_towardと使い分けるのが良いでしょう。

まとめ

move_and_slide()move_toward()は、それぞれが異なる哲学を持つ、全く別のツールです。

  • move_and_slide(): 物理世界の法則に従うCharacterBodyのための、衝突や重力を考慮した高レベルな移動手段
  • move_toward(): 物理を無視し、値を目標に近づける数学的な補間ツールvelocityの制御からUIアニメーションまでこなす縁の下の力持ち。

この2つの特性を深く理解し、ノックバック処理のように適切に組み合わせることで、あなたのゲームのキャラクターは格段に生き生きと、そして意図通りに動くようになります。