パート6

パートの概要

このパートでは、メインメニューと一時停止メニューを追加し、プレーヤーの再出現システムを追加し、サウンドシステムを変更/移動して、どのスクリプトからでも使用できるようにします。

これはFPSチュートリアルの最後の部分です。これが完了すると、GodotですばらしいFPSゲームを構築するための強固な基盤が得られます!

../../../_images/FinishedTutorialPicture.png

注釈

チュートリアルのこの部分に進む前に、パート5 を終了していると想定しています。パート5 の完成したプロジェクトは、パート6の開始プロジェクトになります

では、始めましょう!

メインメニューの追加

まず、Main_Menu.tscn を開き、シーンがどのように設定されているかを見てみましょう。

メインメニューは3つの異なるパネルに分かれており、それぞれがメインメニューの異なる「画面」を表しています。

注釈

Background_Animation ノードは、メニューの背景が単色よりも少し興味深くなるようになっています。 それはスカイボックスの周りを見るカメラであり、空想的なものではありません。

すべてのノードを自由に展開して、どのように設定されているかを確認してください。メインメニューに入ったときに最初に表示したい画面なので、終了したら Start_Menu のみを表示することを忘れないでください。

Main_Menu``(ルートノード)を選択し、\ ``Main_Menu.gd という新しいスクリプトを作成します。次のように追加します:

extends Control

var start_menu
var level_select_menu
var options_menu

export (String, FILE) var testing_area_scene
export (String, FILE) var space_level_scene
export (String, FILE) var ruins_level_scene

func _ready():
    start_menu = $Start_Menu
    level_select_menu = $Level_Select_Menu
    options_menu = $Options_Menu

    $Start_Menu/Button_Start.connect("pressed", self, "start_menu_button_pressed", ["start"])
    $Start_Menu/Button_Open_Godot.connect("pressed", self, "start_menu_button_pressed", ["open_godot"])
    $Start_Menu/Button_Options.connect("pressed", self, "start_menu_button_pressed", ["options"])
    $Start_Menu/Button_Quit.connect("pressed", self, "start_menu_button_pressed", ["quit"])

    $Level_Select_Menu/Button_Back.connect("pressed", self, "level_select_menu_button_pressed", ["back"])
    $Level_Select_Menu/Button_Level_Testing_Area.connect("pressed", self, "level_select_menu_button_pressed", ["testing_scene"])
    $Level_Select_Menu/Button_Level_Space.connect("pressed", self, "level_select_menu_button_pressed", ["space_level"])
    $Level_Select_Menu/Button_Level_Ruins.connect("pressed", self, "level_select_menu_button_pressed", ["ruins_level"])

    $Options_Menu/Button_Back.connect("pressed", self, "options_menu_button_pressed", ["back"])
    $Options_Menu/Button_Fullscreen.connect("pressed", self, "options_menu_button_pressed", ["fullscreen"])
    $Options_Menu/Check_Button_VSync.connect("pressed", self, "options_menu_button_pressed", ["vsync"])
    $Options_Menu/Check_Button_Debug.connect("pressed", self, "options_menu_button_pressed", ["debug"])

    Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)

    var globals = get_node("/root/Globals")
    $Options_Menu/HSlider_Mouse_Sensitivity.value = globals.mouse_sensitivity
    $Options_Menu/HSlider_Joypad_Sensitivity.value = globals.joypad_sensitivity


func start_menu_button_pressed(button_name):
    if button_name == "start":
        level_select_menu.visible = true
        start_menu.visible = false
    elif button_name == "open_godot":
        OS.shell_open("https://godotengine.org/")
    elif button_name == "options":
        options_menu.visible = true
        start_menu.visible = false
    elif button_name == "quit":
        get_tree().quit()


func level_select_menu_button_pressed(button_name):
    if button_name == "back":
        start_menu.visible = true
        level_select_menu.visible = false
    elif button_name == "testing_scene":
        set_mouse_and_joypad_sensitivity()
        get_node("/root/Globals").load_new_scene(testing_area_scene)
    elif button_name == "space_level":
        set_mouse_and_joypad_sensitivity()
        get_node("/root/Globals").load_new_scene(space_level_scene)
    elif button_name == "ruins_level":
        set_mouse_and_joypad_sensitivity()
        get_node("/root/Globals").load_new_scene(ruins_level_scene)


func options_menu_button_pressed(button_name):
    if button_name == "back":
        start_menu.visible = true
        options_menu.visible = false
    elif button_name == "fullscreen":
        OS.window_fullscreen = !OS.window_fullscreen
    elif button_name == "vsync":
        OS.vsync_enabled = $Options_Menu/Check_Button_VSync.pressed
    elif button_name == "debug":
        pass


func set_mouse_and_joypad_sensitivity():
    var globals = get_node("/root/Globals")
    globals.mouse_sensitivity = $Options_Menu/HSlider_Mouse_Sensitivity.value
    globals.joypad_sensitivity = $Options_Menu/HSlider_Joypad_Sensitivity.value

ここのコードの大部分はUIの作成に関連していますが、これはこのチュートリアルシリーズの目的外です。UI関連のコードについては簡単に説明します。

ちなみに

GUIとUIを作成するより良い方法については、タイトル画面のデザイン とそのチュートリアルを参照してください!

まずクラス変数を見てみましょう。

  • start_menu: Start_Menu を保持する Panel 変数 。
  • level_select_menu: Level_Select_Menu を保持する Panel 変数。
  • options_menu: Options_Menu を保持する Panel 変数。
  • testing_area_scene: Testing_Area.tscn ファイルへのパスなので、このシーンから変更できます。
  • space_level_scene: Space_Level.tscn ファイルへのパスなので、このシーンから変更できます。
  • ruins_level_scene: Ruins_Level.tscn ファイルへのパスなので、このシーンから変更できます。

警告

このスクリプトをテストする前に、エディタで正しいファイルへのパスを設定する必要があります! それ以外の場合は動作しません!


さあ、_ready を見てみましょう

まず、すべての Panel ノードを取得し、適切な変数に割り当てます。

次に、すべてのボタンの pressed シグナルをそれぞれの [ここはにはパネル名が入る]_button_pressed 関数に接続します。

次に、マウスモードを MOUSE_MODE_VISIBLE に設定して、プレイヤーがこのシーンに戻るたびにマウスが表示されるようにします。

次に、Globals と呼ばれるシングルトンを取得します。次に、HSlider ノードの値を設定し、その値がシングルトンのマウスとジョイパッドの感度と一致するようにします。

注釈

まだ Globals シングルトンを作成していませんが、心配しないでください! 私たちはすぐにそれを作るつもりです!


start_menu_button_pressed では、どのボタンが押されているかを確認します。

押されたボタンに基づいて、現在表示されているパネルを変更するか、アプリケーションを終了するか、Godot Webサイトを開きます。


level_select_menu_button_pressed で、どのボタンが押されているかを確認します。

back ボタンが押された場合、現在表示されているパネルを変更してメインメニューに戻ります。

シーン変更ボタンのいずれかが押された場合、最初に set_mouse_and_joypad_sensitivity を呼び出すので、シングルトン(Globals.gd)は HSlider ノードからの値を持ちます。 次に、シングルトンに load_new_scene 関数を使用してノードを変更し、プレイヤーが選択したシーンのファイルパスを渡します。

注釈

シングルトンについては心配しないでください、すぐにそこへ到着します!


options_menu_button_pressed で、どのボタンが押されているかを確認します。

back ボタンが押された場合、現在表示されているパネルを変更してメインメニューに戻ります。

fullscreen ボタンが押された場合、OS のフルスクリーンモードを、現在のモードと逆のモードに設定することで切り替えます。

vsync ボタンが押されると、Vsyncチェックボタンの状態に基づいて OS のVsyncを設定します。


最後に、set_mouse_and_joypad_sensitivity を見てみましょう。

まず、Globals シングルトンを取得し、ローカル変数に割り当てます。

次に、mouse_sensitivity および joypad_sensitivity 変数を、それぞれの HSlider ノードの対応する値に設定します。

Globals シングルトンを作る

さて、これがすべて機能するためには、Globals シングルトンを作成する必要があります。Script``タブで新しいスクリプトを作成し、\ ``Globals.gd と呼びます。

注釈

Globals シングルトンを作成するには、エディタの Script タブに移動し、新規スクリプト をクリックすると スクリプト作成 ボックスが表示されます。 スクリプト名 Globals.gd を挿入する必要がある パス 以外は変更しないでください。

Globals.gd に以下を追加します。

extends Node

var mouse_sensitivity = 0.08
var joypad_sensitivity = 2

func _ready():
    pass

func load_new_scene(new_scene_path):
    get_tree().change_scene(new_scene_path)

ご覧のとおり、非常に小さくシンプルです。 この部分が進むにつれて、Globals.gd にさらに複雑なロジックを追加し続けますが、現時点では、2つのクラス変数を保持し、シーンの変更方法を抽象的に定義するだけです。

  • mouse_sensitivity: マウスの現在の感度です。これを Player.gd で読み込むことができます。
  • joypad_sensitivity: ジョイパッドの現在の感度です。これを Player.gd で読み込むことができます。

今のところ、Globals.gd を使用するのは、シーン間で変数をやりとりする手段としてだけです。マウスとジョイパッドの感度は Globals.gd に保存されているため、1つのシーン(Options_Menu など)で行った変更は、プレイヤーの感度に影響します。

load_new_scene で行っているのは、SceneTreechange_scene 関数を呼び出して、load_new_scene で指定されたシーンパスを渡すことです。

Globals.gd に今必要なコードはこれだけです! メインメニューをテストする前に、まず Globals.gd を自動読み込みスクリプトとして設定する必要があります。

プロジェクト設定 を開き、自動読み込み タブをクリックします。

../../../_images/AutoloadAddSingleton.png

次に、パス フィールドの横にあるボタン(フォルダのアイコン)をクリックして、Globals.gd へのパスを選択します。ノード名 フィールドの名前が Globals であることを確認します。 上の図のようになっていれば、追加 を押してください!

これにより、Globals.gd がシングルトン/自動読み込みスクリプトになり、どのシーンのどのスクリプトからでもアクセスできるようになります。

ちなみに

シングルトン/自動読み込みスクリプトの詳細については、シングルトン(自動読み込み) を参照してください。

Globals.gd はシングルトン/自動読み込みスクリプトなので、メインメニューをテストできます!

メインシーンを Testing_Area.tscn から Main_Menu.tscn に変更すると、ゲームをエクスポートするときにプレイヤーがメインメニューで開始するようになります。 これは、プロジェクト設定一般 タブで行うことができます。Application カテゴリの Run サブカテゴリをクリックし、Main Scene の値を変更してメインシーンを変更できます。

警告

メインメニューをテストする前に、エディタの Main_Menu で正しいファイルへのパスを設定する必要があります! そうしないと、レベル選択メニュー/画面からシーンを変更できません。

デバッグメニューの追加

では、ゲーム内でFPS(Frames Per Second)などを追跡できるように、簡単なデバッグシーンを追加しましょう。Debug_Display.tscn を開きます。

画面の右上隅に配置された Panel であることがわかります。Labels が3つあります。1つはゲームが実行されているFPSを表示するためのもの、もう1つはゲームが実行されているOSを示すためのもの、そしてゲームが実行されているGodotバージョンを示すためのラベルです。

これらの Lebels を埋めるために必要なコードを追加しましょう。Debug_Display を選択し、Debug_Display.gd という新しいスクリプトを作成します。 次のように追加します:

extends Control

func _ready():
    $OS_Label.text = "OS: " + OS.get_name()
    $Engine_Label.text = "Godot version: " + Engine.get_version_info()["string"]

func _process(delta):
    $FPS_Label.text = "FPS: " + str(Engine.get_frames_per_second())

このスクリプトの動作について説明します。


_ready では、get_name 関数を使用して、OS によって提供される名前に OS_Label のテキストを設定します。これは、GodotがコンパイルされたOS(またはオペレーティングシステム)の名前を返します。たとえば、Windowsを実行している場合は Windows を返し、Linuxを実行している場合は X11 を返します。

次に、Engine_Label のテキストを Engine.get_version_info によって提供されるバージョン情報に設定します。Engine.get_version_info は、現在実行中のGodotのバージョンに関する有用な情報が満載されたdictionaryを返します。少なくともこのラベルについては、文字列バージョンのみが重要なので、文字列を取得し、それをEngine_Labeltext として割り当てます。get_version_info が返す値の詳細については Engine を参照してください。

_process では、FPS_Label のテキストを Engine.get_frames_per_second に設定しますが、get_frames_per_second は整数を返すため、:ref:`Label <class_Label>` に追加する前に ``str を使用して文字列にキャストする必要があります。


次に、Main_Menu.gd に戻って、options_menu_button_pressed で次の項目を変更してみましょう:

elif button_name == "debug":
    pass

代わりにこれを実行します:

elif button_name == "debug":
    get_node("/root/Globals").set_debug_display($Options_Menu/Check_Button_Debug.pressed)

これにより、シングルトン内で set_debug_display という新しい関数が呼び出されるので、次に追加しましょう!


Globals.gd を開き、次のクラス変数を追加します:

# ------------------------------------
# All the GUI/UI-related variables

var canvas_layer = null

const DEBUG_DISPLAY_SCENE = preload("res://Debug_Display.tscn")
var debug_display = null

# ------------------------------------
  • canvas_layer: Globals.gd で作成されたGUI / UIが常に一番上に描画されるキャンバスレイヤー。
  • DEBUG_DISPLAY: 先ほど取り組んだデバッグ表示シーン。
  • debug_display: デバッグ表示がある場合にデバッグ表示を保持する変数。

クラス変数が定義されたので、_ready に数行を追加して、使用するキャンバスレイヤーを Globals.gd が保持するようにする必要があります。_ready を次のように変更します:

func _ready():
    canvas_layer = CanvasLayer.new()
    add_child(canvas_layer)

_ready で、新しいキャンバスレイヤーを作成し、それを canvas_layer に割り当てて、子として追加します。Globals.gd は自動読み込み/シングルトンであるため、Godotはゲームの起動時に Node を作成し、それに Globals.gd がアタッチされます。Godotは Node を作成するため、子ノードの追加/削除に関しては、他のノードと同様に Globals.gd を扱うことができます。

CanvasLayer を追加する理由は、Globals.gd でインスタンス化/生成するすべてのGUIおよびUIノードが常に他のすべての上に描画されるためです。

シングルトン/自動読み込みにノードを追加するときは、子ノードへの参照を失わないように注意する必要があります。これは、アクティブなシーンを変更してもノードが解放/破棄されないためです。つまり、多くのノードをインスタンス化/生成していて、それを解放していない場合、メモリの問題が発生する可能性があります。


次に、set_debug_displayGlobals.gd に追加する必要があります:

func set_debug_display(display_on):
    if display_on == false:
        if debug_display != null:
            debug_display.queue_free()
            debug_display = null
    else:
        if debug_display == null:
            debug_display = DEBUG_DISPLAY_SCENE.instance()
            canvas_layer.add_child(debug_display)

何が起こっているのか見ていきましょう。

まず、Globals.gd がデバッグ表示をオンにしようとしているか、オフにしようとしているかを確認します。

Globals.gd がディスプレイをオフにしている場合、debug_displaynull と等しくないかどうかを確認します。debug_displaynull と等しくない場合、Globals.gd には現在アクティブなデバッグ表示が必要です。`` Globals.gd`` がデバッグ表示をアクティブにしている場合、queue_free を使用してそれを解放し、nulldebug_display に割り当てます。

Globals.gd がディスプレイをオンにしている場合、Globals.gd のデバッグディスプレイがまだアクティブになっていないことを確認します。これを行うには、debug_displaynull と等しくなるようにします。debug_displaynull の場合、新しい DEBUG_DISPLAY_SCENE をインスタンス化し、それを canvas_layer の子として追加します。


これで、Options_Menu パネルの CheckButton を切り替えることで、デバッグ表示のオンとオフを切り替えることができます。試してみてください!

シーンを Main_Menu.tscn から別のシーン(Testing_Area.tscn など)に変更しても、デバッグ表示がどのように維持されるかに注意してください。これは、シングルトン/自動読み込みにノードをインスタンス化/生成し、それらをシングルトン/自動読み込みに子として追加することの美しさです。シングルトン/自動読み込みの子として追加されたノードは、ゲームが実行されている限り、追加作業なしで残ります!

一時停止メニューの追加

一時停止メニューを追加して、ui_cancel アクションを押したときにメインメニューに戻ることができるようにします。

Pause_Popup.tscn を開きます。

Pause_Popup のルートノードが WindowDialog であることに注意してください。WindowDialogPopup から継承します。つまり、WindowDialog はポップアップのように機能します。

Pause_Popup を選択し、インスペクタの Pause メニューに到達するまで下にスクロールします。一時停止モードが、デフォルトで通常設定されているように、inherit ではなく process に設定されていることに注意してください。これにより、ゲームが一時停止されている場合でも処理を続行します。これは、UI要素と対話するために必要です。

Pause_Popup.tscn の設定方法を確認したので、それを機能させるコードを書きましょう。通常、シーンのルートノード、この場合は Pause_Popup にスクリプトをアタッチしますが、Globals.gd でいくつかのシグナルを受信する必要があるため、ポップアップのすべてのコードをそこに記述します。

Globals.gd を開き、次のクラス変数を追加します:

const MAIN_MENU_PATH = "res://Main_Menu.tscn"
const POPUP_SCENE = preload("res://Pause_Popup.tscn")
var popup = null
  • MAIN_MENU_PATH: メインメニューシーンへのパス。
  • POPUP_SCENE: 先ほど見たポップアップシーン。
  • popup: ポップアップシーンを保持する変数。

ここで _processGlobals.gd に追加して、ui_cancel アクションが押されたときに応答できるようにします。_process に以下を追加します:

func _process(delta):
    if Input.is_action_just_pressed("ui_cancel"):
        if popup == null:
            popup = POPUP_SCENE.instance()

            popup.get_node("Button_quit").connect("pressed", self, "popup_quit")
            popup.connect("popup_hide", self, "popup_closed")
            popup.get_node("Button_resume").connect("pressed", self, "popup_closed")

            canvas_layer.add_child(popup)
            popup.popup_centered()

            Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)

            get_tree().paused = true

ここで何が起こっているのか見ていきましょう。


まず、ui_cancel アクションが押されているかどうかを確認します。次に、popupnull に等しいかどうかを確認することにより、Globals.gdpopup を開いていないことを確認します。

Globals.gd がポップアップが開いていない場合、POPUP_SCENE をインスタンス化し、それを popup に割り当てます。

次に、終了ボタンを取得し、その pressed シグナルを popup_quit に割り当てます。この関数は少し先で追加します。

次に、WindowDialog からの popup_hide シグナルと、再開ボタンからの pressed シグナルの両方を popup_closed に割り当てます。

次に、canvas_layer の子として popup を追加して、上に描画されるようにします。次に、popup_centered を使用して、画面の中央にポップアップするように popup に指示します。

次に、マウスモードが MOUSE_MODE_VISIBLE になっていることを確認して、プレイヤーがポップアップを操作できるようにします。これを行わなかった場合、プレイヤーは、マウスモードが MOUSE_MODE_CAPTURED のシーンでポップアップを操作できなくなります。

最後に、SceneTree 全体を一時停止します。

注釈

Godotでの一時停止の詳細については、ゲームの一時停止 を参照してください


次に、シグナルを接続した関数を追加する必要があります。最初に popup_closed を追加しましょう。

以下を ``Globals.gd``に追加します:

func popup_closed():
    get_tree().paused = false

    if popup != null:
        popup.queue_free()
        popup = null

popup_closed はゲームを再開し、ポップアップがあればそれを破棄します。

popup_quit も似ていますが、マウスを見えるようにし、シーンをタイトル画面に変更することも確認しています。

以下を ``Globals.gd``に追加します:

func popup_quit():
    get_tree().paused = false

    Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)

    if popup != null:
        popup.queue_free()
        popup = null

    load_new_scene(MAIN_MENU_PATH)

popup_quit はゲームを再開し、マウスモードを MOUSE_MODE_VISIBLE に設定して、マウスがメインメニューに表示されるようにし、ポップアップがあればポップアップを破棄し、シーンをメインメニューに変更します。


ポップアップをテストする準備が整う前に、Player.gd の1つを変更する必要があります。

Player.gd を開き、process_input で、カーソルをキャプチャ/解放するためのコードを次のように変更します:

これの代わりに:

# Capturing/Freeing cursor
if Input.is_action_just_pressed("ui_cancel"):
    if Input.get_mouse_mode() == Input.MOUSE_MODE_VISIBLE:
        Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
    else:
        Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)

これだけを残します:

# Capturing/Freeing cursor
if Input.get_mouse_mode() == Input.MOUSE_MODE_VISIBLE:
    Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

ここで、マウスをキャプチャ/解放する代わりに、現在のマウスモードが MOUSE_MODE_VISIBLE であるかどうかを確認します。そうである場合、MOUSE_MODE_CAPTURED に戻します。

ポップアップは一時停止するたびにマウスモードを MOUSE_MODE_VISIBLE にするため、Player.gd でカーソルを解放してキャプチャすることを心配する必要がなくなりました。


これで一時停止メニューのポップアップが終了しました。ゲームの任意の時点で一時停止して、メインメニューに戻ることができます!

再出現システムの開始

プレイヤーは体力をすべて失う可能性があるため、プレイヤーが死亡して復活する事が理想的です。次はそれを追加しましょう!

まず、Player.tscn を開き、HUD を展開します。Death_Screen という ColorRect があることに注意してください。プレイヤーが死んだとき、Death_Screen を表示し、プレイヤーが再出現できるようになるまで待機する時間を表示します。

Player.gd を開き、以下のクラス変数を追加してください:

const RESPAWN_TIME = 4
var dead_time = 0
var is_dead = false

var globals
  • RESPAWN_TIME: 再出現にかかる時間(秒単位)。
  • dead_time: プレイヤーがどれくらいの時間死んでいるかを追跡する変数。
  • is_dead: プレイヤーが現在死んでいるかどうかを追跡する変数。
  • globals: Globals.gd シングルトンを保持する変数。

_ready に数行を追加する必要があります。それにより、Player.gdGlobals.gd が使用できます。以下を _ready に追加します:

globals = get_node("/root/Globals")
global_transform.origin = globals.get_respawn_position()

これで、Globals.gd シングルトンを取得し、それを globals に割り当てています。また、プレイヤーのグローバル Transform の原点を globals.get_respawn_position によって返される位置に設定することにより、プレイヤーのグローバル位置を設定します。

注釈

心配しないでください、さらに下で get_respawn_position を追加します!


次に、 _physics_process にいくつかの変更を加える必要があります。_physics_process を次のように変更します:

func _physics_process(delta):

    if !is_dead:
        process_input(delta)
        process_view_input(delta)
        process_movement(delta)

    if (grabbed_object == null):
        process_changing_weapons(delta)
        process_reloading(delta)

    process_UI(delta)
    process_respawn(delta)

プレイヤーが死亡した場合、プレイヤーは入力または移動入力を処理しません。また、死亡中は process_respawn を呼び出しています。

注釈

if !is_dead: 式は比較であり、if is_dead == false: 式 と同じように機能します。そして、式から ! `` 記号を削除すると、反対の式 ``if is_dead == true: が得られます。これは、同じコード機能を記述する短い方法です。

まだ process_respawn を作成していないので、その変更をしましょう。


process_respawn を追加しましょう。以下を Player.gd に追加します:

func process_respawn(delta):

    # If we've just died
    if health <= 0 and !is_dead:
        $Body_CollisionShape.disabled = true
        $Feet_CollisionShape.disabled = true

        changing_weapon = true
        changing_weapon_name = "UNARMED"

        $HUD/Death_Screen.visible = true

        $HUD/Panel.visible = false
        $HUD/Crosshair.visible = false

        dead_time = RESPAWN_TIME
        is_dead = true

        if grabbed_object != null:
            grabbed_object.mode = RigidBody.MODE_RIGID
            grabbed_object.apply_impulse(Vector3(0, 0, 0), -camera.global_transform.basis.z.normalized() * OBJECT_THROW_FORCE / 2)

            grabbed_object.collision_layer = 1
            grabbed_object.collision_mask = 1

            grabbed_object = null

    if is_dead:
        dead_time -= delta

        var dead_time_pretty = str(dead_time).left(3)
        $HUD/Death_Screen/Label.text = "You died\n" + dead_time_pretty + " seconds till respawn"

        if dead_time <= 0:
            global_transform.origin = globals.get_respawn_position()

            $Body_CollisionShape.disabled = false
            $Feet_CollisionShape.disabled = false

            $HUD/Death_Screen.visible = false

            $HUD/Panel.visible = true
            $HUD/Crosshair.visible = true

            for weapon in weapons:
                var weapon_node = weapons[weapon]
                if weapon_node != null:
                    weapon_node.reset_weapon()

            health = 100
            grenade_amounts = {"Grenade":2, "Sticky Grenade":2}
            current_grenade = "Grenade"

            is_dead = false

この関数の動作を見てみましょう。


最初に、health0 以下であり、is_deadfalse であるかどうかを確認することにより、プレイヤーが死亡したかどうかを確認します。

プレイヤーが死亡したばかりの場合、プレイヤーのコリジョンシェイプを無効にします。これは、プレイヤーが死体で何かをブロックしていないことを確認するために行います。

次に、changing_weapontrue に設定し、changing_weapon_nameUNARMED に設定します。そのため、プレイヤーが武器を使用している場合は、プレイヤーが死んだときに片付けられます。

次に、Death_Screen ColorRect を表示して、プレイヤーが死亡したときにすべての上にグレーのオーバーレイが表示されるようにします。次に、残りのUIである Panel および Crosshair ノードを非表示にします。

次に、dead_timeRESPAWN_TIME に設定して、プレイヤーが死んでいる時間のカウントダウンを開始できるようにします。また、is_deadtrue に設定して、プレイヤーが死亡したことを確認します。

プレイヤーが死亡したときにオブジェクトを保持している場合、それを投げる必要があります。まず、プレイヤーがオブジェクトを持っているかどうかを確認します。プレイヤーがオブジェクトを保持している場合は、パート5 で追加したスローコードと同じコードを使用してオブジェクトをスローします。

注釈

表現 You have died\n\n の組み合わせは、以下の新しい行に続くテキストを表示するために使用されるコマンドです。これは、表示されたテキストを複数の行にうまくグループ化する場合に常に役立ちます。これにより、見栄えが良くなり、ゲームのプレーヤーが読みやすくなります。


次に、プレイヤーが死んでいるかどうかを確認します。その場合、dead_time から delta を減らします。

次に、dead_time_pretty という新しい変数を作成します。この変数では、dead_time を文字列に変換し、左から最初の3文字のみを使用します。これにより、プレイヤーが再出現できるようになるまでにプレイヤーが待機する時間を示す、見栄えの良い文字列が得られます。

次に、Death_ScreenLabel を変更して、プレイヤーが残した時間を表示します。

次に、プレイヤーが十分に待機して再出現できるかどうかを確認します。これを行うには、dead_time0 以下かどうかを確認します。

プレイヤーが再出現するのに十分な時間待機している場合、プレイヤーの位置を get_respawn_position によって提供される新しい再出現位置に設定します。

次に、両方のプレイヤーのコリジョンシェイプを有効にして、プレイヤーが環境と再び衝突できるようにします。

次に、Death_Screen を非表示にし、残りのUI、Panel および Crosshair ノードを再び表示します。

次に、各武器を調べて、その reset_weapon 関数を呼び出します。この関数はすぐに追加します。

次に、health100 にリセットし、grenade_amounts をデフォルト値にリセットし、current_grenadeGrenade に変更します。これにより、これらの変数がデフォルト値に効果的にリセットされます。

最後に、is_deadfalse に設定します。


Player.gd を離れる前に、簡単なことを _input に追加する必要があります。_input の先頭に次を追加します:

if is_dead:
    return

これで、プレイヤーが死んでいるとき、プレイヤーはマウスで周りを見回すことはできません。

再出現システムの終了

まず、Weapon_Pistol.gd を開いて、reset_weapon 関数を追加しましょう。以下を追加します:

func reset_weapon():
    ammo_in_weapon = 10
    spare_ammo = 20

reset_weapon を呼び出すと、ピストルの弾薬とスペアの弾薬がデフォルト値にリセットされます。

Weapon_Rifle.gdreset_weapon を追加しましょう:

func reset_weapon():
    ammo_in_weapon = 50
    spare_ammo = 100

そして、以下を Weapon_Knife.gd に追加します:

func reset_weapon():
    ammo_in_weapon = 1
    spare_ammo = 1

これで、プレイヤーが死亡するとすべての武器がリセットされます。


ここで、Globals.gd にいくつかの項目を追加する必要があります。まず、次のクラス変数を追加します:

var respawn_points = null
  • respawn_points: レベル内のすべてのリスポーンポイントを保持する変数

毎回ランダムな出現ポイントを取得しているため、数値ジェネレーターをランダム化する必要があります。以下を _ready に追加します:

randomize()

randomize は新しいランダムシードを取得するので、任意のランダム関数を使用すると(比較的)ランダムな数字の文字列を取得します。

get_respawn_positionGlobals.gd に追加しましょう:

func get_respawn_position():
    if respawn_points == null:
        return Vector3(0, 0, 0)
    else:
        var respawn_point = rand_range(0, respawn_points.size() - 1)
        return respawn_points[respawn_point].global_transform.origin

この関数の動作について説明します。


まず、 respawn_pointsnull であるかどうかを確認することで、Globals.gdrespawn_points があるかどうかを確認します。

respawn_pointsnull の場合、空の Vector 3 の位置を (0, 0, 0) の位置で返します。

respawn_pointsnull でない場合、0 と要素数の respawn_points から 1 を引いた値との間の乱数を取得します。GDScript を含むほとんどのプログラミング言語が 、リスト内の要素にアクセスするときに 0 からカウントを開始します。

次に、respawn_pointsrespawn_point 位置にある Spatial ノードの位置を返します。


Globals.gd を完了する前に、load_new_scene に以下を追加する必要があります:

respawn_points = null

プレイヤーが再出現ポイントのないレベルに到達した場合、またはそのレベルに達した場合、以前のレベルにあった再出現ポイントでプレイヤーが再出現しないように、respawn_pointsnull に設定します。


ここで必要なのは、再出現ポイントを設定する方法だけです。Ruins_Level.tscn を開き、Spawn_Points を選択します。Respawn_Point_Setter.gd という新しいスクリプトを追加し、それを Spawn_Points に添付します。以下を Respawn_Point_Setter.gd に追加します:

extends Spatial

func _ready():
    var globals = get_node("/root/Globals")
    globals.respawn_points = get_children()

これで、Respawn_Point_Setter.gdを持つノードの_ready関数が呼び出されると、Ruins_Level.tscn の場合、Respawn_Point_Setter.gdSpawn_Points を持つノードのすべての子ノードが Globals.gdrespawn_points に追加されます。

警告

Respawn_Point_Setter.gd を持つノードは、プレイヤーの _ready 関数で必要になる前に、再出現ポイントが設定されるように、SceneTree 内でプレイヤーより上に位置する必要があります。


プレイヤーが死ぬと、4 秒待ってから復活します!

注釈

Ruins_Level.tscn 以外のレベルの出現ポイントはまだ設定されていません!出現ポイントを Space_Level.tscn に追加することは、読者の演習として残しておきます。

どこでも使えるサウンドシステムを書く

最後に、プレイヤーを使用せずにどこからでもサウンドを再生できるように、サウンドシステムを作成しましょう。

まず、SimpleAudioPlayer.gd を開き、次のように変更します:

extends Spatial

var audio_node = null
var should_loop = false
var globals = null

func _ready():
    audio_node = $Audio_Stream_Player
    audio_node.connect("finished", self, "sound_finished")
    audio_node.stop()

    globals = get_node("/root/Globals")


func play_sound(audio_stream, position=null):
    if audio_stream == null:
        print ("No audio stream passed; cannot play sound")
        globals.created_audio.remove(globals.created_audio.find(self))
        queue_free()
        return

    audio_node.stream = audio_stream

    # If you are using an AudioStreamPlayer3D, then uncomment these lines to set the position.
    #if audio_node is AudioStreamPlayer3D:
    #    if position != null:
    #        audio_node.global_transform.origin = position

    audio_node.play(0.0)


func sound_finished():
    if should_loop:
        audio_node.play(0.0)
    else:
        globals.created_audio.remove(globals.created_audio.find(self))
        audio_node.stop()
        queue_free()

古いバージョンからいくつかの変更点があります。何よりもまず、もはや SimpleAudioPlayer.gd にサウンドファイルを保存しなくなっています。これは、サウンドを作成するときに各オーディオクリップを読み込むのではなく、代わりにオーディオストリームを play_sound に強制的に渡すため、パフォーマンスにとってははるかに優れています。

もう1つの変更点は、should_loop という新しいクラス変数があることです。終了するたびにオーディオプレーヤーを破棄する代わりに、オーディオプレーヤーがループするように設定されているかどうかを確認します。これにより、バックグラウンドミュージックのループのようなオーディオを作成できます。古いオーディオプレーヤーが終了したときに、新しいオーディオプレーヤーで音楽を生成する必要はありません。

最後に、Player.gd でインスタンス化/産出される代わりに、オーディオプレーヤーは Globals.gd で産出されるため、どのシーンからでもサウンドを作成できます。オーディオプレーヤーは Globals.gd シングルトンを保存するようになったため、オーディオプレーヤーが破棄されたときに、Globals.gd のリストから削除することもできます。

変更点を調べてみましょう。


クラス変数については、代わりに Globals.gd から渡されるため、すべての audio_[ここに名前を挿入] 変数を削除しました。

また、2つの新しいクラス変数 should_loopglobals を追加しました。should_loop を使用して、オーディオプレーヤーがサウンドの終了時にループする必要があるかどうかを判断し、globalsGlobals.gd シングルトンを保持します。

_ready``の唯一の変更は、オーディオプレーヤーが ``Globals.gd シングルトンを取得し、それを globals に割り当てることです。

play_soundsound_name ではなく、audio_stream という名前のオーディオストリームが渡されることを期待しています。サウンド名を確認してオーディオプレーヤーのストリームを設定する代わりに、オーディオストリームが渡されたことを確認します。オーディオストリームが渡されなかった場合、エラーメッセージを出力し、Globals.gd シングルトンの created_audio という名前のリストからオーディオプレーヤーを削除してから、オーディオプレーヤーを解放します。

最後に、 sound_finished で、オーディオプレーヤーが should_loop を使用するかどうかを最初に確認します。オーディオプレーヤーがループすることになっている場合、0.0 の位置で最初から再びサウンドを再生します。オーディオプレーヤーがループすることになっていない場合、created_audio と呼ばれる Globals.gd シングルトンのリストからオーディオプレーヤーを削除し、オーディオプレーヤーを解放します。


SimpleAudioPlayer.gd への変更が完了したので、次に Globals.gd に注意を向ける必要があります。まず、次のクラス変数を追加します:

# All the audio files.

# You will need to provide your own sound files.
var audio_clips = {
    "Pistol_shot":null, #preload("res://path_to_your_audio_here!")
    "Rifle_shot":null, #preload("res://path_to_your_audio_here!")
    "Gun_cock":null, #preload("res://path_to_your_audio_here!")
}

const SIMPLE_AUDIO_PLAYER_SCENE = preload("res://Simple_Audio_Player.tscn")
var created_audio = []

これらのグローバル変数について説明します。

  • audio_clips: Globals.gd が再生できるすべてのオーディオクリップを保持するdictionary。
  • SIMPLE_AUDIO_PLAYER_SCENE: シンプルなオーディオプレーヤーシーン。
  • created_audio: Globals.gd が作成したすべてのシンプルオーディオプレーヤーを保持するリスト。

注釈

オーディオを追加したい場合は、それを audio_clips に追加する必要があります。このチュートリアルでは音声ファイルは提供されないため、独自の音声ファイルを提供する必要があります。

推奨するサイトの1つは GameSounds.xyz です。Sonniss' GDC Game Audio bundle for 2017 に含まれている Gamemaster audio gun sound pack を使用しています。使用したトラック(若干の編集を含む)は次のとおりです:

  • gun_revolver_pistol_shot_04、
  • gun_semi_auto_rifle_cock_02、
  • gun_submachine_auto_shot_00_automatic_preview_01

ここで、play_sound という新しい関数を Globals.gd に追加する必要があります:

func play_sound(sound_name, loop_sound=false, sound_position=null):
    if audio_clips.has(sound_name):
        var new_audio = SIMPLE_AUDIO_PLAYER_SCENE.instance()
        new_audio.should_loop = loop_sound

        add_child(new_audio)
        created_audio.append(new_audio)

        new_audio.play_sound(audio_clips[sound_name], sound_position)

    else:
        print ("ERROR: cannot play sound that does not exist in audio_clips!")

この関数の動作について説明します。

まず、 Globals.gdaudio_clipssound_name という名前のオーディオクリップがあるかどうかを確認します。そうでない場合は、エラーメッセージを出力します。

Globals.gdsound_name という名前のオーディオクリップがある場合、新しい SIMPLE_AUDIO_PLAYER_SCENE をインスタンス化/生成し、それを new_audio に割り当てます。

次に、should_loop を設定し、Globals.gd の子として new_audio を追加します。

注釈

シーンを変更してもこれらのノードは破棄されないため、シングルトンにノードを追加する場合は注意が必要です。

作成されたすべてのオーディオを保持するために、new_audiocreated_audio リストに追加します。

次に、play_sound を呼び出し、sound_name に関連付けられたオーディオクリップとサウンドの位置を渡します。


Globals.gd を離れる前に、プレイヤーがシーンを変更したときにすべてのオーディオが破棄されるように、数行のコードを load_new_scene に追加する必要があります。

以下を load_new_scene に追加します:

for sound in created_audio:
    if (sound != null):
        sound.queue_free()
created_audio.clear()

さて、Globals.gd はシーンを変更する前に、created_sounds 内の各シンプルオーディオプレーヤーを通して、それらを解放/破棄します。Globals.gdcreated_audio のすべてのサウンドを処理したら、created_audio をクリアして、(既に解放/破棄されている)シンプルオーディオプレーヤーへの参照を保持しないようにします。


この新しいシステムを使用するために、Player.gdcreate_sound を変更しましょう。まず、Player.gd のサウンドを直接インスタンス化/生成することはないため、Player.gd``のクラス変数から ``simple_audio_player を削除します。

次に、create_sound を次のように変更します:

func create_sound(sound_name, position=null):
    globals.play_sound(sound_name, false, position)

さて、create_sound が呼び出されるたびに、Globals.gdplay_sound を呼び出して、受け取ったすべての引数を渡します。


これで、FPSのすべてのサウンドをどこからでも再生できます。必要なのは、Globals.gd シングルトンを取得し、play_sound を呼び出し、再生するサウンドの名前、ループするかどうか、およびサウンドを再生する位置を渡すことだけです。

たとえば、手榴弾が爆発したときに爆発音を鳴らしたい場合は、Globals.gdaudio_clips に新しいサウンドを追加する必要があり、Globals.gd シングルトンを取得し、そして、手榴弾が爆発半径内のすべての体にダメージを与えた直後に、手榴弾の _process 関数に globals.play_sound("explosion", false, global_transform.origin) のようなものを追加する必要があります。

最終ノート

../../../_images/FinishedTutorialPicture.png

これで、完全に機能するシングルプレーヤーFPSが完成しました!

この時点で、より複雑なFPSゲームを構築するための良い基盤ができました。

警告

迷子になったら、必ずコードをもう一度読んでください!

チュートリアル全体の完成したプロジェクトは、ここからダウンロードできます: Godot_FPS_Part_6.zip

注釈

完成したプロジェクトソースファイルには同じコードが含まれており、異なる順序で記述されています。これは、完成したプロジェクトソースファイルがチュートリアルの基になっているためです。

完成したプロジェクトコードは、機能が作成された順序で作成されましたが、必ずしも学習に最適な順序ではありません。

それ以外は、ソースはまったく同じですが、各部分の機能を説明する役立つコメントが付いています。

ちなみに

完成したプロジェクトソースはGithubでもホストされています: https://github.com/TwistedTwigleg/Godot_FPS_Tutorial

Githubのコードは、ドキュメントのチュートリアルと同期している場合と同期していない場合があることに注意してください

ドキュメント内のコードは、より適切に管理されているか、より最新である可能性があります。どちらを使用するかわからない場合は、ドキュメントで提供されているプロジェクトを使用してください。プロジェクトはGodotコミュニティによって管理されています。

このチュートリアルで使用されているすべての .blend ファイルはこちらからダウンロードできます: Godot_FPS_BlenderFiles.zip

開始済みアセットで提供されるすべてのアセットは、(特に明記されていない限り) Godotコミュニティによる変更/追加により、もともとTwistedTwiglegによって作成されました。 このチュートリアルで提供されるすべての元となるアセットは、MIT ライセンスの下でリリースされます。

これらのアセットは自由に使用できます。すべての元のアセットはGodotコミュニティに属し、他のアセットは以下にリストされているものに属します:

スカイボックスは **StumpyStrust ** によって作成され、OpenGameArt.org https://opengameart.org/content/space-skyboxes-0 で見つけることができます。スカイボックスは CC0 ライセンスの下でライセンスされています。

使用されるフォントは Titillium-Regular であり、SIL Open Font License, Version 1.1 の下でライセンスされています。

このツールを使用して、スカイボックスを360の正距円筒イメージに変換しました: https://www.360toolkit.co/convert-cubemap-to-spherical-equirectangular.html

サウンドは提供されませんが、 https://gamesounds.xyz/ で多くのゲーム向きの音を見つけることができます

警告

OpenGameArt.org、360toolkit.co、Titillium-Regular、StumpyStrust、およびGameSounds.xyzの作成者は、このチュートリアルには一切関与していません。