天天看点

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部分的内容确实蛮重要的,所以底层原理的研究就留到下一章了。

继续阅读