版本: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的狀況:
節點是有了,但沒有圖檔顯示總歸是不爽,接下來就搞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,效果就出來了:
當然沒有對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再運作:
完美!
做到這裡是不是有這樣的感覺:一個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元件,并會随着點選其值發生變化:
直接看上去沒有變化的效果,是因為沒有重新整理,你可以先切換到其他節點,再切換回原節點就能看到。
最後一步是移動:
// 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,并運作,效果就出來了:
點選中鍵更換圖檔:
這個功能大家想想看該怎麼做,能把這個功能做出來的話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")就行了。
再試試吧。
最終效果:
個人:
本來是想介紹Entitas運作時的原理的,也就是底層代碼是如何運作的。但如果關聯View部分的内容确實蠻重要的,是以底層原理的研究就留到下一章了。