天天看點

Unity3D ECS架構 Entitas入門學習3 Entity關聯GameObject,顯示一張代表該Entity的圖檔

版本:unity 5.6 語言:C#

總起:

今天主要承接上一節的内容來實作點選右鍵建立角色、點選左鍵移動角色的功能。

這邊會在IComponent中儲存Unity場景中GameObject的引用,以便在各個System中使用,并使用Link方法可以在場景中的看到調試資訊。

如果你第一次學習該内容,請根據第二節内容完成input相關的代碼(主要EmitInputSystem)。這裡我提供一下已經完成的工程:Entitas簡單移動項目。

GameComponents:

這邊提供了所有後面要用到的Components,直接看代碼吧:

// GameComponents.cs
using UnityEngine;
using Entitas;

// 目前GameObject所在的位置
[Game]
public sealed class PositionComponent : IComponent
{
    public Vector2 value;
}

// 目前GameObject朝向
[Game]
public class DirectionComponent : IComponent
{
    public float value;
}

// GameObject顯示的圖檔
[Game]
public class ViewComponent : IComponent
{
    public GameObject gameObject;
}

// 顯示圖檔的名稱
[Game]
public class SpriteComponent : IComponent
{
    public string name;
}

// GameObject是否是Mover的标志
[Game]
public class MoverComponent : IComponent
{
}

// 移動的目标
[Game]
public class MoveComponent : IComponent
{
    public Vector2 target;
}

// 移動完成标志
[Game]
public class MoveCompleteComponent : IComponent
{
}
           

Component的數量有點多,這邊要注意自己在寫的時候,盡量将Component分的細一些,一個功能對應一個Component,這樣在寫System的時候會很舒服,自然而然就出來了。

以上的代碼寫完之後,按住Ctrl + Shift,再按一下G,生成Component對應的Entitas代碼。

點選右鍵産生一個移動者:

首先我們需要在檢測到InputContext有右鍵按下時,就在GameContext中生成一個代表移動者的GameEntity:

// CreateMoverSystem.cs
using System.Collections.Generic;
using Entitas;
using UnityEngine;

// 監聽的是InputContext中的右鍵資料,是以是InputEntity的ReactiveSystem
public class CreateMoverSystem : ReactiveSystem<InputEntity>
{
    readonly GameContext _gameContext;
    public CreateMoverSystem(Contexts contexts) : base(contexts.input)
    {
        _gameContext = contexts.game;
    }

    // 收集有RightMouse和MouseDown的InputEntity
    protected override ICollector<InputEntity> GetTrigger(IContext<InputEntity> context)
    {
        return context.CreateCollector(InputMatcher.AllOf(InputMatcher.RightMouse, InputMatcher.MouseDown));
    }

    // 第二過濾,直接傳回true也無所謂
    protected override bool Filter(InputEntity entity)
    {
        return entity.hasMouseDown;
    }

    // 執行,每次按下右鍵,設定Mover标志,添加Position、Direction,并添加表現該Entity的圖檔名稱
    protected override void Execute(List<InputEntity> entities)
    {
        foreach (InputEntity e in entities)
        {
            GameEntity mover = _gameContext.CreateEntity();
            mover.isMover = true;
            mover.AddPosition(e.mouseDown.position);
            mover.AddDirection(Random.Range(0, 360));
            mover.AddSprite("head1");
        }
    }
}
           

以上的建立Entity代碼處理完,下面就是根據Mover标志、Position、Direction等編寫對應的System處理具體的情況。

在Unity中表現一切都要基于GameObject,是以首先第一步就是建立GameObject:

// AddViewSystem.cs
using System.Collections.Generic;
using Entitas;
using Entitas.Unity;
using UnityEngine;

// 給每個擁有Sprite(該Component隻儲存了圖檔名稱)的GameEntity添加一個View的GameObject
public class AddViewSystem : ReactiveSystem<GameEntity>
{
    // 為了好看,所有ViewGameObject都放在該父節點下
    readonly Transform _viewContainer = new GameObject("Game Views").transform;
    readonly GameContext _context;

    public AddViewSystem(Contexts contexts) : base(contexts.game)
    {
        _context = contexts.game;
    }

    // 建立Sprite的過濾器
    protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)
    {
        return context.CreateCollector(GameMatcher.Sprite);
    }

    // 第二次過濾,沒有View,沒有關聯上GameObject的情況
    protected override bool Filter(GameEntity entity)
    {
        return entity.hasSprite && !entity.hasView;
    }

    // 建立一個View的GameObject,并進行關聯
    protected override void Execute(List<GameEntity> entities)
    {
        foreach (GameEntity e in entities)
        {
            GameObject go = new GameObject("Game View");
            go.transform.SetParent(_viewContainer, false);
            e.AddView(go);  // Entity關聯GameObject
            go.Link(e, _context);   // GameObject關聯Entity
        }
    }
}
           

将以上的System都添加到System組中運作就可以看到效果了。右鍵點選,在Game Views的父節點就會添加一個Game View節點。

在上面的代碼中不寫go.Link(e,_context)這行完全也是可以的,這行的目标就是為了調試友善,能直接在節點中看到關聯的Entity的狀況:

Unity3D ECS架構 Entitas入門學習3 Entity關聯GameObject,顯示一張代表該Entity的圖檔

節點是有了,但沒有圖檔顯示總歸是不爽,接下來就搞Sprite渲染的System:

// RenderSpriteSystem.cs
using System.Collections.Generic;
using Entitas;
using UnityEngine;

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

    // 過濾擁有Sprite的Entity
    protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)
    {
        return context.CreateCollector(GameMatcher.Sprite);
    }

    protected override bool Filter(GameEntity entity)
    {
        return entity.hasSprite && entity.hasView;
    }

    // 在這裡的時候Entity已經建立了關聯的節點,是以隻要添加Sprite的渲染就OK了。
    // 是以當然也要注意,在添加程式組的時候要先添加AddViewSystem,在添加該System。
    // 不然GameObject都沒有建立就執行該代碼肯定報錯的。
    protected override void Execute(List<GameEntity> entities)
    {
        foreach (GameEntity e in entities)
        {
            GameObject go = e.view.gameObject;

            // 先擷取SpriteRenderer元件,沒有擷取到再添加,大家還記得隻要改變Sprite的内容就會執行這邊的代碼吧?
            SpriteRenderer sr = go.GetComponent<SpriteRenderer>();
            if (sr == null) sr = go.AddComponent<SpriteRenderer>();

            sr.sprite = Resources.Load<Sprite>(e.sprite.name);
        }
    }
}
           

寫完一添加System,效果就出來了:

Unity3D ECS架構 Entitas入門學習3 Entity關聯GameObject,顯示一張代表該Entity的圖檔

當然沒有對Position和Direction進行處理,是以生成的所有GameView都在中間,也就是(0, 0)位置。

OK,接下來就是對Position和Direction進行處理,一樣注意需要放在AddViewSystem之後添加System:

// RenderPositionSystem.cs
using System.Collections.Generic;
using Entitas;

// 處理Position值發生變化後的處理,直接指派就OK,不多說
public class RenderPositionSystem : ReactiveSystem<GameEntity>
{
    public RenderPositionSystem(Contexts contexts) : base(contexts.game)
    {
    }

    protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)
    {
        return context.CreateCollector(GameMatcher.Position);
    }

    protected override bool Filter(GameEntity entity)
    {
        return entity.hasPosition && entity.hasView;
    }

    protected override void Execute(List<GameEntity> entities)
    {
        foreach (GameEntity e in entities)
        {
            e.view.gameObject.transform.position = e.position.value;
        }
    }
}

// RenderDirectionSystem.cs
using System.Collections.Generic;
using Entitas;
using UnityEngine;

// 該System也一樣處理比較直接,不多說
public class RenderDirectionSystem : ReactiveSystem<GameEntity>
{
    readonly GameContext _context;

    public RenderDirectionSystem(Contexts contexts) : base(contexts.game)
    {
        _context = contexts.game;
    }

    protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)
    {
        return context.CreateCollector(GameMatcher.Direction);
    }

    protected override bool Filter(GameEntity entity)
    {
        return entity.hasDirection && entity.hasView;
    }

    protected override void Execute(List<GameEntity> entities)
    {
        foreach (GameEntity e in entities)
        {
            float ang = e.direction.value;
            e.view.gameObject.transform.rotation = Quaternion.AngleAxis(ang - 90, Vector3.forward);
        }
    }
}
           

好了,添加System再運作:

Unity3D ECS架構 Entitas入門學習3 Entity關聯GameObject,顯示一張代表該Entity的圖檔

完美!

做到這裡是不是有這樣的感覺:一個Component對應一個System,Component的内容進行改變時就由System進行處理。如果去掉RenderDirectionSystem,則它的Direction不會起效,所有圖檔就都會正着顯示;如果去掉RenderPositionSystem,則Position就不會起效。

嗯,有這樣的感覺就說明掌握了Entitas的基本用法,Component和ReactiveSystem相對應進行使用。是其架構的主要思想,但也不僅僅局限于此,接下來的兩個System用于完成點選左鍵,圖檔移動的功能。

首先Entitas的Context本身就是個消息池,或者說是NotificationCenter,是以這邊要點選左鍵發出指令進行移動就特别友善。

我們首先來看建立指令的System:

// CommandMoveSystem.cs
using System.Collections.Generic;
using Entitas;

// 點選左鍵後,用于建立移動指令
public class CommandMoveSystem : ReactiveSystem<InputEntity>
{
    readonly IGroup<GameEntity> _movers;

    // 擷取擁有Mover标志Entity的組
    public CommandMoveSystem(Contexts contexts) : base(contexts.input)
    {
        _movers = contexts.game.GetGroup(GameMatcher.AllOf(GameMatcher.Mover));
    }

    // 過濾左鍵點選,和右鍵點選那個System一樣
    protected override ICollector<InputEntity> GetTrigger(IContext<InputEntity> context)
    {
        return context.CreateCollector(InputMatcher.AllOf(InputMatcher.LeftMouse, InputMatcher.MouseDown));
    }

    protected override bool Filter(InputEntity entity)
    {
        return entity.hasMouseDown;
    }

    // 在Entity上設定移動指令Move
    protected override void Execute(List<InputEntity> entities)
    {
        foreach (InputEntity e in entities)
        {
            GameEntity[] movers = _movers.GetEntities();
            foreach (GameEntity entity in movers)
                entity.ReplaceMove(e.mouseDown.position);
        }
    }
}
           

添加System,并運作,添加幾個Mover,并在螢幕上點選左鍵時,就會在Game View的Entity Link中就可以看到Move元件,并會随着點選其值發生變化:

Unity3D ECS架構 Entitas入門學習3 Entity關聯GameObject,顯示一張代表該Entity的圖檔

直接看上去沒有變化的效果,是因為沒有重新整理,你可以先切換到其他節點,再切換回原節點就能看到。

最後一步是移動:

// MoveSystem.cs
using Entitas;
using UnityEngine;

// 根據Move指令,執行移動,實作IExecuteSystem的Execute方法,每幀都會執行,
// ICleanupSystem實作Cleanup方法,同樣每幀執行,但會在所有System的Execute之後
public class MoveSystem : IExecuteSystem, ICleanupSystem
{
    readonly IGroup<GameEntity> _moves;
    readonly IGroup<GameEntity> _moveCompletes;
    const float _speed = 4f;

    // 擷取有移動目标Move組和完成移動MoveComplete組
    public MoveSystem(Contexts contexts)
    {
        _moves = contexts.game.GetGroup(GameMatcher.Move);
        _moveCompletes = contexts.game.GetGroup(GameMatcher.MoveComplete);
    }

    // 擁有目标的Mover每幀執行
    public void Execute()
    {
        foreach (GameEntity e in _moves.GetEntities())
        {
            // 計算下一個GameObject的位置,并替換
            Vector2 dir = e.move.target - e.position.value;
            Vector2 newPosition = e.position.value + dir.normalized * _speed * Time.deltaTime;
            e.ReplacePosition(newPosition);

            // 計算下一個方向
            float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;
            e.ReplaceDirection(angle);

            // 如果距離在0.5f之内,則判斷為移動完成,移除Move指令,并添加移動完成标志
            float dist = dir.magnitude;
            if (dist <= 0.5f)
            {
                e.RemoveMove();
                e.isMoveComplete = true;
            }
        }
    }

    // 清除所有MoveComplete,MoveComplete暫時沒有作用
    public void Cleanup()
    {
        foreach (GameEntity e in _moveCompletes.GetEntities())
        {
            e.isMoveComplete = false;
        }
    }
}
           

OK,添加System,并運作,效果就出來了:

Unity3D ECS架構 Entitas入門學習3 Entity關聯GameObject,顯示一張代表該Entity的圖檔

點選中鍵更換圖檔:

這個功能大家想想看該怎麼做,能把這個功能做出來的話Entitas就基本掌握了,沒有什麼思路的話請看看我的方法,再揣摩揣摩:

// MiddleMouseKeyChangeSpriteSystem.cs
using UnityEngine;
using Entitas;

public class MiddleMouseKeyChangeSpriteSystem : IExecuteSystem
{
    readonly IGroup<GameEntity> _sprites;

    // 擷取所有擁有Sprite的組
    public MiddleMouseKeyChangeSpriteSystem(Contexts contexts)
    {
        _sprites = contexts.game.GetGroup(GameMatcher.Sprite);
    }

    // 如果按下的中鍵,則替換
    public void Execute()
    {
        if(Input.GetMouseButtonDown(2))
        {
            foreach(var e in _sprites.GetEntities())
            {
                e.sprite.name = "head2";
            }
        }
    }
}
           

添加到System組中,在運作,效果就出來了……那是不可能的。

這邊講一個知識點,就是你在調試模式下,在Inspector調整Entity的Component,比如說是Sprite,執行的是e.ReplaceSprite方法。也就是說e.sprite.name = "head2"并不會觸發ReactiveSystem,這點需要注意。

把上面的e.sprite.name = "head2"改成e.ReplaceSprite("head2")就行了。

再試試吧。

最終效果:

Unity3D ECS架構 Entitas入門學習3 Entity關聯GameObject,顯示一張代表該Entity的圖檔

個人:

本來是想介紹Entitas運作時的原理的,也就是底層代碼是如何運作的。但如果關聯View部分的内容确實蠻重要的,是以底層原理的研究就留到下一章了。

繼續閱讀