Godot4 Tipsいろいろ適宜更新
このスクラップではGodot4のTipsを書いていきます。
C#関連はこっちに書きます。
スクリプトエディタにCtrl+ドラッグドロップすると変数がさくっとできる
さらに!
スクリプトからシーンツリー内のノードを参照するのは%アクセスが便利!
- あとからツリー内の位置を変えてもコードは%NodeNameのままなのでそのままでよい!
- $はもう使わなくてよいかも、それぐらい%は便利
ノード表示の%がつく。
get_tree().create_timer()はなにがどこにできるのか理解して使う必要ありっ
get_tree().create_timer()で生成されるのはSceneTreeTimerなので
timeoutに消えるかもしれないノードにconnectするとエラーが発生する
- get_tree().create_timer()のtimeoutに、ノードの関数を設定する
- SceneTreeTimerが作成され、タイマーカウントがはじまる。SceneTreeTimerはシーンツリーにひもづく。
- 設定した関数のノードが消える。SceneTreeTimerはシーンツリーにひもづいているので消えない。
- タイマーが終了し、timeoutに設定した関数を呼ぶぞ!ないぞ!エラー
解決策
消えるノードの、タイマーNodeを子ノードにしてそれを使う
- タイマーNodeのtimeoutに、ノードの関数を設定する
- タイマーカウントをはじめる。
- 設定した関数のノードが消える。タイマーNodeも子ノードなので一緒に消える
- タイマーはもうないのでタイマーが終了しない
exportするプロパティは@アノテーションが便利
@export_category
@export_group
まとめて見やすく!
こんな感じの、組み込みノードの折り畳みやグループが自分でもできますよ!
プロジェクト設定のAdvancedSettingは、まったくAdvancedではない基本設定が埋まりまくってるので常時表示がよいです。
てきとうにツイートしたものから載せていこうの回
Godotプロジェクトが大きくなっていってエディタ動作やゲーム起動後のデバッガ接続が遅くなっていったときは.godotのeditorとimportedディレクトリを消すといい感じになります
もちろん再起動するとアドオンとかクラス再読み込みの関係でエラー出ますが3回ぐらい再起動すると再読み込みされて快適に!
UIとStateChartsの相性めちゃくちゃいいのでおすすめ
コードから状態が消えるのでシンプルになって良い
ついでにデバッガもついてて遷移も分かりやすい
アクションゲームの敵キャラのAIもStateChartsが各状態のprocessで処理を分けれるので相性悪くない。
さらにアクションだとBehaviorTreeを組み合わせるとうまくまとめられそう。
ターン制はBehaviorTreeを使った方が良い感じ。BeeTreeとか。
Godot4のオートタイル作成スクリプトを作りました。
RPGツクールMVMZ,VXのタイルセット画像をGodot4のタイルセットと、展開した画像に変換します。
ディレクトリを指定して、実行するだけなので楽です。
Godotのタイルはいろいろできるんだけど複雑かつ手数が多いので
ツクール移行者がてこずってるイメージ。
Godot4のオートタイルは3よりはやりやすくなったんだけど大量にやるのは相変わらずたいへん。
GLTFDocumentとGLTFStateを使えば3Dモデルのインポートエクスポートがかんたんに!
エクスポートしたゲームでも3Dモデルをロードできるんですねえ。MODもいける?
とりあえずアドオンでは大活躍です。
# Save a new glTF scene.
var gltf_document_save := GLTFDocument.new()
var gltf_state_save := GLTFState.new()
gltf_document_save.append_from_scene(gltf_scene_root_node, gltf_state_save)
# The file extension in the output `path` (`.gltf` or `.glb`) determines
# whether the output uses text or binary format.
# `GLTFDocument.generate_buffer()` is also available for saving to memory.
gltf_document_save.write_to_filesystem(gltf_state_save, path)
1スクリプトの処理をすぐ実行できるEditorScriptはとても便利。
アドオンにしてGUIをつくるまでもないバッチ処理はEditorScriptで決まり!
注意点
EditorScriptはRefCountedのみ継承で参照がないと即開放されてしまうのでawaitで待つなどができない
なのでEditorScriptの_run内でawaitを使いたいときはreference()とunreference()を使おう
Imageで作ってsave_pngなどで保存したファイルは保存時点ではインポートされていないので
後の処理でそのImageをリソースとして使うときはEditorInterface.get_resource_filesystem().scan()でスキャンさせてawait EditorInterface.get_resource_filesystem().filesystem_changedで待とう
Godot4だとブレークポイントに来た時にゲームのウィンドウがドラッグで位置変えれない問題があって、
デバッグ時に覆いかぶさって邪魔すぎます。
「エディタ設定」のウィンドウ位置を変えておくと、ひとまず邪魔にはならない!
ゲームのウィンドウ位置設定が反映されず真ん中に出る問題は
エディタだとプロジェクト設定よりエディタ設定のほうが優先されるので注意!
プロジェクト設定のウィンドウ変えてるのに反映されなーい!ってなっていいた…わかりにくくない?
Blender→Godotのアニメインポートで
SpineのEventみたいにアニメーションの任意タイミングでパラメータ拾いたいな~って思ってたけど
外部のJSONファイルに書いて、Godotのカスタムインポーターでインポート時に読み込んでAnimationに関数呼び出し追加するのが良さそうな気がする、やってみよ
Input.get_vectorを使ってたのですが
コントローラーをつないだ状態でキーボードのキー移動をすると
設定したデッドゾーンを無視してコントローラーの微妙な傾きが加えられてしまうので
Input.get_action_strengthに変更しました。ヨシ
PhantomCameraでマップ移動の際に、テレポートしたいがカメラが泳いでしまう問題
こんな感じで防げるもよう
カメラテレポートって必須な気がするんだけど意外とそうでもないんかな?
Input.get_vectorを使ってたのですが
コントローラーをつないだ状態でキーボードのキー移動をすると
設定したデッドゾーンを無視してコントローラーの微妙な傾きが加えられてしまうので
Input.get_action_strengthに変更しました。ヨシ
Godot4.2からスクリプトエディタでregion機能が追加されていますね。
regionはアンチパターンにもなりうる機能ですが
欲しくなるときもある……かも?
とか言っていましたが
regionはコードに載せたものの機能ぜんぜん使ってませんね・・・
Godotのカスタムインポーターはいろいろ便利
インポーターの処理内でGodotのシーンを好きに組み立てて3Dシーンとして読み込む。
名前のプレフィックスやサフィックス、モデルファイル保存先のディレクトリパス、ファイル名などから判定して
シェーダーやナビゲーション、割り当てるメッシュを変えたり、
アニメーションのトリムをしたり、アニメーションにフレームを追加したりと
痒い所に手が届かなければ自分で痒い所をポリポリかくことができるぞ
3DモデルのアニメーションはGLTFを分けて作成したほうが楽で良さそう。
BlenderのGLTFエクスポートで毎回アニメーションをベイク後出力するのは時間がかかるので……
また、汎用キャラクターは同じアニメーションを使うので、最初からアニメとメッシュのGLTFを分けるようにしておく。
ループ付きのアニメーションの終了や、AnimationTreeのtravelの途中アニメーションなど終了・開始タイミングが簡単にとれないものがあります。
@toolでAnimationのデータを加工して、任意のタイミングでSignalをemitする関数トラックを作ることで解決しました。
具体的なやり方
3Dならモデルのインポーター内でやると簡単にAnimationLibraryの加工ができます。
2DだとEditorScriptを使って仮のAnimationPlayerを作って外部保存したAnimationLibraryリソースを加工するバッチ処理を作ると良さそうです。
他にも同様のやり方で好きなタイミングデータを拾うことができます。
3Dアニメの途中で音やエフェクトを再生したいなど
別途アニメーションとタイミング、引数の情報を格納したJSONを用意して、同じように関数Trackを差し込めば直接関数に処理を書くなり引数付きSignalを発行して処理するなり、いろいろな応用ができそうな感じです。
GodotはインポーターやEditorScriptのような@toolをうまく使うと効率よく制作を進められますね。
MeshInstance2Dのスクリプトからの作り方
MeshInstance2Dはmesh
とtexture
の2つプロパティがあり、
MeshInstance3DのようにArrayMeshを登録してそれに対応するUVを登録すれば期待通りの動きをする。
MeshInstance2Dの使い方について、公式ドキュメントでは
「Sprite2Dを配置して、MeshInstance2Dに変換する」しか記載されていない。
確かに、このやり方をするとmeshとtextureに正しく設定されるのだが、
単純に表示させるのみで頂点の細かい制御やUVの割り当てはできない。
とにかく正しく設定されるmeshのデータは作成できるため、これを参考にスクリプトから作ってみた。
三角ポリゴン2枚の正方形で作成するとこんな感じのデータができた。
(.tscnに保存してテキストで確認)
頂点は4隅の4、index数は三角ポリゴン2枚の6なのでindexが作られていることがわかる。
"2d": true,
のデータがあり、vertex_data
の数もVector3ではなくVector2になっているような気がする。
material
は存在しない。
[sub_resource type="ArrayMesh" id="ArrayMesh_g4yoq"]
_surfaces = [{
"2d": true,
"aabb": AABB(-16, -16, 0, 32, 32, 0),
"attribute_data": PackedByteArray(0, 0, 128, 63, 0, 0, 128, 63, 0, 0, 0, 0, 0, 0, 128, 63, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 63, 0, 0, 0, 0),
"format": 34393296913,
"index_count": 6,
"index_data": PackedByteArray(3, 0, 0, 0, 1, 0, 1, 0, 2, 0, 3, 0),
"primitive": 3,
"uv_scale": Vector4(0, 0, 0, 0),
"vertex_count": 4,
"vertex_data": PackedByteArray(0, 0, 128, 65, 0, 0, 128, 65, 0, 0, 128, 193, 0, 0, 128, 65, 0, 0, 128, 193, 0, 0, 128, 193, 0, 0, 128, 65, 0, 0, 128, 193)
}]
失敗
これを確認して、
3Dメッシュでのやり方と同様に、
SurfaceTool
やMeshDataTool
でZを0にしたArrayMeshを生成して割り当ててみたが、表示されない。
MeshInstance2Dの公式ドキュメントに
「 You can experiment creating them yourself using SurfaceTool from code and displaying them in a MeshInstance2D node.」
と記載はあるが、SurfaceTool
を使ってもうまく生成できなかった。
SurfaceTool
、Vector3にしか対応していないのでは……?
ArrayMeshのformatを2Dに設定しないといけないようだが、SurfaceTool
だけではおそらく不可能。
MeshDataTool
でもformatは設定できなさそう。
成功
ArrayMeshの関数add_surface_from_arrays
を使ってArrayMeshを作る。
頂点はVector2の配列を使用して作成する。大きさはピクセル
var arr = []
arr.resize(Mesh.ARRAY_MAX)
var verts = PackedVector2Array([
Vector2(0,0),
Vector2(32, 0),
Vector2(0, 32),
Vector2(32, 32),
Vector2(0, 32)
])
var indices = PackedInt32Array([0, 1, 2, 4, 1, 3])
var uvs = PackedVector2Array([
Vector2(0.0,0.0),
Vector2(1.0, 0.0),
Vector2(0.0,1.0),
Vector2(1.0, 1.0),
Vector2(0.0, 1.0)
])
arr[Mesh.ARRAY_VERTEX] = verts
arr[Mesh.ARRAY_TEX_UV] = uvs
arr[Mesh.ARRAY_COLOR] = colors
var ary_mesh = ArrayMesh.new()
ary_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arr,[],{}, Mesh.ARRAY_FLAG_USE_2D_VERTICES)
materialが存在せず、
テクスチャはMeshInstance2Dに1枚だけ指定する。
そのため、3Dメッシュの複数Surfaceのように複数のテクスチャを割り当てることはできなさそう。
テクスチャは1枚にまとめる必要がある。
これで大きなイベントCGをメッシュにして、透明部分の描画をしないようにしたり、
差分の一部分だけのメッシュを切り替えることでいい感じにできそう。
できてよかった~。
Godot4でのゲーム内状態のデバッグ監視はWindowノードで作って別ウィンドウがいいかも
デバッガーやprint、画面文字表示よりも表示対象を好きに選べて分離しやすいのが◎
別窓でも同ノードツリーなので全情報にアクセスできるし外すときも楽。
デバッグ用Windowのスクリプトの_readyで、
get_tree()してメインツリーのnode_addedシグナルとかに関数をconnectしておく。
関数では追加されたノードが監視したいものかどうかチェックして、
監視対象ならプロパティにセットして、それをprocessで毎フレームチェックする。
StateChartsのツリー作成をちょっとだけ楽にするTips
ただしアドオンコードに追記するので、アドオンを更新すると消えます。
res://addons/godot_state_charts/transition.gd
to プロパティのsetterに、エディタ内のみ実行するコードを追加します。
## The target state to which the transition should switch
@export_node_path("StateChartState") var to:NodePath:
set(value):
to = value
update_configuration_warnings()
if Engine.is_editor_hint() and self.event == "":
var tonode:Node = get_node_or_null(to)
if tonode:
self.event = tonode.name
self.name = "To" + tonode.name
インスペクターでToにStateを設定したときに、
Transitionの名前とEventを埋めます。
send_event(&"イベント名")のタイプミスを減らすために、コピペして定数にします。
res://addons/godot_state_charts/state_chart.gd
プロパティと関数を追加します。
@export var exec_copy_event_names:bool:
set(v):
exec_copy_event_names = false
var s:String = ""
var names:Array[String] = []
for n in get_all_children(self):
if n is Transition\
and not names.has(n.event.to_snake_case().to_upper()):
s += "const EVENT_" + n.event.to_snake_case().to_upper() + ':StringName = &"' + n.event + '"\n'
names.append(n.event.to_snake_case().to_upper())
DisplayServer.clipboard_set(s)
func get_all_children(in_node:Node) -> Array[Node]:
var children = in_node.get_children()
var ary:Array[Node] = []
while not children.is_empty():
var node = children.pop_back()
children.append_array(node.get_children())
ary.append(node)
ary.reverse()
return ary
インスペクターのbool値をボタン代わりにします。
押すとすべての子のTransitionのイベント名を定数としてクリップボードにコピーするので
const EVENT_EVENT_NAME_1:StringName = &"EventName1"
const EVENT_EVENT_NAME_2:StringName = &"EventName2"
send_eventを使うスクリプトに貼って
sendevent(EVENT_EVENT_NAME_1)のように使ってタイプミスしないようになります。
エディタアドオンでの多言語はいろいろめんどくさいので
CSVとかTranslationServer
よりは単純にこんなんでよいですね。
エディタアドオンでコンソールにだすときはprint_rich
で色をつけたりアドオン名をいれてあげると
「このログなにから出てるの?」って思われることが減ってよいかも。
var is_ja:bool = EditorInterface.get_editor_settings()\
.get_setting("interface/editor/editor_language")\
.contains("ja")
var is_ko:bool = EditorInterface.get_editor_settings()\
.get_setting("interface/editor/editor_language")\
.contains("ko")
if is_ja:
print_rich("[godot-node-live-debugger][color=LIME_GREEN][b]全てのスクリプト情報を更新しました。対象スクリプトgdの数="+str(script_file_count)+", 対象プロパティの数=" + str(props_count) + ", 対象関数の数=" + str(funcs_count) + "[/b][/color]")
elif is_ko:
print_rich("[godot-node-live-debugger][color=LIME_GREEN][b]모든 스크립트 정보를 업데이트했습니다. 대상 스크립트 gd 수="+str(script_file_count)+", 대상 프로퍼티 수=" + str(props_count) + ", 대상 함수 수=" + str(funcs_count) + "[/b][/color]")
else:
print_rich("[godot-node-live-debugger][color=LIME_GREEN][b]All scripts Informations updated. Debug script gd count="+str(script_file_count)+", Debug property count=" + str(props_count) + ", Debug function count=" + str(funcs_count) + "[/b][/color]")
CanvasGroupのシェーダーだとTEXTUREとUVが使えないので変形のシェーダーがむずかしすぎるので
1枚のTextureに描画してSprite2Dにセットしてそこでシェーダー当てたら良さそうとなりました
1フレーム待つ必要はあるけど、描画したものをテクスチャにまとめることができたので解決
カードゲーム制作中に、カード内に文字や画像切り替えがいっぱいあるので
カード1枚全体に対してシェーダーかけたいなあという動機です。
class_name RenderStatic
extends Object
## キャンバスアイテムをTextureに変換する
static func convert_canvas_item_to_texture(canvas_item:CanvasItem, size:Vector2i, offset:Vector2) -> ImageTexture:
# メインゲームのViewportからcanvas_itemが消えるので、その間メインVPは描画を更新しないようにする。終わったら戻す。
var main_vp_id:RID= canvas_item.get_viewport().get_viewport_rid()
var main_vp_update_mode: = RenderingServer.viewport_get_update_mode(main_vp_id)
RenderingServer.viewport_set_update_mode(main_vp_id,RenderingServer.VIEWPORT_UPDATE_DISABLED)
var org_global_position:Vector2 = canvas_item.global_position
var tmp_viewport_rid:RID = RenderingServer.viewport_create()
RenderingServer.viewport_set_size(tmp_viewport_rid, size.x, size.y)
RenderingServer.viewport_set_update_mode(tmp_viewport_rid,RenderingServer.VIEWPORT_UPDATE_ONCE)
var tmp_canvas_rid:RID = RenderingServer.canvas_create()
var canvasitem_rid:RID = canvas_item.get_canvas_item()
RenderingServer.canvas_item_set_parent(canvasitem_rid,tmp_canvas_rid)
RenderingServer.canvas_item_set_transform(canvasitem_rid, Transform2D(0.0,offset))
RenderingServer.viewport_attach_canvas(tmp_viewport_rid,tmp_canvas_rid)
RenderingServer.viewport_set_active(tmp_viewport_rid, true)
RenderingServer.force_draw()
var tmp_tex_rid:RID = RenderingServer.viewport_get_texture(tmp_viewport_rid)
var image:Image = RenderingServer.texture_2d_get(tmp_tex_rid)
var texture = ImageTexture.create_from_image(image)
RenderingServer.free_rid(tmp_viewport_rid)
RenderingServer.free_rid(tmp_canvas_rid)
RenderingServer.free_rid(tmp_tex_rid)
# 元のキャンバス、Viewportに戻す
RenderingServer.canvas_item_set_parent(canvasitem_rid,canvas_item.get_canvas())
RenderingServer.canvas_item_set_transform(canvasitem_rid, Transform2D(0.0,org_global_position))
# 終わったのでメインゲームのViewportの更新を元に戻す。
RenderingServer.viewport_set_update_mode(main_vp_id, main_vp_update_mode)
return texture
使う方
@onready var canvas_group: CanvasGroup = %CanvasGroup
@onready var canvas_group_copy: Sprite2D = %CanvasGroupCopy
func _ready():
var tex:= await RenderStatic.convert_canvas_item_to_texture(canvas_group, ParamsConst.CARD_SIZE, ParamsConst.CARD_HALF_SIZE)
canvas_group_copy.texture = tex
canvas_group.visible = false
canvas_group_copy.visible = true
シーンをinstantiate()で作成して追加するときに
シーンのルートノードのスクリプト内で、シーン内の子ノードとかの初期化処理をしたいとき、
instantiate()だと_initはつかえないし、_readyは引数を渡せない。
なのでinit_scene()みたいな関数を作っておいて呼びます。
const XX_PACKED_SCENE:PackedScene = preload("res://xxx.tscn")
func _on_pressed_xxx():
var new_node:Node = XX_PACKED_SCENE.instantiate()
new_node.init_scene("abcde")
ですが、ここではまだ作成したシーンのreadyはまだ実行されていないので、@onreadyのプロパティはnullになっています。
が ここで%アクセスや$アクセスなどのget_nodeはできます。
extends Node
@onready child_node:Label = %ChildNode
func init_scene(arg:String) -> void:
child_node.text = arg #child_nodeはまだnullなのでエラーになる
こうする
extends Node
@onready child_node:Label
func init_scene(arg:String) -> void:
child_node = %ChildNode
child_node.text = arg
シーンツリーに追加する必要がなく、ノードにしたいカスタムクラスは何をextendsする?
→RefCountedかResourceが良さそう。
雑な解説
Resource extends RefCounted
ResourceSaveやstr_to_varで保存、読み込みができる
@exportにはResourceを継承したものや組み込み型にする必要がある
changed などsignalがある
duplicate()がある。duplicate()でコピーさせたいプロパティは@export
をつける必要がある
RefCounted extends Object
参照カウントをもっており、参照されると自動的に増え、参照がなくなると自動的に減る。
0になるとなにもしなくてもメモリから開放される。
reference(), unreference() で手動で増やすこともできる。
RefCountedで、かつ保存もしたいならRefSerializerアドオンが便利かも。
Object
手動で開放する必要がある