ゲームエンジンGodot4.0で3Dスマホゲームを作りたいと思いますが、その前にお勉強しています。
2023/3/1にstable版がリリースされました。
Godot_v4.0-stable_win64.exe.zipを使用しています。
目的
ゲームシステムをつくっていきます。
タイトルシーンを作りたいと思います。
Godotの「シングルトン(自動読み込み)機能」を使用して、タイトルシーンとGameSystemシーンを切り替えます。
シングルトン(自動読み込み)とは?
Godotのシングルトン(自動読み込み)を使用すると、スクリプトに下記のような特徴を持たせることができます。
・常にロードされています。
・プレイヤー情報のような複数のシーンで共通で使用したいグローバル変数を保管できます。
・シーンの入れ替えをする事ができます。
・アプリケーションで唯一存在します。
つまりシングルトン(自動読み込み)にしたスクリプトがゲームの一番ベースとなる神様スクリプトになります。
タイトルシーンとゲームプレイシーンをまるごと入れ替えることができるようになります。
ベースプロジェクト
下記で作成したプロジェクトをベースに機能追加をします。
【Godot 4.0】スマホ3Dゲームを作るための勉強 その19 ゲームシステムを作る その4
https://qiita.com/FootInGlow/items/a12d1ae9b4b8e1afb053
github(Godotのプロジェクトマネージャーからインポートして利用できます)
https://github.com/footinglow/Godot4/tree/main/02_study/S19_GameSystem_004
タイトルシーンを作成します。
最初にタイトルシーンを作成します
タイトル文字と「ゲーム開始」ボタンを作成します。あと背景にキャラクターを配置します。
新規シーンを作成してControlノードを追加する
シーンメニューから「新規シーン」を選択して、「ユーザインタフェース」ボタンを押下します。
Controlノードが追加されたと思います。これからボタンを追加しますが、ボタンなどのコントロールの位置は親Controlが基準になるため、ボタンなどはControlノードの子ノードとして管理したほうがよさそうです。
名称を「Title」に変更します。
保存しましょう。title.tscnファイルができました。
Titleを選択して、右クリックします。子ノードを追加から「Label」を追加します。
Labelを選択した状態で、インスペクタのLabel/Textにゲームタイトルを入力します。「MyGame」とします。
LabelSettingsの右の<空>をクリックして「新規LabelSettings」を選択します。
追加された「LabelSettings]をクリックして詳細設定を開き、 Font/Sizeに100を設定します。
Titleを右クリックして、子ノードを追加から「Button」を追加します。
Buttonを選択した状態で、インスペクタの「Button/Text」に「Game Start!」と入力します。
配置を決める
画面上部の「2D」をクリックすると表示位置を確認することができます。
シーンの「Label」を選択します。
「ビュー」メニューの右にあるアイコンをクリックして、中央上のアイコンをクリックします
次はシーンの「Button」を選択します。
「ビュー」メニューの右にあるアイコンをクリックして、中央下のアイコンをクリックします。
ボタンの位置を少し上にあげたいので、カーソルキー上を押下して、少し上に移動します。
中央上に「MyGame」が、中央下に、ボタンが表示されたと思います。
背景に3Dキャラクターを配置する
背景を作成します。
「Title」を右クリックして、子ノードを追加から「Node3D」を追加します。
名称を「Background」に変更します。
「Background」を右クリックして、子ノードを追加から「Camera3D」を追加します。
「Background」を右クリックして、子ノードを追加から「DirectLight3D」を追加します。
ファイルシステムの下記4つのシーンをCtrlキーを押下しながらクリックして選択し、シーンのBackGroundにドラッグアンドドロップします。
・res://Design/design_cannon.tscn
・res://Design/design_enemy_fort.tscn
・res://Design/design_enemy_soldier.tscn
・res://Design/design_my_soldier.tscn
各シーンのNode3D/Transform/positionを下記のように変更しました。
シーン名 | x | y | z |
---|---|---|---|
Camera3D | 0m | 0m | 5m |
DirectLight3D | -10m | 0m | 0m |
design_cannon.tscn | -0.5m | -2m | 1.5m |
design_enemy_fort.tscn | 2m | 0m | 0m |
design_enemy_soldier.tscn | 1.5m | 0m | -4m |
design_my_soldier.tscn | -2m | 0m | -2m |
初期画面をtitle.tscnに変更する
アプリ起動時の初期画面はtitle.tscnにします。
プロジェクトメニューの「プロジェクト設定」を開き、アプリケーション/実行にあるメインシーンを「res://title.tscn」に変更します。
実行するとタイトルを表示します。
何も実装していないので、「Game Start!」ボタンを押下しても何も起きません。
神様スクリプトを登録する
シングルトン(自動読み込み)機能を使用して、常に常駐するスクリプトを作成して登録します。
画面上部の「Script」をクリックして、ファイルメニューの「新規スクリプト」を実行します。
ファイル名を「global.gd」にして「作成」ボタンを押下します。
追加されました。
「global.gd」を神様スクリプトとして登録します。
プロジェクトメニューの「プロジェクト設定」を開きます。
「Autoload」タブを開きます。
パスに「res://global.gd」を設定すると、ノード名も自動で設定されます。
追加ボタンを押下します。
「グローバル変数」項目の下の「有効」がチェックされていることも確認してください。
有効にチェックが入っていると、「Global」という名前で他のスクリプトファイルからアクセスすることができるようになります。
実行して、シーンの「リモート」をクリックすると、「Global」という名前でロードされていることが確認できます。
シーンの入れ替えを実装する
res://global.gdを開きます。
シーンの入れ替えをするメソッドを、下記のように実装します。
extends Node
func goto_scene(scene_path):
call_deferred("_deferred_goto_scene", scene_path)
func _deferred_goto_scene(scene_path):
get_tree().change_scene_to_file(scene_path)
func goto_scene(scene_path):
scene_pathはシーンファイルを指定します。
scene_pathには今回、res://game_system.tscnもしくはres://title.tscnが指定されます。
- call_deferred("_deferred_goto_scene", scene_path)
call_deferred関数を使用して、_deferred_goto_scene(scene_path)の実行を予約します。
「goto_scene」メソッドを起動するのは、タイトルシーンです。ここでタイトルシーン削除してGameシーンを追加するのは良い方法ではありません。なぜならタイトルシーンはまだやることがあるかもしれないためです。
call_deferred関数を利用することで、タイトルシーンの処理が終了してからシーンの入れ替えを実行します。
このような順序になるようです。
1. title.tscnがgoto_scene(scene_path)メソッドを呼び出し
2. call_deferredで_deferred_goto_scene(scene_path)呼び出しを予約(まだ実行しない)
2. title.tscnは必要な処理を実行する。
2. _deferred_goto_scene(scene_path)呼び出し
ノードを削除する時に使用するqueue_free()メソッドも同じような目的で使用しています。いきなりfree()でノードが削除されるとそのノードを参照している人が困ってしまいますので、free()のかわりにqueue_free()を使用することで、対象ノードは処理がひととおり終わった後に削除されるような仕組みになっています。
func _deferred_goto_scene(scene_path):
- get_tree().change_scene_to_file(scene_path)
get_tree()でSceneTreeを取り出して、scene_pathで指定したシーンファイルに入れ替えます。
参考
GODOT4.0 DOC Change scenes manually
https://docs.godotengine.org/en/latest/tutorials/scripting/change_scenes_manually.html
GODOT3.5 DOC シーンを手動で変更する
https://docs.godotengine.org/ja/stable/tutorials/scripting/change_scenes_manually.html
タイトルの「Game Start!」ボタン押下でゲーム開始する
タイトルのボタンを押下したときのシグナルを受信して、ゲームシーンへの入れ替え指示まで実装します。
res://title.tscnを開きます。
「Title」ノードを右クリックして、「スクリプトをアタッチ」を実行します。
「title.gd」という名称で保存します。
保存されました。
次にシグナルを接続します。
シーン内の「Button」を選択した状態で、インスペクタの横のノード/シグナルを選択します。
「BaseButton/pressed」を右クリックして「接続」を実行します。
「Title」を選択して、受信側メソッド名を確認して、「接続」ボタンを押下します。
res://title.gdに「func _on_button_pressed():」が追加されました。
下記のように修正します。
extends Control
func _on_button_pressed():
Global.goto_scene("res://game_system.tscn")
「Global」は先ほどAutoloadに追加したGlobalです。
goto_sceneメソッドを呼び出して、GameSystem.tscnへの入れ替えを指示しています。
実行しましょう。
タイトル画面で「Game Start!」ボタンを押下します。
ゲーム開始しました。
ステージ失敗時に、タイトルに戻る
ステージクリアを失敗した場合、タイトルに戻るようにスクリプトを修正します。
res://game_system.gdを開きます。
下記のように修正します。
EN_GAME_STS.STAGE_FAILED:
# 画面タッチされたら、Readyに遷移する
if m_f_touch:
- set_new_stage(m_i_current_stage_idx)
- # ゲーム状態を「レディ」状態にして、「Ready」を表示する
- m_en_game_sts = EN_GAME_STS.READY
- hide_all_messages()
- $Messages/Ready.show()
+ Global.goto_scene("res://title.tscn")
res://game_system.gdスクリプト全体は下記のようになります。
extends Node3D
enum EN_GAME_STS {
READY,
IN_PLAY,
STAGE_CLEAR,
STAGE_FAILED,
}
var m_en_game_sts = EN_GAME_STS.READY
# ステージシーンリスト(PackedScene型)
var m_nodearray_stages = [
preload("res://Stages/stage001.tscn"),
preload("res://Stages/stage002.tscn"),
preload("res://Stages/stage003.tscn"),
]
# 現在実行中のステージ番号
var m_i_current_stage_idx = 0
# タッチイベント処理
var m_f_touch = false
func _input(event):
if ( event is InputEventScreenTouch ) and event.pressed:
m_f_touch = true
func set_new_stage(idx):
# CurrentStageにあるステージをすべて削除する
var nodes = $CurrentStage.get_children()
for node in nodes:
node.queue_free()
# idx番目のステージシーンのインスタンスを生成して、CurrentStageに追加する
var new_stage = m_nodearray_stages[idx].instantiate()
$CurrentStage.add_child(new_stage)
func hide_all_messages():
# メッセージを表示をすべて隠す
var nodes = $Messages.get_children()
for node in nodes:
node.hide()
func _ready():
set_new_stage(m_i_current_stage_idx)
func _physics_process(delta):
match m_en_game_sts :
EN_GAME_STS.READY:
# 画面タッチされたら、Ready表示を刑してプレイ中にする
if m_f_touch:
hide_all_messages()
m_en_game_sts = EN_GAME_STS.IN_PLAY
# "GameControl"グループを持つ、PlayerとEnemyのスクリプトのgame_start()メソッドを起動する
get_tree().call_group("StartStopControl", "game_start")
# 「Go」を表示して2秒後にメッセージを消す
$Messages/Go.show()
await get_tree().create_timer(2.0).timeout
hide_all_messages()
EN_GAME_STS.IN_PLAY:
var nodes = $CurrentStage.get_children()
if nodes:
# nodesは1個か0個のどちらかなので、0番目を固定的に使用する
if nodes[0].is_stage_clear():
# ゲーム状態を「ステージクリア」状態にして「Stage Clear」を表示
m_en_game_sts = EN_GAME_STS.STAGE_CLEAR
hide_all_messages()
$Messages/StageClear.show()
if nodes[0].is_stage_failed():
# ゲーム状態を「ステージ失敗」状態にして「Stage Failed」を表示
m_en_game_sts = EN_GAME_STS.STAGE_FAILED
hide_all_messages()
$Messages/StageFailed.show()
EN_GAME_STS.STAGE_CLEAR:
# 画面タッチされたら、Readyに遷移する
if m_f_touch:
# 次のステージに進む
m_i_current_stage_idx = ( m_i_current_stage_idx + 1 ) % m_nodearray_stages.size()
set_new_stage(m_i_current_stage_idx)
# ゲーム状態を「レディ」状態にして、「Ready」を表示する
m_en_game_sts = EN_GAME_STS.READY
hide_all_messages()
$Messages/Ready.show()
EN_GAME_STS.STAGE_FAILED:
# 画面タッチされたら、Readyに遷移する
if m_f_touch:
Global.goto_scene("res://title.tscn")
# _physics_process()の最後にm_f_touchフラグを落とす
m_f_touch = false
実行します。
ゲームクリア失敗の画面でタッチするとタイトルに戻ります。
タイトルに戻ったでしょうか?
実行中のノード構成を確認する。
実行してノード構成を確認します。
実行して、シーンの「リモート」をクリックします。
タイトルシーンの時は先ほども確認したように、root直下にGlobalとTitleの2つのノードが存在しています。
「Start Game!」ボタンを押下してゲームを開始します。
下記のように、Titleが消えてGameSystemが追加されました。
Globalは常にありますね。
以上です。