This document is about: QUANTUM 2
SWITCH TO

Actors

Overview

The ActorSystem provides a convenient way to create custom entity behavior.

Setup

To setup a new actor follow these steps:

  1. Create a new controller class;
  2. Inherits from ActorController and / or UpdatableActorController;
  3. Create new quantum component for specific rollbackable actor data ( optional );
  4. Add non-rollbackable properties to controller;
  5. Override the OnInitialize(), OnDeinitialize() and OnTick() methods;
  6. Write the entity logic;
  7. Rebuild Quantum solution; and,
  8. Use this controller type in Unity by creating a new instance of this class on the EntityComponentController component.

As shown in the diagram, the ActorController class handles the initializing and deintializing an entity, while the UpdatableActorController manages all things updatable.

updatable actor controller
Updatable Actor Controller.

Example Implementations

This section presents several examples of controllers included in the FPS Template.

Destructible Object Controller

C#

internal override sealed void Update(Frame frame, EntityRef entity)
{
    // 1. Check Health state every frame
    
    Health* health = frame.GetComponent<Health>(entity);
    if (health->IsAlive == false)
    {
        Transform3D* transform3D = frame.GetComponent<Transform3D>(entity);

        Destroy(frame, entity, transform3D->Position, transform3D->Rotation);
    }
}

private void Destroy(Frame frame, EntityRef entity, FPVector3 position, FPQuaternion rotation)
{
    EntityRef ownerEntity = frame.TryGetOwnerEntity(entity);

    // 2. Spawn effects
    
    foreach (DestroyEffect effect in DestroyEffects)
    {
        frame.CreateEntity(effect.EntityPrototype, ownerEntity, position + effect.PositionOffset, rotation);
    }
    
    // 3. Invoke event and destroy entity

    frame.Events.DestructibleObjectDestroyed(entity);
    frame.DestroyEntity(entity);
}

Health Area

C#

public unsafe class HealthAreaController : UpdatableActorController, IInteractableTriggerController
{
    //========== PUBLIC MEMBERS ===================================================================================

    public EHealthAction Action        = EHealthAction.Remove;
    public EHealthType   Type          = EHealthType.Health;
    public FP            HealthPerTick = FP._10;
    public FP            TickInterval  = FP._1;
    public bool          InitEnabled   = true;
    public LayerMask     TargetLayerMask;

    //========== ActorController INTERFACE ========================================================================

    internal override void Initialize(Frame frame, EntityRef entity)
    {
        // 1. Add HealthArea component (support for custom rollbackable data), set initial state
        
        HealthArea* healthArea = frame.TryGetOrAddComponent<HealthArea>(entity);
        healthArea->SetEnabled(frame, entity, InitEnabled);
    }

    //========== UpdatableActorController INTERFACE ===============================================================

    internal override void Update(Frame frame, EntityRef entity)
    {
        HealthArea* healthArea = frame.GetComponent<HealthArea>(entity);
        if (healthArea->IsEnabled == false)
            return;
            
        // 2. Health action ticks in intervals

        healthArea->TickDelay -= frame.DeltaTime;

        if (healthArea->TickDelay <= FP._0)
        {
            Tick(frame, entity);

            healthArea->TickDelay += TickInterval;
        }
    }

    //========== IInteractableTriggerController INTERFACE ===================================================================

    bool IInteractableTriggerController.OnEnter(Frame frame, EntityRef entity, EntityRef instigator, TriggerInfo3D info)
    {
        // 3. Only entities with Health component and valid layer are tracked
        
        if (frame.HasComponent<Health>(instigator) == false)
            return false;
        if (info.Other.HasValidLayer(frame, TargetLayerMask) == false)
            return false;

        return true;
    }

    void IInteractableTriggerController.OnExit(Frame frame, EntityRef entity, EntityRef instigator, ExitInfo3D info)
    {
    }

    //========== PRIVATE METHODS ==================================================================================

    private void Tick(Frame frame, EntityRef entity)
    {
        InteractableTrigger* interactableTrigger = frame.GetComponent<InteractableTrigger>(entity);
        QList<EntityRef>     instigators         = frame.ResolveList(interactableTrigger->Instigators);

        if (instigators.Count == 0)
            return;

        // 4. Prepare hit and health data
        
        HitData hitData = new HitData()
        {
            Source       = entity,
            Instigator   = entity,
            BodyPartType = EBodyPartType.None,
            HitSource    = EHitSource.Environment,
        };

        HealthData healthData = new HealthData
        {
            Action        = Action,
            DesiredAmount = HealthPerTick,
            Source        = entity,
            Instigator    = entity,
            Type          = Type,
        };

        // 5. Iterate over all instigators (allowed entities) and hit them if they are alive
        
        for (int i = instigators.Count; i --> 0;)
        {
            EntityRef instigator = instigators[i];
            if (frame.DestroyPending(instigator) == true)
                continue;

            Health* health = frame.GetComponent<Health>(instigator);
            if (health->IsAlive == false)
                continue;

            hitData.Target   = instigator;
            hitData.Position = frame.GetComponent<Transform3D>(instigator)->Position;

            healthData.Target = instigator;

            frame.Signals.Hit(hitData, healthData);
        }
    }
}

Damage Area

It is possible to turn the Health Area shown in the previous section into a Damage Area by tweaking the properties to subtract health and shield rather than add to it.

damage area
Damage Area.

Creating a New Actor

This is the step-by-step procedure to create a new Actor in the FPS Template. The steps are identical for both player actors and AI actors.

  1. In the Quantum solution, create a new MyActorController which inherits from
    • ActorController: for static actors; or,
    • UpdatableActorController: for dynamic actors with update every frame.
  2. Add serializable fields to the controller (configuration)
  3. If needed, create a MyActor.qtn containing the definition for the MyActor component and other data structures such as roll-backable data.
  4. Implement the Initialize(), Deinitialize() and Update() methods in the controller.
  5. Implement the available controller interfaces (e.g.: IInputController).
  6. Recompile the quantum.code solution.
  7. In Unity, create an empty GameObject and add the visual components / model as a child object.
  8. Add the 3 core scripts Entity, EntityPrototype and EntityComponentController to the GameObject.
  9. On the Entity component, set Transform Synchronization to
    • None: if the position / rotation never changes after spawn
    • Simple: if the position / rotation does not need to be interpolated
    • Interpolate / Error Correction: for smooth position / rotation synchronization
  10. On the EntityPrototype component, set Transform to 3D
  11. On the EntityComponentController component, select MyActorController in the ActorController drop-down menu and fill out the defined properties.
  12. Add other entity components to define the entity's behavior (EntityComponentMyActor, EntityComponentHealth, EntityComponentPhysicsCollider3D, etc...).
  13. Create a prefab of the entity (Optional).
  14. Execute the asset resource generation via the Quantum > Generate Asset Resources menu.
  15. The entity is now ready to use - drag and drop it into scene, reference the entity prototype to other entity fields.
Back to top