【Godot】キーコンフィグを作る
はじめに
PC向けにゲームを作ると様々な入力デバイスがあることに気づきます。
Godotではプロジェクト設定からインプットマップを設定できますね。
複数のデバイスを1つのactionに割り当てることができます。
しかし、キーアサインを好みに設定したいプレイヤーもいて、キーコンフィグ機能があるゲームも多いです。
本記事ではインプットマップを実行時に設定する方法を解説します。
InputMapを使う
プロジェクト設定から設定したインプットマップは、InputMap
というAPIでアクセスできます。今回はこのAPIを主に使います。
キーコンフィグを作る
今回は下記のようにキーボードとゲームパッド両方から操作できるような設定です。
InputMapの設定
ここでは入力イベントを受け取り、InputMapに上書きする方法を解説します。
InputMap設定メソッド
InputMapに設定します。設定を上書きするAPIは存在しないので、一旦InputMap.action_erase_events
で削除してからInputMap.action_add_event
でInputEventを設定します。
func set_inputmap(action: String, event_index: int, event: InputEvent): # InputMapからactionを取得 var list = InputMap.get_action_list(action) # actionを一旦削除 InputMap.action_erase_events(action) # リストを上書き list[event_index] = event # actionを改めて追加 InputMap.action_add_event(action, list[0]) # キー設定 InputMap.action_add_event(action, list[1]) # パッド設定
入力を受けるメソッド
_gui_input
で入力を受け付けます (Controlノードにスクリプトを付けます)
受け取ったInputEventをset_inputmap
に設定します。
func _gui_input(event: InputEvent): var action = "act_jump" # 暫定的にact_jump固定 if event is InputEventKey: # キー入力された var key := event as InputEventKey if key.is_pressed(): set_inputmap(action, 0, event) elif event is InputEventJoypadButton: # パッド(ボタン)入力された var joypad_button := event as InputEventJoypadButton if joypad_button.is_pressed(): set_inputmap(action, 1, event) elif event is InputEventJoypadMotion: # パッド(スティック)入力された var joypad_motion := event as InputEventJoypadMotion if joypad_motion.axis_value != 0: set_inputmap(action, 1, event)
InputMapのセーブとロード
InputMapは再起動するとプロジェクト設定のデータにリセットされます。
キーコンフィグ機能として成り立たせるためには、設定時にファイルにコンフィグ情報を保存して、ゲーム起動時にロードする必要があります。
保存対象のaction
const input_actions := [ "act_attack", "act_jump", "act_up", "act_down", "act_left", "act_right", "ui_accept", "ui_cancel", "ui_up", "ui_down", "ui_left", "ui_right", ]
InputMapのシリアライズ
設定したInputMapを保存するときに使います。
func _serialize_input() -> Dictionary: var data = {} for action in input_actions: var list = InputMap.get_action_list(action) var item = {} item["key"] = list[0].scancode if list[1] is InputEventJoypadButton: item["button"] = list[1].button_index elif list[1] is InputEventJoypadMotion: item["axis"] = list[1].axis item["axis_dir"] = 1 if list[1].axis_value > 0 else -1 data[action] = item return data
InputMapのデシリアライズ
保存されたコンフィグ情報をInputMapに再設定するときに使います。
func _deserialize_input(data): if not data is Dictionary: return for action_name in data: var item = data[action_name] as Dictionary if not item: continue var list = InputMap.get_action_list(action_name) as Array if not list: continue if item.has("key"): var keycode = item["key"] if typeof(keycode) == TYPE_REAL: list[0].scancode = keycode if item.has("button"): var button_index = item["button"] if typeof(button_index) == TYPE_REAL: var event = InputEventJoypadButton.new() event.button_index = button_index list[1] = event elif item.has("axis") and item.has("axis_dir"): var axis_id = item["axis"] var axis_dir = item["axis_dir"] if typeof(axis_id) == TYPE_REAL and typeof(axis_dir) == TYPE_REAL: var event = InputEventJoypadMotion.new() event.axis = axis_id event.axis_value = 1 if axis_dir > 0 else -1 list[1] = event InputMap.action_erase_events(action_name) InputMap.action_add_event(action_name, list[0]) InputMap.action_add_event(action_name, list[1])
JSONにセーブ
シリアライズしたInputMapの情報をJSON化してファイルに保存します。
func savefile(): var file := File.new() if file.open("user://settings.json", File.WRITE) != OK: return var jsonstr := JSON.print({ "input": _serialize_input(), }, " ") file.store_string(jsonstr) file.close()
JSONからロード
JSON形式で読み込んだ情報をInputMapに再設定します。
func loadfile(): var file := File.new() if file.open("user://settings.json", File.READ) != OK: return var jsonstr := file.get_as_text() file.close() if not jsonstr: return var json_res := JSON.parse(jsonstr) if json_res.error: printerr("failed to load settings: " + json_res.error_string) var json := json_res.result as Dictionary _deserialize_input(json.get("input"))
注意
JSONはユーザーでもテキストエディタを使っていじれるため、 エラー処理をしっかりしないとセキュリティホールになるかもしれないので注意です。
【Godot】GDScriptのエラー: The class "ClassName" couldn't be fully loaded (script error or cyclic dependency). について
概要
GDScriptを書いているとよく見る「The class "ClassName" couldn't be fully loaded (script error or cyclic dependency).」について解説します。
エラーを調べる
GDScriptでスクリプトから別のスクリプト(のクラス)を参照するときに、発生することがあるエラーです。
エラー文の括弧内にもある通り、以下の2つが原因の可能性が高いです。
- Script error (スクリプトエラー)
- Cyclic dependency (循環依存エラー)
スクリプトエラー?
例えばScriptAからScriptBを参照しているとき、ScriptBで何かエラーが起きています。 そっちを直すと解決することがあります。
循環参照エラー?
こちらが本題です。
以下の2つのスクリプトを書くとどちらかで cyclic dependency
エラーが発生します。
- ScriptA.gd
class_name ScriptA func _ready(): var b = get_node("/Root/ScriptB") as ScriptB b.method_b() func method_a(): print("method_a")
- ScriptB.gd
class_name ScriptB func _ready(): var a = get_node("/Root/ScriptA") as ScriptA a.method_a() func method_b(): print("method_b")
GodotはGCではなく参照カウンタ方式のメモリ管理なので、循環参照するのはよろしくないです。でもクラスメンバに持つだけでなく、メソッド内で一時的に循環参照するのもダメなのはどうしてかなと。
原因
エラーの原因はどうも循環参照ではなく、スクリプトのロード順の問題とのことらしいです。
C/C++でヘッダ同士がお互いをインクルードしてはいけないやつに似ていますが…。
解決方法
片方のスクリプトから型情報を消します。
class_name ScriptB func _ready(): var a = get_node("/Root/ScriptA") a.method_a() func method_b(): print("method_b")
えって思うかもしれませんが、GDScriptは基本的に動的型付け言語なので問題は無いです。
このようにすると型の解決がパース時ではなく実行時になるため、スクリプトのロード順の問題は起きません。
型情報を失うと型チェックやインテリセンスが使えなくなるのでちょっと…ってなるのですが、現状ではこうするしかないようです。
C/C++では前方宣言で解決できるのですが、GDScriptにそのような仕組みは無いようですね…
Godot4では解決?
Godot4ではその辺考えなくてもよくなるとの噂を聞いたのですが、果たしてどうなるのでしょうか 今後にGodot Engineに期待しましょう。
【Godot】TileMapのコリジョンからタイル情報を検出する
やりたいこと
GodotのTileMapで横スクロールな2Dステージを作ったとします。
そこでキャラが乗っているタイルの情報が欲しくなりました。
ダメージ床だったらダメージ処理したり、衝突したタイルを破壊したりできます。
タイル情報を検出する方法
TileSetの設定
まずTileSetを設定します。
画像から領域を切って、コリジョンを設定します。
タイルでステージを作る
TileSetからタイルを選んでTileMapに配置します。
今回は横長タイルにしました。
キャラの作成
キャラにはKinematicBody2Dを設定します。
今回は2Dユニティちゃんをお借りしました。🙏🙏🙏🙏🙏🙏🙏
移動処理を書きます。詳細な処理の記載は省いています。
func _physics_process(delta: float): # 落下処理 velocity.y += GRAVITY * delta velocity.y = min(velocity.y, MAX_FALLING_SPEED) # 移動処理 velocity = move_and_slide_with_snap(velocity, Vector2(0, 4), Vector2.UP, true, 4, deg2rad(48))
コリジョンからタイル情報を検出する
ここからが本題です。
move_and_slide_with_snap
で移動している場合、get_slide_collision
で KinematicCollision2D が得られます。
KinematicCollision2Dの各プロパティはタイルマップの場合、以下の通りになります
collider
: CollisionObject2Dcollider_shape
: TileMapcollider_shape_index
: シェイプ番号 (タイルごとのシェイプ)
collider_shape_index
が重要です。乗っているタイルによってここの値が変化することが確認できます。
CollisionObject2Dに登録されているコリジョンシェイプの番号になるので、TIleIDにどうにかして変換する必要があります。
いろいろ調べて、以下の方法で上手くいきました。
func _process(delta: float): var collision := get_slide_collision(0) if collision: # TileMapに衝突しているかどうか var tilemap := collision.collider_shape as TileMap if tilemap: var tileset := tilemap.tile_set # collider_shape_indexを使ってメタデータ(タイル位置)を取得 var tile_pos = Physics2DServer.body_get_shape_metadata( collision.collider.get_rid(), collision.collider_shape_index) # タイル位置を使ってTIleMapからタイルIDを取得 var tile_id := tilemap.get_cellv(tile_pos) # タイルIDを表示 print(tileset.tile_get_name(tile_id));
実行画面
出力ログ
--- Debugging process started --- Godot Engine v3.2.2.stable.official - https://godotengine.org OpenGL ES 3.0 Renderer: Intel(R) Iris(R) Plus Graphics 基本チップ ハーフチップ 右上がり斜面 左上がり斜面 右上がり斜面
まとめ
CollisionObject2Dにメタデータという形で記録されたタイル位置情報を collider_shape_index
を使ってタイル情報を得る方法の紹介でした。
おまけ(調べた方法)
TileMapのソースコードを読みました。
Godotはエンジンのお気持ちが知りたくなったら、ソースコードを見に行けばいいのでお手軽ですね。
タイルのコリジョンシェイプをbody(CollisionObject2D)へ登録している箇所があります。
_add_shape(shape_idx, q, shape, shapes[j], xform, Vector2(E->key().x, E->key().y));
シェイプ情報以外にもタイルの位置情報をメタデータとして登録しています。
void TileMap::_add_shape(int &shape_idx, const Quadrant &p_q, const Ref<Shape2D> &p_shape, const TileSet::ShapeData &p_shape_data, const Transform2D &p_xform, const Vector2 &p_metadata) { PhysicsServer2D *ps = PhysicsServer2D::get_singleton(); if (!use_parent) { ps->body_add_shape(p_q.body, p_shape->get_rid(), p_xform); ps->body_set_shape_metadata(p_q.body, shape_idx, p_metadata);
どうにかしてこのメタデータを取り出せばタイル位置が判明します。
Physics2DServerはGDScriptからも叩けるので、Physics2DServer.body_get_shape_metadataでメタデータを取ることができることが分かりました。
ライセンス表記
この記事はユニティちゃんライセンス条項の元に提供されています
【Godot】Visual StudioでC++のプラグインを作る
Godot EngineはGDScript以外にもC++で開発することができます。
仕組みをGDNativeといい、Godotのエンジン機能にアクセスすることができます。
一応公式ではGDNativeのビルドツールにSConsを推奨していますが、Visual StudioでC++プラグイン開発したいと思う方は結構いるんじゃないでしょうか。
今回はVisual StudioとC++でGodotプラグインを開発する方法をまとめました。
ビルド環境
- Windows10 64bit
- Visual Studio 2019
- Godot Engine 3.2
準備
C++開発するために必要なgodot-cppを準備します。
godot-cppをcloneする
"3.2"は現在使用しているGodotバージョンと同じものを指定します。
git clone --recursive -b 3.2 https://github.com/GodotNativeTools/godot-cpp.git
godot-cppをビルドする
godot-cppのビルドにはSConsを使用します。
持っていない場合はPythonをインストールしてからpip install scons
でSConsをインストールします。
32/64bit版、Debug/Release版をそれぞれビルドします。
scons platform=windows bits=32 generate_bindings=yes target=debug -j4 scons platform=windows bits=32 generate_bindings=yes target=release -j4 scons platform=windows bits=64 generate_bindings=yes target=debug -j4 scons platform=windows bits=64 generate_bindings=yes target=release -j4
Visual Studioでプラグインを作成する
プロジェクト作成
Win32 C++ DLLプロジェクトを作成します。
プロジェクト設定
以下のgodot-cppのヘッダーパスをインクルードディレクトリに追加します。
- $(ProjectDir)godot-cpp\godot_headers
- $(ProjectDir)godot-cpp\include
- $(ProjectDir)godot-cpp\include\core
- $(ProjectDir)godot-cpp\include\gen
※今回はプロジェクトディレクトリ内にgodot-cppを配置しています。別の場所に配置した場合は適宜変更してください。
さらに以下のgodot-cppのライブラリをビルド構成それぞれに指定します。
- [Win32/Debug] $(ProjectDir)godot-cpp\bin\libgodot-cpp.windows.debug.32.lib
- [Win32/Release] $(ProjectDir)godot-cpp\bin\libgodot-cpp.windows.release.32.lib
- [x64/Debug] $(ProjectDir)godot-cpp\bin\libgodot-cpp.windows.debug.64.lib
- [x64/Release] $(ProjectDir)godot-cpp\bin\libgodot-cpp.windows.release.64.lib
プラグインソースの追加
公式のチュートリアルにならってSpriteを拡張したクラスを作成します。
https://docs.godotengine.org/ja/latest/tutorials/plugins/gdnative/gdnative-cpp-example.html
#pragma once #include <Godot.hpp> #include <Sprite.hpp> class MySprite : public godot::Sprite { GODOT_CLASS(MySprite, godot::Sprite) private: float time_passed; public: static void _register_methods(); MySprite(); ~MySprite(); void _init(); void _process(float delta); };
#include "MySprite.h" using namespace godot; void MySprite::_register_methods() { register_method("_process", &MySprite::_process); } MySprite::MySprite() { } MySprite::~MySprite() { } void MySprite::_init() { time_passed = 0.0; } void MySprite::_process(float delta) { time_passed += delta; Vector2 new_position = Vector2(10.0 + (10.0 * sin(time_passed * 2.0)), 10.0 + (10.0 * cos(time_passed * 1.5))); set_position(new_position); }
次にDLLからエクスポートされるC関数を追加します。
#include "MySprite.h" extern "C" void GDN_EXPORT godot_gdnative_init(godot_gdnative_init_options *o) { godot::Godot::gdnative_init(o); } extern "C" void GDN_EXPORT godot_gdnative_terminate(godot_gdnative_terminate_options *o) { godot::Godot::gdnative_terminate(o); } extern "C" void GDN_EXPORT godot_nativescript_init(void *handle) { godot::Godot::nativescript_init(handle); godot::register_class<MySprite>(); }
プラグインのビルド
エラーが出なければ成功です。
Godotプロジェクトにプラグインを登録する
DLLをGodotプロジェクトの以下に配置します。
今回は開発中なのでデバッグ版DLLを配置します。(最終的にはRelease版DLLを配置します)
- myplugin/win32/libmyplugin.dll
- myplugin/win64/libmyplugin.dll
次にプラグインの定義ファイルを追加します。(新規リソース追加)
- myplugin/myplugin.gdnlib
myplugin.gdnlibにDLLを設定します。(今回はWindowsのみ)
プラグイン内のクラスのNativeScriptを追加します(新規スクリプト追加)
- MySprite.gdns
Libraryにmyplugin.gdnlibを設定します。
スプライトにMySpriteを設定して実行。
うまく動いているようです。
Visual Studioでプラグインをデバッグする
デバッグ版DLLをGodotプロジェクト内に配置します。
Godotで実行中のプロセスにアタッチする方法を行います。
Visual Studioメニューのデバッグ→プロセスにアタッチを選択します。
プロセスにアタッチが成功すると、プラグインソース内でブレークできました。
これでプラグイン開発が捗りますね!
サンプル置き場
https://github.com/ueshita/gdnative-vsproj-example
おわりに
本記事はWindowsで効率よくGodot向けのプラグインを開発したい方向けです。
しかし実際のところ、Macやモバイルといったマルチプラットフォーム向けには、結局SConsでビルドできるようにする必要があります。(MacはMacでXCodeでビルドできるようにすると多少捗るかもしれませんが…)
【Godot】Node2DのGizmoを描画する
はじめに
Node2DにGizmoを描画したくて色々ハマったので情報をまとめます。
動作環境
Godot Engine 3.2.1
メモ
SpatialGizmoは2Dでは動かない
SpatialGizmo https://docs.godotengine.org/ja/stable/tutorials/plugins/editor/spatial_gizmos.html
最初これかなと思って試したら、動かなくてウーンウーン悩んでました。 Spatialは3DのNodeなのでNode2Dで動かないのは当然でした。ちなみにGodot4からはSpatialはNode3Dに改名するみたいです。
2D版のGizmoは無かった
EditorPlugin https://docs.godotengine.org/en/stable/classes/class_editorplugin.html
EditorPluginにadd_spatial_gizmo_pluginは存在するが、add_node2d_gizmo_pluginは無さそうなので、2D版のGizmoは無いみたいです。
解決: _draw
で描画する
他のCamera2Dといった2DノードはどうやってEditor画面に描画しているのか気になったのでGitHubのソースを眺めてみました。
https://github.com/godotengine/godot/blob/master/scene/2d/camera_2d.cpp
どうも_notification
のcase NOTIFICATION_DRAW で描画しているみたいです。
エンジンの真のお気持ちはソースコードに書いてあります。
GDScriptでは先頭にtool
を追加して_draw
で描画すればよさそうです。
Node2Dの周りに四角形を描きたいときは以下のコードになります。
tool extends Node2D export var left := -100 export var top := -100 export var right := 100 export var bottom := 100 func _process(delta): if Engine.editor_hint: update() func _draw(): if Engine.editor_hint: var color := Color(1.0, 0.0, 0.0) draw_line(Vector2(left, top), Vector2(right, top), color) draw_line(Vector2(right, top), Vector2(right, bottom), color) draw_line(Vector2(right, bottom), Vector2(left, bottom), color) draw_line(Vector2(left, bottom), Vector2(left, top), color)
EditorでGizmo描画できました。
注意点
_process
は実行時にも走るので、Engine.editor_hint
でEditor上でのみ実行されるように制御する。_process
でupdate()
を呼び出す方式は、毎フレーム描画処理が行われてしまうので、Gizmo描画に影響あるパラメータをプロパティにしてその中でupdate()
したほうがお行儀がよさそう。