シーン構成

この記事では、シーンコンテンツの効果的な編成に関連するトピックについて説明します。どのノードを使用する必要がありますか?どこに配置すればよいですか?彼らはどのように相互作用する必要がありますか?

個々の結びつきを効果的に構築する方法

Godotユーザーが独自のシーンを作成し始めると、次の問題に遭遇することがよくあります。

彼らは最初のシーンを作成し、コンテンツで埋めてから、再利用可能なピースに分割する必要があるという忍び寄る感覚に悩まされます。そこで、彼らは自分のシーンのブランチを自分のシーンに保存します。しかし、以前は頼りにしていたハード参照が不可能になっていることに気付きます。ノードパスがターゲットを見つけられないため、シーンを複数の場所で再利用すると問題が発生します。エディタで確立されたシグナル接続が壊れます。

これらの問題を修正するには、環境に関する詳細を要求せずにサブシーンをインスタンス化する必要があります。サブシーンがどのように使われるかを選り好みせずに自分が作成されることを信頼できる必要があります。

OOP(オブジェクト指向プログラミング)で考慮すべき最大のことの1つは、コードベースの他の部分との `疎結合<https://en.wikipedia.org/wiki/Loose_couple> ` を使用して、焦点を絞った単一目的のクラスを維持することです。これにより、オブジェクトのサイズが(保守性のために)小さく保たれ、再利用性が向上するので、完了したロジックを書き直す必要がなくなります。

これらのOOPのベストプラクティスには、シーン構造とスクリプトの使用におけるベストプラクティスに いくつかの 影響があります。

可能な限り、依存関係を持たないようにシーンを設計する必要があります。 つまり、必要なものすべてを内部に保持するシーンを作成する必要があります。

シーンが外部コンテキストとやり取りする必要がある場合、経験豊富な開発者は、 依存性の注入 <https://en.wikipedia.org/wiki/Dependency_injection> の使用をお勧めします。この手法では、高レベルAPIを使用して低レベルAPIの依存関係を提供します。なぜこれを行うのですか?外部環境に依存するクラスは、誤ってバグや予期しない動作を引き起こす可能性があるためです。

これを行うには、データを公開し、親コンテキストを使用して初期化する必要があります:

  1. シグナルに接続します。非常に安全ですが、動作に「応答」するためだけに使用し、これを起動しないでください。シグナル名は通常、"entered"、"skill_activated"、または "item_collected" のような過去形の動詞であることに注意してください。

    # Parent
    $Child.connect("signal_name", object_with_method, "method_on_the_object")
    
    # Child
    emit_signal("signal_name") # Triggers parent-defined behavior.
    
    // Parent
    GetNode("Child").Connect("SignalName", ObjectWithMethod, "MethodOnTheObject");
    
    // Child
    EmitSignal("SignalName"); // Triggers parent-defined behavior.
    
  2. メソッドを呼び出します。動作を開始するために使用されます。

    # Parent
    $Child.method_name = "do"
    
    # Child, assuming it has String property 'method_name' and method 'do'.
    call(method_name) # Call parent-defined method (which child must own).
    
    // Parent
    GetNode("Child").Set("MethodName", "Do");
    
    // Child
    Call(MethodName); // Call parent-defined method (which child must own).
    
  3. FuncRef プロパティを初期化します。メソッドの所有権は不要なので、メソッドよりも安全です。動作を開始するために使用されます。

    # Parent
    $Child.func_property = funcref(object_with_method, "method_on_the_object")
    
    # Child
    func_property.call_func() # Call parent-defined method (can come from anywhere).
    
    // Parent
    GetNode("Child").Set("FuncProperty", GD.FuncRef(ObjectWithMethod, "MethodOnTheObject"));
    
    // Child
    FuncProperty.CallFunc(); // Call parent-defined method (can come from anywhere).
    
  4. ノードまたはその他のオブジェクト参照を初期化します。

    # Parent
    $Child.target = self
    
    # Child
    print(target) # Use parent-defined node.
    
    // Parent
    GetNode("Child").Set("Target", this);
    
    // Child
    GD.Print(Target); // Use parent-defined node.
    
  5. NodePathを初期化します。

    # Parent
    $Child.target_path = ".."
    
    # Child
    get_node(target_path) # Use parent-defined NodePath.
    
    // Parent
    GetNode("Child").Set("TargetPath", NodePath(".."));
    
    // Child
    GetNode(TargetPath); // Use parent-defined NodePath.
    

これらのオプションは、子ノードからのアクセスのソースを隠します。これにより、子はその環境に 疎結合 されます。 APIに追加の変更を加えることなく、別のコンテキストで再利用できます。

注釈

上記の例は親子関係を示していますが、同じ原則がすべてのオブジェクト間の関係に適用されます。兄弟であるノードは、(親やその親などの)祖先が通信と参照を仲介する間のみ、相手の階層を認識する必要があります。

# Parent
$Left.target = $Right.get_node("Receiver")

# Left
var target: Node
func execute():
    # Do something with 'target'.

# Right
func _init():
    var receiver = Receiver.new()
    add_child(receiver)
// Parent
GetNode<Left>("Left").Target = GetNode("Right/Receiver");

public class Left : Node
{
    public Node Target = null;

    public void Execute()
    {
        // Do something with 'Target'.
    }
}

public class Right : Node
{
    public Node Receiver = null;

    public Right()
    {
        Receiver = ResourceLoader.Load<Script>("Receiver.cs").New();
        AddChild(Receiver);
    }
}

同じ原則は、他のオブジェクトへの依存関係を維持する非Nodeオブジェクトにも適用されます。オブジェクトを実際に所有するオブジェクトがオブジェクト間の関係を管理する必要があります。

警告

データを組織内(シーンの内部)に保持することをお勧めします。疎結合のコンテキストであっても、外部のコンテキストに依存関係を置くことは、ノードは環境内の何かが実在することを期待することを意味します。プロジェクトの設計哲学は、これが起こらないようにする必要があります。そうでない場合、コード固有の責任により、開発者はドキュメントを使用して、オブジェクトの関係を微視的に追跡する必要があります。これは、開発地獄とも呼ばれます。外部ドキュメントに依存するコードを記述すると、安全のために規定でエラーが発生しやすくなります。

そのようなドキュメントの作成と保守を回避するために、依存ノード (上記の "child") を _get_configuration_warning() を実装するツールスクリプトに変換します。空でない文字列を返すと、シーンドックは、ノードのツールチップとして文字列を持つ警告アイコンを生成します。これは、子 CollisionShape2D ノードが定義されていない場合に、Area2D ノードなどのノードに対して表示されるアイコンと同じです。次に、エディタはスクリプト コードを使用してシーンを自己ドキュメント化します。ドキュメントによるコンテンツの複製は必要ありません。

このようなGUIを使用すると、プロジェクトユーザーにノードに関する重要な情報をより適切に通知できます。外部依存関係はありますか?これらの依存関係は満たされていますか?などです。他のプログラマー、特にデザイナーとライターに対し、それを構成する作業のために必要な指示を、メッセージを通して明確に伝えます。

では、なぜこの複雑な切り替えがすべて機能するのでしょうか?シーンは単独で操作するときに最適に動作するためです。単独で作業できない場合は、他のユーザーと匿名で作業します(最小限のハード依存関係、つまり疎結合)。クラスに加えられた避けられない変更により、クラスが予期しない方法で他のシーンと対話するようになった場合、事態は崩壊します。 1つのクラスを変更すると、他のクラスに悪影響を与える可能性があります。

スクリプトとシーンはエンジンのクラスを拡張したものなので、すべてのOOP原則に従うべきです。例として…

ノードツリー構造の選択

そのため、開発者はゲームの作業を開始しますが、膨大な可能性を前に立ち止まってしまいます。 彼らは何をしたいのか、どんなシステムを持ちたいのか知っているかもしれませんが、それらすべてを どこに 置けば良いのか?と。 まあ、彼らのゲームを作る方法は常に彼ら次第です。 無数の方法でノードツリーを構築できます。 しかし、自信がない人のために、この役立つガイドは、まず最初に適切な構造のサンプルを提供できます。

ゲームには常に一種の「エントリポイント」が必要です。開発者はどこで物事が始まるのかを確実に追跡できるため、他の場所でロジックが続く事も追跡できます。この場所は、プログラム内の他のすべてのデータとロジックの鳥瞰図としても機能します。従来のアプリケーションでは、これは「main」関数になります。この場合、メインノードになります。

  • Node "Main" (main.gd)

main.gd スクリプトは、ゲームのプライマリコントローラーとして機能します。

次に、実際のゲーム内「ワールド(World)」(2Dまたは3Dのもの)があります。これは、Mainの子にすることができます。さらに、プロジェクトに必要なさまざまなメニューとウィジェットを管理するゲーム用のプライマリGUIが必要になります。

  • Node "Main" (main.gd)
    • Node2D/Spatial "World" (game_world.gd)
    • Control "GUI" (gui.gd)

レベルを変更する場合、"World"ノードの子を入れ替えることができます。シーンを手動で変更 を使用すると、ユーザーはゲームのワールドがどのように遷移するかを完全に制御できます。

次のステップは、プロジェクトに必要なゲームプレイシステムを検討することです。既存のシステムがある場合...

  1. すべてのデータを内部的に追跡する
  2. グローバルにアクセス可能でなければなりません
  3. 独立して存在する必要があります

...次に、自動ロード 'シングルトン' ノード を作成する必要があります。

注釈

小規模なゲームの場合、コントロールの少ない簡単な代替手段は、単に SceneTree.change_scene() メソッドを呼び出してメインシーンのコンテンツをスワップする「ゲーム(Game)」シングルトンにすることです。この構造により、「ワールド(World)」がメインゲームノードとして保持されます。

また、全てのGUIはシングルトンであるか、「ワールド(World)」の一時的なパーツであるか、またはルートの直接の子として手動で追加する必要があります。そうしないと、シーンの移行中にGUIノードも削除されます。

他のシステムのデータを変更するシステムがある場合、それらをオートロードではなく独自のスクリプトまたはシーンとして定義する必要があります。理由の詳細については、'自動ロードと内部ノード' のドキュメントを参照してください。

ゲーム内の各サブシステムには、シーンツリー内に独自のセクションが必要です。ノードが事実上親の一要素である場合にのみ、親子関係を使用する必要があります。親を削除したときに、その子として一緒に削除されてもかまいませんか?そうでない場合は、兄弟または他の関係として、階層内に独自の場所が必要です。

注釈

場合によっては、これらの分離されたノードが お互い を相対的に配置する必要があります。このために:ref:RemoteTransform <class_RemoteTransform> /RemoteTransform2D ノードを使用できます。ターゲットノードは、Remote*ノードから選択した幾何学変換の要素を条件付きで継承できます。target NodePath を割り当てるには、次のいずれかを使用します:

  1. 割り当てを仲介する信頼できるサードパーティ(おそらく親ノード)。
  2. 目的のノードへの参照を簡単にプルするためのグループ(ターゲットの1つだけが存在すると想定)。

いつこれを行うべきですか?まあ、決定するのは彼ら次第です。ジレンマは、ノードがシーンツリー内を移動して自身を保持する必要がある場合に、細部まで管理する必要があるときに発生します。例えば...

  • 「プレイヤー」ノードを「ルーム」に追加する。

  • 部屋を変更する必要があるため、現在の部屋を削除する必要があり。

  • ルームを削除する前に、プレイヤーを保存および/または移動する必要があります。

    記憶は懸念事項ですか?

    • そうでない場合は、2つの部屋を作成し、プレイヤーを移動して古い部屋を削除するだけです。特に心配はいりません。

    もしそうなら、1つは...

    • プレイヤーをツリー内の別の場所に移動します。
    • 部屋を削除します。
    • 新しい部屋をインスタンス化して追加します。
    • そこにプレイヤーを再度追加します。

問題は、ここでのプレイヤーは「特別なケース」であり、開発者がプロジェクトのためにこの方法でプレイヤーを処理する必要があることを 知っている 必要があります。そのため、この情報をチームとして確実に共有する唯一の方法は、文書化 することです。ただし、実装の詳細をドキュメントに保存することは危険です。これはメンテナンスの負担になり、コードの可読性を悪化させ、プロジェクトの知的コンテンツを不必要に肥大化します。

より大きなアセットを使用したより複雑なゲームでは、シーンツリーのどこかでプレイヤーを完全に保持する方が良いでしょう。これには...

  1. より一貫性が高くなります。
  2. どこかに文書化して保守する必要がある「特別なケース」はありません。
  3. これらの詳細を考慮する必要がないため、エラーが発生する可能性はありません。

これに対し、親の幾何学変換を継承しない子ノードが必要な場合、次のオプションがあります:

  1. declarative ソリューション: それらの間に Node を配置します。幾何学変換を行わないノードとして、ノードはそのような情報を子に渡しません。
  2. imperative ソリューション: CanvasItem ノードまたは Spatial ノードに set_as_toplevel セッターを使用します。これにより、ノードは継承された幾何学変換を無視します。

注釈

ネットワーク化されたゲームを構築する場合、どのノードとゲームプレイシステムがすべてのプレイヤーに関連するのか、権限のあるサーバーに関連するのかを念頭に置いてください。たとえば、ユーザーがすべてのプレイヤーの「PlayerController」ロジックのコピーを持っている必要はありません。代わりに、彼らは自分自身だけが必要です。そのため、これらを「ワールド」とは別のブランチに保持することで、ゲーム接続などの管理を簡素化できます。

シーン編成の鍵は、空間的な用語ではなく関係性の用語でシーンツリーを考えることです。ノードは親の存在に依存する必要がありますか?その必要がないのなら、彼らはどこか他の場所で親に依存せずに一人で成長することができます。もし依存しているのなら、それは彼らがその親の子であるべきという理にかなっています(そして、まだそうでない場合も、その親のシーンの一部である可能性があります)。

これは、ノード自体が構成要素であることを意味しますか? 全く違います。Godotのノードツリーは、構図ではなく集合の関係性を形成します。ただし、ノードを自由に移動できる柔軟性はありますが、デフォルトではそのような移動が不要な場合に最適です。