Godot勉強メモ
※ここではGodotの概要、仕様やGodotを利用したゲーム制作全般に関連した内容をまとめる。
関連記事
エンジン仕様
フリー、オープンソース、商用可能、売上でライセンスが変わったりもしない。
IDE上で編集するタイプ。マウスで視覚的に編集できる。
テキストエディタ内蔵だがVSCodeとも連携でき、拡張も用意されている
3Dもいけるがどちらかというと2Dのほうが強い
ビルドインのAssetLib(Unityで言うアセットストア)がある。数は少ない。
独自スクリプト言語GDScript(一応他の言語もいける)
筆者の個人的な印象
テキストエディタは高性能だが、ウィンドウパネルに問題がある気がする
現在開いているシーンと関係ないスクリプトを編集できてしまうので混乱しやすい。スクリプトエディタの上にシーンを切り替えるタブがついてる(このタブは2D/3Dビューのためのものだが、同じパネルでスクリプトエディタを入れてしまっている)のが一番の混乱のもとだと思う。スクリプトは別パネルであるべき。ちなみにスクリプトエディタはフローティングできるので、本当はこの状態がベストな気がする。
2Dゲームを作っている最中は3Dエディタはほぼ使わないのだが、開けて便利なときがあるのだろうか?
逆のケース、3Dゲーム制作中にUIなどの2D設計を行うことはあるが…。
筆者はUnityとUnrealEngine4を触ったことがある(どちらも少し前なので最新版では違うかも)ので、記憶を頼りに比較してみると、UnityとUEよりも…
2Dゲームに向いている。
2Dシーンの画面の基本単位はピクセル(整数)。3D座標系(実数)で擬似的に2Dを動かしているUnityやUEとは違って画面制御がしやすくわかりやすい。
Unityよりも…
👍️UIが見やすい。
👍️エディタが高性能。
👍️ライセンスに不安がない。
👎️AssetStoreと比べてAssetLibは物足りない。
UEよりも…
👍️エディタが軽い。起動が速い。
👍️アカウントがいらない。ログインがいらない。
👍️(特にカスタマイズせずとも)ビルドサイズが小さい。
👎️UEのノードプログラミングにはお手軽さでは負ける
用語
シーン(Scene)
ゲーム内に存在するすべての要素。
Unityで言うところのGameObject。Unreal Engineでいうところのアクタやポーン。
*.tscnファイルとして保存される。
(余談)オブジェクトなのにシーン(場面)と呼ぶことに最初違和感あったが、シーンはレイヤーのようなもので、キャラクター個々ではなくキャラクターが動き回る空間全体を管理している、と考えると理解しやすいかもしれない。
ノード(Node)
シーンに含まれている項目。画像、アニメーション、効果音、当たり判定のBOXなど。
スクリプトはノードにくっつける。
ノードの追加のショートカットはCtrl+A。
シグナル(Signal)
いわゆるイベント。キーが押されただとか、キャラクターが衝突したとか、フレームが更新されたとかのたびに発行される。当文書では「発火」という言葉をよく使う。
メインシーン
ゲーム起動時に最初に開かれるシーン。
ゲーム全体を管理し、このなかにタイトル画面、キャラクターやステージやUIなどの子シーンが入る。
基本的にはNode2D/3Dシーンをひとつ作ってこれをメインシーンに設定する。
メインシーンは「プロジェクト」>「プロジェクト設定」画面で設定する。メインシーンが無い状態で実行しようとしたときはメッセージが出て設定させられる。
スクリプト
公式にサポートしているのはGDScript, C#, C(GDExtensionが必要), C++(GDExtensionが必要)の4つ。
GDExtensionを使用することで、上記以外にもRust、D、Haxe、Swiftなどのコンパイル言語を使用できるが非公式。(pythonやluaやJavascirptなどのインタプリタ言語には通常対応しない…が、Codonとか使ってコンパイル済ならいけるのか?)
IDE雑多メモ
ショートカット
選択したものを中心に移動する: F
シーンの複製: Ctrl+D
テストプレイ中にコリジョン形状を表示する(デバッグ)
メニュー>デバッグ>コリジョン形状を表示
ドット絵がボケる
プロジェクト設定>一般>レンダリング>テクスチャ>デフォルトのテクスチャフィルタがLinerになっているならNearestにする
ゲームウィンドウ設定
ウィンドウサイズ(ビューポート)の変更
プロジェクト設定>表示>ウィンドウ>サイズ
ウィンドウのアスペクト比を固定
プロジェクト設定>表示>ウィンドウ>ストレッチ>アスペクト>Keep
画面解像度について
ターゲットがPCで、ユーザーが任意にウィンドウサイズをいじれるようにするつもりがないなら以下のようにすれば良い
サイズ>ビューポートの幅/高さ:1920 x 1080 以下の16:9
モード>Windowed
初期位置タイプ: Center Primary Screen
サイズを変更可能: オフ
ストレッチ
モード: viewport
アスペクト: Keep
スケール: 任意
スケールモード: ドット絵ならinteger, 3Dならfractional
他シーンのインスタンス化
インスタンスのプロパティの扱い
個別のインスタンスのプロパティを書き換える
エディタのインスペクタ上で変更する
プログラム上で実行時にプロパティを書き換える
同じインスタンスを持つシーンすべてを書き換える
*.tscnを開いて編集する
IDE上でインスタンス化
左上の🔗ボタンを押す
選ぶ
CTRL+Dで複製
ソースでインタラクティブにインスタンス化
code:gd:Player
signal shoot(direction: Vector2, location: Vector2)
const BULLET_SPEED = 500.0
...
func _input(event:InputEvent):
if event.is_action_pressed("shoot"):
var pos = ($ShotMarker as Marker2D).global_position
shoot.emit(Vector2(1.0,0.0)*BULLET_SPEED, pos)
code:gd:Stage
const BulletScene = preload("res://player/Bullet.tscn") # *.gdではない
# プレイヤーキャラから弾の発射要求を受け取った時
func _on_player_shoot(velocity: Vector2, location: Vector2):
var spawned_bullet:Bullet = BulletScene.instantiate()
spawned_bullet.rotation = velocity.angle()
spawned_bullet.position = location
spawned_bullet.velocity = velocity
add_child(spawned_bullet)
preloadの代わりにクラス宣言class_nameして他シーンで使えるようにする方法もある
GDScript
概要
Godot独自言語。
書き味はPython。
高速に動作するがコンパイルされたC#/C++には速度では叶わない。
プロジェクト内に複数言語の同居ができるので、速度にクリティカルな部分のみC#/C++で書き、それ以外をGDScriptで書くことを公式にオススメしている。
ガベージコレクションは無いが、メモリ管理はエンジン側で行うため気にする必要はない(自前で制御もできる)。
内部でC++に変換されているらしい(たまにC++側のエラーが出てくることがある)
言語仕様
インデントでブロックを区切る。
文末(区切り)記号は無し。
コメントは#。
変数宣言はvar <変数名> = ... (初期値を指定しない場合はnull)
代入は=、加算代入+=なども使えるがインクリメント++は無い
無効はnull。
ブーリアンは true / false。
定数はconst
文字列型はダブルクォーテションでもシングルクォ―テションでもどちらでも良い。a = "some" / b= 'thing'
r"Raw\nStrings": 生文字列。エスケープシーケンスを無視する。
$"StringName" / %"Node/Path" というのもあるがあまり気にしなくてよさそう
列挙型はenum
code:gd
enum DIRECTION {
right,
left
}
配列はarr[i]
配列の長さはarr[i].size()
型の指定もできる。var a: Array[int]
辞書(オブジェクト,構造体)は
code:gd
var d = {4: 5, "A key": "A value", 28: 1, 2, 3} d = {
22: "value",
"some_key": 2,
"more_key": "Hello"
}
# lua風
var d = {
test22 = "value",
some_key = 2,
more_key = "Hello"
}
条件式はif <式>:(カッコ不要)
else + if は elif:
三項演算子は<変数> = <値true> if <条件> else <値false>
式にはandornotが使える。
<A> in <B>:B文字列やB配列にAが含まれているか
<obj> is <Type>:型チェック。
switch文はmatch。
code:gd
match <式>:
<値>:
<文>
_:
<デフォルト文>
# breakは不要
ループはfor / while
code:gd
for i in range(10):
if ...:
break
for i in range(5, 10, 2):
if ...:
continue
for s in strings:
print(s)
for key in dict:
print(key, " -> ", dictkey) var i = 0
while i < strings.size():
i += 1
関数定義は func <関数名>(<引数>,...):
関数はなにかを入れなければならない。なにもしない場合はpassを入れておく。
引数、戻り値の型指定ができる。
引数のデフォルトを指定できる(python風)。ただし呼び出し側で引数名指定はできない(python風じゃない)
code:gd
# 型指定、デフォルト値指定
func do_something(enabled:bool, label:String="default"): -> void
pass
do_something(true, "some text") # OK
do_something(false, label="some text") # エラー
ノードのプロパティはインスペクタータブ内では「Title Case」(タイトルケース)で表示するが、GDScriptコード内では「snake_case」(スネークケース)で扱う
コンソール出力はprint()
乱数(整数)はrandi()
クラス名宣言はclass_name <クラス名>
クラスを調査する方法
クラスの型を調べるにはis
code:gd
if object is Smashable:
object.smash()
クラスがメソッドを持っているかチェックするにはhas_method()
でもわざわざこれでチェックしても警告は取れないことが多い…
code:gd
if object.has_method("smash"):
object.smash()
なおGDscriptでは型チェックをせず直接呼んでも問題にならないらしい(ダック・タイピング)
code:gd
object.smash()
型変換はas
($AnimationPlayer as AnimationPlayer).play("jump") もしくは
code:gd
var ap:AnimationPlayer = $AnimationPlayer
ap.play("jump")
型にまつわる警告
code:gd
# ヒットエリア(食らい判定)の処理
# areaはAttackArea2DかItemArea2Dのどちらかが来る。(基底クラスはどちらもArea2D)
func _on_area_entered(area: Area2D):
# -----------------------------------------------------------------------
# AttackArea2Dにattack_powerというパラメータがあるのでそれを取得したい。
# 型が不定でも(プロパティが存在しなくても)エラーにならずに参照する方法→get("プロパティ名")
# 存在しなければnullが返る。
# どちらかというと「特定の型」ではなく、「特定のプロパティ名を持ついくつかの型」に対応したい時に使う
var ap = area.get("attack_power")
if ap:
var attack_power = (ap as int)
# -----------------------------------------------------------------------
# AttackArea2Dにremove()というメソッドを持っているのでそれを実行したい。
# なにも考えずに直接実行。一応コレでも動く。ただし警告がでる。実行時エラーの可能性もある。
area.remove()
# The method "remove()" is not present on the inferred type "Area2D" (but may be present on a subtype).
...
# メソッドが存在するか確認してから実行。実行時エラーはなくなる(引数とかが合ってれば)が、警告は出たまんま。
if area.has_method("remove"):
area.remove()
# The method "remove()" is not present on the inferred type "Area2D" (but may be present on a subtype).
# -----------------------------------------------------------------------
# いっそ直接変換してしまえば上記手間はなくなる
# AttackArea2D.gd の先頭に class_name AttackArea2D が必要。たまにクラス名を認識してくれない時がある
if area is AttackArea2D:
var aa:AttackArea2D = (area as AttackArea2D)
var attack_power = aa.attack_power
aa.remove()
注釈
変数をプロパティ化し、インスペクタで変更できるようにする
@export var <プロパティ名> = <初期値>
code:gd
@export var speed: int = 400
@export_...でインスペクタ上でどう編集できるかをいろいろ指定できる。例えばファイル名をプロパティにする場合、@export var filename: Stringのようにただの文字列を要求するよりは@export_file("*.txt") var filename: Stringとしたほうがファイルダイアログも出せるし便利。詳細 @onready: _init時は値が不定なノードの内容を_ready時に取得したいとき。
code:gd
var my_label
func _ready():
my_label = get_node("MyLabel")
これが面倒くさい場合
code:gd
@onready var my_label = get_node("MyLabel")
シーン仕様
1シーン1ファイル(*.tscn)
シーンはクラスである。
コンストラクタは_init()、だが初期化は通常_readyのほうを使う
ソースの地べた(インデントがない状態)に書いた変数定義は、基本_initのタイミングで処理されるようだ。子ノードの参照がしたい場合はreadyで行う。→@onready
code:gd
var count = 0 # OK
var my_label = get_node("MyLabel") # NG。nullが返る
@onready var my_label = get_node("MyLabel") # OK
準備完了は_ready()
ノード自身と子ノードの準備が完了したときに実行される。ゲームの初期化は基本的にここで行う。
注: _ready はrequest_readyを使用すれば再度呼び出せるが、その他の方法、例えば一度ツリーから削除してもう一度追加しても呼び出されることはない。
code:gd
func _ready():
var timer = get_node("Timer")
timer.timeout.connect(_on_timer_timeout)
_readyにゲームの初期化を直接書くのではなく、初期する関数を別に作って_readyから呼ぶのが良さそう。そのほうがリトライもしやすくなる。
毎(描画)フレーム処理は_process( delta )
deltaはフレーム間の経過時間(単位は秒)
code:gd
func _process(delta):
rotation += PI * delta
毎物理フレーム処理は_physics_process( delta )
deltaはフレーム間の経過時間(単位は秒)
_process()と_physics_process()は別に平行に動いている。
負荷がかかったりして描画フレームが遅れる場合でも、より正確な物理計算結果を出すために_physics_processは一定タイミングを保とうとする。基本的な入力やアニメーションや判定処理など場合によって重くなりそうな処理は_processに記述し、移動や物理に関わる処理のみなるべくコンパクトに_physics_processに書くのがよさそうだ
なおArea2Dなど物理がないノードでも_pysics_processは動いている。ゲーム内の他の場所で物理(Rigidbody)による移動計算が行われている場合、齟齬がないようにこちらを使うほうがよさそう
入力検知は_unhandled_input(<InputEvent>) / _unhandled_key_input(<InputEvent>)
これはキーやボタンが押された/離された瞬間(ジャンプとか攻撃とか)を検出するのに向いている。押しっぱなし(移動とか)の検出は_process内でInputを使う方が良い
ため攻撃はどうするのか考え中
システム周りのキー操作は_unhandled_key_input、それ以外は_unhandled_inputを使うのがよさそう
システム周りのキー操作とはESCでポーズ(もしくはゲーム終了)、Alt+Enterで全画面表示、などゲーム内容に関係なく割とクリティカルなやつ
マウス操作とかも_unhandled_inputで取れる
シーン内のノードを参照系
シーン内の他ノードを参照するには get_node("ノード名")か$ノード名
ノードが見つからない場合は null を返します
code:gd
var timer = get_node("Timer")
timer.timeout.connect(_on_timer_timeout)
# あるいは
$Timer.timeout.connect(_on_timer_timeout)
シーン内のすべてのノードを調べたいならget_children()
シーン内のノードから特定の型を探したいならfind_children()…だが、固定で孫まで探してしまい遅いのでおすすめしないとか。
親はget_parent()…だが、単体テストしたいと考えた時、get_parentで親の特定の挙動を期待するのはあまり良くない。子でシグナルを発火させて親に接続するのが良いと思う。手作業での接続の手間が面倒ならスクリプトで親から全子を調べて接続などする。
code:gd
func _ready() -> void:
# ステージ内のすべての敵ノードの「弾発射」シグナルを接続する
for node in childrenNodes:
if node is Enemy:
(node as Enemy).enemy_shoot.connect(_on_enemy_enemy_shoot)
変数をプロパティ化し、インスペクタで変更できるようにするには @export
自分自身を削除するには queue_free()
シグナル
エディタからシグナルの接続
インスペクターの隣にノードパネルがある。
シグナルタブで選択しているノードのシグナル一覧が見える。
ダブルクリックしてスクリプトに接続する。
例えばGUIボタンが押されたかどうかをキャラクターが受け取りたければ、ツリーからGUIボタンを選択してパネルからpressed()シグナルをダブルクリックしてキャラクターのスクリプトに接続する。
接続すると以下の関数がソースに追加される。
code:gd
func _on_button_pressed():
pass # Replace with function body.
passはなにもしない行なのでこの行を消して内容を書き換える。
ソースからシグナルの接続
<ノードオブジェクト>.<シグナル名>.connect(<関数名>)
code:gd
func _ready():
var timer = get_node("Timer")
timer.timeout.connect(_on_timer_timeout)
func _on_timer_timeout():
visible = not visible
カスタムシグナル
signal <シグナル名>
<シグナル名>.emit(<渡すパラメータ>)
例えばライフがゼロになったときにhealth_depletedシグナルを発行したい
シグナル名は通常、過去形の動詞を使用。
code:gd
signal health_depleted
...
func take_damage(amount):
health -= amount
if health <= 0:
health_depleted.emit()
シグナルの発火をその場で待つ
await <シグナル>
code:gd
print("Timer started.")
await get_tree().create_timer(1.0).timeout
print("Timer ended.")
座標系
2Dの座標単位はピクセル。
位置、加速度は Vector2。
3Dの座標単位はメートル。
角度はラジアン。(度数に対応したプロパティもある)
PI(定数)で半回転。TAU(定数)で1回転。
deg_to_rad() / rad_to_deg() : 度数→ラジアン / ラジアン→度数の変換。
ベクトル(Vector2)
code:gd
var location = Vector2(100.0, 200.0)
Vector2.ZERO: Vector2型のゼロ。=(0,0)
<Vector2>.angle(): ベクトルから角度(ラジアン)を取得。
<Vector2>.length(): ベクトル長さ。
<Vector2>.normalized(): ベクトルのノーマライズ
ノーマライズとは長さを1に揃えること。これを単位にして移動スピードをかけることで、斜めに入ったときに加速することを防ぐ。例えば(1,0)、(0,1)のベクトル長さは1だが、(1,1)のベクトル長さは√2(1.414...)になってしまうのでノーマライズが必要になる
code:gd
if velocity.length() > 0:
velocity = velocity.normalized() * speed
<Vector2>.direction_to( <Vector2> ): ある位置から観たある位置の方向(ノーマライズされたベクトル)。
<Vector2>.angle_to( <Vector2> ): ある位置から観たある位置の方向(ラジアン)。
2D基本
Node2D (< CanvasItem < Node < Object)
Node2D.transform
position: 位置
rotation: 回転(ラジアン)
rotation_degrees: 回転(度数)
scale: サイズ(拡大率)
skew: 傾き
CanvasItem.hide(): 非表示
コリジョン(衝突判定)
Shapeプロパティでコリジョンの形状を指定
コリジョン形状の拡大率は必ず等倍(x1)のまま変更しないこと。でないと計算が狂うらしい
コリジョンをいきなり削除しないこと。衝突処理中だった場合エラーが発生する
Collision>Layer: 自分が所属するグループ
Collision>Mask: 衝突判定をするグループ
プロジェクト設定 → Layer Names でグループの名前をわかりやすいように変更できる
衝突判定はArea2Dの以下のシグナルで行う。
シグナル: area_entered(area: Area2D)
Area2Dを持つノードが重なった。
両方がArea2Dを持っている場合は両方同時にシグナルが発火する。
シグナル: body_entered(body: Node2D)
???Body2D(StaticBody2D, RigidBody2D, CharacterBody2Dのどれか)を持つノードが重なった。
???Body2Dには衝突検知シグナルがないため、必ずArea2D側で処理することになる。
コリジョンの無効化の方法
Area2Dの場合、monitoringとmonitorableプロパティがあり、それぞれ衝突を「監視する」「監視される」か否かを設定できる。
<Area2D>.set_deferred("disabled", true)でもできる
StaticBody2D/RigidBody2D/CharacterBody2Dの場合、disable_modeがRemoveになっていることを確認したうえで、process_mode = PROCESS_MODE_DISABLEDおよびprocess_mode = PROCESS_MODE_INHERITを使って無効化/有効化できる…が、多分スマートな方法じゃない気がする。
海外掲示板を見てたら「コリジョンのサイズをゼロにした」ってのもあった。そういうのもあるのか…。
タイルマップ
ステージを作ったりする
TileMapを追加する。
TileSetを作る。
プロパティからあらかじめタイルの大きさを設定しておく。
プロパティから新規Tilesetを作る。
IDE下のTilesetパネルを開く(TileMapパネルではない)
+ボタンを押して新規アトラスを追加。画像をロード。自動的にタイル分けされる。
タイルとして使いたくない領域がある場合は右クリックして削除する。
TileMapパネルに移動し、ステージを作成する。
クリックで設置、Eで削除、Pでスポイト
Cキー、Vキーで左右上下反転、X,Zで90度回転できる。(v4では代替えタイルは必要なくなった)
このままだとコリジョンがないためキャラクターはすり抜けてしまう。
TileMapのインスペクタのTileSetのPhysics Layerを追加する。
ここで衝突するかすり抜けるかのグループ設定ができる
TileSetパネルで適当にタイルを選択し、選択>物理>PhysicsLayer0であたり判定のポリゴンを設定できるようになる。
Shiftキーで複数のタイルを選択して一度に同じ当たり判定を設定できる。
タイル全面が当たり判定ならFキーを押す。
左クリックでポイント追加、右クリで削除。
微妙な数ドットを調整するくらいならグリッドスナップを活用したほうがよい
レイキャスト(遮蔽物判定)
レイキャストは他ゲームエンジンでも搭載されている機能で、地点Aから地点Bまでの直線上になにかが存在しているか/なにもないかを判定する機能。これによって、例えば「移動先(足元)に地面があるか」「敵からプレイヤーが視認できるか(敵の視界に入ったか)」を判定でき、特に敵の行動パターン(敵AI)の判断材料などに使われる。レイ(光線)を当てて光が届いたかどうかを確認する様に似ていることからレイキャストと呼ばれる。
特に「普段はじっとしている(あるいは決まった巡回ルートを移動している)が、プレイヤーが視界に入ったら襲ってくる」という敵行動パターンは鉄板で、この処理と地形があるだけで鬼ごっこゲームが成立する。
get_collider(): なにかに当たればそのオブジェクトを返す。当たらなければnullを返す。
アニメーション
インスペクターからAnimation>SpriteFramesに「新規SpriteFrames」を設定
IDE下部のパネルにアニメーションパネル。
defaultを変更してアニメーション名を指定。walk, stopなど(大文字小文字注意)
アニメーションごとにセルを追加する
連番画像を使う場合
画像をファイルシステムパネルからひとつづつD&D(エクスプローラーから直接D&Dしないこと)
スプライト シートを使う場合
網目アイコン(🏁)をクリックして画像をシートとして読み込む。
ウィンドウが出てフレームの大きさ調整ができる。
フレームをクリックした順番にセルが登録される。
再生ボタンでアニメの確認。
再生速度は「ト(FPS)」で指定
まず先にSprite2Dが必要なので追加する。
プロパティのTextureにスプライトシートをロードする。
Animationプロパティでフレームの大きさを設定する。
Frameを変更して絵が切り替わることを確認する。
AnimationPlayerを追加。IDE下部のパネルにアニメーションパネル。
アニメーションをクリックして「新規」
トラックの右上の数値が長さ(単位:秒)、その隣がループの有無 を設定
Sprite2D側のフレームを変更しながら、横の鍵🔑マークを押して登録していく。
登録するごとにアニメーションパネル側のフレームは先に進むのでプロパティパネル側を操作するだけで次々登録していける。
アニメーションごとに表示位置をずらしたいとき
トラックを追加ボタンを押して「プロパティトラック」を追加する。(しなくてもOK)
Sprite2Dのオフセットを開く。
アニメーションフレームを表示し、ずらしたいフレームでオフセット値を調整して🔑ボタンをおしてキーフレームを登録する。
ちなみにflip_vでセルを左右反転してもオフセットは反転されないので、右と左で別のアニメを用意する必要があるかも
animation = "<アニメーション名>": アニメーションを切り替える(大文字小文字注意)
play(): 再生
play("<アニメーション名>")でアニメーション名を指定して再生。
stop(): 停止
flip_h/flip_v = true/false: セル画像の表示の反転。hは横、vは縦。
sprite_frames.get_animation_names(): アニメーション一覧を取得。配列で文字列が返る。
AnimationPlayerをあらゆる条件にそって操作するノード
AnimationNodeAnimation: ただのアニメーション
AnimationNodeBlendTree: Transition,Blendなどの条件に従って再生すべきアニメーションを選択する。例えば、同じ「移動」でも地上を歩く時と空中や水中を動く時は別のアニメになるとき、「移動してる」「空中にいる」などのフラグを別に管理して与えるだけで適切なアニメを選んでくれる。
AnimationNodeStateMachine: ステートマシン。あるアニメーションから別のアニメーションへ複数のアニメーションをスムースにつなぎ渡り歩く。踏ん張りからのジャンプ、構えからの斬り、コンボ攻撃の2段め、などアニメーションの繋がりがある程度決まっている時、かつ、それをボタン操作などの任意のタイミングで切り替えたいときに主に活躍する。(すべての動きをステートマシンに頼ろうとするとかなり難解になるのでおすすめしない)
AnimationNodeBlendSpace2D: 与えられた2次元値=x,y平面(平面でなく2つの別の値の扱いでも良い)に従ってアニメーションを切り替える、あるいは混ぜ合わせる。例えば見下ろし画面のキャラクターを上下左右に動かす時に、移動方向によってそれぞれの方向を向くアニメーションを切り替える、など。2Dアニメ(パラパラアニメ)で使う場合、ブレンドカーブを「離散的」に設定すること。
AnimationNodeBlendSpace1D: 与えられた1つの値にしたがって…要するに上記の簡易版。Tree上でBlendノードを使えば同じことができるため、わかりにくくなるこのBlendSpaceをあえて使う意義は薄い。
AnimationTreeはTransitionやBlendなどの分岐ができるノードを使って各アニメーションを接続する。スクリプト上で各分岐ノードにパラメータを与えることで選択させる。
パラメータ名はツリーを組み上げた後、AnimationTreeのパラメータを見れば一覧として見れる。各パラメータの上にマウスを乗せるとパラメータのパス文字列が見えるほか、右クリックして「プロパティのパスをコピー」でパスが取れる。このパス文字列を使ってスクリプトから操作する。
$Animation_tree.set("<パラメータパス>", <値>)
code:gd
# トランジションの分岐をstate_2に切り替えることを要求する
$Animation_tree.set("parameters/Transition/transition_request", "state_2")
シェーダー
シェーダーはプログラムによって画像や画面効果をリアルタイムに生成する方法。GodotではGLSLを独自拡張したものをgdshaderとして実装している。詳細 GLSLの書き味はJavascriptっぽい感じ。
超簡易シェーダーの例:キャラクター画像の色を変える
Sprite2Dを用意する。あらかじめ適当な画像ファイルをTextureにロードしておく。
CanvasItem>MaterialのMaterialに新規ShaderMaterialを作成。
さらにShaderを作成。ファイル名は*.gdshader。内容は以下(スクリプトエディタではなくその下のシェーダーエディターで編集を行う)。
code:gdshader
shader_type canvas_item;
void fragment() {
COLOR.r = 1.0;
}
実行して表示してみると画像が赤っぽくなってるはず。
COLOR.rのrをgとかbにしてみると緑っぽくなったり青っぽくなったりする。
状態検知ノード
タイマー(時間が経過したかの判定)
start() / stop() : 開始/停止
Wait Time: 待ち時間
One Shot: 1回のみ
Autostart: 自動で開始
タイマーが完了するとtimeout()シグナルが発火する。
code:gd
func _ready():
$Timer.wait_time = 3.0
$Timer.start()
...
func _on_timer_timeout():
print("timeout")
短い間、一時停止する必要がある場合は、Timerノードを使用する代わりに、SceneTreeの create_timer() 関数を使用します。 これは、遅延させるのに非常に役立ちます。
code:gd
# この場で3秒待つ
# 他の処理は平行して動いており、ゲーム全体を停止させるわけではないことに注意
await get_tree().create_timer(3.0).timeout
画面内に存在しているか(出ていったか)の判定
VisibleOnScreenNotifier2D
signal screen_exited(): 画面外に出た
入力
Godotでは、プレイヤーの入力を処理するために、主に2つのツールが用意されています。
組み込みの入力コールバック。主に _unhandled_input()です。例えば Space を押してジャンプするような、毎フレームで発生しないイベントに反応するために使用するツールです。
Inputシングルトン。これは、毎フレーム入力があるかどうかを確認するのに適したツールです。
Input
Input.is_action_pressed(<キーラベル>): キー(PAD)入力判定
code:gd
var direction = 0
if Input.is_action_pressed("ui_left"):
direction = -1
if Input.is_action_pressed("ui_right"):
direction = 1
InputEvent
Node._unhandled_input(<InputEvent>)
他の箇所で拾われていないキーイベントに反応する。例えばステータス画面表示中などでGUIが表示中でキーで操作可能になっている最中は反応しない。ゲームでは一般的にGUIが表示中ならそちらが優先されるべきだからである。
入力キーの種類
プロジェクト > プロジェクト設定からインプットマップタブをクリックすると、プロジェクト内の入力アクションを確認・編集できる。(デッドゾーンの設定も可能) デフォルトのアクションは非表示になっているので組み込みアクションを表示をオンにする。
例えばキーボードのカーソルキー、JOYPADの十字キー、アナログスティックの状態をまとめてui_up / ui_down / ui_left / ui_rightで取れるが、それらラベル名を覚えるくらいなら自分で設定してしまったほうが良い。
カメラ
基本的にはCamera2Dをプレイヤーキャラクターの子に追加しておけば、カメラ(視点)は勝手にプレイヤーを追従する。
ユーザーインターフェース/HUD
Sprite2Dなど描画オブジェクトを持つシーンの「表示」を管理する。以下のような設定ができる
Layer: 表示順、奥行き。
Follow Viewport: カメラに追従するか否か。
つまり、CanvasLayerを利用して「表示順が位置番手前で」「カメラに追従しない(画面固定)」レイヤーを作れば、HUDやUIとして利用できる。
2DでFollow Viewportをオフにした場合、原点(0,0)が左上になることに注意。
Control : ユーザーインターフェース(ラベル、ボタンなど)の基底クラス フォントの変更: Theme Overrides>Font, Font Sizes
Label: HUD上に文字を置く…が、殆どの場合画像で作ったほうが見栄えがいいので開発中に「とりあえず」置く用途しかない。しかもデフォルトのフォントは、小さすぎるフォントサイズに対応していなかったりして使い勝手が悪い。 背景
ParallaxLayerはSprite2Dを子ノードに必要とする
motion_mirroring: 繰り返しの幅(名前のくせにミラーされない…と公式ドキュメントにも書いてある)
motion_offset: 視差ズレ幅
motion_scaling: 視差拡大率
特にスクリプトなど必要とせず、置いておけば勝手に背景として機能するようだ
スクロールしない画面固定の背景(空など)を置きたいならCanvasLayerを使う。
サウンド
AudioStreamPlayer2D
.play(): 再生
.stop(): 停止
オーディオは ループしない状態でインポートされる。ループさせたい場合は「Stream」プロパティの矢印「v」をクリックして「ユニーク化」を選択し、「Loop」にチェック。
パーティクル
ParticleProcessMaterialを新規作成
なんか小便みたいに出てくる
Textureを設定
Time>Lifetimeで消えるまでの時間を設定
ProcessMaterial内で細かく設定
セーブデータ
リソース(JSONファイルなどの読み込み)
敵のパラメータなどのデータベースをJSONなどでまとめてゲーム内で利用したい時は、リソース(Resource)にしておくといろいろな面で有利。 バイナリパック化されるので効率的。
ユーザーによる安易な改竄を防げる(完全に改竄を防げるわけではない)
プロパティで直接参照できる
参照はエンジンの管理化に置かれるので明示的にファイルハンドルやメモリを開放する必要がない
キャッシュが効く(最初の1回だけ読み込まれ、以降は何度読み込んでも負荷がかからない)
作成方法
ファイルシステムを右クリックして「新規作成>リソース」
JSONを検索、選択して「作成」
名前をつけて保存。拡張子は「.tres」になる
エディタ上で編集できる。JSON形式に従う。
例:チュートリアル用に、キャラクターが重なったときにメッセージが表示される看板を作る場合
code:json://dialogue/turorial.tres
[
"左右移動: 左スティック/カーソルキー",
"ジャンプ: Aボタン/Zキー"
]
code:gd
...
const tutorial_text_resource = preload("res://dialogue/turorial.tres")
...
func _on_area_entered(_area: Area2D) -> void:
$CanvasLayer/Label.text = tutorial_text_resource.dataid $CanvasLayer.visible = true
...
func _on_area_exited(_area: Area2D) -> void:
$CanvasLayer.visible = false
全画面モード
full_screenキーイベント(=Alt+Enter)をプロジェクト設定で設定済のこと
code:gd
func _unhandled_key_input(event):
if event.is_action_pressed("full_screen"):
swap_fullscreen_mode()
func swap_fullscreen_mode():
if DisplayServer.window_get_mode() == DisplayServer.WINDOW_MODE_FULLSCREEN:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
else:
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN)
フルスクリーンがちゃんと拡大されず黒帯になるのは調査中
ESCキーで即時終了
この場合、ESCキーはキーカスタマイズの対象にしないほうが安全な気がする
ゲームの終了はget_tree().quit()
code:gd
func _unhandled_key_input(event):
if event is InputEventKey:
if event.pressed and event.keycode == KEY_ESCAPE:
get_tree().quit()