GodotEngine小技集
Godot Engineに関する小技を書いていき、整理したら記事にする用スクラップ
日々の開発中に気づいたことを気ままに書いていくだけ。
(他ユーザー投稿歓迎)
現在のカメラ取得
get_viewport().get_camera()
2Dでも3Dでも、現在のビューポートが映し出しているカメラを取得する。
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
Focus vs Input
GUIを作成する上で、フォーカスとインプットはそれぞの特徴で使い分けたほうが良さそう。
Focus
フォーカスは便利で、キーボードでもゲームパッドでも、UI入力の上で自動的にフォーカスしてくれる。
ただ、全体的に自動的に選択されるので、フォーカスできるコントロールノードを大量においている場合、制御しきれない可能性がある。
focus_mode
を一時的に変更してフォーカスを管理することはできる。
ノードグループ(Button Groupではない)を使ってうまくやることはできそう。
grab_focus
を多用して、フォーカスを管理することができる。
というか、フォーカスしていたノードが非表示になるとフォーカスは外れてしまうので、表示状態に応じてgrab_focus
するのがよい。
Input
インプットイベントで特定のコントロールノードを独自のステートとして選択状態にすることもできる。
完全に管理下にあるので自由度は高いが、望んだ挙動にするには細かく作り込む必要がある。
また、表示状態に関わらずInputは機能しているので、非表示にしたウィンドウ内で勝手に操作できてしまう。
自由度が高い分、自前で色々制限をかけていく作業になる。
シングルトン(自動読み込み)
忘れがちなので。
設定したらONにする。
シングルトンクラスはNodeだが、何より早くメインツリーに生成される
onready
や_ready()
メソッドでget_node()
でノードを取得しようとしてもnull
になる。
コントローラーノード(Mainなどの別ノード)でシングルトンクラスの変数に直接ノードを代入する方法であれば問題ない。
Godot Engineでビットを扱う
export(int, FLAGS) var scenario_flags = 0b000000
値の取得
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)
オペレーター | 説明 |
---|---|
~ | NOT(ビットを反転します) |
<< >> | 左側のオペランドのビットをn桁左または右にシフトします(1<< 2 == 4) |
& | 2つの値の論理積 |
^ | 2つの値の論理XOR |
| | 2つの値の論理OR |
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)
exportキーワードでビットフラグを扱う
export(int, FLAGS, "zoon in", "zoom out", "slow motion") var flags
intで直接指定すると面倒くさいし、Dictionaryで定義するのも変換が手間なので、FLAGSキーワードを入れることで、自動的にビットフラグのリストをインスペクタに表示してくれる。
テクスチャとsRGB
自前で作ってリニア色空間にコンバートしてないテクスチャは、インポート設定でsRGBをenable
で取り込む。
Spatialシーン上に置くと顕著に白んで見えるので、インポート設定には注意
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
ViewportTextureを設定したメッシュで影が投影されなかった問題
Spatial MaterialでTransparentをONにするだけと影が投影されない。
解決法は2つ。
- TransparentをONにした上で、Parametersの
Depth Draw Mode
をOpaque Pre-Pass
に設定する(添付画像:赤線) - TransparentをOFFにした上で、Parametersの
Use Alpha Scissors
をONにする(添付画像:青線)
1のほうがきれいに透過の影が投影されるが負荷が高い。
シーンの継承
シーンを継承することでツリー構成はそのままに拡張することができる。
スクリプトも継承することができるので、一旦スクリプトをデタッチして以下のように継承する。
extends "res://Character.gd"
直接スクリプトパスを書くのでパスが変わったら修正が必要になる。
ownerプロパティの活用
Node.owner
は便利ですが少し癖があるのでメモ
まずNode.owner
はブランチシーンとして保存されたPackedScene
のルートを自動的に持っている状態になる。
なので、複雑な構造のツリーを持っていても、そのシーンのルートにすぐにアクセスすることができる。
ただし、型がNode
として保存されているため、オートコンプリートが意図通り機能しない。
var player := owner as KinematicBody
上記のように型チェックを入れるとオートコンプリートも機能して使いやすくなる。
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
を設定しない限りメインツリー上では動かないと思われる。
図解
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)
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
型を修正してオートコンプリートに対応する
実際に個別ステートが継承するのはこっち
class_name PlayerState
extends State
# ownerノードの型を定義
var player: KinematicBody
func _ready() -> void:
# ownerはNode型なのでKinematicBodyとして代入
player = owner as KinematicBody
# もしNullならデバッグ時点でエラーが出るように指定
assert(player != null)
Viewportのツリー順序によってエラーが出る
Sprite3DでViewportTextureを参照するにあたって、以下のような構成にしないとエラーが出る
- Viewport
- Sprite3D
- Sprite3D
- Viewport
CanvasLayerのサイズをViewportに揃える
フォントが固定値のため高い解像度の場合小さくなってしまう問題を解決する。
CanvasLayer
のFollow Viewport
をEnable
にする。
このままだとフォントがガビガビになってしまうので、フォントリソース側にも少し手を入れる。
フォントリソースのUse Mipmap
とUse Filter
を有効にする。
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()
move_and_slide()
などの引数のvelocity
にdelta
を乗算するのは間違い
内部的にdeltaを乗算して処理しているので、引数で指定するvelocity
にdelta
を乗算すると二重に処理してしまう。
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)
タイルマップのチラつき
2Dゲームを開発するにあたって、タイルマップで背景を構成することがあるが、
実際に表示してみるとタイルとタイルの隙間が一瞬表示され、画面上チラつきが発生する。
GPUピクセルスナップを有効にすることで解消する
GUIのFocusの挙動がバグる件をissueに投げた。
ダイナミックフォントを使用して、V Box Container
のseparation
プロパティを0に設定すると、ボタン同士が若干重なってしまい、Focusの挙動が怪しくなる。
プロジェクト設定の「フォントオーバーサンプリングを使用」のチェックを外すことで回避できると教えてもらった。
オーディオバスでボリュームコントロール
var _bus:int = AudioServer.get_bus_index("BGM")
AudioServer.set_bus_volume_db(_bus, linear2db(_value))
linear2db()
の関数が大事で、0.0~1.0
で引数を渡すとdbに変換して返してくれる。
これがないとボリュームコントロールは面倒。
get_tree()のエラー
if Node.get_tree() == null:
return null
だと止まらないけどエラーが出る。
どうやら4.1以降から出ちゃうみたいなので、回避方法をメモ
if not Node.is_inside_tree():
return null
同じことなんだけど、get_tree()
で無い状態を取るんじゃなくて、is_inside_tree()
で存在する状態を取る。
シグナルに接続する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が呼ばれないときは引数定義を見直してみるのが良い