最初のゲーム

概要

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

注釈

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

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

../../_images/dodge_preview.gif

Why 2D? 3D games are much more complex than 2D ones. You should stick to 2D until you have a good understanding of the game development process and how to use Godot.

プロジェクトの設定

Launch Godot and create a new project. Then, download dodge_assets.zip. This contains the images and sounds you'll be using to make the game. Unzip these files in your project folder.

注釈

For this tutorial, we will assume you are familiar with the Godot editor. If you haven't read シーンとノード, do so now for an explanation of setting up a project and using the editor.

This game is designed for portrait mode, so we need to adjust the size of the game window. Click on Project -> Project Settings -> Display -> Window and set "Width" to 480 and "Height" to 720.

このセクションでは、ストレッチオプションのところで、「Mode」を「2d」に、「Aspect」を「keep」にします。これによって、ゲームのスケールが異なるサイズのスクリーンでも常時表示されます。

プロジェクトの編成

In this project, we will make 3 independent scenes: Player, Mob, and HUD, which we will combine into the game's Main scene. In a larger project, it might be useful to create folders to hold the various scenes and their scripts, but for this relatively small game, you can save your scenes and scripts in the project's root folder, identified by res://. You can see your project folders in the FileSystem Dock in the lower left corner:

../../_images/filesystem_dock.png

Playerシーン

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

ノード構成

はじめに、プレイヤーのオブジェクトにルートノードを選択する必要があります。原則として、シーンのルートノードはオブジェクトの望ましいファンクション(何がオブジェクトの*is*になっているか)を反映する必要があります。「別のノード」のボタンをクリックして、シーンに:ref:`Area2D <class_Area2D>``ノードを加えます。

../../_images/add_node.png

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

With Area2D we can detect objects that overlap or run into the player. Change the node's name to Player by double-clicking on it. Now that we've set the scene's root node, we can add additional nodes to give it more functionality.

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#スタイルガイドを参照)。シグナルを接続するときは、メソッド名を正確に入力してください。

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

Click on the Player node and add an AnimatedSprite node as a child. The AnimatedSprite will handle the appearance and animations for our player. Notice that there is a warning symbol next to the node. An AnimatedSprite requires a SpriteFrames resource, which is a list of the animations it can display. To create one, find the Frames property in the Inspector and click "[empty]" -> "New SpriteFrames". Click again to open the "SpriteFrames" panel:

../../_images/spriteframes_panel.png

On the left is a list of animations. Click the "default" one and rename it to "walk". Then click the "New Animation" button to create a second animation named "up". Find the player images in the "FileSystem" tab - they're in the art folder you unzipped earlier. Drag the two images for each animation, named playerGrey_up[1/2] and playerGrey_walk[1/2], into the "Animation Frames" side of the panel for the corresponding animation:

../../_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

Make sure to save the scene again after these changes.

プレイヤーを動かす

Now we need to add some functionality that we can't get from a built-in node, so we'll add a script. Click the Player node and click the "Attach Script" button:

../../_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方向の入力チェックがあります。入力アクションは、プロジェクト設定の「インプットマップ」で定義されます。ここで、カスタムイベントを定義し、異なるキー、マウスイベント、またはその他の入力を割り当てることができます。このデモでは、キーボードの矢印キーに割り当てられているデフォルトのイベントを使用します。

You can detect whether a key is pressed using Input.is_action_pressed(), which returns true if it's pressed or false if it isn't.

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

We start by setting the velocity to (0, 0) - by default, the player should not be moving. Then we check each input and add/subtract from the velocity to obtain a total direction. For example, if you hold right and down at the same time, the resulting velocity vector will be (1, 1). In this case, since we're adding a horizontal and a vertical movement, the player would move faster diagonally than if it just moved horizontally.

We can prevent that if we normalize the velocity, which means we set its length to 1, then multiply by the desired speed. This means no more fast diagonal movement.

ちなみに

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

We also check whether the player is moving so we can call play() or stop() on the AnimatedSprite.

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

ちなみに

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

Now that we have a movement direction, we can update the player's position. We can also use clamp() to prevent it from leaving the screen. Clamping a value means restricting it to a given range. Add the following to the bottom of the _process function (make sure it's not indented under the 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 パラメータは フレームの長さ - 前のフレームが完了するまでに要した時間を参照します。この値を使うことで、動きの処理はフレームレートの変動の影響を受けなくなります。

Click "Play Scene" (F6) and confirm you can move the player around the screen in all directions.

警告

If you get an error in the "Debugger" panel that says

Attempt to call function 'play' in base 'null instance' on a null instance

this likely means you spelled the name of the AnimatedSprite node wrong. Node names are case-sensitive and $NodeName must match the name you see in the scene tree.

アニメーションの選択

Now that the player can move, we need to change which animation the AnimatedSprite is playing based on its direction. We have the "walk" animation, which shows the player walking to the right. This animation should be flipped horizontally using the flip_h property for left movement. We also have the "up" animation, which should be flipped vertically with flip_v for downward movement. Let's place this code at the end of the _process() function:

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

注釈

The boolean assignments in the code above are a common shorthand for programmers. Since we're doing a comparison test (boolean) and also assigning a boolean value, we can do both at the same time. Consider this code versus the one-line boolean assignment above:

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

Play the scene again and check that the animations are correct in each of the directions.

ちなみに

A common mistake here is to type the names of the animations wrong. The animation names in the SpriteFrames panel must match what you type in the code. If you named the animation "Walk", you must also use a capital "W" in the code.

When you're sure the movement is working correctly, add this line to _ready(), so the player will be hidden when the game starts:

hide()
Hide();

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

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

Add the following at the top of the script, after 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

Notice our custom "hit" signal is there as well! Since our enemies are going to be RigidBody2D nodes, we want the body_entered(body: Node) signal. This signal will be emitted when a body contacts the player. Click "Connect.." and the "Connect a Signal" window appears. We don't need to change any of these settings so click "Connect" again. Godot will automatically create a function in your player's script.

../../_images/player_signal_connection.png

Note the green icon indicating that a signal is connected to this function. Add this code to the function:

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 シグナルを複数回トリガーしないようにする必要があります。

注釈

Disabling the area's collision shape can cause an error if it happens in the middle of the engine's collision processing. Using set_deferred() tells Godot to wait to disable the shape until it's safe to do so.

The last piece is to add a function we can call to reset the player when starting a new game.

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

Enemyシーン

Now it's time to make the enemies our player will have to dodge. Their behavior will not be very complex: mobs will spawn randomly at the edges of the screen, choose a random direction, and move in a straight line.

We'll create a Mob scene, which we can then instance to create any number of independent mobs in the game.

注釈

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

ノードの設定

Click Scene -> New Scene and add the following nodes:

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

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

../../_images/set_collision_mask.png

Set up the AnimatedSprite like you did for the player. This time, we have 3 animations: fly, swim, and walk. There are two images for each animation in the art folder.

Adjust the "Speed (FPS)" to 3 for all animations.

../../_images/mob_animations.gif

Set the Playing property in the Inspector to “On”.

We'll select one of these animations randomly so that the mobs will have some variety.

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

As in the Player scene, add a CapsuleShape2D for the collision. To align the shape with the image, you'll need to set the Rotation Degrees property to 90 (under "Transform" in the Inspector).

シーンを保存します。

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.

}

When we spawn a mob, we'll pick a random value between min_speed and max_speed for how fast each mob will move (it would be boring if they were all moving at the same 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)];
}

First, we get the list of animation names from the AnimatedSprite's frames property. This returns an Array containing all three animation names: ["walk", "swim", "fly"].

We then need to pick a random number between 0 and 2 to select one of these names from the list (array indices start at 0). randi() % n selects a random integer between 0 and n-1.

注釈

You must use randomize() if you want your sequence of "random" numbers to be different every time you run the scene. We're going to use randomize() in our Main scene, so we won't need it here.

The last piece is to make the mobs delete themselves when they leave the screen. Connect the screen_exited() signal of the VisibilityNotifier2D node and add this code:

func _on_VisibilityNotifier2D_screen_exited():
    queue_free()
public void OnVisibilityNotifier2DScreenExited()
{
    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/grid_snap_button.png

重要

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

../../_images/draw_path2d.gif

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

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

Your scene should look like this:

../../_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;
    }
}

Click the Main node and you will see the Mob property in the Inspector under "Script Variables".

You can assign this property's value in two ways:

  • Drag Mob.tscn from the "FileSystem" panel and drop it in the Mob property .
  • Click the down arrow next to "[empty]" and choose "Load". Select Mob.tscn.

Next, select the Player node in the Scene dock, and access the Node dock on the sidebar. Make sure to have the Signals tab selected in the Node dock.

You should see a list of the signals for the Player node. Find and double-click the hit signal in the list (or right-click it and select "Connect..."). This will open the signal connection dialog. 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 signal connection dialog and click "Connect". Add the following code to the new function, as well as a new_game function that will 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();
}

Now connect the timeout() signal of each of the Timer nodes (StartTimer, ScoreTimer , and MobTimer) to the main script. StartTimer will start the other two timers. ScoreTimer will increment the score by 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);
}

重要

Why PI? In functions requiring angles, GDScript uses radians, not degrees. If you're more comfortable working with degrees, you'll need to use the deg2rad() and rad2deg() functions to convert between the two.

Testing the scene

Let's test the scene to make sure everything is working. Add this to _ready():

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

Let's also assign Main as our "Main Scene" - the one that runs automatically when the game launches. Press the "Play" button and select Main.tscn when prompted.

You should be able to move the player around, see mobs spawning, and see the player disappear when hit by a mob.

When you're sure everything is working, remove the call to new_game() from _ready().

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

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

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

The HUD needs to display the following information:

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

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

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

  • ScoreLabel という名前のラベル
  • Label named Message.
  • StartButton という名前のボタン
  • MessageTimer という名前のTimer

Click on the ScoreLabel and type a number into the Text field in the Inspector. The default font for Control nodes is small and doesn't scale well. There is a font file included in the game assets called "Xolonium-Regular.ttf". To use this font, do the following:

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

Once you've done this on the ScoreLabel, you can click the down arrow next to the DynamicFont property and choose "Copy", then "Paste" it in the same place on the other two Control nodes.

注釈

アンカーとマージン: 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

On the MessageTimer, set the Wait Time to 2 and set the One Shot property to "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();
}

This function is called when we want to display a message temporarily, such as "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」ボタンが表示されます。

注釈

When you need to pause for a brief time, an alternative to using a Timer node is to use the SceneTree's create_timer() function. This can be very useful to add delays such as in the above code, where we want to wait some time before showing the "Start" button.

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

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

Connect the timeout() signal of MessageTimer and the pressed() signal of StartButton and add the following code to the new functions:

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をメインに接続する

Now that we're done creating the HUD scene, go back to Main. Instance the HUD scene in Main like you did the Player scene. The scene tree should look like this, so make sure you didn't miss anything:

../../_images/completed_main_scene.png

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

In the Node tab, connect the HUD's start_game signal to the new_game() function of the Main node by typing "new_game" in the "Receiver Method" in the "Connect a Signal" window. Verify that the green connection icon now appears next to func new_game() in the script.

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 を選択します。

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

If you play until "Game Over" and then start a new game right away, the creeps from the previous game may still be on the screen. It would be better if they all disappeared at the start of a new game. We just need a way to tell all the mobs to remove themselves. We can do this with the "group" feature.

In the Mob scene, select the root node and click the "Node" tab next to the Inspector (the same place where you find the node's signals). Next to "Signals", click "Groups" and you can type a new group name and click "Add".

../../_images/group_tab.png

Now all mobs will be in the "mobs" group. We can then add the following line to the game_over() function in Main:

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

The call_group() function calls the named function on every node in a group - in this case we are telling every mob to delete itself.

仕上げ

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

Background(背景)

The default gray background is not very appealing, so let's change its color. One way to do this is to use a ColorRect node. Make it the first node under Main so that it will be drawn behind the other nodes. ColorRect only has one property: Color. Choose a color you like and select "Layout" -> "Full Rect" so that it covers the screen.

You could also add a background image, if you have one, by using a TextureRect node instead.

効果音

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

Keyboard shortcut

Since the game is played with keyboard controls, it would be convenient if we could also start the game by pressing a key on the keyboard. We can do this with the "Shortcut" property of the Button node.

In the HUD scene, select the StartButton and find its Shortcut property in the Inspector. Select "New Shortcut" and click on the "Shortcut" item. A second Shortcut property will appear. Select "New InputEventAction" and click the new "InputEventAction". Finally, in the Action property, type the name ui_select. This is the default input event associated with the spacebar.

../../_images/start_button_shortcut.png

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

プロジェクトファイル

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