キャラクターのアニメーション

最後のレッスンでは、Godot に内蔵されたアニメーションツールを使って、キャラクターを浮かせたり羽ばたかせたりします。エディターでアニメーションをデザインし、プログラムのコーディングを行ってゲームを生き生きとしたものにすることを学びます。

image0

まずはアニメーションエディターの使い方から始めます。

アニメーションエディターを使う

Godot エンジンのエディタには、アニメーションを作成するためのツールが付属しています。さらに、Godot エンジンのエディタでコードを書いて、実行時にアニメーションを再生したり、制御したりすることができます。

プレイヤーシーンを開き、 Player ノードを選択し、 AnimationPlayer ノードを追加します。

下のパネルに アニメーション (Animation)のドックが表示されます。

image1

上部にはツールバーとアニメーションのドロップダウンメニュー、中央にはトラックエディター(現在は空)、下部にはフィルター、スナップ、ズームの各オプションを備えています。

それではアニメーションを作ってみましょう。 アニメーション (Animation) -> New... をクリックします。

image2

アニメーションの名前は "float" にします。

image3

アニメーションを作成すると、タイムラインに秒単位の時間が表示されます。

image4

ゲーム開始時に自動的にアニメーションの再生が開始されるようにします。また、アニメーションがループ再生するようにします。

これを行うには、アニメーションツールバーの自動再生ボタン (アニメーションの自動再生) とアニメーションループの矢印をそれぞれクリックします。

image5

また、右上のピンアイコンをクリックすることで、アニメーションエディタを固定することが可能です。これにより、ビューポートをクリックしてノードの選択を解除したときに、エディタが折りたたまれるのを防ぐことができます。

\ image6

ドック右上のアニメーションの長さを 1.2 秒に設定します。

灰色のリボンが少し広がっているのが見えるはずです。これはアニメーションの開始と終了を表し、青い縦線がタイムカーソルになります。

\ image7

右下のスライダーをクリックし、左右にドラッグすることで、タイムラインの拡大・縮小が可能です。

\ image8

浮遊アニメーション

アニメーション プレーヤー ノードを使用すると、必要な数だけノード上のほとんどのプロパティをアニメーション化できます。 インスペクター (Inspector) のプロパティの横にある鍵のアイコンに注目してください。これらをクリックすると、対応するプロパティの時間と値のペアであるキーフレームを作成することができます。キーフレームは、タイムライン上のタイムカーソルがある場所に挿入されます。

最初のキーを挿入してみましょう。ここでは Character ノードの位置と回転の両方をアニメーションさせます。

Character を選択し、 インスペクター (Inspector)で Transform セクションを展開します。 Position(位置)Rotation(回転) の横にある鍵のアイコンをクリックします。

\ image9

../../_images/curves.webp

このチュートリアルでは、デフォルトの選択としてRESETトラックを作成します

エディタに2つのトラックが表示され、それぞれのキーフレームを表すひし形のアイコン(◆)が表示されます。

\ image10

ひし形のアイコンをクリック&ドラッグすると、時間軸の中を移動させることができます。position のキーを 0.3 秒、rotation のキーを 0.1 秒に合わせてください。

\ image11

Move the time cursor to 0.5 seconds by clicking and dragging on the gray timeline, or by entering it into the input field.

\ timeline_05_click

インスペクター (Inspector)で、 Position(位置)y 軸を 0.65 メートルに、 Rotation(回転)x 軸を 8 ° に設定します。

インスペクター (Inspector)パネルにプロパティが表示されていない場合は、まず シーン (Scene)ドックにある Character ノードをもう一度クリックしてください。

\ image12

両方のプロパティのキーフレームを作成します

\ second_keys_both

次に、position のキーフレームをタイムライン上でドラッグして、 0.7 秒まで移動させてください。

\ image13

注釈

アニメーションの原理についての講義は、このチュートリアルの範囲を超えています。ただ、すべての動作のタイミングとスペーシングを均等にしたいわけではないことに注意してください。アニメーターはアニメーションの2つの基本原則であるタイミングとスペーシングを使います。キャラクターを生きているように感じさせるには、オフセット(前の位置からどれくらいキャラクターをずらしたか)とコントラスト(どれくらい違いがあるか)をキャラクターの動きに設定することが必要です。

タイムカーソルをアニメーションの終了地点( 1.2 秒)に合わせます。Y軸の位置を約 0.35 、X軸の回転を -9 度に設定します。もう一度、両方のプロパティにキーを作成します。

\ animation_final_keyframes

再生ボタンをクリックするか、 Shift + D を押すと、結果をプレビューすることができます。停止ボタンをクリックするか S を押すと再生が停止します。

\ image14

エンジンがキーフレーム間を補間して連続したアニメーションを生成しているのがわかります。しかし現時点では、この動きは非常に機械的な感じがします。これは、デフォルトの補間が直線的で、現実世界での生き物の動きとは異なり、キーフレーム間の動作が一定だからです。

イージングカーブ(モーションカーブ)を使い、キーフレーム間の遷移をコントロールすることができます。

タイムライン上の最初の2つのキー周辺をクリックしてドラッグし、ボックス選択します。

\ image15

インスペクター (Inspector) では、両方のキーのプロパティを同時に編集できます。そこに Easing のプロパティが表示されています。

\ image16

線をクリックし、ドラッグして左方向に動かします。これによりイーズアウト(ease-out)、つまり最初は速く移行し、タイムカーソルが次のキーフレームに近づくにつれて遅くなるようにします。

\ image17

もう一度アニメーションを再生して、その違いを確認しましょう。もう既に、前半の動作は少し弾力が増したように感じるはずです。

回転(rotation_degrees)トラックの2番目のキーフレームにイーズアウトを適用してみてください。

image18

2つ目の位置にあるキーフレームは逆に右にドラッグしてください。

\ image19

アニメーションはこのような感じになります。

\ image20

注釈

アニメーションは、フレームごとにアニメーション ノードのプロパティを更新し、初期値を上書きします。 Player ノードを直接アニメーションさせると、コードを使って移動や回転を行うことができなくなります。そこで、 Pivot ノードが役に立ちます。 Character をアニメーションしていても、 Pivot を移動および回転させて、アニメーションしているキャラクターの移動や回転をスクリプト内で行うことができるのです。

ゲームをプレイすると、プレイヤーのクリーチャーが浮き上がるようになりました!

クリーチャーが少し床に近すぎる場合は、 Pivot を上に動かして高さを調整することができます。

コードからアニメーションを制御する

コーディングによって、プレイヤーの入力に応じてアニメーションの再生を制御することができます。キャラクターが動いているときのアニメーションの速度を変えてみましょう。

Player ノードの横にあるスクリプトアイコンをクリックして Player のスクリプトを開きます。

\ image21

_physics_process() 関数の中で、 direction ベクトルを確認する処理の後の行に、次のコードを追加します。

func _physics_process(delta):
    #...
    if direction != Vector3.ZERO:
        #...
        $AnimationPlayer.speed_scale = 4
    else:
        $AnimationPlayer.speed_scale = 1

このコードでは、プレイヤーがクリーチャーを動かしたときに、再生速度を 4 倍にします。停止したら、再生速度を通常に戻します。

先ほど、 Pivot ノードは親ノードのアニメーションに移動や回転などの変化を追加することができると述べました。そこで、次のコードでキャラクターがジャンプした時に弧を描かせることができます。これを _physics_process() 関数の最後に追加します。

func _physics_process(delta):
    #...
    $Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse

モブキャラクターをアニメーションさせる

Godot のアニメーションには、もう一つ便利な機能があります。同じようなノード構造を使用する限り、そのノードに設定されているアニメーションを異なるシーンにコピーできるのです。

例えば、 Mob シーンと Player シーンの両方が Pivot ノードと Character ノードを持っているので、これらの間でアニメーションを再利用することができます。

Player シーンを開き、AnimationPlayerノードを選択し、下部のメニュー内の アニメーション > アニメーションの管理... をクリックします。 float アニメーションの横にある クリップボードにアニメーションをコピー ボタン(2つの小さな四角のアイコンのボタン)をクリックします。OKをクリックしてウィンドウを閉じます。

Then open mob.tscn, create an AnimationPlayer child node and select it. Click Animation > Manage Animations, then New Library. You should see the message "Global library will be created." Leave the text field blank and click OK. Click the Paste icon (clipboard) and it should appear in the window. Click OK to close the window.

次に、下のパネルのアニメーション・エディターで、自動再生ボタン (アニメーションの自動再生) とアニメーションループのアイコン (Animation looping) もオンになっていることを確認しましょう。これですべてのモンスターが宙に浮かぶアニメーションを再生するようになりました。

クリーチャーの random_speed に基づいて、アニメーションの再生速度を変更できます。 Mob のスクリプトを開き、 initialize() 関数の最後に、次の行を追加してください。

func initialize(start_position, player_position):
    #...
    $AnimationPlayer.speed_scale = random_speed / min_speed

これで、初めての完璧な3Dゲームのコーディングが完了しました。

おめでとうございます!

次のパートでは、学んだことを簡単にまとめ、さらに発展的な内容を学ぶためのリンクもいくつか紹介します。しかしその前に、ここに player.gdmob.gd の全体のコードがあるので、自分のコードと照らし合わせてチェックしてみてください。

Player スクリプトは以下の通りです。

extends CharacterBody3D

signal hit

# How fast the player moves in meters per second.
@export var speed = 14
# The downward acceleration while in the air, in meters per second squared.
@export var fall_acceleration = 75
# Vertical impulse applied to the character upon jumping in meters per second.
@export var jump_impulse = 20
# Vertical impulse applied to the character upon bouncing over a mob
# in meters per second.
@export var bounce_impulse = 16

var target_velocity = Vector3.ZERO


func _physics_process(delta):
    # We create a local variable to store the input direction
    var direction = Vector3.ZERO

    # We check for each move input and update the direction accordingly
    if Input.is_action_pressed("move_right"):
        direction.x = direction.x + 1
    if Input.is_action_pressed("move_left"):
        direction.x = direction.x - 1
    if Input.is_action_pressed("move_back"):
        # Notice how we are working with the vector's x and z axes.
        # In 3D, the XZ plane is the ground plane.
        direction.z = direction.z + 1
    if Input.is_action_pressed("move_forward"):
        direction.z = direction.z - 1

    # Prevent diagonal movement being very fast
    if direction != Vector3.ZERO:
        direction = direction.normalized()
        # Setting the basis property will affect the rotation of the node.
        $Pivot.basis = Basis.looking_at(direction)
        $AnimationPlayer.speed_scale = 4
    else:
        $AnimationPlayer.speed_scale = 1

    # Ground Velocity
    target_velocity.x = direction.x * speed
    target_velocity.z = direction.z * speed

    # Vertical Velocity
    if not is_on_floor(): # If in the air, fall towards the floor
        target_velocity.y = target_velocity.y - (fall_acceleration * delta)

    # Jumping.
    if is_on_floor() and Input.is_action_just_pressed("jump"):
        target_velocity.y = jump_impulse

    # Iterate through all collisions that occurred this frame
    # in C this would be for(int i = 0; i < collisions.Count; i++)
    for index in range(get_slide_collision_count()):
        # We get one of the collisions with the player
        var collision = get_slide_collision(index)

        # If the collision is with ground
        if collision.get_collider() == null:
            continue

        # If the collider is with a mob
        if collision.get_collider().is_in_group("mob"):
            var mob = collision.get_collider()
            # we check that we are hitting it from above.
            if Vector3.UP.dot(collision.get_normal()) > 0.1:
                # If so, we squash it and bounce.
                mob.squash()
                target_velocity.y = bounce_impulse
                # Prevent further duplicate calls.
                break

    # Moving the Character
    velocity = target_velocity
    move_and_slide()

    $Pivot.rotation.x = PI / 6 * velocity.y / jump_impulse

# And this function at the bottom.
func die():
    hit.emit()
    queue_free()

func _on_mob_detector_body_entered(body):
    die()

Mob のスクリプトは以下の通りです。

extends CharacterBody3D

# Minimum speed of the mob in meters per second.
@export var min_speed = 10
# Maximum speed of the mob in meters per second.
@export var max_speed = 18

# Emitted when the player jumped on the mob
signal squashed

func _physics_process(_delta):
    move_and_slide()

# This function will be called from the Main scene.
func initialize(start_position, player_position):
    # We position the mob by placing it at start_position
    # and rotate it towards player_position, so it looks at the player.
    look_at_from_position(start_position, player_position, Vector3.UP)
    # Rotate this mob randomly within range of -45 and +45 degrees,
    # so that it doesn't move directly towards the player.
    rotate_y(randf_range(-PI / 4, PI / 4))

    # We calculate a random speed (integer)
    var random_speed = randi_range(min_speed, max_speed)
    # We calculate a forward velocity that represents the speed.
    velocity = Vector3.FORWARD * random_speed
    # We then rotate the velocity vector based on the mob's Y rotation
    # in order to move in the direction the mob is looking.
    velocity = velocity.rotated(Vector3.UP, rotation.y)

    $AnimationPlayer.speed_scale = random_speed / min_speed

func _on_visible_on_screen_notifier_3d_screen_exited():
    queue_free()

func squash():
    squashed.emit()
    queue_free() # Destroy this node