最初のゲーム

概要

このチュートリアルではあなたの最初のGodotのプロジェクトの作りかたを紹介します。エディタの使い方、プロジェクトの構成方法、2Dゲームの作りかたを学びます。

注釈

このプロジェクトはGodotエンジンへの入門用です。これは、あなたがすでにプログラミングの経験があるものとして書かれています。もし、まったくプログラミングの経験がなければ、スクリプトから開始してください。

このゲームは「クリープをよけろ!」といいます。あなたのキャラクターはできるだけ長く動いて敵を避けなければなりません。以下が最終結果のプレビューです:

../../_images/dodge_preview.gif

なぜ2Dから? 3Dゲームは2Dより複雑です。ゲーム開発のプロセスやGodotの使い方をよく理解するまでは、2Dにとどまるべきです。

プロジェクトの設定

Godotを起動して新しいプロジェクトを作ります。dodge_assets.zipをダウンロードします。これにゲームを作るのに使う画像とサウンドが含まれます。これらのファイルをプロジェクトのフォルダに展開します。

注釈

このチュートリアルでは、Godotエディタの使い方が分かることを前提としています。まだシーンとノードを読んでいない場合は、プロジェクトのセットアップ方法とエディタの使用についての説明があるので、先に読んでください。

このゲームはポートレートモードを使うので、ゲームウィンドウのサイズを設定する必要があります。プロジェクト -> プロジェクト設定 -> Display -> Windowの順にクリックして 「Width」 を 480 に、「Height」を 720 にセットします。

このセクションでは、「Stretch」オプションのところで、 Mode を「2d」に、 Aspect を「keep」にします。これによって、異なるサイズのスクリーンでも、同じようにゲームが拡大縮小されて表示されます。

プロジェクトの編成

このプロジェクトでは、PlayerMobHUD の3つの独立したシーンを作成し、これらをゲームの Main シーンに結合します。大規模なプロジェクトでは、さまざまなシーンとそのスクリプトを保持するフォルダを作成すると便利かもしれませんが、この比較的小さなゲームでは、 res:// と呼ばれるプロジェクトのルートフォルダにシーンとスクリプトを保存します。プロジェクトフォルダは、左下隅の[ファイルシステム] ドックで確認できます:

../../_images/filesystem_dock.png

Playerシーン

最初のシーンでは、 Player オブジェクトを定義します。Player シーンを独立して作成する利点の 1 つは、たとえゲームの他の部分を作成する前であっても、個別にテストができることです。

ノード構成

はじめに、プレイヤーオブジェクトのルートノードを選択する必要があります。原則として、シーンのルートノードはオブジェクトに望まれる機能(オブジェクトが何 である か)を反映する必要があります。「その他のノード」のボタンをクリックして、シーンに Area2D <class_Area2D>` ノードを加えます。

../../_images/add_node.png

Godotは、シーンツリーのノードの横に警告アイコンを表示します。現時点では無視して構いません。後で対処します。

Area2D を使用すると、プレイヤーに重なり合ったり、プレイヤーに衝突したりするオブジェクトを検出できます。ノードの名前をダブルクリックして、その名前を Player に変更します。シーンのルートノードを決めたので、これにノードを追加して機能を追加できます。

Player ノードに子を追加する前に、子をクリックして誤って移動したりサイズを変更したりしないようにします。ノードを選択し、ロックの右側にあるアイコンをクリックします。ツールチップには「オブジェクトの子を選択不可にする。」と書いてあります

../../_images/lock_children.png

シーンを保存します。 [シーン]-> [シーンを保存]をクリックするか、Windows/Linuxでは Ctrl + S を押し、macOSでは Cmd + S を押します。

注釈

このプロジェクトでは、Godotの命名規則に従います。

  • GDScript: クラス(ノード)はPascalCaseを使用し、変数と関数はsnake_caseを使用し、定数はALL_CAPSを使用します(GDScriptスタイルガイドを参照)。

  • C#: クラス、export変数、メソッドはPascalCaseを使用し、プライベートフィールドは_camelCaseを使用し、ローカル変数とパラメーターはcamelCaseを使用します(C#スタイルガイドを参照)。シグナルを接続するときは、メソッド名を正確に入力してください。

スプライトアニメーション

Player ノードをクリックし、AnimatedSprite ノードを子として追加します。 AnimatedSprite は、Playerの外観とアニメーションを処理します。ノードの横に警告マークがあることに注意してください。` AnimatedSprite` には SpriteFrames リソースが必要です。これは、表示できるアニメーションのリストです。作成するには、インスペクタで Frames プロパティを見つけ、「[空]」→「新規 SpriteFrames」をクリックします。これにより、SpriteFramesパネルが自動的に開きます:

../../_images/spriteframes_panel.png

左側にはアニメーションの一覧があります。「default」をクリックし、「walk」に名前を変更します。次に、「新規アニメーション」ボタンをクリックして、「up」という名前の 2 番目のアニメーションを作成します。 playerGrey_up[1/2]playerGrey_walk[1/2] という名前の各アニメーションの 2 つの画像をパネルの 「アニメーション フレーム」側にドラッグします:

../../_images/spriteframes_panel2.png

プレイヤーの画像はゲームウィンドウに対して少し大きすぎるため、縮小する必要があります。 AnimatedSprite ノードをクリックし、 Scale プロパティを (0.5,0.5) に設定します。Node2D の下のインスペクタにあります。

../../_images/player_scale.png

最後に、CollisionShape2DPlayer の子として追加します。これにより、プレイヤーの「ヒットボックス」、またはその衝突領域の境界が決定されます。このキャラクターでは、 CapsuleShape2D ノードがうまくフィットするので、インスペクタの「Shape」の隣で「[空]」->「新規 CapsuleShape2D」をクリックします。2つのサイズハンドルを使用して、スプライトを覆うようにシェイプのサイズを変更します:

../../_images/player_coll_shape.png

完了すると、 Player シーンは次のようになります:

../../_images/player_scene_nodes.png

これらの変更後は、必ずシーンを再度保存してください。

プレイヤーを動かす

次に、組み込みノードからは得られない機能を追加する必要があるので、スクリプトを追加します。 Player ノードをクリックし、「スクリプトをアタッチ」ボタンをクリックします:

../../_images/add_script_button.png

スクリプト設定ウィンドウでは、デフォルトの設定だけを残すことができます。「作成」をクリックするだけです:

注釈

C#スクリプトまたはその他の言語を作成する場合は、作成を実行する前に [言語] ドロップダウン メニューから言語を選択します。

../../_images/attach_node_window.png

注釈

GDScriptに初めて触れる場合は、続行する前にスクリプトをお読みください。

まず、このオブジェクトに必要なメンバー変数を宣言します:

extends Area2D

export var speed = 400  # How fast the player will move (pixels/sec).
var screen_size  # Size of the game window.
public class Player : Area2D
{
    [Export]
    public int Speed = 400; // How fast the player will move (pixels/sec).

    private Vector2 _screenSize; // Size of the game window.
}

最初の変数 speedexport キーワードを使用すると、インスペクタでその値を設定できるようになります。これは、値をノードの組み込みプロパティのように調整できるようにする場合に便利です。 Player ノードをクリックすると、インスペクタの "Script Variables" セクションにプロパティが表示されます。ここで値を変更すると、スクリプトに記述された値が上書きされることに注意してください。

警告

C#を使用している場合、新しいエクスポート変数またはシグナルを表示する場合は、プロジェクトアセンブリを(再)ビルドする必要があります。このビルドは、エディタウィンドウの下部にある「Mono」をクリックしてMonoパネルを表示し、「Build Project」ボタンをクリックして手動でトリガーできます。

../../_images/export_variable.png

_ready() 関数は、ノードがシーンツリーに入ると呼び出されます。これは、ゲームウィンドウのサイズを調べる良いタイミングです:

func _ready():
    screen_size = get_viewport_rect().size
public override void _Ready()
{
    _screenSize = GetViewport().Size;
}

これで _process() 関数を使用して、プレイヤーが何をするかを定義できます。 _process() はフレームごとに呼び出されるため、これを使用してゲームの要素を更新しますが、これは頻繁に変更されることが予想されます。プレイヤーの場合、次のことを行う必要があります:

  • 入力をチェックします。

  • 指定した方向に移動します。

  • 適切なアニメーションを再生します。

まず、入力をチェックする必要があります - プレイヤーはキーを押していますか?このゲームでは、4方向の入力チェックがあります。入力アクションは、プロジェクト設定の「インプットマップ」で定義されます。ここで、カスタムイベントを定義し、異なるキー、マウスイベント、またはその他の入力を割り当てることができます。このデモでは、キーボードの矢印キーに割り当てられているデフォルトのイベントを使用します。

キーが押されているかどうかを Input.is_action_pressed() を使用して検出できます。これは、押された場合は true 、押されていない場合は false を返します。

func _process(delta):
    var velocity = Vector2()  # The player's movement vector.
    if Input.is_action_pressed("ui_right"):
        velocity.x += 1
    if Input.is_action_pressed("ui_left"):
        velocity.x -= 1
    if Input.is_action_pressed("ui_down"):
        velocity.y += 1
    if Input.is_action_pressed("ui_up"):
        velocity.y -= 1
    if velocity.length() > 0:
        velocity = velocity.normalized() * speed
        $AnimatedSprite.play()
    else:
        $AnimatedSprite.stop()
public override void _Process(float delta)
{
    var velocity = new Vector2(); // The player's movement vector.

    if (Input.IsActionPressed("ui_right"))
    {
        velocity.x += 1;
    }

    if (Input.IsActionPressed("ui_left"))
    {
        velocity.x -= 1;
    }

    if (Input.IsActionPressed("ui_down"))
    {
        velocity.y += 1;
    }

    if (Input.IsActionPressed("ui_up"))
    {
        velocity.y -= 1;
    }

    var animatedSprite = GetNode<AnimatedSprite>("AnimatedSprite");

    if (velocity.Length() > 0)
    {
        velocity = velocity.Normalized() * Speed;
        animatedSprite.Play();
    }
    else
    {
        animatedSprite.Stop();
    }
}

まず velocity(0, 0) に設定することから始めます - デフォルトでは、プレイヤーは動いてはいけません。次に、各入力をチェックし、 velocity から加算/減算して全体の方向を取得します。たとえば、 を同時に押した場合、結果の velocity ベクトルは (1, 1) になります。この場合、水平方向と垂直方向の動きを追加しているため、プレイヤーは水平方向に移動した場合よりも、斜め方向に速く移動します。

加速度を正規化すれば、これを防ぐことができます。つまり、速度の長さ1 に設定し、それから希望の速度を乗算します。これは、これ以上速い対角移動がないことを意味します。

ちなみに

以前にベクトル演算を使用したことがない場合、あるいは忘れてしまった場合は、ベクトル演算でGodotのベクトル使用の説明をみることができます。これは知っておくと良いですが、このチュートリアルの残りの部分では必要ないでしょう。

また、AnimatedSpriteの play() または stop() を呼び出せるようにするため、プレイヤーが移動中かどうかも確認します。

$get_node() の省略形です。したがって、上記のコードでは、$AnimatedSprite.play()get_node("AnimatedSprite").play() と同じです。

ちなみに

GDScriptでは、$ は現在のノードからの相対パスにあるノードを返し、ノードが見つからない場合は null を返します。 AnimatedSpriteは現在のノードの子であるため、$AnimatedSprite を使用できます。

移動方向がわかったので、プレイヤーの位置を更新します。clamp() を使用して、プレイヤーが画面を離れないようにします。Clampingの意味は長さに制限をかける事です。_process 関数の下部に追加して下さい (else までインデントしないように注意):

position += velocity * delta
position.x = clamp(position.x, 0, screen_size.x)
position.y = clamp(position.y, 0, screen_size.y)
Position += velocity * delta;
Position = new Vector2(
    x: Mathf.Clamp(Position.x, 0, _screenSize.x),
    y: Mathf.Clamp(Position.y, 0, _screenSize.y)
);

ちなみに

_process() 関数の delta パラメータは フレームの長さ - 前のフレームが完了するまでに要した時間を参照します。この値を使うことで、動きの処理はフレームレートの変動の影響を受けなくなります。

「シーンを実行」(F6)をクリックして、スクリーンで全方向にプレイヤーを移動できることを確認してください。

警告

「デバッガ」パネルで以下のようなエラーが発生した場合

null インスタンス上の ベース 'null インスタンス' の関数 'play' を呼び出そうとしています。

この場合、AnimatedSpriteノード名のスペルが間違っている可能性があります。ノード名は大文字と小文字を区別するので、$NodeName はシーン ツリーに表示されている名前と一致させる必要があります。

アニメーションの選択

プレイヤーを移動できるようになったので、AnimatedSpriteが再生するアニメーションを方向に合わせて変更させる必要があります。今あるのは「walk」アニメーションで、プレイヤーは右方向へ歩きます。左への動きには flip_h プロパティを使用して水平に反転させます。また「up」アニメーションもあり、これは flip_v で垂直に反転させれば、下への動きになります。では、このコードを _process() 関数の最後に配置します:

if velocity.x != 0:
    $AnimatedSprite.animation = "walk"
    $AnimatedSprite.flip_v = false
    # See the note below about boolean assignment
    $AnimatedSprite.flip_h = velocity.x < 0
elif velocity.y != 0:
    $AnimatedSprite.animation = "up"
    $AnimatedSprite.flip_v = velocity.y > 0
if (velocity.x != 0)
{
    animatedSprite.Animation = "walk";
    animatedSprite.FlipV = false;
    // See the note below about boolean assignment
    animatedSprite.FlipH = velocity.x < 0;
}
else if (velocity.y != 0)
{
    animatedSprite.Animation = "up";
    animatedSprite.FlipV = velocity.y > 0;
}

注釈

上記のコードにおけるブール値の代入は、プログラマーがよく使う略式記法です。比較テスト(ブール値)とブール値の代入を行っていますが、両方とも同時に行うことができます。次のコードと、上記の1行でのブール値代入とを比べてみましょう。

if velocity.x < 0:
    $AnimatedSprite.flip_h = true
else:
    $AnimatedSprite.flip_h = false
if (velocity.x < 0)
{
    animatedSprite.FlipH = true;
}
else
{
    animatedSprite.FlipH = false;
}

もう一度シーンを再生して、それぞれの方向のアニメーションが正しいことを確認してください。

ちなみに

ここでよくある間違いは、アニメーションの名前を間違って入力してしまうことです。SpriteFramesパネルに表示されるアニメーションの名前は、コードの中で入力したものと一致していなければなりません。もしアニメーションの名前を "Walk" とした場合、コード中では大文字の "W" も使わなければなりません。

動きが正しく機能していることを確認したら、次の行を _ready() に追加して、ゲームの開始時にプレイヤーが非表示になるようにします:

hide()
Hide();

コリジョン(衝突/当り判定)の準備

Player には敵に攻撃されたことを検知してもらいたいのですが、まだ敵を作っていません!Godotのシグナル機能を使って動作させるので、大丈夫です。

スクリプトの先頭で extends Area2d の後に、次の行を追加します:

signal hit
// Don't forget to rebuild the project so the editor knows about the new signal.

[Signal]
public delegate void Hit();

これは、プレイヤーが敵と衝突したときにプレイヤーが発信する(送り出す)"hit"と呼ばれるカスタムシグナルを定義します。衝突を検出するために Area2D を使用します。 Player ノードを選択し、インスペクタタブの横にある「ノード」タブをクリックすると、プレイヤーが発信するシグナルのリストが表示されます:

../../_images/player_signals.png

カスタムの「hit」シグナルもありますね! 敵は RigidBody2D ノードになるため、 body_entered(body: Node) シグナルが必要です。これは、ボディがプレイヤーに接触したときに発信されます。「接続」をクリックすると、「シグナルの接続」ウィンドウが現れます。これらの設定を変更する必要はないので、再度「接続」をクリックしてください。Godotはプレイヤーのスクリプトに自動的に関数を作成します。

../../_images/player_signal_connection.png

信号がこの関数に接続されていることを示す緑色のアイコンに注意してください。次のコードを関数に追加します:

func _on_Player_body_entered(body):
    hide()  # Player disappears after being hit.
    emit_signal("hit")
    $CollisionShape2D.set_deferred("disabled", true)
public void OnPlayerBodyEntered(PhysicsBody2D body)
{
    Hide(); // Player disappears after being hit.
    EmitSignal("Hit");
    GetNode<CollisionShape2D>("CollisionShape2D").SetDeferred("disabled", true);
}

敵がプレイヤーに当たるたびに、シグナルが発せられます。プレイヤーの衝突を無効にして、 hit シグナルを複数回トリガーしないようにする必要があります。

注釈

エリアのコリジョン形状を無効にすると、それがエンジンの衝突処理の途中だったときにエラーが発生する可能性があります。 set_deferred() を使用すると、安全にシェイプを無効にできるようになるまでGodotを待機させることができます。

最後のピースとなるのは、新しいゲームの開始時にPlayerをリセットするため、呼び出す関数を追加することです。

func start(pos):
    position = pos
    show()
    $CollisionShape2D.disabled = false
public void Start(Vector2 pos)
{
    Position = pos;
    Show();
    GetNode<CollisionShape2D>("CollisionShape2D").Disabled = false;
}

Enemyシーン

次はプレイヤーが避けるべき敵を作りましょう。敵の行動はあまり複雑ではありません。モブは画面の端でランダムに生まれると、ランダムな方向を選び、一直線に進みます。

これから Mob シーンを作り、それをインスタンス化して、ゲーム内に任意の数の独立したモブを作成します。

注釈

インスタンス化の詳細については、インスタンスを参照してください。

ノードの設定

[シーン → 新規シーン] をクリックして、次のノードを追加します:

Playerシーンと同様に、選択できないように子を設定することを忘れないでください。

RigidBody2Dプロパティで、 Gravity Scale0 に設定して、モブが下方に落ちないようにします。さらに、 PhysicsBody2D セクションの下にある Mask プロパティをクリックし、最初のチェックボックスをオフにします。これにより、モブが互いに衝突しないようになります。

../../_images/set_collision_mask.png

プレイヤーに対して行ったように AnimatedSprite を設定します。 今回は、3つのアニメーションがあります: flyswimwalkです。 artフォルダ内には、各アニメーション用の画像が2枚あります。

全てのアニメーションの "Speed (FPS)" を 3 に調整してください。

../../_images/mob_animations.gif

インスペクタの Playing プロパティを「On」に設定します。

モブにバラエティを持たせるために、1つのアニメーションをランダムに選択します。

プレイヤーの画像と同様に、これらのモブ画像は縮小する必要があります。 AnimatedSpriteScale プロパティを (0.75, 0.75) に設定します。

Player シーンと同様に、コリジョンに CapsuleShape2D を追加します。図形を画像に合わせるには、Rotation Degrees プロパティを 90 に設定する必要があります (インスペクタの「Transform」の下にあります)。

シーンを保存します。

Enemyスクリプト

Mob にスクリプトを追加し、次のメンバー変数を追加します:

extends RigidBody2D

export var min_speed = 150  # Minimum speed range.
export var max_speed = 250  # Maximum speed range.
public class Mob : RigidBody2D
{
    // Don't forget to rebuild the project so the editor knows about the new export variables.

    [Export]
    public int MinSpeed = 150; // Minimum speed range.

    [Export]
    public int MaxSpeed = 250; // Maximum speed range.

}

モブを生み出す時、各モブの移動速度について min_speedmax_speed の間のランダムな値を選択します (すべて同じ速度で移動していると退屈なので)。

それでは、スクリプトの残りの部分を見てみましょう。 _ready() では、次の3つのアニメーションタイプのいずれかをランダムに選択します:

func _ready():
    var mob_types = $AnimatedSprite.frames.get_animation_names()
    $AnimatedSprite.animation = mob_types[randi() % mob_types.size()]
// C# doesn't implement GDScript's random methods, so we use 'System.Random' as an alternative.
static private Random _random = new Random();

public override void _Ready()
{
    var animSprite = GetNode<AnimatedSprite>("AnimatedSprite");
    var mobTypes = animSprite.Frames.GetAnimationNames();
    animSprite.Animation = mobTypes[_random.Next(0, mobTypes.Length)];
}

まず、アニメーション名のリストを AnimatedSprite の frames プロパティから取得します。これは3つのアニメーション名をすべて含む配列を返します: ["walk", "swim", "fly"]

次に、 0 から 2 の間の乱数を選んで、リストから名前を選ぶ必要があります(配列のインデックスは 0 から始まります)。 randi() % n0 から n-1 の間の乱数を選びます。

注釈

シーンを実行するたびに「ランダム」な数字のシーケンスを異なるようにするには、 randomize() を使用する必要があります。 Main シーンで randomize() を使用するので、ここでは必要はありません。

最後のピースは、モブが画面を離れたときにモブ自身を削除することです。 VisibilityNotifier2D ノードの screen_exited() シグナルを接続し、次のコードを追加します:

func _on_VisibilityNotifier2D_screen_exited():
    queue_free()
public void OnVisibilityNotifier2DScreenExited()
{
    QueueFree();
}

これで Mob シーンが完成します。

Mainシーン

さあ、すべてをまとめましょう。 新しいシーンを作成し、 Main という名前の Node を追加します。 Node2D ではなく 、Nodeを作成していることを確認してください。「インスタンス」ボタンをクリックして、保存した Player.tscn を選択します。

../../_images/instance_scene.png

次に Main の子供として以下のノードを追加します(値は秒単位です)。

  • Timer( MobTimer という名前) - モブが出現する頻度を制御する

  • Timer ( ScoreTimer という名前) - 一秒ごとに得点を上げる

  • Timer ( StartTimer という名前) - 開始する前に遅延させる

  • Position2D (StartPosition という名前) - プレイヤーの開始位置を示す

Timer ノードの Wait Time プロパティを次のように設定します:

  • MobTimer: 0.5

  • ScoreTimer: 1

  • StartTimer: 2

さらに、 StartTimerOne Shot プロパティを「On」に設定し、 StartPosition ノードの Position(240, 450) に設定します。

モブの生成

メインノードは新しいモブを生成し、画面の端のランダムな場所に表示するようにします。 Main の子として MobPath という名前のPath2Dノードを追加します。 Path2D を選択すると、エディタの上部にいくつかの新しいボタンが表示されます:

../../_images/path2d_buttons.png

中央のアイコン([点を追加])を選択し、表示されているコーナーをクリックしてポイントを追加してパスを描画します。ポイントをグリッドにスナップするには、[グリッドスナップを使う]が選択されていることを確認します。このオプションは、[ロック]ボタンの左側にあり、「交差する線と磁石」のアイコンで表示されています。

../../_images/grid_snap_button.png

重要

時計回りにパスを描画します。そうしないと、モブは内側ではなく外側を向いて発生します!

../../_images/draw_path2d.gif

画像にポイント 4 を配置した後、「カーブを閉じる」ボタンをクリックすると、カーブが完成します。

パスが定義されたので、 MobPath の子としてPathFollow2Dノードを追加し、 MobSpawnLocation という名前を付けます。このノードは自動的に回転し、パスの移動に従うので、パスに沿ってランダムな位置と方向を選択できます。

シーンは次のようになります:

../../_images/main_scene_nodes.png

Mainスクリプト

スクリプトを Main に追加します。 スクリプトの上部で、export(PackedScene) を使用して、インスタンス化するMobシーンを選択できるようにします。

extends Node

export (PackedScene) var Mob
var score

func _ready():
    randomize()
public class Main : Node
{
    // Don't forget to rebuild the project so the editor knows about the new export variable.

    [Export]
    public PackedScene Mob;

    private int _score;

    // We use 'System.Random' as an alternative to GDScript's random methods.
    private Random _random = new Random();

    public override void _Ready()
    {
    }

    // We'll use this later because C# doesn't support GDScript's randi().
    private float RandRange(float min, float max)
    {
        return (float)_random.NextDouble() * (max - min) + min;
    }
}

Main ノードをクリックすると、インスペクタの「Script Variables(スクリプト変数)」の下に Mob プロパティがあります。

このプロパティの値は、ふたつの方法で指定できます:

  • 「ファイルシステム」パネルから Mob.tscn をドラッグし、 Mob プロパティにドロップします。

  • [空] の隣にある下矢印をクリックして「読み込み」を選び、Mob.tscn を選択します。

次に、シーンドックの Player ノードを選択し、サイドバーのノードドックにアクセスします。ノードドックでは、シグナルタブが選択されていることを確認してください。

Player ノードのシグナルのリストが表示されているはずです。リストの中から hit のシグナルを見つけてダブルクリックしてください (または右クリックして "接続..." を選択)。これでシグナルの接続ダイアログが開きます。ゲームが終了したときに必要な処理を行う game_over という名前の新しい関数をこれから作ります。シグナル接続ダイアログの下部にある「受信側メソッド」ボックスに「game_over」と入力し、「接続」をクリックしてください。新しい関数に以下のコードを追加し、さらに、新しいゲームのための設定を行う new_game 関数も追加します:

func game_over():
    $ScoreTimer.stop()
    $MobTimer.stop()

func new_game():
    score = 0
    $Player.start($StartPosition.position)
    $StartTimer.start()
public void GameOver()
{
    GetNode<Timer>("MobTimer").Stop();
    GetNode<Timer>("ScoreTimer").Stop();
}

public void NewGame()
{
    _score = 0;

    var player = GetNode<Player>("Player");
    var startPosition = GetNode<Position2D>("StartPosition");
    player.Start(startPosition.Position);

    GetNode<Timer>("StartTimer").Start();
}

次に、各Timerノード (StartTimerScoreTimer、および MobTimer) の timeout() シグナルをメインスクリプトに接続します。StartTimer は他の2つのタイマーを開始します。ScoreTimer はスコアを1増やします。

func _on_StartTimer_timeout():
    $MobTimer.start()
    $ScoreTimer.start()

func _on_ScoreTimer_timeout():
    score += 1
public void OnStartTimerTimeout()
{
    GetNode<Timer>("MobTimer").Start();
    GetNode<Timer>("ScoreTimer").Start();
}

public void OnScoreTimerTimeout()
{
    _score++;
}

_on_MobTimer_timeout() では、mobインスタンスを作成し、 Path2D に沿って開始位置をランダムに選択してから、mobを動かします。 PathFollow2D ノードはパスに沿って自動的に回転するので、それを使用してmobの方向と位置を決めます。

注意点として、新しいインスタンスは add_child() を使ってシーンに追加しなければなりません。

func _on_MobTimer_timeout():
    # Choose a random location on Path2D.
    $MobPath/MobSpawnLocation.offset = randi()
    # Create a Mob instance and add it to the scene.
    var mob = Mob.instance()
    add_child(mob)
    # Set the mob's direction perpendicular to the path direction.
    var direction = $MobPath/MobSpawnLocation.rotation + PI / 2
    # Set the mob's position to a random location.
    mob.position = $MobPath/MobSpawnLocation.position
    # Add some randomness to the direction.
    direction += rand_range(-PI / 4, PI / 4)
    mob.rotation = direction
    # Set the velocity (speed & direction).
    mob.linear_velocity = Vector2(rand_range(mob.min_speed, mob.max_speed), 0)
    mob.linear_velocity = mob.linear_velocity.rotated(direction)
public void OnMobTimerTimeout()
{
    // Choose a random location on Path2D.
    var mobSpawnLocation = GetNode<PathFollow2D>("MobPath/MobSpawnLocation");
    mobSpawnLocation.Offset = _random.Next();

    // Create a Mob instance and add it to the scene.
    var mobInstance = (RigidBody2D)Mob.Instance();
    AddChild(mobInstance);

    // Set the mob's direction perpendicular to the path direction.
    float direction = mobSpawnLocation.Rotation + Mathf.Pi / 2;

    // Set the mob's position to a random location.
    mobInstance.Position = mobSpawnLocation.Position;

    // Add some randomness to the direction.
    direction += RandRange(-Mathf.Pi / 4, Mathf.Pi / 4);
    mobInstance.Rotation = direction;

    // Choose the velocity.
    mobInstance.LinearVelocity = new Vector2(RandRange(150f, 250f), 0).Rotated(direction);
}

重要

なぜ PI なのでしょう? 角度を必要とする関数では、GDScriptは度ではなくラジアンを使用します。度を使用する方が快適な場合は、 deg2rad() 関数と rad2deg() 関数を使用して 2 つの間で変換する必要があります。

シーンのテスト

シーンをテストして、すべてが動作していることを確認してみましょう。これを _ready() に追加してください:

func _ready():
    randomize()
    new_game()
    public override void _Ready()
    {
        NewGame();
    }
}

また、 Main を「メインシーン」として設定してみましょう - ゲームが起動したときに自動的に実行されるシーンです。「実行」ボタンを押して、プロンプトが表示されたら Main.tscn を選択してください。

プレイヤーを移動でき、モブが発生したり、モブに当たった時にプレイヤーが消えるようになっているはずです。

全て動作していることが確認できたら、 _ready() から new_game() の呼び出しを削除してください。

ヘッドアップディスプレイ(HUD)

ゲームに必要な最後の部分は、スコア、「ゲームオーバー」メッセージ、再起動ボタンなどを表示するインターフェイスです。新しいシーンを作成し、 HUD という名前のCanvasLayerノードを追加します。「HUD」は、ゲームビューの上にオーバーレイとして表示される情報表示である「ヘッドアップディスプレイ」の略です。

CanvasLayerノードを使用すると、ゲームの他の部分よりも上のレイヤにUI要素を描画することができるため、表示される情報がプレイヤーやモブなどのゲーム要素によって隠されることがなくなります。

HUDには、次の情報を表示する必要があります:

  • ScoreTimer によって変更されるスコア。

  • 「Game Over」や「Get Ready! (よーい!)」 などのメッセージ

  • ゲームを開始する「スタート」ボタン。

UI要素の基本ノードはコントロールです。UIを作成するには、ラベルボタンの2種類のコントロールノードを使用します。

HUD ノードの子として次を作成します:

  • ScoreLabel という名前のラベル

  • Message という名前のLabel

  • StartButton という名前のボタン

  • MessageTimer という名前のTimer

ScoreLabel をクリックし、インスペクタのTextフィールドに数字を入力します。 Control ノードのデフォルトのフォントは小さく、うまくスケールしません。そこで、ゲームアセットに含まれる 「Xolonium-Regular.ttf」というフォントファイルがあります。 このフォントを使用するには、次のようにしてください:

  1. 「カスタムフォント」で 「新しい動的フォント」を選択します

../../_images/custom_font1.png
  1. 追加した "DynamicFont" をクリックし、"Font/Font Data" の下で [読み込み] を選択し、"Xolonium-Regular.ttf" ファイルを選択します。 また、フォントの Size も設定する必要があります。 64 の設定はうまく機能します。

../../_images/custom_font2.png

これを ScoreLabel で行ったら、DynamicFont プロパティの横にある下向き矢印をクリックして「コピー」を選択し、他の2つのコントロールノードの同じ場所に「ペースト」してください。

注釈

アンカーとマージン: Control ノードには、位置とサイズがありますが、アンカーとマージンもあります。アンカーによって、原点 (節点のエッジの参照点) が定義されます。余白は、コントロールノードを移動またはサイズ変更すると自動的に更新されます。コントロールノードのエッジからアンカーまでの距離を表します。詳細はコントロールノードを使用したインターフェイスの設計を参照してください。

以下に示すようにノードを配置します。「レイアウト」ボタンをクリックして、コントロールノードのレイアウトを設定します:

../../_images/ui_anchor.png

ノードをドラッグして手動で配置したり、より正確な配置を行うには、次の設定を使用します:

ScoreLabel

  • Layout : "Top Wide"

  • Text : 0

  • Align : "Center"

Message

  • Layout : "HCenter Wide"

  • Text : Dodge the Creeps!

  • Align : "Center"

  • Autowrap : "On"

StartButton

  • Text : Start

  • Layout : "Center Bottom"

  • Margin :

    • Top: -200

    • Bottom: -100

MessageTimer 上で、Wait Time2 に設定し、One Shot プロパティを「On」に設定してください。

次に、このスクリプトを HUD に追加します:

extends CanvasLayer

signal start_game
public class HUD : CanvasLayer
{
    // Don't forget to rebuild the project so the editor knows about the new signal.

    [Signal]
    public delegate void StartGame();
}

start_game シグナルは、ボタンが押されたことを Main ノードに通知します。

func show_message(text):
    $Message.text = text
    $Message.show()
    $MessageTimer.start()
public void ShowMessage(string text)
{
    var message = GetNode<Label>("Message");
    message.Text = text;
    message.Show();

    GetNode<Timer>("MessageTimer").Start();
}

この関数は、「Get Ready」などのメッセージを一時的に表示させたい場合に呼び出されます。

func show_game_over():
    show_message("Game Over")
    # Wait until the MessageTimer has counted down.
    yield($MessageTimer, "timeout")

    $Message.text = "Dodge the\nCreeps!"
    $Message.show()
    # Make a one-shot timer and wait for it to finish.
    yield(get_tree().create_timer(1), "timeout")
    $StartButton.show()
async public void ShowGameOver()
{
    ShowMessage("Game Over");

    var messageTimer = GetNode<Timer>("MessageTimer");
    await ToSignal(messageTimer, "timeout");

    var message = GetNode<Label>("Message");
    message.Text = "Dodge the\nCreeps!";
    message.Show();

    await ToSignal(GetTree().CreateTimer(1), "timeout");
    GetNode<Button>("StartButton").Show();
}

この関数は、プレイヤーが負けたときに呼び出されます。 2秒間「Game Over」と表示され、タイトル画面に戻り、少し間を置いて「Start」ボタンが表示されます。

注釈

短い間、一時停止する必要がある場合は、Timerノードを使用する代わりに、SceneTreeの create_timer() 関数を使用します。 これは、上記のコードのように、"Start" ボタンを表示する前に少し時間を置きたい場合など、遅延させるのに非常に役立ちます。

func update_score(score):
    $ScoreLabel.text = str(score)
public void UpdateScore(int score)
{
    GetNode<Label>("ScoreLabel").Text = score.ToString();
}

この関数はスコアが変わるたびに Main によって呼び出されます。

MessageTimertimeout() シグナルと StartButtonpressed() シグナルを接続してから、下記のコードを新しい関数に加えます:

func _on_StartButton_pressed():
    $StartButton.hide()
    emit_signal("start_game")

func _on_MessageTimer_timeout():
    $Message.hide()
public void OnStartButtonPressed()
{
    GetNode<Button>("StartButton").Hide();
    EmitSignal("StartGame");
}

public void OnMessageTimerTimeout()
{
    GetNode<Label>("Message").Hide();
}

HUDをメインに接続する

HUD シーンの作成が完了したら、 Main に戻ります。 Player のシーンと同じように HUD シーンを Main にインスタンス化し、ツリーの一番下に配置します。ツリー全体は次のようになるはずです。何も見落としていないか確認してください:

../../_images/completed_main_scene.png

次に、 HUD 機能を Main のスクリプトに接続します。これには、 Main シーンにいくつかの追加が必要です:

ノードタブで、「シグナルを接続」ウィンドウの 「受信側メソッド」に "new_game" と入力して、HUDの start_game のシグナルをメインノードの new_game() 関数に接続します。スクリプトの func new_game() の横に緑色の接続アイコンが表示されていることを確認してください。

new_game() で、スコア表示を更新し、「Get Ready」メッセージを表示します:

$HUD.update_score(score)
$HUD.show_message("Get Ready")
var hud = GetNode<HUD>("HUD");
hud.UpdateScore(_score);
hud.ShowMessage("Get Ready!");

game_over() では、対応する HUD 関数を呼び出す必要があります:

$HUD.show_game_over()
GetNode<HUD>("HUD").ShowGameOver();

最後に、これを _on_ScoreTimer_timeout() に追加して、変更されたスコアと同期して表示を維持します:

$HUD.update_score(score)
GetNode<HUD>("HUD").UpdateScore(_score);

これでプレイの準備が整いました! [プロジェクトを実行] ボタンをクリックします。メインシーンを選択するように求められますので、 Main.tscn を選択します。

古い「クリープ」を削除する

「ゲームオーバー」までプレイしてから新しいゲームを開始すると、前のゲームの「クリープ」が画面に表示されたままになっています。 それらすべて、新しいゲームの開始時には消したほうがいいでしょう。それには、すべてのモブたちに自身の削除を指示する方法が必要です。これは「グループ」機能を使えば可能です。

Mob シーンでルートノードを選択し、インスペクタの隣にある「ノード」タブをクリックしてください(ノードのシグナルを見つけるのと同じ場所です)。「シグナル」の横にある「グループ」をクリックして、新しいグループ名を入力して「追加」をクリックします。

../../_images/group_tab.png

これですべてのモブが 「mobs」グループに入るようになります。あとは、 Main 内の game_over() 関数に次の行を追加します。

get_tree().call_group("mobs", "queue_free")
GetTree().CallGroup("mobs", "queue_free");

call_group() 関数はグループ内の全てのノードに対して名前付きの関数を呼び出します - この場合は全てのモブに自分自身を削除するように指示しています。

仕上げ

これで、ゲームのすべての機能が完了しました。以下は、ゲーム体験を向上させるためにもう少し「精彩」を加えるための、いくつかの残りの手順です。あなた自身のアイデアによって、ゲームプレイを自由に拡張してください。

Background(背景)

デフォルトのグレーの背景はあまり魅力的ではないので、色を変更してみましょう。これを行う 1 つの方法は、ColorRect ノードを使用することです。他のノードの後ろに描画されるように、それを Main の下の最初のノードにします。 ColorRect には、 Color というプロパティのみがあります。好きな色を選択し、「レイアウト」→「Rect全面」を選べば、画面はそれで覆われます。

背景画像がある場合は、代わりに TextureRect ノードを使用して、それを追加することもできます。

効果音

サウンドと音楽は、ゲーム体験に魅力を与えるには最上級に効果的な方法となりえます。ゲームのアセットフォルダには、BGMの House In a Forest Loop.ogg と、プレイヤーが負けたとき用の gameover.wav という2つのサウンドファイルがあります。

2つのAudioStreamPlayerノードを Main の子として追加します。その1つに Music 、もう1つに DeathSound と名前を付けます。各ファイルにて Stream プロパティをクリックし、「読み込み」を選択してから、対応するオーディオファイルを選択します。

音楽を再生するには、 new_game() 関数に $Music.play() を追加し、 game_over() 関数に $Music.stop() を追加します。

最後に、 game_over() 関数に $DeathSound.play() を追加します。

キーボード ショートカット

このゲームはキーボード コントロールにてプレイするので、キーボードのキーを押してゲームを開始できると便利です。それには Button ノードの「Shortcut」プロパティを使用すれば可能です。

HUD シーンで StartButton を選択し、インスペクタでその Shortcut プロパティを見つけます。「新規 Shortcut」を選択し、その「Shortcut」項目をクリックします。 2番目の Shortcut プロパティが表示されます。「新規 InputEventAction」を選択し、新しい「InputEvent」をクリックします。 最後に、 Action プロパティに ui_select という名前を入力します。これは、スペースバーに関連付けられたデフォルトの入力イベントです。

../../_images/start_button_shortcut.png

スタートボタンが表示されたら、クリックするか、Space を押してゲームを開始します。

プロジェクトファイル

このプロジェクトの完成バージョンは、次の場所にあります。