[Godot Engine 4]セーブとロード機能をつけてみた

はじめに

最近Godot Engine 4でゲーム制作をしております。そこでセーブとロードの機能をつけたのですが、苦労しました…
ほかにも困っている方がいるのでは?と思い、記すことを決めました。

作成したゲーム(?)の内容

カウントアップとカウントダウンのボタンがあり、それぞれを押すと中央の数字が増えたり減ったりします。また、セーブボタンを押すとそのときの数字が保存され、ロードボタンを押すと保存した数字が呼び戻されます。
(図を分けているのは画像のサイズが大きく上手くアップロードできなかっただけです。)

完成図1(カウントアップとセーブ)
完成図2(カウントダウンとロード)

ノードの操作などに慣れている方はSTEP3へ行ってもいいかもです。

必要なもの

  • Node

  • Label

  • Button×4

STEP1 ノードの配置

左上のシーンの下にある+から必要なものに記したノードを配置します。

ノード「Node」「Label」「Button」を追加
「Node」と検索すればすぐに見つけられます。
「Label」と「Button」も同じように(Buttonは4つ必要です)
こんな感じになればOK
(並び順や親子関係はドラッグ&ドロップで変更できます)

STEP1-α 保存しよう

ここまでできたら一度シーンを保存します。
「Ctrl+S」を押すか、左上のシーンから「名前をつけてシーンを保存」をクリック。下図のような画面が出てくるので好きな名前をつけて保存を押す。
(好きな名前と言っても「〇〇.tscn」の形は崩さないように。)

これで今作ったシーンが保存されました。ちなみに私は「main.tscn」にしました。ほかの場合は「player.tscn」や「enemy.tscn」などシーンの役割に応じて名付けています。

STEP2 ノードを配置しよう

これからノードを配置します。
まずはわかりやすいように名前をつけます。名前を変えたいノードを右クリックして、名前を変更をクリックします。

ちなみに選択した状態でF2(PCによってはFn+F2)を押せばすぐに名前が変更できます。これはGodot関係なしにWindowsで使えるショートカットです。
私が名付けた名前は以下の通りです。

今回はノードの数が少ないのであまり困らないですが、規模が大きくなると「このノードなんだっけ?」となりそうなので、先頭に何のノードか種類を書きました。(冗長かなぁ…)
以降、これらの名前を使うので、できれば同じ名前でお願いします。

さて、ノードをたくさん用意しましたが、この状態では操作しにくいので、右側にあるインスペクターの中にあるTextに何か適当な文字を入力して見やすくしましょう。

また、文字が小さくて見にくいので、同じくインスペクター中にある「Theme Overrides>Font Size」から大きさを変更します。デフォルト(最初の状態)では1pxなので相当小さいです。今回は思い切って60pxにしました。

下の方にスクロールしてください

これで操作しやすくなったので適当に配置します。

配置はおまかせします

ちなみにセーブとロードは3文字なのでカウントアップとカウントダウンに比べて幅が小さくなってしまったと思います。その場合は前後にスペースを入力したり、ボタンをクリックすると出てくる赤丸を押しながら引っ張ったりして上手く調整しました。
(中央揃えとか何pxにするとか指定できるはずですが見当たらなくて…)

古典的な方法…

STEP3 ソースコードを書こう

いよいよソースコードを書きます。
まずNodeを右クリックして「スクリプトをアタッチ」をクリック

名前を入力する画面になると思いますが特に変更しなくても構わないと思います。つまり「main.gb」で。

これがソースコードを書く画面です。

ソースコードにも起こしておきます。

extends Node


# Called when the node enters the scene tree for the first time.
func _ready():
	pass # Replace with function body.


# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	pass

まず1行目の「extends Node」はNodeの機能を使うために必要な命令です。特に理由がなければ基本は残しておきます。
次に4行目と9行目にある「#」で書かれた文章はコメントといい、プログラムに影響はないけれど、機能の説明など、他の人や後日触れる自分のために書いておくメモです。
4行目のコメントは5, 6行目の_ready関数について説明しており、「このシーンが呼び出されたときに一度だけ実行されます」と書いてあります。
また、9行目のコメントは10, 11行目の_process関数について説明しており、「画面の更新がされるごと(毎フレーム)に呼び出されます」と書いてあります。なお、deltaとは更新される時間の間隔を指し、その時間はおよそ1/60秒です。…よくわからないと思うので言い換えると、「1秒間に60回更新されます」ということです。
ちなみに今回、_ready関数は使わないので削除して構いません。また、ソースコードをすっきりさせるためにもコメントも消しておきます。つまりこういうこと。

extends Node

func _process(delta):
	pass

STEP3-A カウントアップとカウントダウンボタンを作ろう

まずはLabel_Numberを増やすボタンButton_CountUpと減らすボタンButton_CountDownを作りましょう。
最初に左側にあるButton_CountUpをクリックし、右側のインスペクターの隣にあるノードをクリックします。すると、上の方にpressed()があると思います。

pressed()以外にもいろいろありますが、これらはシグナルといい、ノードに何かしらの操作があった場合にどんな動きをするか指定できます。今回のpressed()はButton_CountUpが押されたときにどうするかを指定できます。

では、pressed()を右クリックし、接続を選びます。そしてNodeを選び(そもそも他は選べませんが)、接続を押します。

すると、main.gdに自動的に何かが書き込まれていると思います。

この「func _on_button_count_up_pressed()」というとても長い名前の関数が「 Button_CountUpが押されたときにどんな動きをするのか」を指定する関数です。早速次のように書いてみましょう。(#部分のコメントは書かなくてもOKです。)

extends Node
var num: int = 0 # 追加箇所
@onready var label_number: Label = get_node("Label_Number") # 追加箇所

func _process(delta):
	label_number.text = str(num) # 変更箇所 (passを消して書き込んだ)

# 以下追加箇所
func _on_button_count_up_pressed():
	num += 1

2行目で変数numを作っています。これは増やしたり減らしたりする数値を保存しておくために作りました。
3行目はLabel_Numberを操作するために書きました。Step2の終わりの辺りでTextを手動で書き換えましたが、ソースコードからも変更するために必要な命令です。

~3行目の詳しい説明を聞きたい人向け~
初めの@onreadyは「今からこのノードを使うので準備してください」という意味になります。先ほど消した_ready関数の中で命令してもよいのですが、こっちの方が楽です。
「var label_number」は変数の宣言です。名前は何でもいいのですが、わかりやすいと思い、ノードと同じ名前(正確には全部小文字なので違いますが)にしました。
「Label」はこの変数に代入するノードの種類は「Label」です、と教えています。
「get_node("Label_Number")」は親ノードNodeの子ノードLabel_Numberを取得するための命令です。

6行目の「label_number.text = str(num)」ではlabel_numberのテキスト部分をstr(num)にします、という命令です。
numはint型なので、そのままでは表示できません。そのためstr()でstr型(文字列型)に変換(キャスト)してから表示をさせます。
9行目の「num += 1」はnumを1増やします、という命令です。関数も含めて説明すると、「CountUpが押されたらnumを1増やします」ということです。

ここまでできたら(忘れずに上書き保存して)、プロジェクトを実行してみましょう。右上にある再生ボタンを押すかF5を押すことでプロジェクトを実行できます。

実行すると、一度だけ以下のような確認がでると思います。

今回使用するシーンは今操作している「main.tscn」だけなので、現在のものを選択を押してください。すると、新たにウインドウが出てきて実際に触れることができると思います。早速、カウントアップボタンを押してみましょう!

数字がずれている…あとで直します

上記の画像のように中央の数字が増えているでしょうか?もし上手くいかない、エラーが出てしまった場合は、ソースコードを見直したり、シグナルをつなぐものが間違っていないか確認してみたりしてください。

上手くいったらカウントダウンボタンも作成してみましょう。同じことの繰り返しですので、詳細は省き、ソースコードだけ載せます。

extends Node
var num: int = 0
@onready var label_number: Label = get_node("Label_Number")

func _process(delta):
	label_number.text = str(num)

func _on_button_count_up_pressed():
	num += 1

# 以下追加箇所
func _on_button_count_down_pressed():
	num -= 1

_on_button_count_down_pressed関数(カウントダウンボタン)の方は数字を減らしたいので「num -= 1」にするところがポイントです。うまくいけば次のようになるはず。

これでやりたいことの半分が終了です!続いて本題のセーブとロードを作っていきましょう!

STEP3-B [本題]セーブとロードボタンを作ろう

セーブとはゲームデータを保存すること、ロードとは保存したゲームデータを呼び出すことを指します。わざわざこんなことを書いた理由は、何を保存したいのかを明確にしよう、という意図があります。
今回記録したい変数はnumしかない(label_numberも元はnumですからね)ので、numだけを保存すればよいのですが、今後、RPGを作った際は膨大な量の変数を保存することになります。
(偉そうなことを言いましたが私は作ったことがありません…勉強途中なのです。)
さて、ここで実現したいことは

  • セーブボタンを押したらnumの値を保存する。

  • ロードボタンを押したら先ほど保存したnumの値を呼び出す。

以上の2点です。では早速、セーブボタンとロードボタンのシグナルpressed()をNodeに接続しましょう。

extends Node
var num: int = 0
@onready var label_number: Label = get_node("Label_Number")

func _process(delta):
	label_number.text = str(num)

func _on_button_count_up_pressed():
	num += 1

func _on_button_count_down_pressed():
	num -= 1

# 以下追加箇所
func _on_button_save_pressed():
	pass # Replace with function body.

func _on_button_load_pressed():
	pass # Replace with function body.

いろいろ説明する前に書いてしまおうと思います。実を言うと、次のソースコードが完成形です。

extends Node
var num: int = 0
@onready var label_number: Label = get_node("Label_Number")
var save_path = "user://savedata.txt" # 追加箇所

func _process(delta):
	label_number.text = str(num)

func _on_button_count_up_pressed():
	num += 1

func _on_button_count_down_pressed():
	num -= 1

# 以下追加箇所
func _on_button_save_pressed():
	var file = FileAccess.open(save_path, FileAccess.WRITE)
	file.store_var(num)

func _on_button_load_pressed():
	if FileAccess.file_exists(save_path):
		var file = FileAccess.open(save_path, FileAccess.READ)
		num = file.get_var(num)
	else:
		print("セーブデータが見つかりません...")
		num = 0

「var save_path = "user://savedata.txt"」ではセーブデータを保存する場所を指定しています。ちなみに保存場所はプロジェクトから「ユーザーデータフォルダーを開く」を押すと確認できます。

では、セーブボタンの説明をします。

# 再掲
func _on_button_save_pressed():
	var file = FileAccess.open(save_path, FileAccess.WRITE)
	file.store_var(num)

「var file = FileAccess.open(save_path, FileAccess.WRITE)」はこれからsave_pathで指定した場所にあるデータファイルsavedata.txtに書き込みます、という命令です。
では、何を書き込むのか。それは「file.store_var(num)」で指定しています。つまりここでは、numの値を書き込みます、と命令をしています。

続いてロードボタンの説明です。

# 再掲
func _on_button_load_pressed():
	if FileAccess.file_exists(save_path):
		var file = FileAccess.open(save_path, FileAccess.READ)
		num = file.get_var(num)
	else:
		print("セーブデータが見つかりません...")
		num = 0

「if FileAccess.file_exists(save_path):」はsavepathで指定した場所にセーブデータがあるならば以下の命令を実行します、という意味です。
セーブデータが見つかった場合「var file = FileAccess.open(save_path, FileAccess.READ)」と「num = file.get_var(num)」が実行されます。
「var file = FileAccess.open(save_path, FileAccess.READ)」はsave_pathで指定した場所にあるデータファイルsavedata.txtを読み込みます、という命令です。
「num = file.get_var(num)」はややこしいですが、savedata.txtに書き込まれているnumの値をnumに代入します、という命令です。
もし、セーブデータが見つからなかった場合、「else:」以降の命令が実行されます。今回ほど超小規模な場合、あまり意味はありませんが、万が一を考えて必要な操作なのでしょう、たぶん。

では、実行してみましょう。(…最初に見せた画像と同じです。)

"2"をセーブすると…
ロードで"2"が呼び出された!

セーブとロードが実行されたでしょうか?できたらあとはエクスポート(exeファイルにして遊べる?ように)するだけです!

STEP4 エクスポートしよう

これからexeファイルを作ります。
まず、プロジェクトから「エクスポート...」をクリックします。

続いて出てくるウィンドウの上部にある「追加」から「Windows Desktop」を選びます。

もし、エクスポートを一度も使ったことがない場合、エラーが発生するかもしれません。その場合は「エクスポートテンプレートの管理>ダウンロードしてインストール」をクリックして、WindowsDesktopでexeファイルを作るために必要なツールをインストールしてください。(画像が用意できませんでした…すみません。)
ダウンロードできたらいよいよエクスポートです。「組み込みPCK」をオンにすることを忘れずに行い、「プロジェクトのエクスポート」をクリックしてください。

次のウィンドウでエクスポートする場所、ファイルの名前を指定して…
完成です!

ちなみにPCKファイルとはゲームに必要なファイルがいろいろと入っているものです。正直あまり理解していないのですが、セーブとロードの機能をつけるにはPCKファイルを組み込まないといけないのです。(一度エラーを経験済み)一応、組み込まなくてもexeファイルと同じ階層においておけばよいのですが、組み込んでおくと楽です。

終わりに

実はここまでしっかりまとめるつもりはなかったのですが…最初ということで結構しっかり書きました。習得したばかりのパワポを使って拡大したように見せるテクニックも多様しました。見にくかったらすみません。
次回以降はもうちょっと簡素にまとめようと思います。

この記事が気に入ったらサポートをしてみませんか?