見出し画像

Godot Engineでつまづいたところ集 2

Godotエンジンを使ってて、わからなかったところや、気づきがあったところをリストしています。
前記事が長くなりすぎたので分けます。

最終更新: 2024/02/01


ルートノードはグローバル関数から取得できない(※できる)
static関数からシーンツリーやルートノードを取る手段が無い。
get_tree関数はNodeの非static関数であり、しかもおそらくツリーに追加されていない状態では使えない。
グローバル関数的にアクセスできなくすることで、堅牢性を高める意図があるのだろう。

Godot 4.1で追加されたstatic変数を使って、AutoLoadクラスをC#風のシングルトン実装に改めようとしたのだが、上記の理由で、簡単ではないようだ。Nodeは自力で自分をツリーに配置できないから。

# static変数にインスタンスを保持すればAutoLoad要らない!?

class_name MySingleton
extends Node

static var _inst: MySingleton = null

static func _secure_inst() -> void:
  if _inst == null:
    _inst = MySingleton.new()
    var root: Node = something() # これができない
    root.add_child(_inst)

static func do_work() -> void:
  _secure_inst()
  _inst.work()

なので、なんらかの親ノードを渡す初期化関数を呼び出す必要がある。

# secure関数をpublic化して外部から呼び出す
# parent_nodeは別にルートノードでなくてもいい

static func secure_inst(parent_node: Node) -> void:
  if _inst == null
    _inst = MySingleton.new()
    parent_node.add_child(_inst)

static func do_work() -> void:
  _inst.work()


2023/10/10
コメントでご助言いただきました。ありがとうございます!
Engine.get_main_loop()を使うことで、次のようにしてルートを取得することができるようです。
記事書いといてよかった~

# SceneTree.rootを取得してその子になる
static func _secure_inst() -> void:
  if _inst == null:
    _inst = MySingleton.new()
    var scene_tree: SceneTree = Engine.get_main_loop() as SceneTree
    var root: Window = scene_tree.root
    root.add_child(_inst)

ただしこの場合、シーン初期配置ノードの_readyで上記関数が呼ばれないように気を付ける。
ノードの_readyが終わるまでは、scene_tree.rootにadd_childはできない。

# call_deferredを使うことで、_readyから呼ばれても大丈夫
    root.add_child.call_deferred(_inst)

enum Keyを走査するには
例えばひとまず全キー(@GlobalScope.Key)の入力状態を知りたいときに、キーのリストを取得する手段が通常無い。クラス下の普通のenumと違って、グローバルenumであるKeyは
for key in Key.values():
のような書き方はなぜかできない。

enum Keyは、値の順に並べかえると次のようになっている。


# 無し
KEY_NONE = 0

# 主要な文字キー
KEY_SPACE = 32
~KEY_QUOTELEFT = 96

# {, |, }, ~ の4つ
KEY_BRACELEFT = 123
~KEY_ASCIITILDE = 126

# 円マーク
KEY_YEN = 165

# セクションマーク(§)
KEY_SECTION = 167

# 4194304=0x400000で、これでマスクされるキーは文字キーではなく表示不可
KEY_SPECIAL = 4194304

# 主要な非文字キー
KEY_ESCAPE = 4194305
~KEY_F35 = 4194366

# 00年代的な機能キー
KEY_MENU = 4194370
~KEY_LAUNCHF = 4194415

# MacやiPadの言語系キー(地球儀マークとか)
KEY_GLOBE = 4194416
~KEY_JIS_KANA = 4194419

# テンキー
KEY_KP_MULTIPLY = 4194433
~KEY_KP_9 = 4194447

KEY_UNKNOWN = 8388607

for文で走査するときはひとまずこの2つの組み合わせでループを回せば漏れはなさそう。
range(KEY_SPACE, KEY_SECTION + 1) = 32~167
range(KEY_ESCAPE, KEY_KP_9 + 1) = 4194305~4194447


Inputで取得できないキー
・半角/全角
・無変換
・変換
・カタカナ/ひらがな
・ノートPCの「Fn」キー
まだあるかもしれないけど、少なくともこの5つはInput. is_key_pressed関数では取得できないっぽい。
ただし、「半角全角」に関しては、is_physical_key_pressed関数ならばKEY_QUOTELEFT(96)として取得可能。


マルチパスレンダリングはnext_passを使う
複数のパスを持つシェーダーを実現したい場合、マテリアルのMaterial. next_passに2パス目の別のシェーダーのマテリアルをセットする。3パス目が欲しければ2パス目のマテリアルのnext_passに3パス目のシェーダーのマテリアルをセットする。数珠繋ぎ。
マテリアルはStandardMaterial3DかShaderMaterialでさえあればよく、混合して繋げても問題ないようだ。

ちなみに、法線方向に膨らませる方式の輪郭表現は、2パス目にGrowを設定したStandardMaterial3Dをセットするだけで実現できる。「たいていの表現はStandardMaterial3Dだけで済ませてみせるぞ」というGodotの意志を感じる。


SubViewportとレイヤーの関係
カメラは最も近い先祖のViewportに視点を提供する。逆を言うと、Viewportは、「直轄」のカメラを使って描画される。

ただ、描画される対象とその対象を照らすライトは、Viewportのドメインに縛られない。Unity同様、レイヤーで区別する必要がある。
・Camera3D .cull_mask
・VisualInstance3D .layers
・Light3D .light_cull_mask

SubViewportで描画対象が決まるわけではないのだが、つい勘違いしそうになるのは、SubViewport自体がレイヤー=世界を取りまとめる、入れ物のオブジェクトとしての使い方も期待されてる側面があるからだろう。
実際、SubViewport下はすべてレイヤーを統一する、というのがよくある使い方になると思う。


Environmentの適用優先順
Environmentの適用条件と、その優先順は、今のところ下記の理解。
1. カメラにセットされたEnvironment
2. ViewportにセットされたWorld3DのEnvironment
3. (Viewport外の)WorldEnvironmentノードにセットされたEnvironment
4. プロジェクト設定のDefault Environment

デバフとかアイテム(暗視ゴーグルとか)による一時的な変化はカメラ、そのマップにおける常態的なコンディションはWorldEnvironment、というのがいい感じの使い分けになると思う。

3でわざわざ「Viewport外」と注釈を付けたのは、SubViewportの中に入れるとSubViewportのEnvironmentが乗っ取られてしまうのだ。
どういうこっちゃ? 次の項目参照。


SubviewportのWorld3Dが上書きされる
エディタのGUI上の操作だけども、
・WorldEnvironmentノードをSubviewportの子にすると、
・子Subviewportに元々セットされていたWorld3DのEnvironmentが、
・親WorldEnvironmentノードと同一のEnvironmentに置き換わる。
(ややこしい)

これ自体が仕様なのかバグなのかわからないけど、仕様でも全然普通だと思う。

でも、そこからCtrl+ZでWorldEnvironmentの親子付けをアンドゥすると、SubviewportのEnvironmentが空になってしまう。元に戻らない。
これはさすがに、仕様ではなくない・・・?

なんか、これに限らず、SubviewportのWorld3D周りの挙動、ちょっと怪しい感じがする。


clampしたのに+=
ついついやっちゃうバグ
誤: count += clampi(count + sub_count, COUNT_MIN, COUNT_MAX)
正: count = clampi(count + sub_count, COUNT_MIN, COUNT_MAX)


intからStringのフォーマット変換
"%d" % countのような、書式指定を使った文字列への変換。
これ系はいつまで経っても慣れない。ドキュメント見ても毎回忘れる。

10進数(d)の場合はまだ割とわかりやすい。

          0     5    -5    12345   -12345
   %d:   <0>   <5>  <-5>  <12345> <-12345>
  %+d:  <+0>  <+5>  <-5> <+12345> <-12345>
  %3d: <  0> <  5> < -5>  <12345> <-12345>
 %+3d: < +0> < +5> < -5> <+12345> <-12345>
 %03d: <000> <005> <-05>  <12345> <-12345>
%+03d: <+00> <+05> <-05> <+12345> <-12345> 
 %-3d: <0  > <5  > <-5 >  <12345> <-12345> 
%-03d: <0  > <5  > <-5 >  <12345> <-12345>
  • "d"の前の数字が、変換後の文字列の最小幅。
    通常はスペースでパディングされる。
    "%03d"のように、最初に"0"を付けるとゼロでパディングされる。
    パディング前の幅は、符号があれば符号付きで評価される。

  • "+"を付けると、ゼロや正であっても符号が付く。

  • "-"を付けると、左詰めされる。スペースでパディングされる。
    (ゼロでパディングはされない。123→"123000"じゃ、わけわからないもんね。)


floatからStringのフォーマット変換
"%f" % ratioのような、書式指定を使った文字列への変換。小数の場合。

横に長~~~~~~~~~~~~~~~~~~~い
                      0.0              1.2             -1.2          0.12345         -0.12345          0.98765         -0.98765           1234.5           1234.5         0.000123        -0.000123    
      %f:       <0.000000>       <1.200000>      <-1.200000>       <0.123450>      <-0.123450>       <0.987650>      <-0.987650>    <1234.500000>   <-1234.500000>       <0.000123>      <-0.000123> 
     %+f:      <+0.000000>      <+1.200000>      <-1.200000>      <+0.123450>      <-0.123450>      <+0.987650>      <-0.987650>   <+1234.500000>   <-1234.500000>      <+0.000123>      <-0.000123> 
    %+3f:      <+0.000000>      <+1.200000>      <-1.200000>      <+0.123450>      <-0.123450>      <+0.987650>      <-0.987650>   <+1234.500000>   <-1234.500000>      <+0.000123>      <-0.000123> 
  %+0.3f:         <+0.000>         <+1.200>         <-1.200>         <+0.123>         <-0.123>         <+0.988>         <-0.988>      <+1234.500>      <-1234.500>         <+0.000>         <-0.000> 
  %+3.1f:           <+0.0>           <+1.2>           <-1.2>           <+0.1>           <-0.1>           <+1.0>           <-1.0>        <+1234.5>        <-1234.5>           <+0.0>           <-0.0> 
   %6.3f:         < 0.000>         < 1.200>         <-1.200>         < 0.123>         <-0.123>         < 0.988>         <-0.988>       <1234.500>      <-1234.500>         < 0.000>         <-0.000> 
  %+6.3f:         <+0.000>         <+1.200>         <-1.200>         <+0.123>         <-0.123>         <+0.988>         <-0.988>      <+1234.500>      <-1234.500>         <+0.000>         <-0.000> 
  %-6.3f:         <0.000 >         <1.200 >         <-1.200>         <0.123 >         <-0.123>         <0.988 >         <-0.988>       <1234.500>      <-1234.500>         <0.000 >         <-0.000> 
    %.3f:          <0.000>          <1.200>         <-1.200>          <0.123>         <-0.123>          <0.988>         <-0.988>       <1234.500>      <-1234.500>          <0.000>         <-0.000> 
    %6.f:         <     0>         <     1>         <    -1>         <     0>         <    -0>         <     1>         <    -1>         <  1235>         < -1235>         <     0>         <    -0> 
 %+12.3f:   <      +0.000>   <      +1.200>   <      -1.200>   <      +0.123>   <      -0.123>   <      +0.988>   <      -0.988>   <   +1234.500>   <   -1234.500>   <      +0.000>   <      -0.000> 
 %+12.8f:   < +0.00000000>   < +1.20000000>   < -1.20000000>   < +0.12345000>   < -0.12345000>   < +0.98765000>   < -0.98765000> <+1234.50000000> <-1234.50000000>   < +0.00012345>   < -0.00012345> 
%+012.8f:   <+00.00000000>   <+01.20000000>   <-01.20000000>   <+00.12345000>   <-00.12345000>   <+00.98765000>   <-00.98765000> <+1234.50000000> <-1234.50000000>   <+00.00012345>   <-00.00012345> 
  • -0.0は0.0扱いされる。(もしくはGDScriptに-0.0は存在しない。)

  • デフォルトでは小数点以下6桁が、値によらず必ず表示される。
    (例: 12.0→"12.000000")

  • "+3f"のように、ピリオドを伴わずにfの前に数字を書いても何も効果が無い。

  • "+"を付けると、ゼロや正の場合であっても符号が付く。
    テキストの幅はこの符号を含めて評価される。

  • 小数点の前の数字は、変換後の文字列の最小幅。
    これより小さいならば、0かスペースでパディングされる。
    値自体が大きかったり(120000000.1とか)、後述の主数点以下の表示桁数が大きい("%2.8f"とか)場合は、より大きな幅が必要になり、この最小幅を超えて変換される。
    パディング前の幅は、符号があれば符号付きで評価される。

  • 小数点の後ろの数字は、表示させる小数点以下の桁数=文字数。
    キリのいい数字でも0でパディングされる。(例: 1.2→"1.2000")
    最小桁より1つ小さい桁の数字は四捨五入され、最小桁の数字を左右する。正負によらず文字通りの四捨五入。(例: -5.5555→"-5.56")
    この設定は最優先で堅持され、値や条件にかかわらずこれが反故にされることは多分ない。

  • "-"を付けると、左詰めされる。スペースでパディングされる。

  • "%.3f"のように、小数点の前に数字を付けない書き方も許容される。
    この場合、特にパディングされない。

  • "%8.f"のように、小数点の後ろに数字を付けない書き方も許容される。
    この場合、デフォルトの6桁精度になる・・・とかではなく、整数として表示される。小数点も無い。小数点が無いので、その分パディングされるスペースの数は増える。(例: -1234.567 % "8.f" → "□□□-1235")
    一応四捨五入される。

例:
0.0~1024.0の範囲で振れる値を、小数点以下2桁まで、文字数固定で表示したい。
"%7.2f" (7 = 4桁 + 2桁 + 小数点)

例:
0.0~9999.999…の範囲で振れる値を、小数点以下2桁まで、文字数固定で表示したい。
"%7.2f" (7 = 4桁 + 2桁 + 小数点)
※ただしこれは、8文字になる可能性がある。四捨五入されるので。

例:
-100.0~+100.0の範囲で振れる値を、小数点以下1桁まで、文字数固定で表示したい。ただし、正の場合は符号は要らない。
"%6.1f" (6 = 3桁 + 1桁 + 小数点 + マイナス符号)
符号が要る場合でも"6"は変わらない。
"%+6.1f" (6 = 3桁 + 1桁 + 小数点 + 正or負符号)


関数内でnewしたものを引数経由で返すには
C#のout修飾子の付いた引数のように、最初はnullなんだけど関数内でnewして作ってくれる、そんな引数は作れそうで作れない。

var environment: Environment = null
_create_environment(environment)
# この時点でenvironmentは依然null

func _create_sky_environment(dst_environment: Environment) -> void:
	dst_environment = Environment.new()
	dst_environment.background_mode = Environment.BG_SKY

思い返せばこの場合のdst_environmentは、元の参照のコピーだからだ。呼んだ側の参照とは別物だ。だから新しくnewしたオブジェクトをコピー品に差させても、呼んだ側の参照(environment)には影響しない。

なので、関数に何か特定のものを作ってもらいたくて、それを戻り値ではなく、引数でやり取りしたければ、C#のref引数のように呼び出し側でnewするしかない。

var environment: Environment = Environment.new()
_create_environment(environment)
# OK

func _create_sky_environment(environment_ref: Environment) -> void:
	environment_ref.background_mode = Environment.BG_SKY

とはいえ、どうしても呼び出し側でnewしたくないときもある。この場合は「参照の参照」を渡すアイデアがある。Arrayとか。

func create_boss(dst_enemy: Array[Enemy]) -> void:
	# Enemy.newの引数が重要なので、外側に任せられない
	var enemy: Enemy = Enemy.new(_get_param(), _get_main_data(), _get_option())
	dst_enemy.append(enemy)

コンテナも循環参照注意
RefCounted派生クラス同様、コンテナであるArrayとDictionaryも循環参照に気を付けないといけない。循環参照があると自動解放されないようだ。きっとこれも参照カウンタで管理されている。

# この関数を呼ぶと、ローカルなはずのarray_aとarray_bが解放されない。
# デバッグ実行中に「モニター>Object」を見るとリークが確認できる。
func _leak_function() -> void:
	var array_a: Array = []
	var array_b: Array = []
	array_a.append(array_b)
	array_b.append(array_a) # ここをコメントアウトすると結果が変わる

	# オブジェクトを大量に入れておくとわかりやすい
	var objs = []
	for i in range(0, 16 * 1024):
		objs.append(Texture2D.new())
	array_a.append(objs)

# Dictionaryでも同様
	var dict_a: Dictionary = {}
	var dict_b: Dictionary = {}
	dict_a[0] = dict_b
	dict_b[0] = dict_a # ここをコメントアウトすると結果が変わる

マルチキャレットを同じ行に持ってくる
コードエディタのカーソルを複数にする機能。
おもしろテクニックを見つけた。

Shift + Ctrl + カーソルキー上下でカーソルを分身させて、
こんな感じで3文字目に持ってきて、
12|34
12|34
12|34
12|34
ABCDEFGHIJKLMNOPQRSTUVWXYZ
カーソルキー右でカーソルを右にずっと動かすと、
同一行内の等間隔のカーソルになる
1234
1234
1234
1234
ABC|DEFGH|IJKLM|NOPQR|STUVWXYZ

それだけ。


複数カーソルモードの終わらせ方
マルチキャレットモードの終了のさせ方。マウスでクリックするのが確実なんだけど、キーボードだけで済ませるやり方。
これまで「なんとなく」で手グセの勘でカチャカチャ操作して、終了させてきた。
改めてじっくりいじってみると、ちょっと法則とコツが見えてきた気がする。

Ctrl + Shift + 右(または左)を繰り返し押しまくる。そうするとカーソルが1つに統合され、モードが終了する。そうなる理由も、やってるとなんとなくわかる。


DLLは依存関係が解決しないと「見つからない」
GDExtensionのために作ったdllをプロジェクトに追加すると、このようなエラーが出た。

Can't open dynamic library:
H:/MyGodotProj/bin/libgdexample.windows.template_debug.x86_64.dll.
Error: Error 126: 指定されたモジュールが見つかりません。

そこにファイル在るはずなのになぜ!?
答えはDLL間の依存関係。(そういうのあったね・・・)
Godotが内部で呼んでるWin32 API LoadLibraryExWがエラーを返すのは、単に引数として渡したdllがファイルとして存在しないときだけではない。そのファイルの依存関係がすべて解決できないときもエラーを返す。
このマイDLLは、MinGWでコンパイルしたのでMinGW製の標準ライブラリのdllを使っている。<mingw64/bin>下にあるlibstdc++-6.dll等7つのdllを、<C:\Windows\System32>にコピペしたら、エラーが出なくなった。

できればSystem32ではなく、ゲーム本体のフォルダに含める方法も試したいけど、やり方がわからなかった。
- 呼び出し元のdllと同じフォルダ(この場合bin)に置く
- Godotのプロジェクトフォルダに置く
これはどちらもNGだった。


DLLの依存関係の調べ方
今はこのツールがいいらしい。GitHubからダウンロードできる。
lucasg/Dependencies
12MB程度でインストール不要。dllをドラッグ&ドロップするだけで簡単。


blendからメッシュを複製させずにインポートする条件
blendファイル内でオブジェクトがメッシュを共有している場合、Godotインポート後のMeshInstance3Dもまた同一のメッシュがセットされている。メッシュの複製はされない。ただしこれは、オブジェクトに割り当たっている(場合によっては複数の)マテリアルが同一の場合。
マテリアル(場合によってはその組み合わせ)が違うオブジェクトがあれば、インポート後はその数だけメッシュが複製される。なぜなら、Godotではメッシュにマテリアルが紐づくため、セットされるマテリアルを変えないといけないから。


static変数はスタックトレースで見れない?
Godotエディタでstatic関数内でブレーク状態の時に、「デバッガ」>「スタックトレース」を開いても、static変数の値は確認できない。AutoLoadクラスをstatic変数を使ったシングルトンに置き換えてきたのだけど、これはこの方法のデメリットのように思う。
AutoLoadの内容なら表示されるんだけどね。


ArrayはArray[T]にキャストできない
中身の型指定のないArrayを、Array[T]にキャストして代入しようとすると、実行時にエラーとなる。エラーとなる箇所は、エディタには型安全と判定されて、行番号がグレーアウトしない。だけどエラーになる。
エラーメッセージ
Trying to assign an array of type "Array" to a variable of type "Array[T]"
asはキャストに失敗して、単なるArrayを返しているように思う。

Array.assign()関数を使えば問題ない。
おそらく、単なるArrayと型指定のあるArrayはオブジェクトとして別物で、使う側(変数)の対応を変えるだけではダメで、使われる側(オブジェクト)を作り変える必要があるのだろう。

# これは実行時にエラーが出る(エディタには型安全判定されている)
var array: Array = [0, 1, 2]
var array_int: Array[int] = array as Array[int]

# これなら問題ない
# 3行目も型安全OKとされる
var array: Array = [0, 1, 2]
var array_int: Array[int] = []
array_int.assign(array)

このため、enum.keys()はStringの入った素のArrayを返すのだが、Array[String]にイコールで代入しようとするとエラーとなる。


enumをStringにする方法
デバッグやセーブデータのために、enumを文字列化したい場合、次のように、keys()を使えばいい。

text += Combat.Phase.keys()[phase]

ただし上記のような書き方だと、型安全でないとして、行番号がグレーアウトしてしまう。グレーアウトさせたくない場合は下記の通りにするといい。

# asで一度キャストしてしまえばいい
# ただ、実際問題これはコードエディタを騙しているだけで、
# 実行時にはキャストに失敗しているのではないか
text += (Combat.Phase.keys() as Array[String])[phase]

# 冗長に見えても、こちらのほうが誠実かもしれない
var array_phase_str: Array[String] = []
array_phase_str.assign(Combat.Phase.keys())
text += array_phase_str[phase]

# 頻繁に使うenumなら、関数化してしまうか
text += Combat.PhaseToStr(phase)

複数行のラムダ式の書き方
公式ドキュメント等ではラムダ式は1文=1行だけしか書かれてないことが多い。複数文、複数行書くには次のようにする。

# ローカル変数にするとき
var print_sign: Callable = func(val: int) -> void:
	if val >= 0:
		print("pos")
	else:
		print("neg")

# 関数のCallable引数に直接入れるとき
# 最後の閉じ括弧")"はこの段のインデントでないと受け付けない
array.sort_custom(
	func(a: int, b: int) -> bool:
		if a > b:
			return true
		else:
			return false
)

マウスカーソルの位置の取得
マウスカーソルの位置はInputクラスではなく、Viewportから取得する。左上が0, 0の模様。

var main_loop: MainLoop = Engine.get_main_loop()
var window: Window = main_loop.get_root() # <- これはなぜか型安全判定されない
var pos_x: float = window.get_mouse_position().x
var pos_y: float = window.get_mouse_position().y

デバッグのモニターの取得
デバッグ実行中にエディタの「モニター」タブに表示される項目は、スクリプトではPerformanceクラスから取得可能。Performance.get_monitor(Performance.OBJECT_COUNT)


マウスカーソルで3Dオブジェクトを指す
マウスカーソルで3Dオブジェクトに当たり判定を取りたいときは、次のようにノードを組み立てる。物理とか使わない、一番簡易で(多分)処理が軽いやり方。

親Node
	Area3D
		CollisionShape3D

CollisionShape3Dには、Shapde3D派生リソースを割り当てるが、これにはBoxShape3DやSpahreShape3Dを使えばいい。

mose_enteredとmouse_exitedシグナルにコールバック関数をつなぐ。判定は、前後関係が考慮されるようで、一番手前にコリジョンがあるArea3Dだけがシグナルを発する。

area_3d.mouse_entered.connect(_on_area_3d_mouse_entered)
area_3d.mouse_exited.connect(_on_area_3d_mouse_exited)

func _on_area_3d_mouse_entered() -> void:
	処理

func _on_area_3d_mouse_exited() -> void:
	処理

ローカル変数とメンバ関数の名前衝突
メンバ変数名とローカル変数名が一致していると、Ctrl + クリックで関数の定義に飛べなくなる。そもそも名前が衝突すること自体がアウトな気もするので避けた方がよさそう。

# でもこういうの書いちゃう
var is_idx_valid: bool = is_idx_valid(idx_x, idx_z)

列挙体を返す関数をfor文のinで受け取ってしまう
for文のinの後は、配列ではなく単体の整数も受け付けるようだ。for i in Nの場合、N-1回ループすることになる。列挙体を返す関数をうっかりfor文に使ってしまった場合、文法のエラーにならない。バグの原因に気づかない。

# rand_dir4()は東西南北enumをランダムに返す関数

# 間違えてこう書いても文法エラーとはならず通ってしまう
# この場合、0回~3回ランダムに処理が走る
for dir4 in rand_dir4():
    do_something(dir4)

# 本当はこう書きたかった
var dir4: Dir.Dir4 = rand_dir4()
do_something(dir4)

Playbackの意味
関数名とか公式ドキュメントの解説で出てくる"playback"という単語。単に「再生」という意味らしい。意味の広い"play"を、もうちょっと具体的に言い換えた表現で、録画・録音したものを再現するからbackが付いてるっぽい。
日本人的には特定の歌の歌詞を連想して、どうしても「巻き戻す機能」という意味に捉えてしまう。


TODOコメント強調表示
Godot 4.2ではGDScriptのコメントに"TODO"と書くとそこだけ色が変わる。これは、エディター設定の「テキストエディター」>「テーマ」で設定されたキーワードに反応している。
他には、"FIXME"や”CRITICAL”や"NOTE"などが、デフォルトのキーワードとして設定されている。


選択範囲を評価
GDScriptのエディタで範囲選択をして、右クリックすると、コンテクストメニューの中に「選択範囲を評価」という項目がある。これは、この選択範囲のコードをその場で実行する機能のようだ。
"print("abc")"を「評価」すると、ログに"abc"が表示され、選択範囲が"void"に置き換わる。"3+3"を「評価」すると選択範囲が"6"に置き換わる。
何に使えるのかなー。


Rect2iの活用
グリッドやセルで区切られたゲームを作る場合、インデックスの範囲を表すメンバ変数や引数として、4つintを並べることがある。
idx_min_x: int
idx_min_z: int
idx_max_x: int
idx_max_z: int
こういう状況では、これらの代わりにRect2iを使うことを検討してもいいかもしれない。

idx_range: Rect2iで置き換えたとき
idx_min_x == idx_range.position.x
idx_min_z == idx_range.position.y
idx_max_x == idx_range.end.x - 1
idx_max_z == idx_range.end.y - 1
idx_range.size.x == idx_max_x - idx_min_x + 1
idx_range.size.y == idx_max_z - idx_min_z + 1

int 2つを他の組み込み変数(Vector2i)に置き換えるのは、ちょっと迷いがあるところだけど、4つなら十分メリット大きい。気安く使える。


最後の引き数の後に点があってもいい
関数の定義や呼び出しで、引数が多いときは複数行に分けたりすることがある。このとき、最後の引数の後にカンマ(,)が入っていても許される。enumの定義の書き方と同様。
優しい仕様。

# 定義 OK
func do_something(
	arg0,
	arg1,
	arg2,
	) -> void:

# 呼び出し OK
my_obj.do_something(
	get_arg0(),
	get_arg1(),
	get_arg2(),
	)

# 定義 一応一行のときでもOK
func do_something(arg0, arg1, arg2, ) -> void:

# 呼び出し 一応一行のときでもOK
my_obj.do_something(get_arg0(), get_arg1(), get_arg2(), )

影を落とすのはノード単位
影を受ける・受けない(disable_receive_shadows)は、マテリアルの設定値。しかし、影を落とすかどうかは、GeometryInstance3Dの設定値(cast_shadow)。ノード単位で設定する。この差がちょっと厄介な感じがする。

ほとんどのケースにおいて、影を落とすかどうかは、マテリアルに依存する。不透明なものは落とす。半透明なものやシンボル的なオブジェは落とさない。さらに、不透明なものでも、草や髪の毛のようなアルファ抜き(カットオフ/カットアウト)の場合は、面の裏表によらず影を落とす設定にする。
でもGodotは1ノード1メッシュではあるが、1メッシュ1マテリアルではない。不透明・半透明が混在するメッシュの場合、いい感じに影を設定できない。

マテリアルとcast_shadowを紐づけるか、それとも、cast_shadowはマテリアルとは別に意識的に管理するか、運用方法は決めがたい。でもひとつだけ確実なことは、影を落としたいポリゴンと落としたくないポリゴンは、Blenderの時点で別メッシュにする必要がある。特に、影が出るとおかしいくらいに大きなもの。ガラス窓とか。

もし、マテリアルとcast_shadowを紐づけて運用するのであれば、surfaceインデックス=0のマテリルに応じてcast_shadowの値を決めるようにするのが、いい塩梅かもしれない。普通は1つ目のマテリアルが、そのオブジェクトの代表素材なので。


配列はshuffle()でシャッフル可能
配列は、Array.shuffle()関数で文字通りシャッフルできる。
複数のオブジェクトの中から、条件を満たすものをランダムに選ぶときは、これを使うといいかも入れない。

# idxをシャッフルする方法
var idxs: Array[int] = range(0, objs.size()) as Array[int]
idxs.shuffle()
for idx in idxs:
	if objs[idx].is_valid() == true:
		処理
		break

# 有効なidxを事前に収集する方法
# 候補が多くて、当たりが少ないときに有効
var idxs: Array[int] = []
for idx in range(0, objs.size()):
	if objs[idx].is_valid() == true:
		idxs.append(idx)
if idxs.size() > 0:
	var idx_idx: int = randi_range(0, idxs.size() - 1)
	var obj: MyObject = objs[idxs[idx_idx]]
	処理

# 当たるまでひたすら繰り返す方法
for i in range(0, 999):
	var idx: int = randi_range(0, objs.size() - 1)
	if objs[idx].is_valid() == true:
		処理
		break

同じrangeでも意味が違う
range()が返す配列は、第2引数を含まない。
randi_range()が返すint値は、第2引数を含む。
何度書いても「どっちだったっけ・・・」となるし、たびたびヘルプを見ることになる。交互に書いていると混乱する。
ちなみに、randf_range()の返すfloat値は、第2引数を含む。含まなければrandf_range(-1.0, +1.0)みたいなのが書きにくくなるから、当然の仕様に思う。これと動作を統一したとすれば、randi_range()は「含む」方だと思い出すことができる。


UIデバッグ用のアタリ枠
Controlの配置状況、特にLabelノードのサイズ感をデバッグするために、カラフルな枠テクスチャをこれまで使っていた。デバッグモードの特定の設定のときには、このテクスチャをLabelの子として親にフルストレッチするように設定して、Labelのレイアウトを可視化する。前作をUnityで作っていた時にもこういう仕組みを用意していたし、制作終盤に何度も助けられた。
でも実はGodotには、こういう用途のためのノードが組み込みクラスとして存在している。ReferenceRectノードだ。これを使うとスマートかもしれない。

if デバッグ枠を表示するなら:
	# ReferenceRectを使う場合
	var reference_rect: ReferenceRect = ReferenceRect.new()
	reference_rect.border_width = 2.0
	reference_rect.editor_only = false # 実行中に見れないと困る
	reference_rect.set_anchors_preset(Control.PRESET_FULL_RECT)
	add_child(reference_rect)

	# 自前のテクスチャの場合
	# 色を辺ごとに描き分けられるのでこっちも捨てがたい
	var ui_debug_frame: NinePatchRect = NinePatchRect.new()
	add_child(ui_debug_frame)
	ui_debug_frame.texture = load("res://UIDebugFrame.png") as Texture2D
	ui_debug_frame.patch_margin_left = 2
	ui_debug_frame.patch_margin_right = 2
	ui_debug_frame.patch_margin_top = 2
	ui_debug_frame.patch_margin_bottom = 2
	ui_debug_frame.set_anchors_preset(Control.PRESET_FULL_RECT)

つづく~~~



この記事が気に入ったらサポートをしてみませんか?