💬

Godot Engineでノベルゲームエンジンを自作するときのポイント

2021/03/20に公開

概要

現在Godot Engineで自作のノベルゲームエンジンを構築中です。その過程で色々と悩んだ点をまとめてみました。

現状は以下の機能が実装できています

  • テキスト表示とテキスト送り
  • 選択肢の表示と選択
  • 文字の色変え
  • 背景画像の表示
  • 立ち絵と話者名表示
  • 顔ウィンドウの表示

これより前にシューティングゲームを作ったりしたので、基本的な部分で悩むことはなかったですが、テキスト処理で苦戦したので、この記事ではそのあたりの情報が中心となります。

シューティングゲームを作ったときに得た情報は以下のページにまとめています。
https://qiita.com/2dgames_jp/items/a1db3a0a6d67e02a7d43

テキスト表示

テキスト表示はRichTextLabelを使うのがよいです。

RichTextLabelを使うメリット〜BBCodeが使える

RichTextLabelには「BBCode」というテキストに装飾をする仕組みがあり、テキストに色を付けることが簡単にできます。

"redは[color=red]赤色[/color]。aquaは[color=aqua]水色[/color]"
"limeは[color=lime]明るい緑色[/color]。grayは[color=gray]灰色[/color]"
"silverは[color=silver]銀色[/color]。yellowは[color=yellow]黄色[/color]。fuchsiaは[color=fuchsia]赤紫色[/color]/"

例えばこのテキストを BBCodeテキストに設定すると以下のように色付けがされます。

テキストの色変えを自作すると、1文字ごとに色情報を持たせるなど、やや複雑な実装となります。
RichTextLabelであれば、BBCodeを使って楽に実装することができます。

RichTextLabelを使うデメリット〜テキスト表示文字数の制御が難しい

ただ、RichTextLabelを使うとテキスト文字を少しずつ表示する制御が難しくなります。

というのもテキストがBBCodeを含んだ文字となるため、String.left()などで文字を少しずつ切り出すだけでは実装できないからです。

そこで使用するのが、RichTextLabel.visible_charactersで、この値を少しずつ増加させることで、テキストを少しずつ表示することができます。

func _progress(delta):
  # 時間経過で表示する文字数を増やす
  _text_timer += delta
  _talk_text.visible_characters = _text_timer

ただこれにも問題があって、実際に表示される文字数をあらかじめ計算する必要があります。
BBCodeは [] で囲まれた文字列なので、これを正規表現で除外した文字列を求めるようにしました。

# BBCodeを除外した文字数を求める
func _calc_bbtext_length(texts:String):
  var regex = RegEx.new()
  regex.compile("\\[[^\\]]+\\]") # BBCodeを除外した文字列の長さを求める
  var text2 = regex.sub(texts, "", true)
  return text2.length() 

ちなみに、BBCodeには [fade]タグという文字を少しずつフェード表示する機能が用意されていますが、[color]タグとの共存ができなかったのでこれを使うのはあきらめました。
おそらくCustomEffectというBBCodeのタグを自作する仕組みを使えば対応できそうですが、フェードしながら文字を表示するのはいったん保留としています。

カーソル位置の計算

ノベルゲームでよくある、テキスト文字の終端にカーソルを表示する方法です。

調べたところ、直接文字の終端を取ることはできなさそうですが、

  • 文字の幅を取る
  • 行数を取得する

この2つが用意されているので、なんとか実装できました。

まず文字幅の計算ですが、こちらに情報がありました
https://godotengine.org/qa/27137/how-do-i-get-the-text-width-of-a-richtextlabel
Font.get_string_size(String)で幅が取れるよ、との情報です。カーソルのX軸の位置は最終行の幅となるので、改行文字をString.split()で分割して最後の行のサイズを取ります。

	var text2:String # テキスト文字列
	var _talk_text:RichTextLabel # RichTextLabel
	
	# 文字の末尾の座標を計算する
	var font = _talk_text.get_font("normal_font")
	var size := Vector2()
	
	# 文字の幅と高さを求める
	for text in text2.split("\n"):
		# 改行を判定しないので分割して最後のテキストの幅を求める
		size = font.get_string_size(text)
	# 行数を取得
	var line = _talk_text.get_line_count() - 1

	# カーソル座標の計算
	var cursor_x = size.x
	var cursor_y = size.y * line

またRichTextLabel.get_line_count()でテキストの行数を取得できるので、上記のようなコードでおおよそのカーソルの座標が計算できます。

選択肢の実装

選択肢は、Button の上に RichTextLabel を乗せる、という実装にしました。

このシーンをinstance() で複製して配置するようにしています。

選択肢にアタッチするスクリプトは以下のようにしました。

AdvSelectText.gd
extends Control

onready var _text:RichTextLabel = $Text
onready var _bg   := $Bg
var selected      := false

func _ready() -> void:
	pass

func _process(delta: float) -> void:
	pass

func start(pos:Vector2, txt:String):
	rect_position = pos
	# 中央揃え
	_text.bbcode_text = "[center]" + txt + "[/center]"

func destroy():
	queue_free()

func _on_Button_pressed() -> void:
	selected = true

テキストにRichTextLabelを使用することで、文字の中央揃えや色変えが簡単にできるようになります。

背景・キャラクター画像の読み込みと表示

TextureRecttexture に読み込んだ画像を設定すると、表示の切り替えを行うことができます。

onready var bg:TextureRect = $Bg
# 画像の読み込み
func load_texture(id:int):
	bg.texture = load("res://assets/bg/bg%03d.jpg"%id)

読み込み方法はload()に指定するだけで動的に画像を読み込むことができます。
ひょっとしたら、TextureRectよりもSpriteを使ったほうが色々と設定できる項目が多そうなので、場合によってはこちらを使うのもありかもしれませんね。

その他

ノベルゲーム用のスクリプトは、Pythonで自作したものを使っています。
例えば以下のようなテキストを記述して……

def loop {
	while(true) {
		DRB(1)
		select("選択肢テスト")
		{"[color=yellow]テキスト[/color]色変え"
			call color_text
		}
		{"背景描画"
			call draw_bg
		}
		{"キャラクター描画"
			call draw_ch
		}
		{"終了"
			break
		}
	}
	
	"おしまい/"
}

コンパイルすると以下のアセンブリ言語っぽいテキストファイルを出力します。

FUNC_START,loop
WHILE,00000020
BOOL,1
IF,00000020
INT,0
INT,1
DRB
SEL,1,選択肢テスト
SEL_ANS,4,[color=yellow]テキスト[/color]色変え,背景描画,キャラクター描画,終了
SEL_GOTO,00000011,00000013,00000015,00000017
CALL,00000022,color_text
GOTO,00000019
CALL,00000027,draw_bg
GOTO,00000019
CALL,00000040,draw_ch
GOTO,00000019
GOTO,00000020,break:##00000000##
GOTO,00000019
GOTO,00000002
MSG,1,おしまい
FUNC_END,loop

1行に命令コードとパラメータを指定し、ラベルジャンプで条件分岐をしています。
関数のパラメータの制御や数値演算はスタックで行っています。
このあたりの実装方法は、説明がかなり長くなるので説明できませんが、興味があれば「スクリプト言語 作り方」などで検索すると良いかもしれません。

ちなみに私は以下の本を参考に作りました

https://www.amazon.co.jp/dp/4839919232

やや古い本ですが、説明が丁寧でわかりやすく言語処理の基本が学べるので、スクリプト言語を作ってみたい方におすすめできる本だと思います。
(今なら、ひょっとしたらもっとわかりやすい本があるかも……ではありますが)

追記 (2021.11.28)

スクリプトを実装するのが難しい場合は「Dialogic」というアドオンを使うと簡単に会話イベントを実装できます。

使い方は以下のページに書いておきました
https://2dgames.jp/godot-dialogic/

追記 (2022.12.9)

シンプルなスクリプトを自作する方法として、吉里吉里のKAGのようなスクリプトを自作するヒントを書きました。

https://2dgames.jp/godot-kirikiri-kag/

Discussion