ECS: Complete Code

Complete code for this lesson

^ConjureKitManager.cs^:

using System.Collections.Generic;
using UnityEngine;
using Auki.ConjureKit;
using UnityEngine.UI;
using Auki.ConjureKit.Manna;
using Auki.Ur;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;
using State = Auki.ConjureKit.State;

public class ConjureKitManager : MonoBehaviour
{
    [SerializeField] private Camera arCamera;
    [SerializeField] private ARSession arSession;
    [SerializeField] private ARRaycastManager arRaycastManager;

    [SerializeField] private Text sessionState;
    [SerializeField] private Text sessionID;

    [SerializeField] private GameObject cube;
    [SerializeField] private Button spawnButton;

    [SerializeField] Button qrCodeButton;
    private bool _qrCodeBool;

    private IConjureKit _conjureKit;
    private Manna _manna;

    private ARCameraManager _arCameraManager;
    private Texture2D _videoTexture;

    [SerializeField] private GameObject fingertipLandmark;
    private HandTracker _handTracker;
    private bool _landmarksVisualizeBool = false;

    [SerializeField] private AROcclusionManager arOcclusionManager;
    private bool _occlusionBool = true;

    [SerializeField] private Transform arSessionOrigin;

    private ColorSystem _colorSystem;
    private Dictionary<uint, Renderer> _cubes = new Dictionary<uint, Renderer>();

    void Start()
    {
        _arCameraManager = arCamera.GetComponent<ARCameraManager>();

        _conjureKit = new ConjureKit(
            arCamera.transform,
            "YOUR_APP_KEY",
            "YOUR_APP_SECRET");

        _manna = new Manna(_conjureKit);
        _manna.GetOrCreateFrameFeederComponent().AttachMannaInstance(_manna);

        _conjureKit.OnStateChanged += state =>
        {
            if (state == State.JoinedSession)
            {
                Debug.Log("State.JoinedSession  " + Time.realtimeSinceStartup);
            }

            if (state == State.Calibrated)
            {
                Debug.Log("State.Calibrated  " + Time.realtimeSinceStartup);
            }

            sessionState.text = state.ToString();
            ToggleControlsState(state == State.Calibrated);
        };

        _conjureKit.OnJoined += session =>
        {
            Debug.Log("OnJoined " + Time.realtimeSinceStartup);
            sessionID.text = session.Id.ToString();

            _colorSystem = new ColorSystem(session);
            session.RegisterSystem(_colorSystem, () => Debug.Log("System registered in session"));
            _colorSystem.OnColorComponentUpdated += OnColorComponentUpdated;
        };

        _conjureKit.OnLeft += session =>
        {
            sessionID.text = "";
        };

        _conjureKit.OnEntityAdded += CreateCube;
        _conjureKit.Connect();

        _handTracker = HandTracker.GetInstance();
        _handTracker.SetARSystem(arSession, arCamera, arRaycastManager);

        _handTracker.OnUpdate += (landmarks, translations, isRightHand, score) =>
        {
            if (score[0] > 0 && _landmarksVisualizeBool)
            {
                var handPosition = new Vector3(
                    translations[0],
                    translations[1],
                    translations[2]);

                var pointerLandmarkIndex = 8 * 3; // Index fingertip
                var pointerLandMarkPosition = new Vector3(
                    landmarks[pointerLandmarkIndex + 0],
                    landmarks[pointerLandmarkIndex + 1],
                    landmarks[pointerLandmarkIndex + 2]);

                fingertipLandmark.SetActive(true);

                fingertipLandmark.transform.position =
                    arCamera.transform.TransformPoint(handPosition + pointerLandMarkPosition);
            }
            else
            {
                fingertipLandmark.SetActive(false);
            }
        };

        _handTracker.Start();
    }

    private void Update()
    {
        _handTracker.Update();
    }

    private void ToggleControlsState(bool interactable)
    {
        if (spawnButton) spawnButton.interactable = interactable;
        if (qrCodeButton) qrCodeButton.interactable = interactable;
    }

    public void ToggleLighthouse()
    {
        _qrCodeBool = !_qrCodeBool;
        _manna.SetLighthouseVisible(_qrCodeBool);
    }

    public void ToggleHandLandmarks()
    {
        _landmarksVisualizeBool = !_landmarksVisualizeBool;

        if (_landmarksVisualizeBool)
        {
            _handTracker.ShowHandMesh();
        }
        else
        {
            _handTracker.HideHandMesh();
        }
    }

    public void ToggleOcclusion()
    {
        _occlusionBool = !_occlusionBool;

        arOcclusionManager.requestedHumanDepthMode = _occlusionBool ? HumanSegmentationDepthMode.Fastest : HumanSegmentationDepthMode.Disabled;
        arOcclusionManager.requestedHumanStencilMode = _occlusionBool ? HumanSegmentationStencilMode.Fastest : HumanSegmentationStencilMode.Disabled;
        arOcclusionManager.requestedEnvironmentDepthMode = _occlusionBool ? EnvironmentDepthMode.Fastest : EnvironmentDepthMode.Disabled;
    }

    public void CreateCubeEntity()
    {
        if (_conjureKit.GetState() != State.Calibrated)
            return;

        Vector3 position = arCamera.transform.position + arCamera.transform.forward * 0.5f;
        Quaternion rotation = Quaternion.Euler(0, arCamera.transform.eulerAngles.y, 0);

        Pose entityPos = new Pose(position, rotation);

        _conjureKit.GetSession().AddEntity(
            entityPos,
            onComplete: entity =>
            {
                // Initialize with white color
                _colorSystem.SetColor(entity.Id, Color.white);

                CreateCube(entity);
            },
            onError: error => Debug.Log(error));
    }

    private void CreateCube(Entity entity)
    {
        if (entity.Flag == EntityFlag.EntityFlagParticipantEntity) return;

        var pose = _conjureKit.GetSession().GetEntityPose(entity);
        var touchableCube = Instantiate(cube, pose.position, pose.rotation).GetComponent<TouchableByHand>();
        _cubes[entity.Id] = touchableCube.GetComponent<Renderer>();
        _cubes[entity.Id].material.color = _colorSystem.GetColor(entity.Id);

        touchableCube.OnTouched += () =>
        {
            _colorSystem.SetColor(entity.Id, Random.ColorHSV());
            _cubes[entity.Id].material.color = _colorSystem.GetColor(entity.Id);
        };
    }

    private void OnColorComponentUpdated(uint entityId, Color color)
    {
        _cubes[entityId].material.color = color;
    }
}

^ColorSystem.cs^

using System;
using System.Collections.Generic;
using Auki.ConjureKit;
using Auki.ConjureKit.ECS;
using UnityEngine;

/// <summary>
/// The ColorSystem adds and deletes the Color component,
/// maintains and updates a local map with component data 
/// </summary>
public class ColorSystem : SystemBase
{
    // The unique name of the component
    private const string COLOR_COMPONENT_NAME = "color";

    /// <summary>
    /// Triggered when a component data is updated by another participant
    /// </summary>
    public event Action<uint, Color> OnColorComponentUpdated;

    // Local Color component data map
    private readonly IDictionary<uint, Color> _entityColorDataMap = new Dictionary<uint, Color>();

    public ColorSystem(Session session) : base(session)
    {
    }

    // The system will be notified when any component in the returned array is updated or removed
    public override string[] GetComponentTypeNames()
    {
        return new[] { COLOR_COMPONENT_NAME };
    }

    /// Broadcast from the server when another participant updates a Color component with new data.
    public override void Update(IReadOnlyList<(EntityComponent component, bool localChange)> updated)
    {
        foreach (var (entityComponent, localChange) in updated)
        {
            // Update the local data and notify about the update
            _entityColorDataMap[entityComponent.EntityId] = ByteArrayToColor(entityComponent.Data);
            OnColorComponentUpdated?.Invoke(entityComponent.EntityId, _entityColorDataMap[entityComponent.EntityId]);
        }

    }


    /// Broadcast from server when another participant removes a Color component from an entity
    public override void Delete(IReadOnlyList<(EntityComponent component, bool localChange)> deleted)
    {
        foreach (var (entityComponent, localChange) in deleted)
        {
            var entity = _session.GetEntity(entityComponent.EntityId);
            if (entity == null) continue;

            _entityColorDataMap.Remove(entity.Id);
        }
    }



    /// <summary>
    /// Tries to update the Color component data locally and broadcast the update to other participants.
    /// </summary>
    /// <returns> False if entity does not exists, true if component was added/updated successfully.</returns>
    public bool SetColor(uint entityId, Color color)
    {
        // Check if the entity with the given id exists
        var entity = _session.GetEntity(entityId);
        if (entity == null) return false;

        // Store the data locally  
        _entityColorDataMap[entityId] = color;

        // If the entity doesn't already have Color component add one
        var component = _session.GetEntityComponent(entityId, COLOR_COMPONENT_NAME);
        if (component == null)
        {
            _session.AddComponent(
                COLOR_COMPONENT_NAME,
                entityId,
                ColorToByteArray(color),
                () => { },
                error => Debug.LogError(error)
            );

            return true;
        }
        else
        {
            return _session.UpdateComponent(
                COLOR_COMPONENT_NAME,
                entityId,
                ColorToByteArray(color)
            );
        }
    }

    /// <summary>
    /// Get the local Color component data
    /// </summary>
    public Color GetColor(uint entityId)
    {
        if (_session.GetEntity(entityId) == null || !_entityColorDataMap.ContainsKey(entityId))
            return Color.clear;

        return _entityColorDataMap[entityId];
    }

    /// <summary>
    /// Delete the component locally and notify the other participants
    /// </summary>
    public void DeleteColor(uint entityId)
    {
        _session.DeleteComponent(COLOR_COMPONENT_NAME, entityId, () =>
        {
            _entityColorDataMap.Remove(entityId);
        });
    }

    // Convert Color32 to byte array
    private byte[] ColorToByteArray(Color32 color)
    {
        byte[] colorBytes = new byte[4];
        colorBytes[0] = color.r;
        colorBytes[1] = color.g;
        colorBytes[2] = color.b;
        colorBytes[3] = color.a;

        return colorBytes;
    }

    // Convert byte array to Color32 
    private Color32 ByteArrayToColor(byte[] bytes)
    {
        if (bytes.Length < 4)
        {
            Debug.LogError("Byte array must have at least 4 elements (R, G, B, A).");
            return Color.clear;
        }

        byte r = bytes[0];
        byte g = bytes[1];
        byte b = bytes[2];
        byte a = bytes[3];

        Color32 color = new Color32(r, g, b, a);
        return color;
    }
}

^TouchableByHand.cs^:

using System;
using UnityEngine;

public class TouchableByHand : MonoBehaviour
{
    public event Action OnTouched;

    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("hand"))
        {
            OnTouched?.Invoke();
        }
    }
}

The full code for this tutorial can be found on GitHub on the tutorial/ecs branch.

The complete project with all parts and the latest packages is on the master branch of the same repo.

This is the end of this lesson

Need a refresher on the essentials?

Check out more lessons, DIY kits and essentials reading material at the developer learning centre homepage.

Want to build on the posemesh and need a helping hand?

Apply for a grant of AUKI tokens to get your project off the ground, and work directly with the Auki Labs team to get your creation to market. Successful applicants may be granted up to 100k USD worth of AUKI tokens, and development and marketing support from the Auki Labs team.