Skip to content

feat: AttachableBehaviour and ComponentController #3518

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 37 commits into
base: develop-2.0.0
Choose a base branch
from

Conversation

NoelStephensUnity
Copy link
Collaborator

@NoelStephensUnity NoelStephensUnity commented Jun 24, 2025

AttachableBehaviour and Support Components

The purpose of this PR (feat) is to address the complexity of "picking up" or "dropping" an item in the world which can become complex when using the traditional NetworkObject parenting pattern. In this PR there are three primary components added to help reduce this complexity:

  • AttachableBehaviour: Provides "out of the box" support for attaching (i.e. parenting) a nested child GameObject that includes an AttachableBehaviour component to another nested child GameObject with an AttachableNode component that is associated with a different NetworkObject.
  • AttachableNode: This component is required by the AttachableBehaviour component in order to be able to attach (i.e. parent) to another GameObject without having to parent the entire NetworkObject component the AttachableBehaviour component is associated with.
  • ComponentController: This component provides users the ability to synchronize the enabling or disabling of any Object derived component that has an enabled property.

This PR also incorporates a new "Helpers" subfolder under the NGO components folder where additional helper components will live.

Attaching vs NetworkObject parenting

Fundamentally, attaching is another form of synchronized (i.e. netcode) parenting that does not require one to use the traditional NetworkObject parenting. Attaching a child GameObject nested under a NeworkObject (really the GameObject the NetworkObject component belongs to) will only take the child GameObject and parent it under another child GameObject nested under or on the same GameObject with a different NeworkObject.

NetworkObject parenting

The traditional approach has been to spawn two network prefab instances:
image

Then parent one instance under the other:
image

This is simple enough for many scenarios, but can become cumbersome under more specific scenarios where a user might want to have a "world" version of the item and a "picked up" version of the item.

Attaching

With attaching, a user would create nested GameObject children that represent the item when it is picked up and when it is dropped/placed somewhere in the scene (i.e. world).
image

  • The WorldItemRoot is where the NetworkObject component is placed.
  • The NestedChild-World contains the components needed for the item when it is placed in the world.
  • The NestedChild-PickedUp contains the components needed for the item when it is picked up by a player.

By placing an AttachableBehaviour component on the NestedChild-PickedUp GameObject and an AttachableNode component on the TargetNode, a user can then invoke the AttachableBehaviour.Attach method while passing in the AttachableNode component and the NestedChild-PickedUp GameObject will get parented under the TargetNode while also synchronizing this action with all other clients.
image

AttachableBehaviour

image

The basic functionality of the AttachableBehaviour component provides:

  • The ability to assign (make aware) ComponetController components from any part of the parent-child hierarchy.
    • Each ComponentControllerEntry provides the ability to select when the ComponentController should be triggered (via the Auto Trigger property) and whether its enabled state should be enabled or disabled upon attaching (via the Enable On Attach property). The default setting is to be disabled upon the AttachableBehaviour attaching to an AttachableNode and enabled upon detaching. When the Enable On Attach property is enabled, the ComponentController will be set to enabled upon the AttachableBehaviour attaching to an AttachableNode and disabled upon detaching.
  • The ability to control when an AttachableBehaviour component will automatically detach from an AttachableNode via the Auto Detach property.
    • The Auto Detach property can have any combination of the below flags or none (no flags):
      • On Ownership Changed: Upon ownership changing, the AttachableBehaviour will detach from any AttachableNode it is attached to.
      • On Despawn: Upon the AttachableBehaviour being despawned, it will detach from any AttachableNode it is attached to.
      • On Attach Node Destroy: Just prior to the AttachableNode being destroyed, any attached AttachableBehaviour with this flag will automatically detach from the AttachableNode.

Any of the AttachableBehaviour.AutoDetach settings will be invoked on all instances without the need for the owner to synchronize the end result(i.e. detaching) which provides a level of redundancy for edge case scenarios like a player being disconnected abruptly by the host or by timing out or any scenario where a spawned object is being destroyed with the owner or perhaps being redistributed to another client authority in a distributed authority session. Having the ability to select or deselect any of the auto-detach flags coupled with the ability to derive from AttachableBehaviour provides additional levels of modularity/customization.

AttachableNode

image

The simplest component in the bunch, this provides a valid connection point (i.e. what an AttachableBehaviour can attach to) with the ability to have it automatically detach from any attached AttachableBehaviour instances when it is despawned.

ComponentController

image

Taking the above example into consideration, it would make sense that a user would want to be able to easily control whether a specific component is enabled or disabled when something is attached or detached.

As an example:

  • When the WorldItemRoot is in the "placed in the world" state, it would make sense to disable any MeshRenderer, Collider, and other components on the NestedChild-PickedUp GameObject while enabling similar types of components on the NestedChild-World.
  • When the WorldItemRoot is in the "picked up" state, it would make sense to enable any MeshRenderer, Collider, and other components on the NestedChild-PickedUp GameObject while disabling similar types of components on the NestedChild-World.
  • It would also make sense to synchronize the enabling or disabling of components with all instances.

The ComponentController provides this type of functionality:

  • Can be used with AttachableBehaviour or independently for another purpose.
  • Each assigned component entry can be configured to directly or inversely follow the ComponentController's current state.
  • Each assigned component entry can have an enable and/or disable delay.
    • When invoked internally by AttachableBehaviour, delays are ignored when an AttachableNode is being destroyed and the changes are immediate.

The ComponentController could be daisy chained with minimal user script:

/// <summary>
/// Use as a component in the ComponentController that will
/// trigger the Controller (ComponentController).
/// This pattern can repeat.
/// </summary>
public class DaisyChainedController : MonoBehaviour
{
    public ComponentController Controller;

    private void OnEnable()
    {
        if (!Controller || !Controller.HasAuthority)
        {
            return;
        }
        Controller.SetEnabled(true);
    }

    private void OnDisable()
    {
        if (!Controller || !Controller.HasAuthority)
        {
            return;
        }
        Controller.SetEnabled(false);
    }
}

Example of synchronized RPC driven properties

Both the AttachableBehaviour and the ComponentController provide an example of using synchronized RPC driven properties in place of NetworkVariable. Under certain conditions it is better to use RPCs when a specific order of operations is needed as opposed to NetworkVariables which can update out of order (regarding the order in which certain states were updated) depending upon several edge case scenarios.

Under this condition using reliable RPCs will assure the messages are received in the order they were generated while also reducing the latency time between the change and the non-authority instances being notified of the change. Synchronized RPC driven properties only require overriding the NetworkBehaviour.OnSynchronize method and serializing any properties that need to be synchronized with late joining players or handling network object visibility related scenarios.

Usage Walk Through

Introduction

For example purposes, we will walk through a common scenario where you might want to have a world item that had unique visual and scripted components active while while placed in the world but then can switch to a different set of visual and scripted components when picked up by a player's avatar. Additionally, you might want to be able to easily "attach" only the portion of the item, that is active when picked up, to one of the player's avatar's child nodes. Below is a high-level diagram overview of what both the player and world item network prefabs could look like:

image

Player

The player prefab in the above diagram is not complete, includes the components of interest, and some additional children and components for example purposes. A complete diagram would most definitely have additional components and children. The AttachableNode components provide a "target attach point" that any other spawned network prefab with an AttachableBehaviour could attach itself to.

World Item

This diagram has a bit more detail to it and introduces one possible usage of a ComponentController and AttachableBehaviour. The ComponentController will be used to control the enabling and disabling of components and synchronizing this with non-authority instances. The AttachableBehaviour resides on the child AttachedView's GameObject and will be the catalyst for attaching to a player.

World vs Attached View Modes

image

In the diagram above, we see arrows pointing from the ComponentController to the non-netcode standard Unity components such as a MeshRenderer, Collider, or any other component that should only be enabled when either in "World View" or "Attached View" modes. Below is a screenshot of what the ComponentController would look like in the inspector view:

World Item Component Controller

image

Looking at the ComponentController's Components property, we can see two of the component entries have references to the WorldItemView's BoxCollider and MeshRenderer that are both configured to be enabled when the ComponentController's state is true. We can also see that the CarryView's MeshRenderer is added and configured to be the inverse of the current ComponentController's state. Since the ComponentController's Start Enabled property is enabled we can logically deduce the WorldItem network prefab will start with the WorldItemView being active when spawned. Taking a look at the CarryObject child's properties:

image

We can see the AttachableBehaviour's Component Controllers list contains ComponentControllerEntry (WorldItem Component Controller) that references to the WorldItem's ComponentController. We can also see that the ComponentControllerEntry is configured to trigger on everything (OnAttach and OnDetach) and will set the ComponentController's state to disabled (false). This means when the AttachableBehaviour is attached the ComponentController will be in the disabled state along with the WorldItemView components while the CarryView's MeshRenderer will be enabled.

Summarized Overview:

  • AttachableBehaviour sets the ComponentController state (true/enabled or false/disabled).
  • ComponentController states:
    • Enabled (true)
      • World Item View (enabled/true)
      • Carry View (disabled/false)
    • Disabled (false)
      • World Item View (disabled/false)
      • Carry View (enabled/true)

Attaching

image

The above diagram represents what the Player and World Item spawned objects (including cloned/non-authority instances) would look like once the Attached View object has been parented under the avatar's Right Attach object. The green area and arrow represent the still existing relationship that the Attached View has with the World Item's NetworkObject.

AttachableBehaviour & NetworkObject Relationship (convert to note)

Upon a NetworkObject component being spawned, all associated NetworkBehaviour based component instances, that are directly attached to the NetworkObject's GameObject or are on any child GameObject, will be registered with the NetworkObject instance. Even if a child GameObject containing one or more NetworkBehaviour based component instances is parented under some other GameObject whether it is associated with a spawned NetworkObject or not. Of course, there are additional considerations like what happens when the NetworkObject is de-spawned, how to assure the child attachable returns back to its default parent, and several other edge case scenarios.

AttachableBehaviour leverages from this "spawn lifetime" relationship to provide another type of "parenting" while also taking into consideration the previously mentioned "additional considerations" for you.

NetworkBehaviour.OnNetworkPreDespawn

Added another virtual method to NetworkBehaviour, OnNetworkPreDespawn, that is invoked before running through the despawn sequence for the NetworkObject and all NetworkBehaviour children of the NetworkObject being despawned. This provides an opportunity to do any kind of cleanup up or last micro-second state updates prior to despawning.

Changelog

  • Added: AttachableBehaviour helper component to provide an alternate approach to parenting items without using the NetworkObject parenting.
  • Added : AttachableNode helper component that is used by AttachableBehaviour as the target node for parenting.
  • Added: ComponentController helper component that can be used to synchronize the enabling and disabling of components and can be used in conjunction with AttachableBehaviour.
  • Added: NetworkBehaviour.OnNetworkPreDespawn that is invoked before running through the despawn sequence for the NetworkObject and all NetworkBehaviour children of the NetworkObject being despawned.

Testing and Documentation

  • Includes two new integration tests:
    • AttachableBehaviourTests
    • ComponentControllerTests
  • No internal API documentation changes or additions were necessary.
  • Requires public documentation that provides usage and examples (wip).

Backport

This is a v2.x only feature.

Adding AttachableBehaviour and ObjectController.
Renaming ObjectController to ComponentController.
Added some additional validation checking and handling.
Updated XML API.
Adding helpers meta.
Adding an AttachableNode as the target for AttachableBehaviour.
Refactoring AttachableBehaviour.
Adding new test for attachables.
Replacing any improperly spelled "detatch" with "detach".
XML API and private methods.
Minor XML API fixes.
Simplified the nameof AttachableBehaviour.Detach to just Detach.
Refactoring the ComponentController to provide more flexibility as well as being able to have component entries that will apply the inverse of the current ComponentController's current state....which allows for switching between different sets of components depending upon the controller's state.
Made some minor adjustments while writing the base test for ComponentController.
Adding the base ComponentController test.
switching to a Light component as opposed to BoxCollider as BoxCollider requires the physics package and we are just testing the functionality of ComponentController and not specifically any one other type of component.
Removing using directive.
Updating the change log entries.
@NoelStephensUnity NoelStephensUnity marked this pull request as ready for review July 10, 2025 19:43
@NoelStephensUnity NoelStephensUnity requested a review from a team as a code owner July 10, 2025 19:43
Removing debug log info.
Work in progress adjustments for usability under various scenarios.
Using RPCs and synchronizing the property being set using NetworkBehaviour.OnSynchronize in order to assure order of operations when it comes to messages.

Updated the ComponentController to be able to stagger state updates in the event this occurs (wip and I might remove this part and not allow changes to state until any pending state is finished).
Minor adjustment to the range for delays allow it to be zero.
Fixing spelling issues.
Had to make some minor adjustments in order to assure that users could handle sending any last micro-second tasks on any spawned instances prior to them despawning.

Added NetworkBehaviour.OnNetworkPreDespawn.
Did a slight order of operations on NetworkManager.Shutdown internal in order to assure sending RPCs during despawn would still work.

Minor adjustments to the new helper components associated with this PR.
Updating the AttachableBehaviourTests to include testing that an attachable will be automatically detatched upon despawning the AttachableNode it is attached to.
Updating changelog entry.
Doing a second inspector UI pass to include tool tips and rename each element item to the component's standard inspector view naming where it is the GameObject's name followed by the class name that is separated by capitalization and contained within parenthesis.
Fixing an exception that can occur when you have a network prefab opened for editing and then you delete the prefab asset before exiting the prefab edit mode.
Fixing some PVP related issues.
Several improvements on the attach and detach processing.
Added the ability to tie ComponentControllers to AttachableBehaviours in order to auto-notify when something is attaching and detaching.
Several adjustments to fix the issue with ungraceful disconnects and re-synchronizing attachables.
Added forced change based on flags applied, things like when an AttachableNode is despawning, changing ownership, or being destroyed then local instances, whether authority or not, will all force the attach or detach  state.
Added an internal virtual destroy method on NetworkBehaviour to allow for helper components to assure on destroy script is invoked.
Adding documentation to an undocumented enum value.
Updating the core components based on some bugs discovered during testing.
Updated the AttachableBehaviourTests to validate the different types of auto-detach flag combinations.
Fixed some spelling issues with "detatch" (not sure why I got into that habit)...corrected to "detach".
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant