ゲームエンジンGodot4.0で3Dスマホゲームを作りたいと思いますが、その前にお勉強しています。
2023/3/1にstable版がリリースされました。
Godot_v4.0-stable_win64.exe.zipを使用しています。
目的
ゲームシステムをつくっていきます。
いきなりゲームが始まるので、心を準備する時間やステージクリア、失敗を理解する時間もありません。
画面タッチでプレイ開始や次のステージに進むようにします。
ベースプロジェクト
下記で作成したプロジェクトをベースに機能追加をします。
【Godot 4.0】スマホ3Dゲームを作るための勉強 その18 ゲームシステムを作る その3
https://qiita.com/FootInGlow/items/f067ae62fe974b82f28e
github(Godotのプロジェクトマネージャーからインポートして利用できます)
https://github.com/footinglow/Godot4/tree/main/02_study/S18_GameSystem_003
画面遷移を決める
画面遷移を決めます。
最初は「Ready」と表示します。
画面をタッチすると、プレイ中に遷移するようにします。ゲーム開始直後は2秒くらい「Go!」と表示します。
またステージをクリアもしくはクリア失敗した場合は、「Stage Clear」「Stage Failed」と表示をして、画面タッチすると次のステージもしくは同じステージのやり直しの「Ready」が表示されるようにします。
ではGameSystemに実装していきます。
タッチイベントを検知する
タッチイベントを検出するようにします。
res://game_system.gdを開いて下記を追加します。
# タッチイベント処理
var m_f_touch = false
func _input(event):
if ( event is InputEventScreenTouch ) and event.pressed:
m_f_touch = true
「event is InputEventScreenTouch」は画面をタッチもしくは指を離すときに通知されるイベントです。
タッチした時はevent.pressed=trueになるのでand条件で見ることでタッチを検出します。
func _physics_process(delta):
:
(省略)
:
# _physics_process()の最後にm_f_touchフラグを落とす
m_f_touch = false
_physics_process()の一番最後にm_f_touchをfalseにすると、次の周期でm_f_touchがtrueの場合、今タッチイベントが発生したと判断できます。
Readyを表示する
ゲーム画面の上に「Ready」の文字列を表示しましょう。
res://game_system.tscnを開きます。
GameSystemを右クリックして、子ノードを追加から「Node」を追加しましょう。
2Dの文字列を表示するだけなので、Node3Dではありません。
名称を「Messages」に変更します。
Messagesを右クリックして、子ノードを追加から「Label」を追加しましょう。(Label3Dではありません)
名称を「Ready」に変更します。
「Ready」を選択した状態でインスペクタのLabel/Textに「Ready]と入力します。
またLabelSettingsの<空>をクリックして、「新規LabelSettings」を実行します。
設定した「LabelSettings」をクリックすると詳細設定が開くので、Fontの中の「Size」を70に変更します。
画面上部の「2D」をクリックしましょう。2Dの編集画面に「Ready」という文字列が表示されています。
画面中央に表示したいので、「ビュー」メニューの右にあるアイコンをクリックして、中央を示すアイコンを選択します。
中央に移動しました。(マウスホイール操作で表示範囲を拡大しました)
実行してみましょう。画面中央に「Ready」が表示されました。
3D画面より2Dの画面の方が優先して表示されるようですね。
READY状態とプレイ状態の遷移を作成する
まずREADY状態の遷移をつくります。
res://game_system.gdを開いて編集します。
extends Node3D
enum EN_GAME_STS {
READY,
IN_PLAY,
}
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()
print(nodes)
for node in nodes:
node.queue_free()
# idx番目のステージシーンのインスタンスを生成して、CurrentStageに追加する
var new_stage = m_nodearray_stages[idx].instantiate()
$CurrentStage.add_child(new_stage)
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:
$Messages/Ready.hide()
m_en_game_sts = EN_GAME_STS.IN_PLAY
EN_GAME_STS.IN_PLAY:
var nodes = $CurrentStage.get_children()
if nodes:
# nodesは1個か0個のどちらかなので、0番目を固定的に使用する
if nodes[0].is_stage_clear():
print("Stage Clear")
# ステージは0,1,2,0,1,2,0...と繰り返す
m_i_current_stage_idx = ( m_i_current_stage_idx + 1 ) % m_nodearray_stages.size()
set_new_stage(m_i_current_stage_idx)
if nodes[0].is_stage_failed():
print("Stage Failed")
set_new_stage(m_i_current_stage_idx)
# _physics_process()の最後にm_f_touchフラグを落とす
m_f_touch = false
変更した点について説明します。
enum EN_GAME_STS {
READY,
IN_PLAY,
}
var m_en_game_sts = EN_GAME_STS.READY
ゲーム状態をenumで定義しました。
m_en_game_stsに現在のゲーム状態を保持するようにします。初期値はREADYです。
func _physics_process(delta):
match m_en_game_sts :
EN_GAME_STS.READY:
# 画面タッチされたら、Ready表示を刑してプレイ中にする
if m_f_touch:
$Messages/Ready.hide()
m_en_game_sts = EN_GAME_STS.IN_PLAY
EN_GAME_STS.IN_PLAY:
:
(以下今までの_physics_processの内容)
_physics_process()メソッドはゲーム状態によって処理内容が変わります。
- m_en_game_stsがEN_GAME_STS.READYの場合
「if m_f_touch:」によりタッチ判断をします。タッチされるまでは何もしません。
タッチ検出した場合、「$Messages/Ready.hide()」でReadyの表示を消します。hide()関数でノードの表示を消すことができます。
次の周期ではプレイ中の処理を実行したいので、m_en_game_sts = EN_GAME_STS.IN_PLAYに設定することで、次に_physics_process()が呼ばれたときはEN_GAME_STS.READYの場合の処理を実行するようになります。 - m_en_game_stsがEN_GAME_STS.READYの場合
今までと同じステージクリア/失敗の処理を実行します。
しかし放っておくと敵の砦からEnemyBullet(敵ソルジャー)がどんどん出てきます。
プレイ中に遷移してから敵の砦が攻撃開始するように変更する
原因はEnemy.tscnのTimer_fireノードのAutoStartをONにしているためです。
初期値はAutoStartをOFFにして、ゲーム状態がIN_PLAY(プレイ中)に遷移するときにAutoStartをONにするようにしましょう。
res://Characters/enemy.tscnを開きます。
Timer_fireを選択して、インスペクタの「Timer/AutoStart」をOffにします。
プレイ中開始/停止を指示したいノードを一括して制御するために、グループ機能の「call_group」メソッドを使います。
GODOT DOC Scripting Groups
https://docs.godotengine.org/en/latest/tutorials/scripting/groups.html#managing-groups
プレイ開始/中止を制御するノードに「StartStopControl」グループを設定して、play_start/play_stopメソッドを実装しましょう。
res://Characters/enemy.tscnを開きます。
Enemyを選択して、インスペクタの右のノード/グループを選択します。
「StartStopControl」を入力して、「追加」ボタンを押下します。
追加されました。Enemyには合計3つのグループが登録されています。
res://Characters/Enemy.gdを開いて、play_start/play_stopメソッドを追加します。
func game_start():
$Timer_fire.start()
func game_stop():
$Timer_fire.stop()
それぞれTimer_fireタイマーを開始、停止するメソッドになっています。
ではGameSystemから制御しましょう。
res://game_system.gdを開きます。
EN_GAME_STS.READY:
# 画面タッチされたら、Ready表示を刑してプレイ中にする
if m_f_touch:
$Messages/Ready.hide()
m_en_game_sts = EN_GAME_STS.IN_PLAY
# "GameControl"グループを持つ、PlayerとEnemyのスクリプトのgame_start()メソッドを起動する
get_tree().call_group("StartStopControl", "game_start")
EN_GAME_STS.IN_PLAY:
- get_tree().call_group("StartStopControl", "game_start")
get_tree()で取得したアクティブなノードに対して、"StartStopControl"グループを持つノードにアタッチされたスクリプトの「game_start」メソッドを起動します。
実行しましょう。
タッチするまでは敵が攻撃しなくなりました。
GameSystem/Messagesのメッセージ表示をすべてhide()するメソッドの追加
ゲーム状態の実装を進める前に、GameSystem/Messagesのメッセージ表示をすべてhide()するメソッドを追加します。
res://game_system.gdを開いて修正します。
func hide_all_messages():
# メッセージを表示をすべて隠す
var nodes = $Messages.get_children()
for node in nodes:
node.hide()
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
「$Messages/Ready.hide()」から「hide_all_messages()」に変更しました。
ゲーム状態を実装する
メッセージを追加する
res://game_system.tscnを開きます。
「Ready」ノードをコピーしてから、Messagesノードを右クリックして「貼り付け」します。
合計3つ張り付けて、名称を「Go」「StageClear」「StageFailed」にします。
「Go]を選択して、インスペクタのLabel/Textに「Go!」を設定します。
同様に、StageClearは「Stage Clear」、StageFailedは「Stage Failed」にします。
初期状態はReadyのみ表示したいので、シーン内のGo,StageClear,StageFailedの右に表示されている目のアイコンをクリックして、目をつむっているアイコンに変更してください。
ゲーム状態遷移を実装する
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:
# 次のステージに進む
# ステージは0,1,2,0,1,2,0...と繰り返す
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:
set_new_stage(m_i_current_stage_idx)
# ゲーム状態を「レディ」状態にして、「Ready」を表示する
m_en_game_sts = EN_GAME_STS.READY
hide_all_messages()
$Messages/Ready.show()
# _physics_process()の最後にm_f_touchフラグを落とす
m_f_touch = false
変更点を説明します。
enum EN_GAME_STS {
READY,
IN_PLAY,
STAGE_CLEAR,
STAGE_FAILED,
}
ゲーム状態「STAGE_CLEAR」と「STAGE_FAILED]を追加しました。
次に「_physics_process()」メソッドの中のEN_GAME_STS.READY状態の処理です。
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()
「Ready」表示中の処理です。
タッチ検出した処理の最後に処理を追加して、「$Messages/Go.show()」で「Go!」を表示するようにしました。
また2秒後に「Go!」の表示を消したいのですが、awaitを使用して、「await get_tree().create_timer(2.0).timeout」とすると2秒経過後に次の行「hide_all_messages()」を実行します。
2秒待っている間も_physics_process()は周期的に呼び出されています。
GODOT3.5の時のyield関数が、Godot4.0ではawaitに変更されています。
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()
プレイ中の処理です
ステージクリア/ステージ失敗判定時の処理が、状態遷移処理に変更しました。
m_en_game_stsに新しいゲーム状態を設定して、hide_all_messages()でメッセージ表示(何も表示されていないと思いますが)を消した後、StageClearもしくはStageFailedのメッセージを表示します。
EN_GAME_STS.STAGE_CLEAR:
# 画面タッチされたら、Readyに遷移する
if m_f_touch:
# 次のステージに進む
# ステージは0,1,2,0,1,2,0...と繰り返す
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()
「Stage Clear」を表示中の状態です。
タッチされるまではなにもしません。
タッチを検出すると、次のステージに進む処理を実行します。
その後READY状態に遷移(m_en_game_sts = EN_GAME_STS.READY)して、メッセージ表示を消した後、「Ready」を表示します。
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()
「Stage Failed」を表示中の状態です。
処理に内容は「Stage Clear」表示中の時とほぼ同じ処理です。
実行しましょう。
実行するとReadyが表示されて、タッチするとGo!が2秒間表示されます。
ステージをクリアすると「Stage Clear」と表示されて、タッチすると次のステージに進み「Ready」が表示されます。
ステージ失敗すると「Stage Failed」が表示されて、タッチすると次のステージに進み「Ready」が表示されます。
以上