版本: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
{
readonly GameContext _gameContext;
public CreateMoverSystem(Contexts contexts) : base(contexts.input)
{
_gameContext = contexts.game;
}
// 收集有RightMouse和MouseDown的InputEntity
protected override ICollector GetTrigger(IContext 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 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
{
// 为了好看,所有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 GetTrigger(IContext 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 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
{
public RenderSpriteSystem(Contexts contexts) : base(contexts.game)
{
}
// 过滤拥有Sprite的Entity
protected override ICollector GetTrigger(IContext 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 entities)
{
foreach (GameEntity e in entities)
{
GameObject go = e.view.gameObject;
// 先获取SpriteRenderer组件,没有获取到再添加,大家还记得只要改变Sprite的内容就会执行这边的代码吧?
SpriteRenderer sr = go.GetComponent();
if (sr == null) sr = go.AddComponent();
sr.sprite = Resources.Load(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
{
public RenderPositionSystem(Contexts contexts) : base(contexts.game)
{
}
protected override ICollector GetTrigger(IContext context)
{
return context.CreateCollector(GameMatcher.Position);
}
protected override bool Filter(GameEntity entity)
{
return entity.hasPosition && entity.hasView;
}
protected override void Execute(List 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
{
readonly GameContext _context;
public RenderDirectionSystem(Contexts contexts) : base(contexts.game)
{
_context = contexts.game;
}
protected override ICollector GetTrigger(IContext context)
{
return context.CreateCollector(GameMatcher.Direction);
}
protected override bool Filter(GameEntity entity)
{
return entity.hasDirection && entity.hasView;
}
protected override void Execute(List 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
{
readonly IGroup _movers;
// 获取拥有Mover标志Entity的组
public CommandMoveSystem(Contexts contexts) : base(contexts.input)
{
_movers = contexts.game.GetGroup(GameMatcher.AllOf(GameMatcher.Mover));
}
// 过滤左键点击,和右键点击那个System一样
protected override ICollector GetTrigger(IContext 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 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 _moves;
readonly IGroup _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 _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部分的内容确实蛮重要的,所以底层原理的研究就留到下一章了。