モンスターの出現
このパートでは、パスに沿ってランダムにモンスターを出現させます。最終的には、ゲームボード上をモンスターが歩き回ることになります。

ファイルシステム ドックで main.tscn をダブルクリックして Main シーンを開きます。
パスを描画する前に、ゲームの解像度を変更します。デフォルトのウィンドウサイズは 1152x648 ですが、これを 720x540 のコンパクトなサイズに設定します。
プロジェクト -> プロジェクト設定...(Project -> Project Settings...)に移動します。

まだ インプットマップ が開いているなら、 一般 タブを開きます。
In the left menu, navigate down to Display -> Window. On the right, set the
Viewport Width to 720 and the Viewport Height to 540.

出現パスの作成
2Dゲームのチュートリアルで行ったように、パスを設計し、その上のランダムな位置をサンプリングするために PathFollow3D ノードを使用します。
しかし、3D の場合、パスを描くのは少し複雑です。モンスターが画面のすぐ外に現れるように、ゲームビューの周囲にパスを描きたいのです。しかし、パスを描くと、カメラのプレビューではそれが見えません。
ビューの制限を確認するために、プレースホルダーメッシュを使用します。ビューポートは引き続き2つの部分に分割されており、下部にカメラプレビューが表示されているはずです。もし表示されていない場合は、 Ctrl + 2 (Cmd + 2 on macOS) を押してビューを2分割してください。次に Camera3D ノードを選択し、下部ビューポートにある Preview チェックボックスをクリックします。

プレースホルダー用のシリンダーを追加する
プレースホルダメッシュを追加しましょう。新しい Node3D を Main ノードの子として追加し Cylinders と名付けます。これをシリンダーのグループとして使います。 Cylinders を選択し、 MeshInstance3D ノードを追加します

インスペクター 内で、 Mesh プロパティに CylinderMesh を割り当てます。

一番上のビューポートを、ビューポートの左上にあるメニューを使って上からの平行投影ビューに設定します。または、キーパッドの7キーを押すこともできます。

グリッドが気になる場合は、非表示にできます。ツールバーの View メニューに移動し、View Grid をクリックして切り替えてください。

次に、下部ビューポートのカメラプレビューを見ながら、円柱を地面の平面上で移動させます。その際、グリッドスナップを使用することをお勧めします。グリッドスナップは、ツールバーの磁石アイコンをクリックするか、Y キーを押して切り替えることができます。

円柱をカメラの視界の左上隅、すぐ外側に移動させてください。

メッシュのコピーを作成し、ゲームエリアの周囲に配置していきます。ノードを複製するには、Ctrl + D`(macOSでは :kbd:`Ctrl + D)を押します。または、Scene *ドック内でノードを右クリックし、*Duplicate を選択することもできます。コピーを青い Z 軸に沿って下に移動させ、カメラプレビューのすぐ外側に配置してください。
Shift キーを押しながら未選択の円柱をクリックして、両方の円柱を選択し、複製します。

赤いX軸をドラッグして右へ移動します。

白ではちょっと見にくいですよね? 新しいマテリアルを与えて、目立たせてあげましょう。
3D では、マテリアルがサーフェスの色や光の反射などの視覚的プロパティを定義します。これを利用して、メッシュの色を変更することができます。
4 つのシリンダーをすべて一度に更新することができます。シーン(Scene)のドック内のメッシュ インスタンスをすべて選択します。これを行うには、最初の 1 つをクリックし、最後の 1 つを Shift キーを押しながらクリックします。

インスペクタ で、 マテリアル セクションを開いて StandardMaterial3D を 0 に割り当てます。

球体アイコンをクリックしてマテリアルリソースを開きます。マテリアルのプレビューと、プロパティが詰まったセクションの長いリストが表示されます。これらを使って、金属や岩、さらには水など、さまざまな表面を作成できます。
Albedo セクションを開きましょう。
明るいオレンジ色のような、背景と対照的な色に設定してください。

これで、シリンダーをガイドとして使用できるようになりました。シーン(Scene)のドックで、Cylindersの横にあるグレーの矢印をクリックして、シリンダーを折りたたみます。また、Cylindersの横にある目のアイコンをクリックすると、その可視性を切り替えることができます。

Main ノードの子ノード Path3D を追加します。ツールバーに4つのアイコンが出現します。緑色の "+" アイコンの Add Point ツールをクリックしましょう。

注釈
アイコンにカーソルを合わせると、そのツールの説明をツールチップで見ることができます。
各シリンダーの中心をクリックして、ポイントを作成します。次に、ツールバーの曲線を閉じる(Close Curve) アイコンをクリックし、パスを閉じます。点が少しずれている場合は、その点をクリックしてドラッグすることで位置を変更できます。

パスはこのようになります。

ここからランダムな場所を得るため、 PathFollow3D ノードが必要です。 PathFollow3D を Path3D の子として追加します。2つのノードにそれぞれ SpawnLocation と SpawnPath と名前を付けましょう。これで何に使うかがより明示的になります。

これで、出現メカニズムのコードが準備できました。
ランダムなモブの生成
Main ノードを右クリックし、新しいスクリプトをアタッチします。
まずは変数を インスペクタ にエクスポートして mob.tscn や他のモンスタースクリプトを割り当てられるようにします。
extends Node
@export var mob_scene: PackedScene
using Godot;
public partial class Main : Node
{
// Don't forget to rebuild the project so the editor knows about the new export variable.
[Export]
public PackedScene MobScene { get; set; }
}
モンスターは一定時間ごとに出現させたいと思います。そのためにはシーンに戻りタイマーを追加しなければなりません。しかしその前に、 mob.tscn ファイルを上の mob_scene プロパティに割り当てておきましょう(そうしないとnullのままです!)
3Dシーンに戻り Main ノードを選択します。 mob.tscn を ファイルシステム から インスペクタ の Mob Scene スロットにドラッグしましょう。

新しいTimerノードを Main の子として追加します。 MobTimer と名付けましょう。

インスペクター(Inspector)でWait Timeを0.5秒に設定し、ゲーム実行時に自動的に起動するように Autostartをオンにします。

タイマーは、timeoutシグナルを、Wait Timeが終了する度に出します。デフォルトでは、自動的にリスタートし、1周期でシグナルを発信します。Mainノードからこのシグナルに接続することで、0.5秒ごとにモンスターを出現させることができます。
MobTimer を選択したまま、右の ノード ドックに移動し、 timeout シグナルをダブルクリックしましょう。

Main ノードに接続します。

これでスクリプトに戻り、新しく空の _on_mob_timer_timeout() 関数が追加されます。
モブの出現ロジックをコーディングしてみましょう。次のように行います:
モブシーンをインスタンス化します。
出現パスのランダムな位置をサンプリングします。
プレイヤーの位置を取得します。
モブの
initialize()メソッドを呼び、ランダムな位置とプレイヤーの位置を渡します。Mainノードの子としてモブを追加します。
func _on_mob_timer_timeout():
# Create a new instance of the Mob scene.
var mob = mob_scene.instantiate()
# Choose a random location on the SpawnPath.
# We store the reference to the SpawnLocation node.
var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
# And give it a random offset.
mob_spawn_location.progress_ratio = randf()
var player_position = $Player.position
mob.initialize(mob_spawn_location.position, player_position)
# Spawn the mob by adding it to the Main scene.
add_child(mob)
// We also specified this function name in PascalCase in the editor's connection window.
private void OnMobTimerTimeout()
{
// Create a new instance of the Mob scene.
Mob mob = MobScene.Instantiate<Mob>();
// Choose a random location on the SpawnPath.
// We store the reference to the SpawnLocation node.
var mobSpawnLocation = GetNode<PathFollow3D>("SpawnPath/SpawnLocation");
// And give it a random offset.
mobSpawnLocation.ProgressRatio = GD.Randf();
Vector3 playerPosition = GetNode<Player>("Player").Position;
mob.Initialize(mobSpawnLocation.Position, playerPosition);
// Spawn the mob by adding it to the Main scene.
AddChild(mob);
}
上の randf() は 0 から 1 の間のランダムな値を生成し、これは PathFollow ノードの progress_ratio が期待する値です: 0 がパスの始点、1 がパスの終点になります。設置したパスはカメラのビューポートを囲んでいるので、0から 1 のランダムな値はビューポートの端に沿ったランダムな位置になります!
もしメインシーンから Player を削除した場合、以下の行に注意してください
var player_position = $Player.position
Vector3 playerPosition = GetNode<Player>("Player").Position;
$Player が無いのでエラーが発生します!
参考までに、main.gdスクリプトの完全なコードを以下に示します。
extends Node
@export var mob_scene: PackedScene
func _on_mob_timer_timeout():
# Create a new instance of the Mob scene.
var mob = mob_scene.instantiate()
# Choose a random location on the SpawnPath.
# We store the reference to the SpawnLocation node.
var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
# And give it a random offset.
mob_spawn_location.progress_ratio = randf()
var player_position = $Player.position
mob.initialize(mob_spawn_location.position, player_position)
# Spawn the mob by adding it to the Main scene.
add_child(mob)
using Godot;
public partial class Main : Node
{
[Export]
public PackedScene MobScene { get; set; }
private void OnMobTimerTimeout()
{
// Create a new instance of the Mob scene.
Mob mob = MobScene.Instantiate<Mob>();
// Choose a random location on the SpawnPath.
// We store the reference to the SpawnLocation node.
var mobSpawnLocation = GetNode<PathFollow3D>("SpawnPath/SpawnLocation");
// And give it a random offset.
mobSpawnLocation.ProgressRatio = GD.Randf();
Vector3 playerPosition = GetNode<Player>("Player").Position;
mob.Initialize(mobSpawnLocation.Position, playerPosition);
// Spawn the mob by adding it to the Main scene.
AddChild(mob);
}
}
F6 を押してシーンをテストすることができます。モンスターが出現して一直線に移動するのが見えるはずです。

今のところ、モブは互いにぶつかり合い、すれ違うだけです。この点については、次回以降で触れていきます。