ゲームエンジンGodotでテキストエディタを作る
Godot について
Godot はMITライセンスのゲームエンジンですが、Godotエディタ自体がGodotエンジンで動作していることもあり、ゲーム以外に使用できそうなGUI関連のクラスが割りと充実している印象があります。
紹介動画の一番最初に流れたエディタを見て、アニメーションをつけたりかわいくできるのはおもしろそうだなと思ったのでエディタを作ろうと思います。ただ今回は簡単なエディタを作ります。(※Godot初心者なため凝ったことはできないので)
使用するバージョンは Godot 4.1 です。
幸いGodotには、CodeEdit というそのものズバリのクラスがありました。
アドオンなどではなく標準機能として公式がメンテしているものを使っていろいろ作れそうなのはいいですね。
参考
下記の動画は Godot 3 で作成されていますが、エディタを作る参考になりました。
1. CodeEdit を配置
まずはルートシーンに Main(Controlクラス)、その子ノードに CodeArea(CodeEditクラス)を作成します。それぞれアンカーを"Rect全面"に設定して画面全体まで広げます。
そのまま実行します。
CodeEditクラスをControlクラスの子ノードとして作成
エディタが起動しました。簡単ですね。
hiDPIをオフにする
ただ文字が小さいです。Font Sizeを40pxぐらいにしたらいい感じの大きさにはなりますが、普段使っているエディタでそんなサイズを指定したことはないのでなにかおかしいです。
ググってもなかなかわからなかったですが、解像度の問題なのでプロジェクト設定の「hiDPIを許可」のチェックをOFFにします。これでFont Sizeが16pxぐらいで読める大きさになります。
「hiDPIを許可」のチェックをOFFにする
2. "File"メニューの追加
画面上部に"File"メニューを表示するようにします。
Mainの子ノードに TopBar(MenuBarクラス)を追加。その子ノードに FileMenu(MenuButtonクラス)を追加します。
TopBarのアンカーを"上伸長"にして画面上部に表示し、FileMenuの「Text」に"File"を入れてボタン名として"File"が表示されるようにします。
マージンの追加
そのまま実行すると CodeArea の上に TopBar が被さって表示されるため、CodeArea の上部にマージンを追加します。
ここで地味にハマったのが、「インスペクター」で margin やら offset やらのキーワードでプロパティを絞ってもそれらしいのは見つかりません。
CodeAreaのアンカーに"Rect全面"を指定しているのでそれを "カスタム" に変更する必要があります。(anchor で検索すると見つかります)
"カスタム" に設定するとオフセットに関するプロパティが見えるようになるので、「Anchor Offsets」の「Top」に "30px" ぐらいを設定します。これでマージンは追加できました。
アンカーを"カスタム"にする
メニューアイテムの追加
FileMenuの「Items」に5つ、それぞれ「Text」を
- "New File"
- "Open File"
- "Save"
- "Save As File"
- "Quit"
として追加します。実行すると "File"メニュー にプルダウンが表示されるようになりました。
"File"メニューのプルダウン
スクリプトの追加
Mainにスクリプトを追加して、"File"メニューのプルダウンが押されたときの処理を書きます。
プルダウンそれぞれが押されたタイミングで、ファイルを開いたり保存したりの処理を行えるよう、シグナルを発行したいですが FileMenu(MenuButtonクラス)の「ノード」パネルからは設定できません。
プルダウンそれぞれが押されたことを知るには、MenuButtonクラスのメソッド get_popup()
で取得できる PopupMenuクラスからシグナルを発行しないといけません。めんどくさいですね。
Godot画面からは設定できないので _ready()
の処理内で紐づけます。id_pressed
がプルダウンそれぞれが押されたときのシグナルなので下記のように書きます。
extends Control
enum FileMenuId { NEW, OPEN, SAVE, SAVE_AS, QUIT }
func _ready() -> void:
$TopBar/FileMenu.get_popup().id_pressed.connect(_on_file_menu_selected)
func _on_file_menu_selected(id: int) -> void:
match id:
FileMenuId.NEW:
print("File New")
FileMenuId.OPEN:
print("File Open")
FileMenuId.SAVE:
print("File Save")
FileMenuId.SAVE_AS:
print("File Save as")
FileMenuId.QUIT:
get_tree().quit()
それぞれ押して「出力」パネルに文字が表示されることを確認できました。
3. ファイルダイアログの追加
"File"メニューのプルダウンそれぞれが押された時に表示するファイルダイアログを追加します。
Mainの子ノードに OpenFileDiaglog(FileDiaglogクラス)と SaveFileDiaglog(FileDiaglogクラス)を追加します。
OpenFileDiaglog の「File Mode」を "Open File"、「Access」を "File System" に設定、SaveFileDiaglog の「Access」を "File System" に設定します。
FileDiaglogクラスを追加し、「File Mode」と「Access」を変更する
デフォルトは非表示ですが、とりあえずシーンドックで目のアイコンをクリックすれば見た目は確認できます。
そのままでは小さいので、それぞれ「Size」を 600x480px ぐらいに設定して、「Initial Position」も "Center of Main Window Screen" にして画面中央に表示されるようにします。
シグナルの設定
「ノード」パネルから、OpenFileDialog と SaveFileDialog の file_selected
をそれぞれ、_on_open_file_dialog_file_selected
と _on_save_file_dialog_file_selected
として紐づけます。
func _on_file_menu_selected(id: int) -> void:
match id:
FileMenuId.NEW:
print("File New")
FileMenuId.OPEN:
$OpenFileDialog.popup()
FileMenuId.SAVE:
print("File Save")
FileMenuId.SAVE_AS:
$SaveFileDialog.popup()
FileMenuId.QUIT:
get_tree().quit()
func _on_open_file_dialog_file_selected(path: String) -> void:
var f = FileAccess.open(path, FileAccess.READ)
$CodeArea.text = f.get_as_text()
f.close()
func _on_save_file_dialog_file_selected(path: String) -> void:
var f = FileAccess.open(path, FileAccess.WRITE)
f.store_string($CodeArea.text)
f.close()
ファイルを開いて保存できるようになりました。これで、エディタとしての最低限の機能はできました。
4. "File"メニューの完成
残りの "New File" と "Save" にも対応します。current_file
変数に現在開いているファイルのパスを記憶してファイルダイアログを表示せずに保存できるようにします。
また、ウィンドウのタイトルにファイル名を表示するため update_window_title()
メソッドを用意し、さらに、ファイルの変更をしたときに米印"*"を表示するため CodeArea(CodeEditクラス)の text_changed
シグナルを Main に紐づけて、file_dirty
変数でファイルの変更有無を管理します。
それらを踏まえると下記のようになりました。
extends Control
enum FileMenuId { NEW, OPEN, SAVE, SAVE_AS, QUIT }
const NEW_FILE_PLACEHOLDER = "Untitled"
var current_file : String
var file_dirty = false
func _ready() -> void:
$TopBar/FileMenu.get_popup().id_pressed.connect(_on_file_menu_selected)
$CodeArea.grab_focus() # ← 起動したときにエディタに自動フォーカス
update_window_title()
func update_window_title():
var title = \
ProjectSettings.get_setting("application/config/name") \
+ ' - ' + current_file_name()
if file_dirty: title += " *"
get_window().title = title
func current_file_name() -> String:
if current_file:
return current_file.get_file()
else:
return NEW_FILE_PLACEHOLDER
func new_file():
current_file = ''
$CodeArea.text = ''
file_dirty = false
update_window_title()
func save_file():
if current_file:
var path = current_file
var f = FileAccess.open(path, FileAccess.WRITE)
f.store_string($CodeArea.text)
f.close()
current_file = path
file_dirty = false
update_window_title()
else:
$SaveFileDialog.popup()
func _on_file_menu_selected(id: int) -> void:
match id:
FileMenuId.NEW:
new_file()
FileMenuId.OPEN:
$OpenFileDialog.popup()
FileMenuId.SAVE:
save_file()
FileMenuId.SAVE_AS:
$SaveFileDialog.popup()
FileMenuId.QUIT:
get_tree().quit()
func _on_open_file_dialog_file_selected(path: String) -> void:
var f = FileAccess.open(path, FileAccess.READ)
$CodeArea.text = f.get_as_text()
f.close()
current_file = path
file_dirty = false
update_window_title()
func _on_save_file_dialog_file_selected(path: String) -> void:
var f = FileAccess.open(path, FileAccess.WRITE)
f.store_string($CodeArea.text)
f.close()
current_file = path
file_dirty = false
update_window_title()
func _on_code_area_text_changed() -> void:
if file_dirty: return
file_dirty = true
update_window_title()
ショートカットキーの追加
上記で一通りできましたが、最後にそれぞれにショートカットキーを割り当てます。
プロジェクト設定のインプットマップを下記のように設定します。
インプットマップ
Main に _input
メソッドを追加して、イベントをフックするようにします。
func _input(event):
if event.is_action_pressed("file_new"):
new_file()
elif event.is_action_pressed("file_open"):
$OpenFileDialog.popup()
elif event.is_action_pressed("file_save_as"):
$SaveFileDialog.popup()
elif event.is_action_pressed("file_save"):
save_file()
これでショートカットキーで動作するようになりました。
5. シンタックスハイライトの設定
これで一応エディタはできあがりですが、欲張ってシンタックスハイライトにも対応します。
CodeArea(CodeEditクラス)にスクリプトを追加します。
CodeEditクラスが持っている syntax_highlighter
に CodeHighlighterクラスのインスタンスを突っ込んでいろいろ設定すれば色が変わるようなので試してみます。
extends CodeEdit
func _ready() -> void:
syntax_highlighter = CodeHighlighter.new()
syntax_highlighter.add_keyword_color("TEST", "#FF0000")
syntax_highlighter.add_color_region("\"", "\"", "#00FF00")
syntax_highlighter.add_color_region("//", "", "#8888FF")
syntax_highlighter.set_symbol_color("#FFFF88")
syntax_highlighter.set_number_color("0088FF")
syntax_highlighter.set_function_color("F9B7DC")
syntax_highlighter.set_member_variable_color("88FF88")
syntax_highlighter のテスト
add_keyword_color
が単語をキーに、add_color_region
が開始文字と終了文字をキーにして色が変わるようです。add_color_region
は終了文字に空文字を指定すると行末までが範囲になります。
数字(set_number_color
)、記号(set_symbol_color
)、括弧の前の文字列(set_function_color
)、ドット以降の文字列(set_member_variable_color
)は色は変えれますが、色を変える範囲は変更できません。
各言語のシンタックスを設定するには、add_keyword_color
と add_color_region
をうまく組み合わせるしかありません。正規表現とかは無いです。
カスタムリソースの定義
言語のシンタックスとエディタのテーマをそれぞれカスタムリソースとして作成しようと思います。
下記URLが参考になりました。
まずはリソースの定義をします。
SyntaxResource.gd ファイルと ThemeResource.gd ファイルを作成し以下のようにします。
extends Resource
class_name SyntaxResource
@export var regions = {
"key": [["begin", "end"]]
}
@export var keywords = {
"key": ["keyword"]
}
extends Resource
class_name ThemeResource
@export var background_color = "#FFFFFF"
@export var foreground_color = "#000000"
@export var symbol_color = "#008888"
@export var number_color = "#0000FF"
@export var function_color = "#FF0000"
@export var member_variable_color = "#000000"
@export var syntax_colors = {
"boolean": "#FF8800",
"keyword": "#FF00FF",
"string": "#008800",
"comment": "#888888",
"constant": "#FF4444",
}
仕組みとしては、SyntaxResource の regions
もしくは keywords
で定義した key
に該当する箇所を、ThemeResource の syntax_colors
のキーで指定することで色を変えようと思います。
カスタムリソースの作成
次に上記で定義したリソースの型に対して実際のリソースを作成していきます。
「ファイルシステム」ドックから右クリックで「新規作成」→「リソース」でひな形が作成できるので、それを編集します。
画面から編集してもいいですが、エディタで編集した方が楽です。幸いエディタは手元にありますね。
Ruby-syntax.tres
↑で試しに入れた色がいい感じに出ています。
編集するのは [resource]
の配下です。
regions = {
"comment": [["#", ""]],
"constant": [[":", " "]],
"string": [["'", "'"], ["\"", "\""]]
}
keywords = {
"boolean": ["true", "false", "nil"],
"keyword": [
"class", "def", "end", "module", "require", "return", "initialize", "attr_accessor"
]
}
background_color = "#FFFFFF"
foreground_color = "#000000"
symbol_color = "#008888"
number_color = "#0000FF"
function_color = "#FF0000"
member_variable_color = "#880088"
syntax_colors = {
"boolean": "#FF8800",
"comment": "#888888",
"constant": "#FF4444",
"keyword": "#FF00FF",
"string": "#008800"
}
テーマとシンタックスの適用
set_code_theme
メソッドと set_code_syntax
メソッドを作成して、先ほど作成したリソースを読み込んでみます。
extends CodeEdit
var current_theme : ThemeResource
var current_syntax : SyntaxResource
func _ready() -> void:
syntax_highlighter = CodeHighlighter.new()
set_code_theme(load("res://code-themes/Default-theme.tres"))
set_code_syntax(load("res://code-syntaxes/Ruby-syntax.tres"))
func set_code_theme(_theme: ThemeResource) -> void:
current_theme = _theme
add_theme_color_override("background_color", _theme.background_color)
add_theme_color_override("font_color", _theme.foreground_color)
syntax_highlighter.set_symbol_color(_theme.symbol_color)
syntax_highlighter.set_number_color(_theme.number_color)
syntax_highlighter.set_function_color(_theme.function_color)
syntax_highlighter.set_member_variable_color(_theme.member_variable_color)
func set_code_syntax(syntax: SyntaxResource) -> void:
current_syntax = syntax
for key in syntax.regions:
for arr in syntax.regions[key]:
if current_theme.syntax_colors.has(key):
syntax_highlighter.add_color_region(
arr[0], arr[1], current_theme.syntax_colors[key]
)
for key in syntax.keywords:
for value in syntax.keywords[key]:
if current_theme.syntax_colors.has(key):
syntax_highlighter.add_keyword_color(
value, current_theme.syntax_colors[key]
)
Ruby-syntax.tres と Default-theme.tres を適用
ばっちり変わりました。
6. コード補完の追加
シンタックスハイライトに対応したので今度はコード補完も欲しくなりました。
下記のように SyntaxResource に completions
を追加します。
extends Resource
class_name SyntaxResource
@export var regions = {
"key": [["begin", "end"]]
}
@export var keywords = {
"key": ["keyword"]
}
@export var completions = [
["display_text", "insert_text"]
]
Ruby-syntax.tres にも completions
を追加します。
completions = [
["class", "class"], ["def", "def"], ["end", "end"], ["return", "return"], ["module", "module"],
["require", "require"], ["attr_accessor", "attr_accessor"]
]
で、CodeArea.gd を修正しますが、コード補完に関してはググってもなかなか情報が出てこず、公式のドキュメントにも使い方は書いていないので以下のやり方がスマートなのかはわからないです。。
とりあえず見つけたやり方としては、CodeEditクラスが持っている add_code_completion_option
と update_code_completion_options
を使えばコード補完できるようです。
code_completion_enabled
というプロパティもありますが、ONにしてもそれでコード補完は動かなかったです。
CodeArea の「ノード」パネルから text_changed
と code_completion_requested
シグナルを CodeArea 自身で受け取るようにします。
_on_text_changed
の中で request_code_completion
を実行して、テキスト入力されたタイミングで都度コード補完をします。
func _on_code_completion_requested() -> void:
if !current_syntax: return
for arr in current_syntax.completions:
add_code_completion_option(CodeEdit.KIND_PLAIN_TEXT, arr[0], arr[1])
update_code_completion_options(true)
func _on_text_changed() -> void:
if get_word_at_pos(get_caret_draw_pos()).length() > 0:
request_code_completion(true)
これで code-syntaxes/Ruby-syntax.tres の completion
で定義した内容に従ってコード補完されるようになりました。
できたもの
最終的にはシンタックスを自動判定したり、プルダウンで変更できるようにしたり、テーマを変えるようにしたりなどの修正を行ない下記のようなものができました。
できたものは以下に置いています。
Godotはいいぞ
Discussion