Godot 3系のstore_varで保存したファイルをGodot 4で読み込めない問題

GodotのFileクラスのstore_varというメソッドを使うと、容易にオブジェクトの状態を保存できる。

https://docs.godotengine.org/ja/stable/classes/class_file.html#class-file-method-store-var

store_varで保存したファイルをget_varで簡単に復元することができるので、Godotで色々なオブジェクトの状態を保存/復元する方法として紹介されることがある。便利なので自分のアプリでも使っている箇所があったのだが、今回そのアプリのGodot 4対応を進めようとして思わぬ問題にぶつかってしまった。

https://godotforums.org/d/31966-filestore-var-in-35-to-godot-4/16

フォーラムでも報告している人がいるように、Godot 3系のstore_varメソッドで作成したファイルをGodot 4のget_varで復元しようとすると正しく復元できないのだ。

https://github.com/godotengine/godot/issues/73530#issuecomment-1435792453

残念ながら、これは不具合ではなくGodot 4で想定済みの仕様のようで、今後バージョンアップなどで対応される予定もないそうだ。この問題に対する対策としては、下記の二つの方法が提案されている。

  1. Godot 4系にアップデートする前に、ファイルをJSONなど別のフォーマットに移行する機能を作る
  2. 自前でバイナリーローダーを書いて、3系で保存されたバイナリファイルを解読して4系に読み直す

後者は現実的に面倒で難易度も高そうなので、3系のstore_varで作成したデータを引き継ぐには前者の方法をとるしかなさそうだ。ただし、その場合は当面の間3系を使い続けることになってしまうので頭がいたい。

store_varは仕様上今後のアップデートでも同様の問題が起きる可能性があるので、オブジェクトの状態を保存したいときは面倒だがstore_stringとget_as_textを使って自前でシリアライズ/デシリアライズを実装した方が良いようだ。(現在公式ドキュメントのサンプルもそうなっている)

Godot 4でtresファイルのダイナミックロードがやりにくくなった問題

Godot 4で、フォルダ内にあるtresファイルを動的に読み込むプログラムを書いたところうまく動作しなかった。

for file in dir_contents(folder_path):
	if file.get_extension() == "tres":
		tres_files.append(file)

上のコードは、特定のフォルダにあるtresファイルを全てリストアップするためのコードだ。Godot Editor上でこのコードを実行すると、フォルダ内のtresファイルがリストアップされるが、iOS上ではファイルが一つもリストアップされない。調べてみると、.tresファイルは見つからないが代わりに.tres.remapというファイルが存在していることがわかった。

https://github.com/godotengine/godot/issues/66014

Godot 3では動作していたのだが、どうやら仕様が変わりtresファイルもRemapされるようになったらしい。Remapされたファイルはload()やpreload()でres://プロトコルでファイルを指定して呼び出す際には内部的に自動でRemap後のデータが参照されるため、プログラム内で参照先をハードコーディングしているような場合は特にファイルの実態を意識する必要がないが、今回のように「フォルダからファイルを探して動的に読み込む」というような実装では「フォルダ上に見つかるファイル(remapファイル)」と「res://プロトコルで参照する際のURL」に差異が生まれてしまうのでそのままでは動かなくなってしまう。

for file in dir_contents(folder_path):
	if file.get_extension() == "tres":
		tres_files.append(file)
	if '.tres.remap' in file: # 追加
		image_files.append(file.trim_suffix(".remap"))

Godot 4でそのようなことをやりたい場合は、フォルダ内から.tres.remap ファイルを探し、読み込む際には.remapを取り除いたURLでload() するように実装する。ただし、Godot Editor上で作業している際にはRemapが行われていない元ファイルを参照しに行くことになるので、上のコードのように.tresファイルと.tres.remapファイルの両方に対応できるようなコードを書く必要がある。

https://github.com/godotengine/godot/issues/25672

最初にはったIssueと、上に貼ったもう一つのIssueでも指摘されているが、なかなかわかりにくい仕様だと思う。

GodotプロジェクトのiOSアプリ出力で、リソースをpckファイルにまとめるかフォルダ参照するかで挙動が変わる話

GodotではiOS用のエクスポートテンプレートを追加すると、ExportメニューからXcodeプロジェクトを書き出してiOSでアプリを動かすことができる。書き出し作業自体は手軽なのだが、毎回数十秒程度処理に時間がかかることやXcode側での設定(アプリ名のローカライズなど)がリセットされてしまうので、コードやリソースを変更するたびにExportを実行するのはなかなか面倒だ。

そこで、公式ドキュメントの「Active Development Considerations」の項では、XcodeプロジェクトからGodotのプロジェクトをフォルダ参照することで、変更のたびに毎回Exportし直さなくても常に最新の内容でiOSアプリをビルド・実行可能にする方法が説明されている。(※プラグインの追加時など、Xcodeプロジェクトの構造変更が必要な時はプロジェクトの出力し直しが必要になる)これは実に便利で、特にアプリ内課金の機能などiOSのネイティブ機能を使う部分の開発・デバッグには不可欠な手順になっている。

ただし、上の公式ドキュメントには特に記載がないがプロジェクトを毎回Exportした場合と、一度ExportしたXcodeプロジェクトからGodotフォルダ参照する場合ではResourceに関する挙動が異なるので注意する必要がある。

GodotからXcodeプロジェクトをExportすると、プロジェクト内で使用される画像やSceneなどのResourceを一つにまとめたリソースパック(pckファイル)が生成される。このpckファイルには、GodotでResourceとして扱われるファイルのみが含まれている。例えば2022年9月現在Godot 3.5ではJSONファイルはResourceファイルとしては扱われないため、プロジェクトフォルダ内に存在したとしてもpckファイルには含まれない。(2022年9月4日のissues #65295でJSONをResourceとして扱うための変更が提案されMergeされたため、将来はJSONもpckファイルに含まれるようになる予定)

一方でフォルダ参照をしている場合は、Godotのプロジェクトディレクトリ内にある全てのファイルをres://プロトコルのURLで参照することができる。Godotでは本来Resourceとして扱われないJSONファイル等もフォルダ参照の場合は参照することができてしまう。そのようなコードを書いてしまうと、フォルダ参照時は動作するのにpckファイル読み込み時は動作しない、という現象を引き起こしてしまうことになる。

一見全て参照できる分、基本的にはフォルダ参照にしておいた方が便利そうな気もするが、そうとも言えない。AppStoreへのサブミット時にリジェクト要因になってしまうファイルがバイナリに組み込まれてしまう場合があるためだ。例えばアプリ内課金を利用する場合、公式ドキュメントのやり方に従うとプロジェクトのres://ios/pluginsにiOSプラグインのファイルを配置することになるが、このプラグインのファイルをフォルダ参照で含めたままAppStoreにサブミットしようとするとInvalid Bundle Structureエラーが発生してしまう。Xcodeのビルド設定で特定のファイルを明示的に除外することでエラーを回避することも可能かもしれないが、開発をしているとプラグイン追加のタイミングなどXcodeプロジェクト自体を出力し直す機会が時々発生し、その度にこの辺りの細かい設定するのはできれば避けたい。

おそらく、現実的に無難な選択肢はpckファイルのみを更新するやり方だろう。Godotには、pckファイルのみをExportする機能が用意されている。

ワンステップ挟むことになるのでフォルダ参照に比べると多少不便ではあるが、この方法なら「フォルダ参照で想定外に動いてしまった」というような実装も防げるので、他プラットフォームへの展開なども考えると最も無難なのではないかと思う。

年末年始なのでGodotでゲームでも作ってみる 5日目

今日の目標

  • Nodeを移動させる
  • ゲームを作り始める

年末年始一週間あると思ってたら6日しかなかった。急いでゲーム作り始めないとまずい。

1フレーム毎にNodeを動かす

Godotで毎フレーム何かしらの処理をしたい場合はIdle processingとPhysics processingという二つの機構が用意されている。後者は基本的に物理演算を使うゲーム用で、今回は物理演算は使っていないので前者のIdle processingの機構を使ってNodeに何かしらの処理を加えてみる。Nodeにアタッチしたスクリプトに_process(delta)という関数を定義すると、1フレーム毎にこの関数が呼び出される。注意しなければいけないのは、フレームレート(FPS)は可変であるため、この中で処理をするときは基本的にdelta(前のフレームからの経過時間)をみて計算を行う必要がある。

func _process(delta):
	self.position = Vector2(self.position.x + delta * 100, self.position.y)

例えば上のようなコードを書くと、フレームが更新されるごとに、自身のNodeの座標(position)が右にdelta * 100ピクセル分移動していく(つまり1秒に100ピクセル)。

4日までに作っていたプロジェクトで、子Sceneの方に上記の_process(delta)を定義して実行すると、それぞれの子Sceneが少しずつ右に動いていく様子。

画面端まで行ったNodeをワープさせる

勉強がてら、もう少しこのプロジェクトをいじってみる。子SceneのNodeが画面右端に到達したら画面の左側にワープさせて、画面内をループするようにしてみたい。

func _process(delta):
	var children = get_tree().get_nodes_in_group("mygroup")
	for child in children:
		if child.position.x >= get_viewport_rect().size.x:
			child.position = Vector2(-100, child.position.y)

今度は親Sceneの方にも_process(delta)を定義した。get_tree().get_nodes_in_group(“mygroup”)で、グループに所属するNode(この場合は生成した子SceneのNode)のリストを取得できるので、それぞれのNodeに対して、位置のx座標(position.x)が画面の右端に到達していないかを調べる。画面のサイズはget_viewport_rect().sizeで取得することができる(get_viewport().sizeを使うと後で画面のストレッチを指定した時におかしくなるので注意)。もし画面の右端に到達していた場合はx座標を-100の位置に移動する。0を指定すると画面左端に急に出現したような感じになってしまうため、100ピクセル分左に移動して画面の外側から出現するようにしている。

スクリプティングまとめ

以上でなんとなくGodotにおけるスクリプトの書き方、Node / Sceneの扱い方は分かったので、これからもう少しゲームらしいものを実際に作っていこうと思う。冬休みはあと1.5日しかないけど。

新しいプロジェクトを作る

新しいプロジェクトを作る。どういうゲームを作るのかについては、今は秘密だが名前はHaetatakiにした。OpenGLはES 3.0を使用。

何を作るかは最後まで秘密にしておきたいが、とりあえずハエの画像が配置されているだけのHae.tscnというSceneを作る。これを敵キャラのSceneとして使いたい。

もう一つ別に空の2D Sceneを作り、GameStage.gdというスクリプトをアタッチする。

extends Node2D

const HaeScene = preload("res://Hae.tscn")

var rng = RandomNumberGenerator.new()

# Called when the node enters the scene tree for the first time.
func _ready():
	rng.randomize()
	
	for n in 10:
		var anInstance = HaeScene.instance()
		anInstance.name = "Hae" + str(n)
		anInstance.position = Vector2(rng.randf_range(0.0, get_viewport_rect().size.x), rng.randf_range(0.0, get_viewport_rect().size.y))
		anInstance.add_to_group("enemies")
		self.add_child(anInstance)

_ready()の中で10個ほどHae.tscnのインスタンスをランダムな位置に追加する。Godotでランダムな数値を使いたいときはRandomNumberGeneratorを使用できる。

まだどんなゲームになるのか予想がつかないが、とりあえずハエが10匹画面上のランダムな位置に表示されるようになった。

標的をランダムな方向に動かしてみる

Hae.tscnにもスクリプトをアタッチする。ファイル名はHae.gdとした。ランダムな方向に動かしたいので、orientationというプロパティを追加し、_process(delta)の中でそのorientationの方向に毎秒30ピクセルのスピードで動くようにコードを書く。

extends Node2D

var orientation = 0

func _process(delta):
	var speed = 30 * delta
	self.position = Vector2(self.position.x + speed * cos(orientation), self.position.y + speed * sin(orientation))

なお、Godotの2D座標系は下方向がY軸プラスになる座標系なので、数学でよくある上方向がプラスになる座標系と逆になっていることに注意したい。

extends Node2D
...
func _ready():
	rng.randomize()
	
	for n in 10:
		var anInstance = HaeScene.instance()
		anInstance.name = "Hae" + str(n)
		anInstance.orientation = rng.randf_range(0.0, PI * 2) // Added
		anInstance.position = Vector2(rng.randf_range(0.0, get_viewport_rect().size.x), rng.randf_range(0.0, get_viewport_rect().size.y))
		anInstance.add_to_group("enemies")
		self.add_child(anInstance)

親Sceneの方で、Haeのインスタンスを生成する際にorientationをランダムに設定するように変更。orientationの値はラジアン値で0.0〜6.28(PI * 2)の間でランダムに決定する。

実行すると、ハエがそれぞれランダムな方向に移動していく。

画面の端に行ったら反対の端にワープさせる処理をかく。

extends Node2D

const HaeScene = preload("res://Hae.tscn")

var rng = RandomNumberGenerator.new()

const STAGE_MARGIN = 100

# Called when the node enters the scene tree for the first time.
func _ready():
	...

func _process(delta):
	var stageEdgeLeft = -STAGE_MARGIN
	var stageEdgeRight = get_viewport_rect().size.x + STAGE_MARGIN
	var stageEdgeTop = -STAGE_MARGIN
	var stageEdgeBottom = get_viewport_rect().size.y + STAGE_MARGIN
	for enemy in get_tree().get_nodes_in_group("enemies"):
		if enemy.position.x < stageEdgeLeft:
			enemy.position = Vector2(stageEdgeRight, enemy.position.y)
		if enemy.position.x > stageEdgeRight:
			enemy.position = Vector2(stageEdgeLeft, enemy.position.y)
		if enemy.position.y < stageEdgeTop:
			enemy.position = Vector2(enemy.position.x, stageEdgeBottom)
		if enemy.position.y > stageEdgeBottom:
			enemy.position = Vector2(enemy.position.x, stageEdgeTop)
		

親Sceneの_process(delta)の中で各ハエのインスタンスを監視し、上下左右それぞれのステージの端に到達していたら反対の端にワープさせる。

タッチ判定を入れる

マウスで標的となるハエがクリックされたことを検知できるようにしてみよう。子SceneのHae.tscn自体に、クリックされたときのコールバックを実装しても良いのだが、今回は親Sceneの方にコールバックを実装する形をとった。理由としては、標的が重なっていたりした場合に複数の標的に同時にクリック判定をしたいことと、標的がいないところをクリックした時にも何かしらの処理を行いたいからだ。

extends Node2D

const HaeScene = preload("res://Hae.tscn")

var rng = RandomNumberGenerator.new()

const STAGE_MARGIN = 100
const ENEMY_RADIUS = 50

...
		
func _input(event):
	if event is InputEventMouseButton and event.button_index == BUTTON_LEFT and event.pressed:
		for enemy in get_tree().get_nodes_in_group("enemies"):
			var distance = event.position.distance_to(enemy.position)
			if distance <= ENEMY_RADIUS:
				enemy.position = Vector2(rng.randf_range(-STAGE_MARGIN, get_viewport_rect().size.x + STAGE_MARGIN), -STAGE_MARGIN)

マウスクリックはコールバック関数_input(event)内で取得できる。_input(event)はマウスクリック以外にもキーボードのキーが押された時やマウスのカーソルが押された時など様々な時に呼び出されるため、まずif文でマウスの左ボタンが押された時のみ処理が走るようにする。

if event is InputEventMouseButton and event.button_index == BUTTON_LEFT and event.pressed:

クリックされた場所は event.position でVector2型の座標として取得できる。Vector2には別のVector2との間の距離を計算するdistance_toメソッドが用意されているので、これを使ってステージ上にいる各標的(ハエ)との距離を調べる。距離がENEMY_RADIUSよりも小さかった場合に「あたり」と判定する。あたりと判定した場合は、とりあえずその標的をステージの上端のどこか見えない場所にランダムでワープさせている。

点数をカウントする

点数を表示させるためのLabelをGameStageに配置する。名前はScoreLabelとしておいた。

extends Node2D

const HaeScene = preload("res://Hae.tscn")

var rng = RandomNumberGenerator.new()

const STAGE_MARGIN = 100
const ENEMY_RADIUS = 50

var score = 0

...
		
func _input(event):
	if event is InputEventMouseButton and event.button_index == BUTTON_LEFT and event.pressed:
		for enemy in get_tree().get_nodes_in_group("enemies"):
			var distance = event.position.distance_to(enemy.position)
			if distance <= ENEMY_RADIUS:
				enemy.position = Vector2(rng.randf_range(-STAGE_MARGIN, get_viewport_rect().size.x + STAGE_MARGIN), -STAGE_MARGIN)
				score += 1
		get_node("ScoreLabel").text = "Score: " + str(score)

scoreという変数を用意し、標的がクリックされる毎に1を追加する。そして、マウスクリックがある度に最新のscoreの値をScoreLabelに表示する。

実際に動かして、点数が加算されていくことを確認。

まとめ

急に駆け足になってしまったが、一旦ゲームっぽい物の骨格を作ることができた。明日はこれをベースにもうちょっとゲームっぽくしていきたいと思う。

年末年始なのでGodotでゲームでも作ってみる 4日目

今日の目標

  • スクリプトを書いてみる

Godotで使えるスクリプトについて

GodotではGDScriptの他にVisualScript、C#、C++でスクリプトを書くことができる。メイン言語はGDScriptということなので、今回もGDScriptを使うことにする。

テスト用のSceneの作成

公式ドキュメントのScriptingの内容に従って、LabelとButtonが配置されたSceneを新しく作成する。

Root Nodeに対して右クリックメニューからAttach Scriptを選択。

ScriptのPath(保存先のファイル名)を適当に指定してCreateする。

追加されたScriptファイルを編集する画面が開く。別の編集画面を開いてしまった場合は、Scene dockの赤枠で囲ったScriptマークを押すとこのスクリプト編集画面に戻ってくることができる。

extends Node2D

func _ready():
	get_node("Button").connect("pressed", self, "_on_Button_pressed")

func _on_Button_pressed():
	get_node("Label").text = "HELLO!"

ドキュメントに従って、上記のコードを記述する。

_ready()はAttachされたNode(この場合はRoot NodeのNode2D)がScene上でアクティブになったタイミングで呼ばれるコールバック関数。get_node()はそのNodeの子Nodeの中から、名前が一致するNodeを検索する関数。Sceneに配置されたボタンの名前(Scene dock上にリストアップされている名前)はButtonとなっているので、get_note(“Button”)でこのボタンへの参照が取得できる。get_nodeは子Nodeに対して検索をする関数なので、もしスクリプトが別の場所にアタッチされていたり、Buttonが別の場所(Labelの子Nodeになっていたり)にあったりすると検索に失敗するので注意が必要。

connect(“pressed”, self, “_on_Button_pressed”)では、ボタンにおける”pressed”というSignalがあった場合に、自身(_self)の”_on_Button_pressed”という関数を実行させる、という設定を行っている。この設定はコードで書く代わりにエディタ右側の「Node」からUI上で設定することもできるが、慣れないうちは「どこで何をやっているか」分からなくなりがちなので全てコード上で管理した方が混乱しなくて良さそうだ。

get_node(“Label”).text = “HELLO!” は書いてある通りだが、Labelの文字列をHELLO!に書き換えるためのコード。

この状態でSceneを実行し、ボタンを押してみるとLabelの文字列が変わることが確認できた。

カウンターにしてみる(変数を使ってみる)

コードを少しいじってカウンターを作ってみる。

extends Node2D

var counter = 0

func _ready():
	get_node("Button").connect("pressed", self, "_on_Button_pressed")

func _on_Button_pressed():
	counter += 1
	get_node("Label").text = str(counter)

var counterでカウントアップ用の変数を用意し、_on_Button_pressed()内でカウントアップする。counterの中身はintだが、LabelのtextはString型なので代入する際にはstr関数を使ってintからStringへの変換が必要になる。

実行してボタンを何度か押してみるとちゃんとカウントアップされていることが確認できた。

複数のSceneインスタンスがあった場合の挙動

複数のSceneインスタンスがあった場合の挙動も確認しておこう。

新しい2D Sceneを作成し、そこに先ほど作ったLabelとButtonから構成されるSceneのインスタンスを複数配置して実行してみる。

ボタンを押してみると、それぞれのインスタンスごとに独立してカウンターがカウントアップされていくのを確認できた。

親Sceneから子SceneのNodeを操作する

親Sceneから子SceneのNodeを操作してみる。親SceneにButtonを設置しスクリプトをAttachし、コードを記述する。

extends Node2D

func _ready():
	get_node("Button").connect("pressed", self, "_on_Button_pressed")

func _on_Button_pressed():
	get_node("Node2D3/Label").text = "Nantekottai..."

get_node()ではスラッシュ(/)を使うことで階層を指定してNodeを探索することができる。get_node(“Node2D3/Label”)は親SceneにあるNode2D3というNodeの子NodeのLabelというNodeを探索する。

上記のコードで、子SceneのNodeを操作できることを確認できた。ただし、親Sceneが子Sceneの構造を指定して操作するやり方は、後の子Sceneの構造変更の可能性などを考えるとあまり良いやり方ではないので、基本的には子Sceneの方に関数を定義しておいて、それを呼び出す形にした方が良いだろう。

// 子Sceneのコード
extends Node2D


func sayNantekottai():
	get_node("Label").text = "Nantekottai!"


// 親Sceneのコード
extends Node2D

func _ready():
	get_node("Button").connect("pressed", self, "_on_Button_pressed")

func _on_Button_pressed():
	get_node("Node2D3").sayNantekottai()

コード内から子Sceneのインスタンスを追加してみる

エディタ上で子Sceneを追加し、それらに対して親Sceneから操作を行う方法は分かったが、実際のゲームではインスタンスはコードから生成したいことが多いと思うので、その方法についても調べておく。

extends Node2D

const ChildScene = preload("res://LabelAndButton.tscn")

var instanceNumber = 0

func _ready():
	get_node("Button").connect("pressed", self, "_on_Button_pressed")

func _on_Button_pressed():
	var anInstance = ChildScene.instance()
	anInstance.name = "ChildInstance" + str(instanceNumber)
	anInstance.position = Vector2(instanceNumber * 100, 0)
	instanceNumber += 1
	self.add_child(anInstance)

子Sceneを追加するには、まずpreloadを使って予めtscnファイルの中身をコード上で読み込んでおく必要がある。tscnファイルのパスはFileSystem dockから確認することができる。

上のコードではボタンが押される度にインスタンスが追加されるようになっている。上で読み込んだtscnファイルに対して、instance()を呼ぶことで新しいインスタンスが作られる。作られたインスタンスは後から参照できるようにユニークなnameをつけておきたいので、追加した順番にChildInstance0, ChildInstance1…といったnameをつけるようにした。親Sceneのself.add_childを呼ぶことで、生成した新しい子Sceneインスタンスを親Sceneに追加することができるのだが、同じ場所にインスタンスを追加すると追加されたのかがわかりにくいので、表示位置を表すpositionプロパティに2次元座標(Vector2D)でx: instanceNumber * 100, y: 0の位置に追加されるようにした。

実行してみた様子。親SceneのAdd child sceneボタンを押す度に子Sceneのインスタンスが追加されていく。子Sceneのインスタンスはエディタで追加した時同様独立して動作するので、ボタンを押すとカウンターがそれぞれカウントアップされる。

子SceneをGroupで管理する

子SceneはGroupという機能でグルーピングして管理することができる。親Sceneに別のButton(Button2)を追加して、押されたら全ての子Sceneに対してsayNantekottai関数を呼び出すようにしてみた。

extends Node2D

const ChildScene = preload("res://LabelAndButton.tscn")

var instanceNumber = 0

func _ready():
	get_node("Button").connect("pressed", self, "_on_Button_pressed")
	get_node("Button2").connect("pressed", self, "_on_Button2_pressed")

func _on_Button_pressed():
	var anInstance = ChildScene.instance()
	anInstance.add_to_group("mygroup")
	anInstance.name = "ChildInstance" + str(instanceNumber)
	anInstance.position = Vector2(instanceNumber * 100, 0)
	instanceNumber += 1
	self.add_child(anInstance)

func _on_Button2_pressed():
	get_tree().call_group("mygroup", "sayNantekottai")

上のコードを少し変更し、生成した子Sceneのインスタンスに対して、add_to_group(“mygroup”)という関数を呼んでおくと、インスタンスがmygroupという名前のグループに追加されて管理される。グループ名は文字列で自由に決めて設定できる。タグのような感覚で使えば良い。

グループに追加されたインスタンスに対してはget_tree().call_group()関数を使うことで、一括で関数を呼び出すことができる。get_tree().call_group(“mygroup”, “sayNantekottai”)ではmygroupに登録されている全てのインスタンスのsayNantekottai関数を呼び出す。

実行してみた様子。mygroupに追加した全てのインスタンスに対して一括でsayNantekottaiを呼び出すことができた。

まとめ

スクリプトを使う基本的な方法と、スクリプト上でInstance/Nodeを操作する方法を確認した。ペース配分を間違えたので後3日でゲームが完成するのか不安になってきた。

年末年始なのでGodotでゲームでも作ってみる 3日目

今日の目標

  • 複数のSceneを使う(Instancingを試す)

GodotにおけるSceneの扱いについて

Godotでは複数のNodeをまとめて一つのSceneとして扱うことができる。Sceneは.tscnファイルとしてプロジェクト内に保存される。そして、Sceneの中では保存されている別のScene(.tscnファイル)を読み込んで、自身のSceneのNodeツリーの中に組み込むことができる。(Instancing)

Godotのゲームでは、Scene(tscn)ファイルを多数用意して、必要に応じて親Scene内でインスタンスを作る(Instancing)というフローが発生するので、まずtscnファイルを作る/親Sceneに子Sceneを追加するというのがどういうことなのかを、コードを書く前に確認しておく。

検証用のプロジェクトの作成

子となるSceneの作成

先に「子」として親SceneにインポートするSceneを作る。いらすとやからダウンロードしてきたハエの画像に、Labelで「HAE」というキャプションをつけたものをHae.tscnというファイル名で保存した。

親となるSceneの作成

次に親となるSceneを作成する。新しいSceneの作成はSceneメニューのNew Sceneから行う。ここでも「2D Scene」を選択して空の2D Sceneを作成した。

子Nodeのインスタンス化(Instancing)

Scene dockにあるチェーンのボタン(Instanc Child Scene)を押すと、他のSceneのインスタンスを取り込むことができる。

先ほど作成したHae.tscnを選んでOpenするとSceneの下にHae.tscnのインスタンスが追加された。

同様の手順で、一つのSceneに複数のHae.tcsnのインスタンスを作っていくことができる。

この状態でHae.tscnに戻ってラベルの中身を書き換えてみる。

保存して親Sceneの方に戻ってみる。

すると親Sceneの方に追加されていたHae.tscnのインスタンスのラベルも全てKANIに書き換わった。Instance Child Sceneを使って他のSceneを追加した場合、その時点でのSceneの内容がそのままコピーされるわけではなく、「実行時に、指定されていたSceneの内容からインスタンス(コピー)が作られる」という挙動になる。

まとめ

無理ないペースで進めようと思ったら、まだコードを一切書いていないのに三日経ってしまった。今年の年末年始休みは短いので間に合うのか不安だが、今日は大晦日なのでゆっくりしたい。

年末年始なのでGodotでゲームでも作ってみる 2日目

初日はサンプルを動かすところまでで力尽きてしまったので、今日はドキュメントを読み進めながら新しいプロジェクトを作るところまで進みたい。

今日の目標

  • Hello worldを目指す

ドキュメントを読む

Godotはドキュメントが充実している。残念ながら日本語の翻訳は追いついていないようだが、少なくとも英語版については分かりやすくてしっかりしたドキュメントが用意されていて、最初の導入はそれを読めばなんとかなりそうだ。

Step by stepガイドには、エディタの説明やGodotに登場する設計・思想の説明が用意されている。Godotのエディタは比較的シンプルで直感的に使えるが、Your first look at Godot’s editorあたりを読んで各UIパーツの名前を覚えておくと、以降ドキュメントなどを読み進めやすくなる。

Scenes and nodesInstancingのページには、シーンやノード、オブジェクトのインスタンス生成などについての説明がある。このあたりの考え方はUnityやSceneKitなどとかなり近しいので、その辺りに触れたことがある人はスキップしても問題ないのではないかと思う。

新しいプロジェクトを作る

このStep by stepのScenes and nodesを読み進めながら、新しいプロジェクトを作ってみる。

Project ManagerのNew Projectから新しいプロジェクトを作る。RendererにはOpenGL ES 3.0とOpenGL ES 2.0のいずれかを選択できる。今回は特に古いデバイスで動かしたいとかブラウザ上で動かしたいというような目的はないので、OpenGL ES 3.0を選んだ。

Sceneを作る

Godotには他のゲームエンジンと同様にSceneという概念がある。一つのゲームの中に「オープニングシーン」「メニューシーン」「バトルシーン」「結果表示シーン」のように複数のSceneを含めることができる。

後で理解すれば良い話だが、Sceneは「複数のNodeから構成されるNode」でSceneの下に別のSceneを読み込んでくっつける、というようなこともできる。Sceneという名前からは少しイメージしにくいがキャラクターやUIなど、グループで管理した方が都合が良いNodeもSceneとして作って利用することができる。

Godotアプリケーションでは、最低一つのScene(メインシーン)を作成する必要があるので、とりあえずシンプルなシーンを一つ作ってそれを実行してみる。

画面左上のScene dockからシーンを追加することができる。明示的に2D Sceneや3D Scene、User Interfaceを作っても良いし、他のNodeからSceneを作ることもできる(SceneはただのNode)。2D Sceneを作ってからそこにNodeを設置した方が分かりやすそうな気もするが、とりあえずドキュメントに従ってLabel nodeをRoot nodeとするSceneを作るため、Other Nodeを選択する。

作成できるNodeの種類がリストアップされる。Label nodeを選択してCreateを実行するとLabel nodeが作られる。

Labelという名前のScene(Node)が作られた。

追加されたLabelにはまだテキストが入力されていないので、InspectorからText欄に適当な文字を入力する。Inspectorでは他にも左寄せ/中央揃え/右寄せといったアラインメントの設定やクリッピング(Textが長くなった場合にLabelのサイズを拡張するのか、元のサイズを維持して表示を省略するのか)などの指定が可能。

テキストを指定したら、実行して表示を確認したい。GodotはScene単位でプログラムを実行する。Sceneを実行する前に、作成したSceneをファイルとして保存する必要がある。Label.tscnというファイル名で保存した。

エディタ右上のPlaytest buttons郡の中に、「Play Scene」というボタンがある。このボタンをクリックすると現在エディタで作業中のSceneを実行して動作確認することができる。

Hello Worldを画面に表示できた。ウィンドウを閉じるか、エディタのPlaytest buttonsにあるStopボタンを押すとプログラムを終了できる。

ちなみに、Play Sceneの代わりにPlayボタンを押すと上記のようなダイアログが表示される。ここでSelect Currentを押すと、現在編集中のSceneがこのアプリケーションのメインシーン(アプリケーション起動時に一番最初に実行されるScene)として設定されることになる。後で、メインシーンを別のSceneに変更したい場合はProject Settingsメニューから設定し直す必要があるので一応頭に入れておく。

なお、Labelを表示するだけのSceneを作る場合はLabelをルートノードにしたSceneを作っても良いが、通常のSceneには複数のNodeが含まれることになる。複数のNodeを追加する場合は最初に2D Sceneをルートノードとして追加して、その下にAdd Child Nodeでノードを追加していけば良い。

イメージの追加

画像を追加する場合は、まずFinderからFileSystem dockにドラッグしてインポートを行う。

FileSystemにインポートされた画像をWorkspaceにドラッグすると、その画像を使ったSpriteノードが作成される。

まとめ

公式ドキュメントに従って、エディタの簡単な使い方とScene / Nodeの追加の仕方を学んだ。もうゲームは半分完成したといっても過言ではないだろう。あとはスクリプトの書き方を学び、作るゲームの内容を考え、素材を用意して実装し、プログラムの書き出しについて学ぶぐらいしか作業は残っていないはずだ。コンディション維持のために今日はこれぐらいにしてストレッチに励もうと思う。

年末年始なのでGodotでゲームでも作ってみる 1日目

特に深い理由はないものの、今年の年末年始は久しぶりにゲームでも作ってみようかという気分になった。Godot Engineというフリーのゲームエンジンが気になっていたので、今回は休みの間気力が続く限りGodot Engineを試してみようと思う。

Godot Engineとは

Godot EngineはiOS / Android / Windows / macOS / HTML5…など様々な環境に対応したゲームを作ることができるゲーム開発環境で、無料で利用することができる。2D/3Dの両方に対応している。登場する概念や設計・思想はUnityやCocos2d、SpriteKitなどといった近年のゲームエンジン/フレームワークのそれに近しく、ドキュメントも充実しているので学習の材料として良さそうだったので、今回これを勉強していくことにした。

今日の目標

  • Godotをインストールする
  • Godotでサンプルプロジェクトを開いて実行してみる

新しいことを始めるときに、全容が見えないうちに高すぎる目標を掲げることは避けたい。初日は環境を構築してサンプルが無事に動いたら上出来ぐらいのテンションで行きたい。

Godotのインストール

公式サイトからGodotの最新版をインストールする。2021年12月29日現在の最新版は3.4.2。

DownloadページからGodotをダウンロードする。独自言語であるGDScriptが使えるStandard版とC#が使えるMono版がある。せっかくなので独自言語であるGDScriptを使うStandard版をインストールした。なおbrew caskを使ってもインストールできるようだ。Zipファイルを解凍して出てきたGodot.appをApplicationsディレクトリに移動するだけでインストールは完了。

起動してみたところ。「プロジェクトが一つもないけど、Asset Libraryにある公式のサンプルでもみてみる?」とのことなので「Open Asset Library」を開いて公式サンプルを見てみようと思う。

Asset LibraryのDemoのリストを見ていると、Skeleton2D DemoというシンプルそうなDemoがあったので、こちらを試してみよう。

詳細画面で「Download」を押すと、Asset Libraryからローカルにプロジェクトがダウンロードされる。

ダウンロードしたデモをインストールする場所を指定する。Project Pathには実在する空のフォルダのパスを指定する。

「Install & Edit」を選ぶと、指定した場所にプロジェクトがインストールされエディタが起動する。

エディタの右上にある「Play」を押すと、プロジェクトを実行して動作を試すことができる。

ゲームが起動した。矢印キーでキャラクターを操作できる。

ゲームを終了してエディタに戻ったら、「Quit to Project List」でプロジェクトを閉じてプロジェクト一覧画面に戻ろう。

プロジェクト一覧画面(Project Manager)に行くと、今開いていたSkeleton2D Demoプロジェクトがリストアップされている。別のデモを試したいときは画面上部のタブを「Asset Library Projects」に切り替えると、そこから先ほどのようにデモを検索してインストールすることができる。

まとめ

インストールはバイナリをzipからコピーするだけ、パスの設定なども不要で躓く点はなかった。サンプルプロジェクトも動いたことだし、今日はここまでにしておいてあとは体調を整えることにでも時間を使いたい。プログラマーはやはり体が資本。少し寒いがランニングにでも行こうと思う。アスファルトの上を走ると膝が痛くなるのが悩みだ。ずっと昔スポーツで膝の半月板を痛めたことの後遺症だと思っていたが、少し前にCTをとってみたらそれが原因ではなく姿勢の悪さ(骨盤の歪み)が原因だと言われた。実際、その後足を組むのをやめて、ストレッチの時間を増やしてから膝の痛みはかなり軽減した。そんなことを思い出した。明日は公式ドキュメントを読みながらプロジェクトを作ってみよう。