シーン構成
この記事では、シーンコンテンツの効果的な構成に関連するトピックについて説明します。どのノードを使用すればよいでしょうか。どこに配置すればよいでしょうか。どのように相互作用すればよいでしょうか?
個々の結びつきを効果的に構築する方法
Godotユーザーが独自のシーンを作成し始めると、次の問題に遭遇することがよくあります。
彼らは最初にシーンを作成しコンテンツで埋めてから、パーツを分割すべきだという煩わしい思いが募っていき、結局シーンのブランチを別のシーンに保存することにします。 しかしここで、それまで頼りにしていたハードリファレンスがもはや不可能であることに気づきます。シーンを複数の場所で再利用すると、ノードパスがターゲットを見つけられないかったり、エディタで確立されたシグナル接続が壊れたりして問題が発生します。
これらの問題を解決するには、環境の詳細を必要とせずにサブシーンをインスタンス化する必要があります。サブシーンがどのように使用されるかについてこだわらなくても、サブシーンが自動的に作成されることを信頼できる必要があります。
OOP(オブジェクト指向プログラミング)で考慮すべき最も重要なことの1つは、焦点を絞り、単一目的を持つクラスを維持することであり、これらのクラスは他のコードベース部分と loose coupling であるべきです。これにより、オブジェクトのサイズが(保守性のために)小さく保たれ、再利用性が向上します。
これらのOOPのベストプラクティスには、シーン構造とスクリプトの使用におけるベストプラクティスにも いくつかの 影響があります。
可能な限り、依存関係を持たないようにシーンを設計する必要があります。 つまり、必要なものがすべてシーン内に保持されるシーンを作成する必要があります。
シーンが外部コンテキストとやり取りする必要がある場合、経験豊富な開発者は、 依存性の注入 の使用をお勧めします。この手法では、高レベルAPIを使用して低レベルAPIの依存関係を提供します。なぜこれを行うのですか?外部環境に依存するクラスは、誤ってバグや予期しない動作を引き起こす可能性があるためです。
これを行うには、データを公開し、親コンテキストに依存してデータを初期化する必要があります:
シグナルに接続します。非常に安全ですが、動作を始めるためではなく、動作に「応答」するためだけに使用するべきです。シグナル名は通常、"entered"、"skill_activated"、または "item_collected" のような過去形の動詞であることに注意してください。
# Parent $Child.signal_name.connect(method_on_the_object) # Child signal_name.emit() # Triggers parent-specified behavior.
// Parent GetNode("Child").Connect("SignalName", Callable.From(ObjectWithMethod.MethodOnTheObject)); // Child EmitSignal("SignalName"); // Triggers parent-specified behavior.
// Parent Node *node = get_node<Node>("Child"); if (node != nullptr) { // Note that get_node may return a nullptr, which would make calling the connect method crash the engine if "Child" does not exist! // So unless you are 1000% sure get_node will never return a nullptr, it's a good idea to always do a nullptr check. node->connect("signal_name", callable_mp(this, &ObjectWithMethod::method_on_the_object)); } // Child emit_signal("signal_name"); // Triggers parent-specified behavior.
メソッドを呼び出します。動作を開始するために使用されます。
# Parent $Child.method_name = "do" # Child, assuming it has String property 'method_name' and method 'do'. call(method_name) # Call parent-specified method (which child must own).
// Parent GetNode("Child").Set("MethodName", "Do"); // Child Call(MethodName); // Call parent-specified method (which child must own).
// Parent Node *node = get_node<Node>("Child"); if (node != nullptr) { node->set("method_name", "do"); } // Child call(method_name); // Call parent-specified method (which child must own).
Callable プロパティを初期化します。メソッドの所有権は不要なので、メソッドよりも安全です。動作を開始するために使用されます。
# Parent $Child.func_property = object_with_method.method_on_the_object # Child func_property.call() # Call parent-specified method (can come from anywhere).
// Parent GetNode("Child").Set("FuncProperty", Callable.From(ObjectWithMethod.MethodOnTheObject)); // Child FuncProperty.Call(); // Call parent-specified method (can come from anywhere).
// Parent Node *node = get_node<Node>("Child"); if (node != nullptr) { node->set("func_property", Callable(&ObjectWithMethod::method_on_the_object)); } // Child func_property.call(); // Call parent-specified method (can come from anywhere).
ノードまたはその他のオブジェクト参照を初期化します。
# Parent $Child.target = self # Child print(target) # Use parent-specified node.
// Parent GetNode("Child").Set("Target", this); // Child GD.Print(Target); // Use parent-specified node.
// Parent Node *node = get_node<Node>("Child"); if (node != nullptr) { node->set("target", this); } // Child UtilityFunctions::print(target);
NodePathを初期化します。
# Parent $Child.target_path = ".." # Child get_node(target_path) # Use parent-specified NodePath.
// Parent GetNode("Child").Set("TargetPath", NodePath("..")); // Child GetNode(TargetPath); // Use parent-specified NodePath.
// Parent Node *node = get_node<Node>("Child"); if (node != nullptr) { node->set("target_path", NodePath("..")); } // Child get_node<Node>(target_path); // Use parent-specified NodePath.
These options hide the points of access from the child node. This in turn keeps the child loosely coupled to its environment. You can reuse it in another context without any extra changes to its API.
注釈
Although the examples above illustrate parent-child relationships, the same principles apply towards all object relations. Nodes which are siblings should only be aware of their own hierarchies while an ancestor mediates their communications and references.
# 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 partial class Left : Node
{
public Node Target = null;
public void Execute()
{
// Do something with 'Target'.
}
}
public partial class Right : Node
{
public Node Receiver = null;
public Right()
{
Receiver = ResourceLoader.Load<Script>("Receiver.cs").New();
AddChild(Receiver);
}
}
// Parent
get_node<Left>("Left")->target = get_node<Node>("Right/Receiver");
class Left : public Node {
GDCLASS(Left, Node)
protected:
static void _bind_methods() {}
public:
Node *target = nullptr;
Left() {}
void execute() {
// Do something with 'target'.
}
};
class Right : public Node {
GDCLASS(Right, Node)
protected:
static void _bind_methods() {}
public:
Node *receiver = nullptr;
Right() {
receiver = memnew(Node);
add_child(receiver);
}
};
The same principles also apply to non-Node objects that maintain dependencies on other objects. Whichever object owns the other objects should manage the relationships between them.
警告
You should favor keeping data in-house (internal to a scene), though, as placing a dependency on an external context, even a loosely coupled one, still means that the node will expect something in its environment to be true. The project's design philosophies should prevent this from happening. If not, the code's inherent liabilities will force developers to use documentation to keep track of object relations on a microscopic scale; this is otherwise known as development hell. Writing code that relies on external documentation to use it safely is error-prone by default.
To avoid creating and maintaining such documentation, you convert the
dependent node ("child" above) into a tool script that implements
_get_configuration_warnings().
Returning a non-empty PackedStringArray from it will make the Scene dock generate a
warning icon with the string(s) as a tooltip by the node. This is the same icon
that appears for nodes such as the
Area2D node when it has no child
CollisionShape2D nodes defined. The editor
then self-documents the scene through the script code. No content duplication
via documentation is necessary.
このようなGUIを使用すると、プロジェクトユーザーにノードに関する重要な情報をより適切に通知できます。外部依存関係はありますか?これらの依存関係は満たされていますか?などです。他のプログラマー、特にデザイナーとライターに対し、それを構成する作業のために必要な指示を、メッセージを通して明確に伝えます。
So, why does all this complex switcheroo 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 affecting other classes dependent on it.
スクリプトとシーンはエンジンのクラスを拡張したものなので、すべてのOOP原則を順守するべきです。例として…
ノードツリー構造の選択
You might start to work on a game but get overwhelmed by the vast possibilities before you. You might know what you want to do, what systems you want to have, but where do you put them all? How you go about making your game is always up to you. You can construct node trees in countless ways. If you are unsure, this guide can give you a sample of a decent structure to start with.
A game should always have an "entry point"; somewhere you can definitively track where things begin so that you can follow the logic as it continues elsewhere. It also serves as a bird's eye view of all other data and logic in the program. For traditional applications, this is normally a "main" function. In Godot, it's a Main node.
Node "Main" (main.gd)
The main.gd script will serve as the primary controller of your game.
Then you have an in-game "World" (a 2D or 3D one). This can be a child of Main. In addition, you will need a primary GUI for your game that manages the various menus and widgets the project needs.
- Node "Main" (main.gd)
Node2D/Node3D "World" (game_world.gd)
Control "GUI" (gui.gd)
When changing levels, you can then swap out the children of the "World" node. Changing scenes manually gives you full control over how your game world transitions.
The next step is to consider what gameplay systems your project requires. If you have a system that...
すべてのデータを内部的に追跡する
グローバルにアクセス可能でなければなりません
独立して存在する必要があります
... then you should create an autoload 'singleton' node.
注釈
小規模なゲームの場合、制御が少ない簡単な代替手段は「Game」シングルトンを作ることです。これは単に SceneTree.change_scene_to_file() メソッドを呼び出してメインシーンのコンテンツをスワップします。この構造は、「World」をメインゲームノードとしてほぼ維持します。
Any GUI would also need to be either a singleton, a transitory part of the "World", or manually added as a direct child of the root. Otherwise, the GUI nodes would also delete themselves during scene transitions.
If you have systems that modify other systems' data, you should define those as their own scripts or scenes, rather than autoloads. For more information, see Autoloads versus regular nodes.
Each subsystem within your game should have its own section within the SceneTree. You should use parent-child relationships only in cases where nodes are effectively elements of their parents. Does removing the parent reasonably mean that the children should also be removed? If not, then it should have its own place in the hierarchy as a sibling or some other relation.
注釈
In some cases, you need these separated nodes to also position themselves
relative to each other. You can use the
RemoteTransform /
RemoteTransform2D nodes for this purpose.
They will allow a target node to conditionally inherit selected transform
elements from the Remote* node. To assign the target
NodePath, use one of the following:
割り当てを仲介する信頼できるサードパーティ(おそらく親ノード)。
A group, to pull a reference to the desired node (assuming there will only ever be one of the targets).
When you should do this is subjective. The dilemma arises when you must micro-manage when a node must move around the SceneTree to preserve itself. For example...
「プレイヤー」ノードを「ルーム」に追加する。
Need to change rooms, so you must delete the current room.
Before the room can be deleted, you must preserve and/or move the player.
If memory is not a concern, you can...
Create the new room.
Move the player to the new room.
Delete the old room.
If memory is a concern, instead you will need to...
プレイヤーをツリー内の別の場所に移動します。
部屋を削除します。
新しい部屋をインスタンス化して追加します。
Re-add the player to the new room.
The issue is that the player here is a "special case" where the developers must know that they need to handle the player this way for the project. The only way to reliably share this information as a team is to document it. Keeping implementation details in documentation is dangerous. It's a maintenance burden, strains code readability, and unnecessarily bloats the intellectual content of a project.
In a more complex game with larger assets, it can be a better idea to keep the player somewhere else in the SceneTree entirely. This results in:
より一貫性が高くなります。
どこかに文書化して保守する必要がある「特別なケース」はありません。
これらの詳細を考慮する必要がないため、エラーが発生する可能性はありません。
In contrast, if you ever need a child node that does not inherit the transform of its parent, you have the following options:
The declarative solution: place a Node in between them. Since it doesn't have a transform, they won't pass this information to its children.
imperative ソリューション: CanvasItem ノードまたは Node3D ノードに
top_levelプロパティを使用します。これにより、ノードは継承された幾何学変換を無視します。
注釈
If building a networked game, keep in mind which nodes and gameplay systems are relevant to all players versus those just pertinent to the authoritative server. For example, users do not all need to have a copy of every players' "PlayerController" logic - they only need their own. Keeping them in a separate branch from the "world" can help simplify the management of game connections and the like.
シーン編成の鍵は、空間的な用語ではなく関係性の用語でシーンツリーを考えることです。ノードは親の存在に依存する必要がありますか?その必要がないのなら、彼らはどこか他の場所で親に依存せずに一人で成長することができます。もし依存しているのなら、それは彼らがその親の子であるべきという理にかなっています(そして、まだそうでない場合も、その親のシーンの一部である可能性があります)。
Does this mean nodes themselves are components? Not at all. Godot's node trees form an aggregation relationship, not one of composition. But while you still have the flexibility to move nodes around, it is still best when such moves are unnecessary by default.