Open7

自分的GDScriptルール

SLMNLLSLMNLL

これは何?

私がGodot Engineを使っていく中で、「こういう風にコードを書こう」と何となく決めているルールを追記していくスクラップです。

コードの書き方として「未来の自分が困らない」をコンセプトにしており、以下のようなことを念頭に置いています。

  • コードを書いてる途中に迷う時間を無くしたい
  • 見通しの悪いコードが原因で発生するミスを、事前に潰したい
  • コードを見て得られる情報を増やしたい
  • 久しぶりに見返すコードでも、内容をすぐに理解できるように書きたい
SLMNLLSLMNLL

00. コードの一貫性を保とう

恐らく、今後このスクラップに書かれる内容のほとんどが目指すゴールです。

一貫性のあるコードのメリット

  • 一貫性のあるコードによって、自分が書いたコードの「解読」をする作業が激減します[1]
  • 一貫性のあるコードを目指すことで、迷う時間が減って、コードを書く作業に集中できます

一貫性のないコードの問題点

一貫性が損なわれることで、コードは一瞬で迷路になります。

開発が進んで、クラスやシーンが増えれば増えるほど、小さな裏路地のようだった迷路は巨大で手をつけられない攻略不能なダンジョンになってしまいます。そして、遅かれ早かれ開発者にとってのモンスターである、意図しない挙動やバグが跋扈し始めます。

無関係な要素の処理が詰め込まれたクラスや、どこで値が変更されているのかがわからない変数、いろいろなクラスから呼び出されている関数…。

これ以上「自分で書いたコードなのに全く読めない」みたいな経験をしたくないですよね。

脚注
  1. 個人的には、「解読作業」が発生した場合、どこかでコードの書き方を誤っていると認識しています。そのようなコードはリファクタリング対象です。 ↩︎

SLMNLLSLMNLL

01 : GDScriptのスタイルガイドに準拠する

どんな言語でも、個人的には一番最初に押さえるべきポイントは、公式のスタイルガイドを読み込むことだと考えています。

もちろん、Godot Engineにも公式ドキュメントにGDScriptのスタイルガイドというページがあります。

GDScriptに触れ始めた頃はこのページを読み込んで、今も(ごく一部の例外を除いて)スタイルガイドに準拠したコードを書いています[1]

スタイルガイドにはどんなことが書かれているの?

スタイルガイドには例えば以下のようなことが書かれています。

  • 関数とクラスの定義は、2つの空白行で囲む
  • インデントにはスペースではなく、タブを使用する
  • 関数名や変数名には、スネークケースを使用する(クラス名はキャメルケース、定数は大文字スネークケース)
  • 式を垂直方向に揃える(見やすくする)ためにスペースを使用しない

いろいろなことが事細かに書かれているので、まだ読んだことがない人はぜひ読んでみてください。

スタイルガイドに準拠するメリット

スタイルガイドはコードの一貫性を保つのに有用です。スタイルガイドに準拠したコードを書くことで、以下のような利点があると考えています。

  • コードをざっと見渡した時点で、どこに何があるのかがすぐにわかる(見通しがよくなる)
  • 関数名や変数名の名付けルールが定まっていると、迷う時間が減りコードを書くことに集中できる
  • 他の人にコードを共有する際に、意思疎通がしやすい
脚注
  1. コードがよりリーダブルになる明確なメリットがない限りは、公式のスタイルガイドに準拠しています。例外の一例として、ローカル変数、引数の名前の付け方などは、スタイルガイドを少し逸脱しています。 ↩︎

SLMNLLSLMNLL

02 : 原則として変数はプライベートに

Godot Engineではインスタンス変数は基本的にパブリックで、簡単に外部のクラスからアクセスできます。

どこからでもアクセスできるパブリック変数は便利です。しかし、すぐにあらゆる場所から参照・書き換えされるようになり、結果としてバグの温床になりがちです。そうなると参照元や、クラス間の依存関係の把握も大変で、バグの修正どころか原因の究明すら非常に難しくなります。

ですので、私はプライベート変数しか使わないようにしています。

GDScriptのプライベート変数

実はGDScriptの言語仕様上、プライベート変数というものは存在しません。インスタンス変数は常にどこからでもアクセスできてしまいます。

ですが、_ (アンダースコア)を変数名の最初に付けて、プライベートであることを示すことができます[1]

パブリック変数とプライベート変数
extends Node

# 🤔 使わない
# パブリックなインスタンス変数
# どこからでも自由にアクセスできる
var foo: int = 1 

# ✅ こちらを使う
# プライベートなインスタンス変数
# 他のスクリプトからはアクセスしないようにする
var _bar: bool = true

外部からのアクセスは関数を経由する

どうしても外部からアクセスしたい場合は、それぞれにset_xxx()get_xxx()という関数を用意します。

例えば下のスクリプトでは、_bar変数にはset_bar()関数がありません。こう書くことで、「_barは外部から書き換わることがない」ということが、コードを一瞥しただけでわかります。

少しコードは冗長になりますが、分かりやすくなります[2]

var _foo: int = 1
var _bar: bool = true


# fooを外部から書き換えたい場合に使う
func set_foo(foo_: int) -> void:
    _foo = foo_


# fooを外部から参照する場合に使う
func get_foo() -> int:
   return _foo


# barを外部から書き換えたくないので、
# get_bar()関数だけを実装する
func get_bar() -> bool:
    return _bar

採用するメリット

  • あちこちから自由に書き換えられる可能性のある変数を極力減らすことで、潜在的なバグの可能性を減らせる
  • 変数が書き換わっている箇所を探すために、複数のファイルを探し回る機会が減る
脚注
  1. _ (アンダースコア)を付けたとしても変数へのアクセス自体はできます。ですので、「プライベートにする」というのは、あくまでコードを書く人同士の約束事です。 ↩︎

  2. ここに至るまで、変数自体にsetgetを実装して書き換えができないようにしたりもしました…。が、コードの可読性やメンテナンス性などが著しく下がるため、今の方法に落ち着きました。 ↩︎

SLMNLLSLMNLL

ローカル変数、引数の名前の付け方

ここからは公式のスタイルガイドを外れた、個人的なルールです。

ローカル変数(関数内の変数)や、関数の引数は、以下のようなルールで名付けています。

  • ローカル変数は、接頭辞・接尾辞に_を使わない
  • 引数は最後に_を付ける
# インスタンス変数(クラス内の変数)
# 接頭辞として`_`を使っている
var _foo: int = 3 


# 引数のarg_1_やarg_2_は、接尾辞として`_`が付いている
func sample_method(arg_1_: int, arg_2_: int) -> void:
    # ローカル変数には、単語間以外に`_`が無い
    var bar_baz: int

    # ローカル変数に、インスタンス変数(クラス内の変数)と引数を足して代入している処理が、
    # 変数名を見ただけでわかる
    bar_baz = _foo + arg_1_ + arg_2_ 

「関数の外の変数が絶対にプライベートである」ことと組み合わせると、変数名を見た瞬間に_の位置で以下のいずれの変数なのかがわかります。

  • 変数名の冒頭に_がある = インスタンス変数(クラス内の変数)
  • 変数名の最後に_ = 関数の引数
  • それ以外 = ローカル変数(関数内変数)

変数のスコープ範囲[1]、書き換えていいのかどうか[2]などの情報を、変数を見た時点でわかるようにしています。

採用するメリット

  • 変数が書き換わっている箇所を探すために、コードを探し回る機会が減る
  • 計算式を見た時、それぞれの変数に対する解像度が少しだけUPする
脚注
  1. この変数の命名方法だと、接頭辞として_が付いてない変数はスコープが関数内に限られることが一瞬でわかります。 ↩︎

  2. 今後書くと思いますが、引数は基本的にイミュータブル(書き換えない)なものとして扱います。ですので、末尾に_がついている変数に値を代入するようなコードを見つけたら、何かがおかしいことがわかります。 ↩︎

SLMNLLSLMNLL

引数だけで処理ができる関数はstatic

関数の外部の状態を一才参照せず、引数さえ与えれば結果を返してくれる関数は、static修飾子をつけるようにします。

最も単純なコードを例示します。

# 引数であるarg_1_とarg_2_があれば計算が可能で、
# 関数の外部の状態に関わらず、一定の戻り値を返す関数
static func add(arg_1_: int, arg_2_: int) -> int:
    return arg_1_ + arg_2_

Godotのstaticな関数は、クラスがインスタンス化されなくても呼び出すことが可能です。もし、staticな関数内部でインスタンス変数などを参照しようとすると、エディタ上でエラーが表示されます。

言い方を変えれば、staticな関数であることを明示すれば、関数名を見た時点で「インスタンス変数や、インスタンスの状態を参照・書き換えできない」関数であることがわかります。

つまり、インスタンス変数に関連するバグの原因捜索の際、早めに容疑者リストからstaticな関数を外すことができます[1]

採用するメリット

  • バグの原因を究明する際に、読み飛ばして良いコードが増える = バグを発見するまでの時間を減らせる
  • インスタンス変数に依存しない関数であることが、関数名からわかる
脚注
  1. staticな関数を経由してインスタンスの状態を変更している場合もあります。ですので、必ずしも「インスタンス変数にまつわるバグ」の原因から除外はできない点に注意が必要。 ↩︎

SLMNLLSLMNLL

03 : 長い関数はCallableで区切ってしまう

コーディングする際には、基本的に一つ一つの関数が数行以内に収まるようにするのが理想だと思います。

ですが、関数を細かく切り出した結果処理の流れがわかりにくくなってしまう場合や、他にも一つの関数に処理をまとめてしまいたい場合があると思います。

そういった場合、私はその関数内で処理のまとまりをCallableにしてしまうことがあります。

以下のコードは関数として切り出した方が良い例ではありますが、Callableで関数内の処理のまとまりを区切るイメージはしやすいかと思います。

callableで関数を区切る
func long_method() -> Character:
    # キャラクターを作る処理をCallableにして実装
    var setup_character = func(name_: String, max_hp_: int, max_mp_: int) -> Character:
            var callable_result = Character.new()
            callable_result.name = name_
            callable_result.max_hp = max_hp_
            callable_result.current_hp = randi_range(1, max_hp_)
            callable_result.max_mp = max_mp_
            callable_result.current_mp = randi_range(0, max_mp_)
            # ... 他の処理
            return callable_result

    # キャラクターを生成しつつ、配列に代入    
    var characters = [
    	setup_character.call("バイキング", 50, 5),
    	setup_character.call("戦士", 30, 3),
    	setup_character.call("魔法使い", 15, 20),
        # ... 他のキャラクターを作る
    ]

    # 生成したキャラクターのうち一体を、ランダムで返す
    return characters.pick_random()