Godotマイこだわり集
Godotエンジンでゲーム作る運用のマイルール集。結局南極好みの問題なんだけどね。
Godotエンジンの使い方に迷いを覚えたときの、納得感のある落としどころを、この記事に記録します。
enum
Godotでは自作enumは必ずクラスに属しています。ユーザーの用意するスクリプトは全部クラスということにすることで、いろいろシンプルにする狙いがあるんじゃないかと思ってます。
とはいえ、C系の言語の要領で考えると、どこに定義していいかわからなくなることがあります。特定のクラスとの結びつきがよっぽど強いenumは、素直にそのクラスの中に作ればいいと思います。しかしそうでないものもあります。
ひとまず、そのenumが存在するための専用のクラスを作る。という方針にすると迷いが無くなります。これはstaticクラス的な扱いで、インスタンスは使用禁止です。
class_name CombatResult
extends Node # インスタンス作らないけど、ひとまずNodeやObject
# "Type", "State", "Category", "Phase"などをenum名にする
enum Type
{
ELIMINATED,
GOT_WIPED,
RAN_AWAY,
}
そして、このenumの便利関数(ヘルパ)的なものは同じクラスに足していきます。
# static関数として関数を追加する
# enumの意味付け
static func CanAcquireReward(combat_result_type: CombatResult.Type) -> bool:
if combat_result_type == CombatResult.Type.ELIMINATED:
return true
return false
# 最終的には、だいたいこういうのが必要になる
static func GetTextID(combat_result_type: CombatResult.Type) -> TextID:
if combat_result_type == CombatResult.Type.ELIMINATED:
return TextID.COMBAT_RESULT_ELIMINATED # 勝利
if combat_result_type == CombatResult.Type.GET_WIPED:
return TextID.COMBAT_RESULT_GET_WIPED # 全滅
if combat_result_type == CombatResult.Type.RAN_AWAY:
return TextID.COMBAT_RESULT_RAN_AWAY # 逃走
return TextID.ERROR
# 制作序盤はこれでいい
static func GetName(combat_result_type: CombatResult.Type) -> StringName:
if combat_result_type == CombatResult.Type.ELIMINATED:
return &"勝利"
if combat_result_type == CombatResult.Type.GET_WIPED:
return &"全滅"
if combat_result_type == CombatResult.Type.RAN_AWAY:
return &"逃走"
return "エラー"
この方法のいいところは、後々になって、このクラスをそのまま正式なクラスに昇格(?)して使えるところです。
その場合、クラスのインスタンスは、クラス名を体現する複合データです。enumはそのクラスのサブカテゴリ(サブ型・サブ分類)に意味を変え、クラスのメンバ変数として使われます。そしてその他のメンバ変数で、enumを補う情報を付加します。
class_name CombatResult
extends RefCounted # RefCountedやNodeにする
enum Type
{
ELIMINATED,
GOT_WIPED,
RAN_AWAY,
}
# enumがメンバの1つになる
# このクラスインスタンスの、分類・区分として使用する
var type: CombatResult.Type = CombatResult.Type.ELIMINATED
# 付加情報
# 経過ターン数と、スペシャル攻撃による決着かどうか
var total_turn_count: int = 0
var is_sp_atk_finish: bool = 0
使う側はこんな感じに変わります。この運用なら、変数名やクラス名を変えなくても変じゃないはずです。
# enumだけのクラスとして使う時
var combat_result: CombatResult.Type = CombatResult.Type.ELIMINATED
↓
# インスタンス化できるクラスに「成った」時
var combat_result: CombatResult = CombatResult.new()
combat_result.type = CombatResult.Type.ELIMINATED
combat_result.total_turn_count = turn_count
combat_result.is_sp_atk_finish = false
newの代わり
スクリプトエディタ、自作クラスのnewの引数をサジェスト(?)してくれないことがありますよね。筆者の場合はときどきあります。newはnewであって_initじゃないから、いろいろ都合があって解析しにくいんだろうなあと思ってます。
そこで、よく使うクラスはstatic関数で作れるようにしておきます。これなら候補がすぐ出てくれる。
# createとかmakeとかそんな名前で統一する
static func create(cube_count: int, new_scale: float) -> IceCubes:
# newしてreturnするだけ
return IceCubes.new(cube_count, new_scale)
func _init(cube_count: int, new_scale: float) -> void:
_cube_count = cube_count
_base_scale = new_scale
_setup_nodes()
# 作る側
# 引数がサジェストされます
var ice_cubes: IceCubes = IceCubes.create(3, 0.1)
これの副次的ないいところは、create系関数を後から増やせるところです。インスタンスの作り方を複数持てます。関数名を変えられるので、C#/C++で作れる引数違いのコンストラクタ(オーバーロード)よりも、意図が明示的です。
# 引数の型がかぶってもOK
static func create_by_index(idx: int) -> SoundTrig:
static func create_by_id(id: int) -> SoundTrig:
# enumで作る
static func create_by_category(ladder_cat: Ladder.Category) -> Ladder:
# コピーを作る(よく要る)
static func create_copy(src: DamageParam) -> DamageParam:
アセットリソースのファイル名
Godotの公式ドキュメントでは、プロジェクト下のファイルはスネークケースで命名することが推奨されています。kouiu_kanji。
筆者は長年ファイル名をパスカルケースで書く派です。KouiuNamae。でもGodotでは、郷に入らば郷に従えの精神で、スクリプトやシーンのファイル名はスネークケースにしてます。
でもね、外からインポートさせるデータ系ファイルに関しては、パスカルケースで通してます。PNGファイルとかblendファイルとか。
理由
リソースファイルは、Godotだけで使うわけではない。Godotに使うためにファイル名を変えていては、管理が混乱してしまう。
フォントファイルや音源ファイルなど、自分由来のファイルじゃない場合、ファイル名を変えずにそのまま使いたいこともある。管理のためでもあるけど、作者の名づけを尊重したい。
Windows一筋なのでむしろ、大文字小文字の扱いに注意が必要なのは、十分慣れてる。
フォルダ構成
Godotではシーンに近いアセットのファイルをグループ化して、同じフォルダにまとめましょう、ということになってます。
こんな感じです。
/ballista
ballista.tscn
ballista_volt.tscn
ballista.gd
ballista_volt.gd
ballista.blend
ballista.png
ballista_volt_shot.wav
ballista_volt_hit.wav
チームで作っている場合、こういうのは必須だと思うんですよ。大人数でおっきなゲーム作るならなおさら。
筆者は最初はこれに倣おうとしました。でもそれによるうま味を感じなくって、結局はデータとスクリプトを分けることにしました。
/Data
/3D
/Scenery
Ballista.blend
Ballista.png
/SFX
ballista_volt_shot.wav
ballista_volt_hit.wav
/Low
/Scenery
ballista.gd
ballista_volt.gd
# シーンは使わない
理由
一人で作っていて、さらに、スクリプトやシーンを含めたGodot用「アセット」を外部から導入することもないので、上記のballistaのようなフォルダを、着脱したり更新をかけたりする必要が無い。
データファイルはスクリプトにくらべてファイルサイズが大きいので、分かれていたほうがいろいろと都合がいい。
作るのが非アクションゲームなので、スクリプトと見た目と音は、疎な繋がりにできるし、したほうが(自分的には)やりやすい。
データは分かれていたほうが気持ちがいい。迷いが無いのは大事。
ブロックを作る
ローカル変数が衝突しないように、スコープを切りたいときは、if文でブロックを作ってます。
# こういうのが同じ関数でいくつも続いたり、
# 別の場所であっても頻出する場合、
# なるべく変数名と書式を揃えて、一覧性、視認性を上げたい
if true:
var pos_x: float = 16.0
var pos_y: float = 16.0 * float(count + 0)
var item: MenuItem = MenuItemBox.create()
_add_box(item, pos_x, pos_y)
if true:
var pos_x: float = 32.0
var pos_y: float = 16.0 * float(count + 1)
var item: MenuItem = MenuItemFrame.create()
_add_frame(item, pos_x, pos_y)
if true:
var pos_x: float = _calc_pos_x()
var pos_y: float = 16.0 * float(count + 2)
var item: MenuItem = _get_next_item()
_add_item(item, pos_x, pos_y)
以上です~~
この記事が気に入ったらサポートをしてみませんか?