シーン構成

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

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

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

They create their first scene and fill it with content only to eventually end up saving branches of their scene into separate scenes as the nagging feeling that they should split things up starts to accumulate. However, they then notice that the hard references they were able to rely on before are no longer possible. Re-using the scene in multiple places creates issues because the node paths do not find their targets and signal connections established in the editor break.

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

One of the biggest things to consider in OOP is maintaining focused, singular-purpose classes with loose coupling to other parts of the codebase. This keeps the size of objects small (for maintainability) and improves their reusability.

These OOP best practices have several implications for best practices in scene structure and script usage.

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

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

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

  1. Connect to a signal. Extremely safe, but should be used only to "respond" to behavior, not start it. Note that signal names are usually past-tense verbs like "entered", "skill_activated", or "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.
    

These options hide the points of access from the child node. This in turn keeps the child loosely coupled to its environment. One can re-use it in another context without any extra changes to its 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を使用すると、プロジェクトユーザーにノードに関する重要な情報をより適切に通知できます。外部依存関係はありますか?これらの依存関係は満たされていますか?などです。他のプログラマー、特にデザイナーとライターに対し、それを構成する作業のために必要な指示を、メッセージを通して明確に伝えます。

So, why do all this complex switcharoo work? Well, because scenes operate best when they operate alone. If unable to work alone, then working with others anonymously (with minimal hard dependencies, i.e. loose coupling) is the next best thing. Inevitably, changes may need to be made to a class and if these changes cause it to interact with other scenes in unforeseen ways, then things will start to break down. The whole point of all this indirection is to avoid ending up in a situation where changing one class results in adversely effecting other classes.

Scripts and scenes, as extensions of engine classes, should abide by all OOP principles. Examples include...

ノードツリー構造の選択

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

A game should always have a sort of "entry point"; somewhere the developer can definitively track where things begin so that they can follow the logic as it continues elsewhere. This place also serves as a bird's eye view of all of the other data and logic in the program. For traditional applications, this would be the "main" function. In this case, it would be a Main node.

  • 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)」がメインゲームノードとして保持されます。

Any GUI would need to also be a singleton; be a transitory part of the "World"; or be manually added as a direct child of the root. Otherwise, the GUI nodes would also delete themselves during scene transitions.

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

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

注釈

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

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

When should one do this? Well, this is subjective. The dilemma arises when one must micro-manage when a node must move around the SceneTree to preserve itself. For example...

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

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

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

    記憶は懸念事項ですか?

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

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

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

The issue is that the player here is a "special case"; one where the developers must know that they need to handle the player this way for the project. As such, the only way to reliably share this information as a team is to document it. Keeping implementation details in documentation however is dangerous. It's a maintenance burden, strains code readability, and bloats the intellectual content of a project unnecessarily.

In a more complex game with larger assets, it can be a better idea to simply keep the player somewhere else in the SceneTree entirely. This results in:

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

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

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

注釈

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

The key to scene organization is to consider the SceneTree in relational terms rather than spatial terms. Are the nodes dependent on their parent's existance? If not, then they can thrive all by themselves somewhere else. If they are dependent, then it stands to reason that they should be children of that parent (and likely part of that parent's scene if they aren't already).

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