Skip to content

Architecture

This document provides guidelines for developing XRC Toolkit packages following the Logic-Input-Feedback architecture pattern.

Overview

All XRC packages should follow a consistent three-component architecture that separates concerns between core functionality, user input handling, and user feedback. This pattern ensures modularity, maintainability, and consistency across the entire XRC ecosystem.

Core Architecture Pattern: Logic-Input-Feedback

Every XRC package implements three distinct components that work together:

%%{init: {'theme':'neutral'}}%%
classDiagram
    class PackageLogic {
        -XRBaseInteractor m_Interactor
        -float m_Parameter
        +XRBaseInteractor interactor
        +float parameter
        +void SetParameter(float value)
        +event Action~bool~ stateChanged
    }

    class PackageInput {
        -InputActionProperty m_Action
        -PackageLogic m_Logic
        +void OnActionPerformed()
    }

    class PackageFeedback {
        -PackageLogic m_Logic
        +void OnStateChanged(bool state)
    }

    PackageInput --> PackageLogic : calls methods
    PackageLogic --> PackageFeedback : fires events

1. Logic Component ({PackageName}Logic)

Responsibility: Implement the core functionality and business logic of the package.

Key Requirements: - Contains all mathematical calculations and state management - Exposes public properties with private serialized fields for Unity Inspector - Uses component events for communication with feedback components - Independent of input and feedback concerns - Often manages XR Interaction Toolkit components (interactors, colliders)

Example Pattern:

public class SphereSelectLogic : MonoBehaviour
{
    [SerializeField]
    private XRDirectInteractor m_Interactor;
    public XRDirectInteractor interactor
    {
        get => m_Interactor;
        set => m_Interactor = value;
    }

    [SerializeField]
    private float m_Radius = 0.04f;
    public float radius => m_Radius;

    public void SetRadius(float radiusValue)
    {
        m_Radius = Mathf.Clamp(radiusValue, m_MinRadius, m_MaxRadius);
        // Update collider and attach transform
    }
}

2. Input Component ({PackageName}Input)

Responsibility: Handle all user input and translate it to method calls on the Logic component.

Key Requirements: - Uses [RequireComponent(typeof({PackageName}Logic))] to enforce dependency - Subscribes to Unity Input System actions in lifecycle methods - Gets Logic component reference in Awake() or Start() - Only handles input - no business logic

Example Pattern:

[RequireComponent(typeof(SphereSelectLogic))]
public class SphereSelectInput : MonoBehaviour
{
    [SerializeField]
    private InputActionProperty m_ChangeRadius;

    private SphereSelectLogic m_Logic;

    private void Awake()
    {
        m_Logic = GetComponent<SphereSelectLogic>();
    }

    private void Start()
    {
        m_ChangeRadius.action.performed += OnRadiusChanged;
    }

    private void OnEnable() => m_ChangeRadius.action.Enable();
    private void OnDisable() => m_ChangeRadius.action.Disable();

    private void OnRadiusChanged(InputAction.CallbackContext context)
    {
        Vector2 input = context.ReadValue<Vector2>();
        float newRadius = m_Logic.radius + input.y * Time.deltaTime;
        m_Logic.SetRadius(newRadius);
    }
}

3. Feedback Component ({PackageName}Feedback)

Responsibility: Provide visual, audio, or haptic feedback based on the Logic component's state.

Key Requirements: - Uses [RequireComponent(typeof({PackageName}Logic))] to enforce dependency - Subscribes to Logic component events, unsubscribes in OnDisable() - Creates visual elements programmatically when needed - Reacts to state changes rather than driving them

Example Pattern:

[RequireComponent(typeof(GoGoLogic))]
public class GoGoFeedback : MonoBehaviour
{
    [SerializeField]
    private GameObject m_VirtualHandPrefab;

    private GoGoLogic m_Logic;
    private GameObject m_VirtualHand;

    private void Start()
    {
        m_Logic = GetComponent<GoGoLogic>();
        m_Logic.runStarted += OnRunStarted;
        m_Logic.runStopped += OnRunStopped;
    }

    private void OnDisable()
    {
        m_Logic.runStarted -= OnRunStarted;
        m_Logic.runStopped -= OnRunStopped;
    }

    private void OnRunStarted()
    {
        m_VirtualHand = Instantiate(m_VirtualHandPrefab);
    }

    private void OnRunStopped()
    {
        if (m_VirtualHand != null)
            Destroy(m_VirtualHand);
    }
}

Package Structure Requirements

File Organization

Runtime/
├── {PackageName}Logic.cs          # Core functionality
├── {PackageName}Input.cs          # Input handling
├── {PackageName}Feedback.cs       # User feedback
└── XRC.Toolkit.{PackageName}.asmdef

Assembly Definition

Each package must include an assembly definition file:

{
    "name": "XRC.Toolkit.{PackageName}",
    "rootNamespace": "XRC.Toolkit.{PackageName}",
    "references": [
        "Unity.XR.Interaction.Toolkit",
        "Unity.InputSystem"
    ],
    "includePlatforms": [],
    "excludePlatforms": [],
    "allowUnsafeCode": false,
    "overrideReferences": false,
    "precompiledReferences": [],
    "autoReferenced": true,
    "defineConstraints": [],
    "versionDefines": [],
    "noEngineReferences": false
}

Namespace Convention

All scripts must use the namespace pattern: XRC.Toolkit.{PackageName}