Open28

GodotEngine小技集

ピン留めされたアイテム
SaitosSaitos

Godot Engineに関する小技を書いていき、整理したら記事にする用スクラップ
日々の開発中に気づいたことを気ままに書いていくだけ。
(他ユーザー投稿歓迎)

SaitosSaitos

現在のカメラ取得

get_viewport().get_camera()

2Dでも3Dでも、現在のビューポートが映し出しているカメラを取得する。

SaitosSaitos
var _camera:Camera = get_viewport().get_camera()
_camera.current = false

var _new_camera:Camera = Camera.new()
add_child(_new_camera)
_new_camera.cureent = true 

カメラを単純に切り替えるならcurrentプロパティの変更でOK

SaitosSaitos

Focus vs Input

GUIを作成する上で、フォーカスとインプットはそれぞの特徴で使い分けたほうが良さそう。

Focus

フォーカスは便利で、キーボードでもゲームパッドでも、UI入力の上で自動的にフォーカスしてくれる。
ただ、全体的に自動的に選択されるので、フォーカスできるコントロールノードを大量においている場合、制御しきれない可能性がある。

focus_modeを一時的に変更してフォーカスを管理することはできる。
ノードグループ(Button Groupではない)を使ってうまくやることはできそう。

grab_focusを多用して、フォーカスを管理することができる。
というか、フォーカスしていたノードが非表示になるとフォーカスは外れてしまうので、表示状態に応じてgrab_focusするのがよい。

Input

インプットイベントで特定のコントロールノードを独自のステートとして選択状態にすることもできる。
完全に管理下にあるので自由度は高いが、望んだ挙動にするには細かく作り込む必要がある。

また、表示状態に関わらずInputは機能しているので、非表示にしたウィンドウ内で勝手に操作できてしまう。
自由度が高い分、自前で色々制限をかけていく作業になる。

SaitosSaitos

シングルトン(自動読み込み)

忘れがちなので。
設定したらONにする。

SaitosSaitos

シングルトンクラスはNodeだが、何より早くメインツリーに生成される

onready_ready()メソッドでget_node()でノードを取得しようとしてもnullになる。
コントローラーノード(Mainなどの別ノード)でシングルトンクラスの変数に直接ノードを代入する方法であれば問題ない。

SaitosSaitos

Godot Engineでビットを扱う

export(int, FLAGS) var scenario_flags = 0b000000
SaitosSaitos

値の取得

func is_bit_enabled(mask, index):
    return mask & (1 << index) != 0

値の有効化

func enable_bit(mask, index):
    return mask | (1 << index)

値の無効化

func disable_bit(mask, index):
    return mask & ~(1 << index)
SaitosSaitos
オペレーター 説明
~ NOT(ビットを反転します)
<< >> 左側のオペランドのビットをn桁左または右にシフトします(1<< 2 == 4)
2つの値の論理積
^ 2つの値の論理XOR
| 2つの値の論理OR
SaitosSaitos
extends Node2D

# The `<<` operators performs a bit shift to the left, which can be used to elevate a number to the power of the righthand side quickly (when used with `1` on the lefthand side).
const FLAG_FALLING = 1 << 0  # 1
const FLAG_FLAMMABLE = 1 << 1  # 2
const FLAG_SLIPPERY = 1 << 2  # 4
const FLAG_NONSOLID = 1 << 3  # 8


func _ready() -> void:
    # The bitwise `|` operator can be used to combine flags safely.
    # `+` can also be used, but it's less safe as it won't prevent you from adding the same flag multiple times (which is invalid).
    var block_flags = FLAG_FALLING | FLAG_SLIPPERY

    print("Falling: ", has_flag(block_flags, FLAG_FALLING))
    print("Flammable: ", has_flag(block_flags, FLAG_FLAMMABLE))
    print("Slippery: ", has_flag(block_flags, FLAG_SLIPPERY))
    print("Nonsolid: ", has_flag(block_flags, FLAG_NONSOLID))


func has_flag(bitfield, flag):
    # The bitwise `&` operator will return a number with all bits "in common" between the two numbers set to `1`, and all other bits set to `0`. If no flags match, the returned number will be `0` since there will be no bits in common between the two numbers.
    # Here, we convert it to a boolean since we are only interested in whether
    return bool(bitfield & flag)

https://godotengine.org/qa/95753/how-to-check-an-individual-binary-digit?show=95833#a95833

SaitosSaitos

exportキーワードでビットフラグを扱う

export(int, FLAGS, "zoon in", "zoom out", "slow motion") var flags

intで直接指定すると面倒くさいし、Dictionaryで定義するのも変換が手間なので、FLAGSキーワードを入れることで、自動的にビットフラグのリストをインスペクタに表示してくれる。

SaitosSaitos

テクスチャとsRGB

自前で作ってリニア色空間にコンバートしてないテクスチャは、インポート設定でsRGBをenableで取り込む。

Spatialシーン上に置くと顕著に白んで見えるので、インポート設定には注意

SaitosSaitos

ViewportとViewportContainerを使う際にコンテンツの品質が低下する

ViewportのサイズがMainViewport(ゲームの最上位にあるビューポート)よりも小さい場合、ViewportContainerがStretchすることによってテクスチャそのものが引き伸ばされる。
そしてデフォルトではViewportContainerのViewportTextureにはフィルタがかかっていない上にインスペクタからフィルタを設定できない。
なので、スクリプトからViewportTextureに対してフィルタをtrueにする必要がある。

onready var _viewport:Viewport = get_node_or_null("GameLayer/Container/Viewport")
func _ready():
	_viewport.get_texture().flags = Texture.FLAG_FILTER

ウィンドウサイズが変わるとViewportのサイズも変更する必要がある上に、ピクセル密度が高ければ高いほど品質は上がるので以下の様に設定する。

onready var _viewport:Viewport = get_node_or_null("GameLayer/Container/Viewport")
onready var _main_viewport:Viewport = get_viewport()
func _ready():
	_viewport.get_texture().flags = Texture.FLAG_FILTER
	_viewport.size = _main_viewport.size * 2

https://github.com/godotengine/godot/issues/24201

SaitosSaitos

ViewportTextureを設定したメッシュで影が投影されなかった問題

Spatial MaterialでTransparentをONにするだけと影が投影されない。

解決法は2つ。

  1. TransparentをONにした上で、ParametersのDepth Draw ModeOpaque Pre-Passに設定する(添付画像:赤線)
  2. TransparentをOFFにした上で、ParametersのUse Alpha ScissorsをONにする(添付画像:青線)

1のほうがきれいに透過の影が投影されるが負荷が高い。

https://twitter.com/antourenein/status/1516084765509701637?s=20&t=hZVpcbwFL5VxJG0lZ1LjCA

SaitosSaitos

シーンの継承

https://www.gotut.net/godot-inheritance/

シーンを継承することでツリー構成はそのままに拡張することができる。

スクリプトも継承することができるので、一旦スクリプトをデタッチして以下のように継承する。

extends "res://Character.gd"

直接スクリプトパスを書くのでパスが変わったら修正が必要になる。

SaitosSaitos

ownerプロパティの活用

Node.ownerは便利ですが少し癖があるのでメモ

まずNode.ownerはブランチシーンとして保存されたPackedSceneのルートを自動的に持っている状態になる。
なので、複雑な構造のツリーを持っていても、そのシーンのルートにすぐにアクセスすることができる。

ただし、型がNodeとして保存されているため、オートコンプリートが意図通り機能しない。

var player := owner as KinematicBody

上記のように型チェックを入れるとオートコンプリートも機能して使いやすくなる。

SaitosSaitos

GDQuestステートマシン理解メモ

基本的な使い方

Entity、Playerなど呼び方は様々だが、要するにステートを管理したいノードに以下のようなツリーを追加する。

- Entity (KinematicBody etc)
  - Sprite or Mesh
  - Collision
  - StateMachine
    - Idle (extends State)
    - Run (extends State)
    - Jump (extends State)

ブランチシーンとして保存されていることを前提として、それぞれのステートはownerを参照してvelocityなどの変数やmove_and_slide()関数などを実行する。
明示的にownerを設定しない限りメインツリー上では動かないと思われる。

図解

StateMachine.gd
extends Node
class_name StateMachine

signal transitioned(state_name)

export var initial_state := NodePath()

# デフォルトのステートを指定しておく必要がある
onready var state: State = get_node(initial_state)

func _ready() -> void:
	yield(owner, "ready")
	# ステートマシンである自身を子ノードのステートに定義する
	for child in get_children():
		child.state_machine = self
	# ステートを開始する
	state.enter()

func _unhandled_input(event: InputEvent) -> void:
	# ステートのインプットイベントを実行
	state.handle_input(event)

func _process(delta: float) -> void:
	# ステートの更新処理を実行
	state.update(delta)

func _physics_process(delta: float) -> void:
	# ステートの物理更新処理を実行
	state.physics_update(delta)

# ステート切り替え関数
func transition_to(target_state_name: String, msg: Dictionary = {}) -> void:
	# もしステートが存在しなければステートを終わらせて警告を出す
	if not has_node(target_state_name):
		push_warning("State is not exists " + target_state_name)
		return
	# 現在のステートを終了させる
	state.exit()
	# 指定されたステートを現在のステートとして取得
	state = get_node(target_state_name)
	# 現在のステートを開始
	state.enter(msg)
	# ステートの切り替えが完了したシグナルを発行
	emit_signal("transitioned", state.name)
State.gd
extends Node
class_name State

var state_machine = null

# 以下は継承して使うための仮想関数(インターフェイス的な)
func handle_input(_event: InputEvent) -> void:
	pass

func update(_delta: float) -> void:
	pass

func physics_update(_delta: float) -> void:
	pass

func enter(_msg := {}) -> void:
	pass

func exit() -> void:
	pass
SaitosSaitos

型を修正してオートコンプリートに対応する

実際に個別ステートが継承するのはこっち

PlayerState.gd
class_name PlayerState
extends State

# ownerノードの型を定義
var player: KinematicBody

func _ready() -> void:
	# ownerはNode型なのでKinematicBodyとして代入
	player = owner as KinematicBody
	# もしNullならデバッグ時点でエラーが出るように指定
	assert(player != null)
SaitosSaitos

Viewportのツリー順序によってエラーが出る

Sprite3DでViewportTextureを参照するにあたって、以下のような構成にしないとエラーが出る

OK
- Viewport
- Sprite3D
NG
- Sprite3D
- Viewport
SaitosSaitos

CanvasLayerのサイズをViewportに揃える

フォントが固定値のため高い解像度の場合小さくなってしまう問題を解決する。

CanvasLayerFollow ViewportEnableにする。
このままだとフォントがガビガビになってしまうので、フォントリソース側にも少し手を入れる。

フォントリソースのUse MipmapUse Filterを有効にする。

SaitosSaitos

KinematicBody のクセメモ

Physics関連、すごくクセが強くて細かいことをしようとするとうまくいかない事が多いのでメモっていくスレ。

is_on_floorの正しい取得

内部的にmove_and_slide()の処理の中でis_on_floorなどの状態を切り替えているので、
move_and_slide()の後に取得するのが正しいっぽい。

func move():
    motion.y += GRAV
    motion = move_and_slide(motion, JUMP)
    if is_on_floor():
        print(true) # now check is_on_floor()
SaitosSaitos

is_on_floor()を取得するには常に重力を与え続ける必要がある。

先述の通り、move_and_slide()系の処理でis_on_floor()などの状態切換をしているという点と、
補正したvelocityを返しているので、少なくとも以下のような状態でないと狙った動きにし辛い。

velocity.y += gravity
velocity = move_and_slide(velocity, Vector3.UP)

if is_on_floor():
    print(true)
SaitosSaitos

タイルマップのチラつき

2Dゲームを開発するにあたって、タイルマップで背景を構成することがあるが、
実際に表示してみるとタイルとタイルの隙間が一瞬表示され、画面上チラつきが発生する。

Godot Engine 3.5 RC3

GPUピクセルスナップを有効にすることで解消する

SaitosSaitos

GUIのFocusの挙動がバグる件をissueに投げた。

ダイナミックフォントを使用して、V Box Containerseparationプロパティを0に設定すると、ボタン同士が若干重なってしまい、Focusの挙動が怪しくなる。

プロジェクト設定の「フォントオーバーサンプリングを使用」のチェックを外すことで回避できると教えてもらった。

SaitosSaitos

オーディオバスでボリュームコントロール

var _bus:int = AudioServer.get_bus_index("BGM")
AudioServer.set_bus_volume_db(_bus, linear2db(_value))

linear2db()の関数が大事で、0.0~1.0で引数を渡すとdbに変換して返してくれる。
これがないとボリュームコントロールは面倒。

SaitosSaitos

get_tree()のエラー

if Node.get_tree() == null:
    return null

だと止まらないけどエラーが出る。
どうやら4.1以降から出ちゃうみたいなので、回避方法をメモ

if not Node.is_inside_tree():
    return null

同じことなんだけど、get_tree()で無い状態を取るんじゃなくて、is_inside_tree()で存在する状態を取る。

SaitosSaitos

シグナルに接続するCallableの引数に注意

func _init():
    Node.get_tree().process_frame.connect(_proc)

func _proc(_delta):
   print("not working")

上記はシグナル自体の接続はできているが、受け口が間違ってるせいで動いてくれない。
しかもエラーは出ない。

例でいうところのSceneTree.process_frameは特に引数は必要ないので、以下のような定義が正しい。

func _init():
    Node.get_tree().process_frame.connect(_proc)

func _proc():
   print("not working")

シグナルに接続しているのにどうしてもCallableが呼ばれないときは引数定義を見直してみるのが良い