最初のゲーム

概要

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

注釈

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

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

../../_images/dodge_preview.gif

なぜ2Dから始めるか? 3Dゲームは2Dより複雑です。ゲーム開発のプロセスをよく理解するまでは2Dのほうが適しています。

プロジェクトの設定

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

注釈

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

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

プロジェクトの編成

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

../../_images/filesystem_dock.png

Playerシーン

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

ノード構成

まず、「新しいノードを追加/作成」ボタンをクリックし、Area2Dノードをシーンに追加します。

../../_images/add_node.png

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

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

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

../../_images/lock_children.png

Save the scene. Click Scene -> Save, or press Ctrl + S on Windows/Linux or Cmd + S on macOS.

注釈

このプロジェクトでは、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」をクリックし、「right」に名前を変更します。次に、「追加」ボタンをクリックして、「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アニメーションを開始または停止できるように、プレイヤーが移動しているかどうかも確認します。

ちなみに

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

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

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

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インスタンス」を参照したというエラーが「デバッガ」パネルに表示された場合は、ノード名のスペルが間違っている可能性があります。ノード名は大文字と小文字を区別し、 $NodeName または get_node("NodeName") はシーン ツリーに表示される名前と一致する必要があります。

アニメーションの選択

プレイヤーを移動できるようになったので、方向に基づいてAnimatedSpriteが再生するアニメーションを変更する必要があります。左への動きには flip_h プロパティを使用して水平にフリップする「right」アニメーションと、下への動きには flip_v を使用して垂直にフリップする「up」アニメーションがあります。このコードを _process() 関数の最後に配置します:

if velocity.x != 0:
    $AnimatedSprite.animation = "right"
    $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 = "right";
    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;
}

注釈

上記のコードのブール値の割り当ては、プログラマーの一般的な略記です。以下のコードを使うか上記の短縮されたブール値を使うかを検討してみてください:

if velocity.x < 0:
    $AnimatedSprite.flip_h = true
else:
    $AnimatedSprite.flip_h = false
if velocity.x < 0:
    animatedSprite.FlipH = true
else:
    animatedSprite.FlipH = false

シーンをもう一度再生し、アニメーションが各方向で正しいことを確認します。動きが正しく機能していることを確認したら、次の行を _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( Object body ) シグナルが必要です。これは、ボディがプレイヤーに接触したときに発信されます。「接続」をクリックし、「シグナルの接続」ウィンドウで再度「接続」をクリックします。これらの設定を変更する必要はありません。Godotはプレイヤーのスクリプトに関数を自動的に作成します。この関数は、シグナルが発信されるたびに呼び出され - シグナルを「処理」します。

ちなみに

シグナルを接続する際に、Godotに関数を作成させる代わりに、シグナルをリンクする既存の関数の名前を付けることもできます。

次のコードを関数に追加します:

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

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

Enemyシーン

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

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

ノードの設定

[シーン -> 新規シーン]をクリックすると、モブが作成されます。

Mobシーンでは、次のノードが使用されます:

  • RigidBody2D( Mob という名前)
    • AnimatedSprite
    • CollisionShape2D
    • VisibilityNotifier2D( Visibility という名前)

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

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

../../_images/set_collision_mask.png

プレイヤーに対して行ったように AnimatedSprite <class_AnimatedSprite>を設定します。 今回、3つのアニメーションがあります: ``fly`swimwalk。 インスペクタの Playing プロパティを「オン」に設定し、以下に示すように「速度(FPS)」設定を調整します。 これらのアニメーションのいずれかをランダムに選択して、モブにさまざまなバリエーションを持たせます。

../../_images/mob_animations.gif

fly は3FPSに設定し、 swimwalk は4FPSに設定する必要があります。

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

Player シーンと同様に、コリジョンに CapsuleShape2D を追加します。図形をイメージに合わせるには、 Node2D の下に Rotation Degrees プロパティを 90 に設定する必要があります。

Enemyスクリプト

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

extends RigidBody2D

export var min_speed = 150  # Minimum speed range.
export var max_speed = 250  # Maximum speed range.
var mob_types = ["walk", "swim", "fly"]
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.

    private String[] _mobTypes = {"walk", "swim", "fly"};
}

モブを生み出す時、各モブの移動速度について min_speedmax_speed の間のランダムな値を選択します(すべて同じ速度で移動していると退屈になります)。 また、3つのアニメーションの名前を含む配列があり、これを使用してランダムなアニメーションを選択します。 スクリプトとSpriteFramesリソースでこれらのスペルが同じであることを確認してください。

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

func _ready():
    $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()
{
    GetNode<AnimatedSprite>("AnimatedSprite").Animation = _mobTypes[_random.Next(0, _mobTypes.Length)];
}

注釈

シーンを実行するたびに「ランダム」な数字のシーケンスを異なるようにするには、 randomize() を使用する必要があります。 Main シーンで randomize() を使用するので、ここでは必要はありません。 randi() % n は、 0n-1 の間のランダム整数を取得する標準的な方法です。

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

func _on_Visibility_screen_exited():
    queue_free()
public void OnVisibilityScreenExited()
{
    QueueFree();
}

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

Mainシーン

さあ、すべてをまとめましょう。 新しいシーンを作成し、 Main という名前の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/draw_path2d.gif

重要

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

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

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

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;
    }
}

「ファイルシステム」パネルから Mob.tscn をドラッグし、さらに Main ノードのスクリプト変数の下の Mob プロパティにドロップします。

Next, click on the Player and connect the hit signal. We want to make a new function named game_over, which will handle what needs to happen when a game ends. Type "game_over" in the "Receiver Method" box at the bottom of the "Connecting Signal" window. Add the following code, as well as a new_game function to set everything up for a 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.SetOffset(_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.SetLinearVelocity(new Vector2(RandRange(150f, 250f), 0).Rotated(direction));
}

重要

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

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

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

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

HUDには、次の情報が表示されます:

  • ScoreTimer によって変更されるスコア。
  • 「Game Over」や「Get Ready! (よーい!)」 などのメッセージ
  • ゲームを開始する「スタート」ボタン。

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

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

  • ScoreLabel という名前のラベル
  • MessageLabel という名前のラベル
  • StartButton という名前のボタン
  • MessageTimer という名前のTimer

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

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

注釈

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

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

../../_images/ui_anchor.png

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

ScoreLabel

  • Text : 0
  • Layout : "Top Wide"
  • Align : "Center"

MessageLabel

  • Text : Dodge the Creeps!
  • Layout : "HCenter Wide"
  • Align : "Center"
  • Autowrap : "On"

StartButton

  • Text : Start
  • Layout : "Center Bottom"
  • Margin :
    • Top: -200
    • Bottom: -100

次に、このスクリプトを 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):
    $MessageLabel.text = text
    $MessageLabel.show()
    $MessageTimer.start()
public void ShowMessage(string text)
{
    var messageLabel = GetNode<Label>("MessageLabel");
    messageLabel.Text = text;
    messageLabel.Show();

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

この関数は、「Get Ready」などのメッセージを一時的に表示するときに呼び出されます。 MessageTimer で、 Wait Time2 に設定し、 One Shot プロパティを「On」に設定します。

func show_game_over():
    show_message("Game Over")

    yield($MessageTimer, "timeout")

    $MessageLabel.text = "Dodge the\nCreeps!"
    $MessageLabel.show()

    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 messageLabel = GetNode<Label>("MessageLabel");
    messageLabel.Text = "Dodge the\nCreeps!";
    messageLabel.Show();

    GetNode<Button>("StartButton").Show();
}

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

注釈

一時停止する必要がある場合、タイマーノードを使用する代わりに、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():
    $MessageLabel.hide()
public void OnStartButtonPressed()
{
    GetNode<Button>("StartButton").Hide();
    EmitSignal("StartGame");
}

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

HUDをメインに接続する

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

../../_images/completed_main_scene.png

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

ノードタブで、HUDの start_game シグナルをMainノードの 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 を選択します。

古い「這うもの」を削除する

「ゲームオーバー」までプレイしてから新しいゲームを開始すると、前のゲームの「這うもの」が画面に表示されたままになります。 それらがすべて新しいゲームの開始時に消えたらもっといいでしょう。

残りの「這うもの」を除去するためにHUD ノードによってすでに放出されている start_game シグナルを使用します。 ゲームを実行するまで `` Main`` シーンツリーには `` Mob`` ノードがないため、エディタを使用して必要な方法でシグナルをMobに接続することはできません。 代わりに、コードを使用します。

Mob.gd に新しい関数を追加することから始めます。queue_free() は現在のフレームの最後にある現在のノードを削除します。

func _on_start_game():
    queue_free()
public void OnStartGame()
{
    QueueFree();
}

次に Main.gd で、最後に _on_MobTimer_timeout() 関数内に新しい行を追加します。

$HUD.connect("start_game", mob, "_on_start_game")
GetNode("HUD").Connect("StartGame", mobInstance, "OnStartGame");

この行は、新しいMobノード(mob 変数によって参照される)に、_ on_start_game() 関数を実行することにより、HUD ノードによって発行された start_game シグナルに応答するように指示します。

仕上げ

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

Background(背景)

デフォルトのグレーの背景はあまり魅力的ではありませんので、色を変更してみましょう。これを行う 1 つの方法は、ColorRectノードを使用することです。他のノードの後ろに描画されるように、 Main の下の最初のノードにします。 ColorRect には、 color というプロパティが 1 つだけ含まれます。好きな色を選択し、画面を覆う ColorRect のサイズをドラッグします。

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

効果音

サウンドと音楽は、ゲーム体験に魅力を与える唯一の最も効果的な方法です。ゲームアセットフォルダには、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() を追加します。

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

ゲームはキーボードコントロールで再生されるため、キーボードのキーを押してゲームを開始できると便利です。 これを行う1つの方法は、 Button ノードの "Shortcut" プロパティを使用することです。

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

../../_images/start_button_shortcut.png

Now when the start button appears, you can either click it or press Space to start the game.

プロジェクトファイル

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