転がるキューブ
課題
3Dで回転するキューブを作成したい。
解決策
キューブを転がすのは見た目より難しいです。単に中心軸を中心に回すだけではうまくいきません:
代わりに、キューブを底面のエッジを中心に回転させます。
ここが重要なポイントです。どの底面の縁でしょうか? それは、キューブがどの方向に転がっているかによって異なります。
このレシピを作成するにあたり、異なる解決策を試しました。
- 純粋数学 - 回転変換の計算と適用
- AnimationPlayer - アニメーションを用いた回転およびオフセット制御
- ヘルパーノード - RotationHelperとしてNode3Dを利用
すべて正常に動作しましたが、最後のオプションが最も柔軟で設定しやすいと感じたので、ここではその方法を採用します。
ノード設定
Cube: CharacterBody3D
Pivot: Node3D
Mesh: MeshInstance3D
Collision: CollisionShape3D
衝突を検知するノードとして RigidBody3D、CharacterBody3D、または Area3Dを使用することも可能です。ただし、移動制御の方法には若干の違いが生じます。どのノードを選択するかは、ゲームで他にどのような挙動を必要とするかによって決めるべきです。このレシピでは、単に動きに焦点を当てています。
デフォルトでは、すべてが座標 (0, 0, 0) を中心に配置されています。まず最初に行うことは、キューブの底面中央が CharacterBody3D の位置と一致するように全体をオフセットすることです。
デフォルトのサイズが (1, 1, 1) の場合、以下のように設定します。メッシュノードと衝突判定ノードを両方とも (0, 0.5, 0) に移動し、その他は元のままにします。これでルートノードを選択すると、その位置がキューブの 底面 に対応します。
これでキューブを転がしたい場合、Pivotを「移動させたい方向」に0.5ユニット動かす必要があります。メッシュはオブジェクトに取り付けられているため、反対方向に同じ量だけ動かさなければなりません。例えば、右方向へ転がす場合(+X軸方向)、最終的に以下のコードになります。
現在、ピボットノードは正しいエッジ上に配置されており、これを回転させるとメッシュ全体が一緒に回転します。
移動スクリプト
この動作は3つのステップに分かれています。
ステップ1
以下のように、先に示した2つのオフセットを適用します。Pivotを移動方向にシフトし、Meshをその反対方向にシフトします。
ステップ2
このステップでは、回転アニメーションを実装します。方向ベクトルと下方ベクトルの外積を用いて、回転軸を計算します。その後、Tween を使用してピボット要素の transform プロパティをアニメーションさせ、滑らかな回転効果を実現します。
ステップ3
アニメーションが終了したら、初期状態にリセットして次回の動作に備えます。キューブが選択方向に1単位移動し(サイズ1のキューブの場合)、かつピボットとメッシュが元の位置に戻るようにしたいということです。
extends CharacterBody3D
@onready var pivot = $Pivot
@onready var mesh = $Pivot/MeshInstance3D
var cube_size = 1.0
var speed = 4.0
var rolling = false
func _physics_process(delta):
var forward = Vector3.FORWARD
if Input.is_action_pressed("ui_up"):
roll(forward)
if Input.is_action_pressed("ui_down"):
roll(-forward)
if Input.is_action_pressed("ui_right"):
roll(forward.cross(Vector3.UP))
if Input.is_action_pressed("ui_left"):
roll(-forward.cross(Vector3.UP))
func roll(dir):
# Do nothing if we're currently rolling.
if rolling:
return
rolling = true
# Step 1: Offset the pivot.
pivot.translate(dir * cube_size / 2)
mesh.global_translate(-dir * cube_size / 2)
# Step 2: Animate the rotation.
var axis = dir.cross(Vector3.DOWN)
var tween = create_tween()
tween.tween_property(pivot, "transform",
pivot.transform.rotated_local(axis, PI/2), 1 / speed)
await tween.finished
# Step 3: Finalize the movement and reset the offset.
transform.origin += dir * cube_size
var b = mesh.global_transform.basis
pivot.transform = Transform3D.IDENTITY
mesh.position = Vector3(0, cube_size / 2, 0)
mesh.global_transform.basis = b
rolling = false
キューブのテクスチャが非対称の場合、転がすたびにリセットされることに気付くかもしれません。メッシュの回転を保持するには、以下を追加してください。
ステップ1では:
翻訳を修正してください: mesh.translate(-dir) を mesh.global_translate(-dir) に変更してください。
ステップ3では:
Mesh の回転を保持するために 2 行追加してください。
# Step 3: Finalize the movement and reset the offset.
transform.origin += dir * cube_size
var b = mesh.global_transform.basis # Save the mesh rotation.
pivot.transform = Transform3D.IDENTITY
mesh.position = Vector3(0, cube_size / 2, 0)
mesh.global_transform.basis = b # Restore the mesh rotation.
衝突のチェック
ゲームに障害物を導入する場合、移動前に衝突判定を行えます(他のグリッドベース移動方式と同様)。移動処理のステップ1の前に、レイキャストによる衝突チェックを追加してください。
# Cast a ray before moving to check for obstacles
var space = get_world_3d().direct_space_state
var ray = PhysicsRayQueryParameters3D.create(mesh.global_position,
mesh.global_position + dir * cube_size, collision_mask, [self])
var collision = space.intersect_ray(ray)
if collision:
return
以下の方法も使用できます。RayCast3Dノードを使用する場合。ただし、チェックを実行する前に必ずforce_raycast_update()を呼び出すようにしてください。
トランジションで遊んでみよう
使用するTransitionTypeを変更することで、キューブの転がり動作に様々な個性を持たせることができます。デフォルトはTween.TRANS_LINEARで、これにより移動全体を通じて一定速度が得られます。
移行タイプを変更するだけで、全く異なる印象を得られます。例えば。
var tween = create_tween().set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN)
プロジェクトのダウンロード
プロジェクトコードはこちらからダウンロードできます。https://github.com/godotrecipes/rolling_cube