Open27

Godot4 Tipsいろいろ適宜更新

ふぉるたふぉるた

スクリプトエディタにCtrl+ドラッグドロップすると変数がさくっとできる

さらに!
スクリプトからシーンツリー内のノードを参照するのは%アクセスが便利!

  • あとからツリー内の位置を変えてもコードは%NodeNameのままなのでそのままでよい!
  • $はもう使わなくてよいかも、それぐらい%は便利


    ノード表示の%がつく。
ふぉるたふぉるた

get_tree().create_timer()はなにがどこにできるのか理解して使う必要ありっ

get_tree().create_timer()で生成されるのはSceneTreeTimerなので
timeoutに消えるかもしれないノードにconnectするとエラーが発生する

  1. get_tree().create_timer()のtimeoutに、ノードの関数を設定する
  2. SceneTreeTimerが作成され、タイマーカウントがはじまる。SceneTreeTimerはシーンツリーにひもづく。
  3. 設定した関数のノードが消える。SceneTreeTimerはシーンツリーにひもづいているので消えない。
  4. タイマーが終了し、timeoutに設定した関数を呼ぶぞ!ないぞ!エラー

解決策
消えるノードの、タイマーNodeを子ノードにしてそれを使う

  1. タイマーNodeのtimeoutに、ノードの関数を設定する
  2. タイマーカウントをはじめる。
  3. 設定した関数のノードが消える。タイマーNodeも子ノードなので一緒に消える
  4. タイマーはもうないのでタイマーが終了しない
ふぉるたふぉるた

プロジェクト設定のAdvancedSettingは、まったくAdvancedではない基本設定が埋まりまくってるので常時表示がよいです。

ふぉるたふぉるた

Godotプロジェクトが大きくなっていってエディタ動作やゲーム起動後のデバッガ接続が遅くなっていったときは.godotのeditorとimportedディレクトリを消すといい感じになります
もちろん再起動するとアドオンとかクラス再読み込みの関係でエラー出ますが3回ぐらい再起動すると再読み込みされて快適に!

ふぉるたふぉるた

UIとStateChartsの相性めちゃくちゃいいのでおすすめ
コードから状態が消えるのでシンプルになって良い
ついでにデバッガもついてて遷移も分かりやすい

アクションゲームの敵キャラのAIもStateChartsが各状態のprocessで処理を分けれるので相性悪くない。
さらにアクションだとBehaviorTreeを組み合わせるとうまくまとめられそう。
ターン制はBehaviorTreeを使った方が良い感じ。BeeTreeとか。

ふぉるたふぉるた

Godot4のオートタイル作成スクリプトを作りました。
RPGツクールMVMZ,VXのタイルセット画像をGodot4のタイルセットと、展開した画像に変換します。
ディレクトリを指定して、実行するだけなので楽です。

Godotのタイルはいろいろできるんだけど複雑かつ手数が多いので
ツクール移行者がてこずってるイメージ。

Godot4のオートタイルは3よりはやりやすくなったんだけど大量にやるのは相変わらずたいへん。

https://github.com/folt-a/godot4-rpgmaker-tile-converter

ふぉるたふぉるた

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)

https://docs.godotengine.org/en/stable/tutorials/io/runtime_file_loading_and_saving.html#d-scenes

ふぉるたふぉるた

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に変更しました。ヨシ

ふぉるたふぉるた

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はmeshtextureの2つプロパティがあり、

MeshInstance3DのようにArrayMeshを登録してそれに対応するUVを登録すれば期待通りの動きをする。

MeshInstance2Dの使い方について、公式ドキュメントでは

「Sprite2Dを配置して、MeshInstance2Dに変換する」しか記載されていない。

https://docs.godotengine.org/en/stable/tutorials/2d/2d_meshes.html

確かに、このやり方をすると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メッシュでのやり方と同様に、
SurfaceToolMeshDataToolで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
手動で開放する必要がある