Skip to content

UnityView

Guillermo Vilas edited this page Sep 16, 2021 · 3 revisions

Intro

There are many ways to handle the Lifecycle of a View with Entitas and Unity. This article describes a very simple setup to link 1 entity with 1 view that might help you get started.

First we create an abstract class that each of our views can inherit from:

public abstract class UnityView : MonoBehaviour
{
    protected Contexts Contexts;
    protected GameEntity LinkedEntity;

    public virtual void Link(Contexts contexts, GameEntity e)
    {
        Contexts = contexts;
        LinkedEntity = e;
        gameObject.Link(e);
    }
    
    public virtual void DestroyView()
    {
        Destroy(gameObject);
    }
}

The abstract class has to inherit from MonoBehaviour so we can attach it to a prefab. Our views need access to the LinkedEntity in order to listen to changes in the entities components, also we might want to react to global events so we get a connection to Contexts through the Link method.

The Link method basically also replaces the typical Start() and Awake() callbacks from unity.

With gameObject.Link(e) a LinkedEntity (unity)-component will be added to the gameObject providing you with a nice overview of the entities components.

Usage

To create a visual representation of a player character that can move around, we create a PlayerView that inherits from UnityView and attach it to a player-prefab.

[Game, Event(EventTarget.Self)]
public class PositionComponent : IComponent
{
    public Vector3 Value;
}

public class PlayerView : UnityView, IPositionListener
{
    public override void Link(Contexts contexts, GameEntity e)
    {
        base.Link(contexts, e);
        e.AddPositionListener(this);
    }

    public void OnPosition(GameEntity entity, Vector3 value)
    {
        transform.position = value;
    }
}

Since we know the Player Entity has a PositionComponent attached to it and we generated an event system for position by adding the Event(EventTarget.Self) Attribute, we can add a PositionListener to our View.

It is not a good practice to pass object references around with components, but to still be able to interact with the UnityView from within entitas this is a simple solution:

public sealed class UnityViewComponent : IComponent
{
    public UnityView Value;
}

On Runtime we can then create a PlayerView like this:

var playerView = Object.Instantiate(_playerViewPrefab);
var e = _Contexts.game.CreateEntity();
playerView.Link(e); // passing the context is not needed anymore
e.AddUnityView(playerView.GetComponent<PlayerView>());

Destruction

At some point our Player might die and we want to destroy it's entity and view / gameObject. Or we want to load the next level and destroy every gameObject. This is were a DestroyedComponent and DestroySystem come in handy.

public sealed class DestroyedComponent : IComponent
{
}

public sealed class DestroySystem : ReactiveSystem<GameEntity>
{
    public DestroySystem(Contexts contexts) : base(contexts.game) { }

    protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context) =>
        context.CreateCollector(GameMatcher.Destroyed);

    protected override bool Filter(GameEntity entity) => entity.isDestroyed;

    protected override void Execute(List<GameEntity> entities)
    {
        foreach (var e in entities)
        {
            if (e.hasUnityView)
            {
                e.unityView.Value.gameObject.Unlink();
                e.unityView.Value.DestroyView();
            }
           
            e.Destroy();
        }
    }
}

This system gets triggered for all entities flagged with the DestroyedComponent. If there is a UnityView attached to an entity it calls DestroyView() first and then destroys the entity.

This gives you the opportunity to trigger an animation or pool the gameObject on destruction.

public class PlayerView : UnityView, IPositionListener
{
    [SerializeField] private ParticleSystem _deathParticleSystem;

    public override void Link(Contexts contexts, GameEntity e)
    {
        base.Link(contexts, e);
        e.AddPositionListener(this);
    }

    public void OnPosition(GameEntity entity, Vector3 value)
    {
        transform.position = value;
    }
    
    public override void DestroyView()
    {
        _deathParticleSystem.Play();
        Invoke(nameof(DelayedDestroy), time: 1f);
    }
    
    private void DelayedDestroy()
    {
        base.DestroyView();
    }
}

In this example we play a particle system before actually destroying the gameObject one second later.

Clone this wiki locally