A technical design for a flexible, co-op ready object interaction system in Godot, covering architecture, component design, and implementation details.
For a co-op game currently in development, I needed to architect an object interaction system that was both flexible and scalable. The system had to meet several key requirements from the outset:
This document details the architectural design for this system in the Godot engine.
Early prototype demonstrating the need for a robust interaction system.
The core of the design is to keep the player and the interactable objects as decoupled as possible. The player should not need to know the specific details of any object it interacts with. Likewise, an object should not need a direct reference to the player. This is achieved using Godot's built-in signals and a standardized interface enforced via duck-typing.
The player character is responsible for three things:
Area3D
node).Any object that can be interacted with must conform to a specific interface by implementing a set of standard methods. In GDScript, we can check for this using has_method()
. This approach avoids rigid class inheritance and allows any node to become interactable.
The required methods are:
can_be_selected_by(player_node)
: Returns true
or false
. This is the gatekeeper method. It allows an object to define its own interaction prerequisites. For example, a heavy door might check if player_node.role == "Warrior"
.on_selected(player_node)
: Called by the player when this object becomes the active target. The object is responsible for its own highlighting or UI feedback.on_deselected(player_node)
: Called when the player looks away or moves out of range.on_interact_start(player_node)
: Triggered when the player presses the interact button.on_interact_end(player_node)
: Triggered when the player releases the interact button.The entire process, from detection to interaction, follows a clear, event-driven sequence.
sequenceDiagram participant Player participant Interactable Object Player->>Interactable Object: Detects object in range Player->>Interactable Object: call can_be_selected_by(self) Interactable Object-->>Player: return true Player->>Player: Set object as current selection Player->>Interactable Object: call on_selected(self) Interactable Object->>Interactable Object: Enable highlight/UI Note over Player: Player presses 'Interact' Player->>Interactable Object: call on_interact_start(self) Interactable Object->>Interactable Object: Begin interaction logic (e.g., open door) Note over Player: Player releases 'Interact' Player->>Interactable Object: call on_interact_end(self) Interactable Object->>Interactable Object: Finalize interaction logic Note over Player: Player moves out of range Player->>Interactable Object: call on_deselected(self) Interactable Object->>Interactable Object: Disable highlight/UI
To manage simultaneous interactions in a co-op environment, each interactable object maintains a list of players_currently_selecting
.
on_selected
is called, the player is added to the list. The highlight is enabled only if the list is not empty.on_deselected
is called, the player is removed. The highlight is disabled only when the list becomes empty.This ensures that the object remains highlighted as long as at least one player is targeting it, preventing flickering UI and race conditions.
This architectural design provides a robust and designer-friendly foundation for all object interactions in the game. By leveraging Godot's core features like signals and embracing a decoupled, interface-driven approach, the system is:
This approach demonstrates how thoughtful architecture can solve complex gameplay requirements in a clean and efficient manner, paving the way for smoother development as the project grows.