Unity3D ECS框架Entitas入门学习3:Entity关联GameObject

发表于2017-11-04
评论0 4.9k浏览

总起:

今天主要承接上一节的内容来实现点击右键创建角色、点击左键移动角色的功能。

 

这边会在IComponent中保存Unity场景中GameObject的引用,以便在各个System中使用,并使用Link方法可以在场景中的看到调试信息。

 

如果你第一次学习该内容,请根据第二节内容完成input相关的代码(主要EmitInputSystem)。这里我提供一下已经完成的工程:Entitas简单移动项目


GameComponents:

这边提供了所有后面要用到的Components,直接看代码吧:

  1. // GameComponents.cs  
  2. using UnityEngine;  
  3. using Entitas;  
  4.   
  5. // 当前GameObject所在的位置  
  6. [Game]  
  7. public sealed class PositionComponent : IComponent  
  8. {  
  9.     public Vector2 value;  
  10. }  
  11.   
  12. // 当前GameObject朝向  
  13. [Game]  
  14. public class DirectionComponent : IComponent  
  15. {  
  16.     public float value;  
  17. }  
  18.   
  19. // GameObject显示的图片  
  20. [Game]  
  21. public class ViewComponent : IComponent  
  22. {  
  23.     public GameObject gameObject;  
  24. }  
  25.   
  26. // 显示图片的名称  
  27. [Game]  
  28. public class SpriteComponent : IComponent  
  29. {  
  30.     public string name;  
  31. }  
  32.   
  33. // GameObject是否是Mover的标志  
  34. [Game]  
  35. public class MoverComponent : IComponent  
  36. {  
  37. }  
  38.   
  39. // 移动的目标  
  40. [Game]  
  41. public class MoveComponent : IComponent  
  42. {  
  43.     public Vector2 target;  
  44. }  
  45.   
  46. // 移动完成标志  
  47. [Game]  
  48. public class MoveCompleteComponent : IComponent  
  49. {  
  50. }  


Component的数量有点多,这边要注意自己在写的时候,尽量将Component分的细一些,一个功能对应一个Component,这样在写System的时候会很舒服,自然而然就出来了。

 

以上的代码写完之后,按住Ctrl Shift,再按一下G,生成Component对应的Entitas代码。


点击右键产生一个移动者:

首先我们需要在检测到InputContext有右键按下时,就在GameContext中生成一个代表移动者的GameEntity:

  1. // CreateMoverSystem.cs  
  2. using System.Collections.Generic;  
  3. using Entitas;  
  4. using UnityEngine;  
  5.   
  6. // 监听的是InputContext中的右键数据,所以是InputEntity的ReactiveSystem  
  7. public class CreateMoverSystem : ReactiveSystem<InputEntity>  
  8. {  
  9.     readonly GameContext _gameContext;  
  10.     public CreateMoverSystem(Contexts contexts) : base(contexts.input)  
  11.     {  
  12.         _gameContext = contexts.game;  
  13.     }  
  14.   
  15.     // 收集有RightMouse和MouseDown的InputEntity  
  16.     protected override ICollector<InputEntity> GetTrigger(IContext<InputEntity> context)  
  17.     {  
  18.         return context.CreateCollector(InputMatcher.AllOf(InputMatcher.RightMouse, InputMatcher.MouseDown));  
  19.     }  
  20.   
  21.     // 第二过滤,直接返回true也无所谓  
  22.     protected override bool Filter(InputEntity entity)  
  23.     {  
  24.         return entity.hasMouseDown;  
  25.     }  
  26.   
  27.     // 执行,每次按下右键,设置Mover标志,添加Position、Direction,并添加表现该Entity的图片名称  
  28.     protected override void Execute(List<InputEntity> entities)  
  29.     {  
  30.         foreach (InputEntity e in entities)  
  31.         {  
  32.             GameEntity mover = _gameContext.CreateEntity();  
  33.             mover.isMover = true;  
  34.             mover.AddPosition(e.mouseDown.position);  
  35.             mover.AddDirection(Random.Range(0, 360));  
  36.             mover.AddSprite("head1");  
  37.         }  
  38.     }  
  39. }  

以上的创建Entity代码处理完,下面就是根据Mover标志、Position、Direction等编写对应的System处理具体的情况。

 

Unity中表现一切都要基于GameObject,所以首先第一步就是创建GameObject:

  1. // AddViewSystem.cs  
  2. using System.Collections.Generic;  
  3. using Entitas;  
  4. using Entitas.Unity;  
  5. using UnityEngine;  
  6.   
  7. // 给每个拥有Sprite(该Component只保存了图片名称)的GameEntity添加一个View的GameObject  
  8. public class AddViewSystem : ReactiveSystem<GameEntity>  
  9. {  
  10.     // 为了好看,所有ViewGameObject都放在该父节点下  
  11.     readonly Transform _viewContainer = new GameObject("Game Views").transform;  
  12.     readonly GameContext _context;  
  13.   
  14.     public AddViewSystem(Contexts contexts) : base(contexts.game)  
  15.     {  
  16.         _context = contexts.game;  
  17.     }  
  18.   
  19.     // 创建Sprite的过滤器  
  20.     protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)  
  21.     {  
  22.         return context.CreateCollector(GameMatcher.Sprite);  
  23.     }  
  24.   
  25.     // 第二次过滤,没有View,没有关联上GameObject的情况  
  26.     protected override bool Filter(GameEntity entity)  
  27.     {  
  28.         return entity.hasSprite && !entity.hasView;  
  29.     }  
  30.   
  31.     // 创建一个View的GameObject,并进行关联  
  32.     protected override void Execute(List<GameEntity> entities)  
  33.     {  
  34.         foreach (GameEntity e in entities)  
  35.         {  
  36.             GameObject go = new GameObject("Game View");  
  37.             go.transform.SetParent(_viewContainer, false);  
  38.             e.AddView(go);  // Entity关联GameObject  
  39.             go.Link(e, _context);   // GameObject关联Entity  
  40.         }  
  41.     }  
  42. }  

将以上的System都添加到System组中运行就可以看到效果了。右键点击,在Game Views的父节点就会添加一个Game View节点。

 

在上面的代码中不写go.Link(e,_context)这行完全也是可以的,这行的目标就是为了调试方便,能直接在节点中看到关联的Entity的状况:



节点是有了,但没有图片显示总归是不爽,接下来就搞Sprite渲染的System:

  1. // RenderSpriteSystem.cs  
  2. using System.Collections.Generic;  
  3. using Entitas;  
  4. using UnityEngine;  
  5.   
  6. public class RenderSpriteSystem : ReactiveSystem<GameEntity>  
  7. {  
  8.     public RenderSpriteSystem(Contexts contexts) : base(contexts.game)  
  9.     {  
  10.     }  
  11.   
  12.     // 过滤拥有Sprite的Entity  
  13.     protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)  
  14.     {  
  15.         return context.CreateCollector(GameMatcher.Sprite);  
  16.     }  
  17.   
  18.     protected override bool Filter(GameEntity entity)  
  19.     {  
  20.         return entity.hasSprite && entity.hasView;  
  21.     }  
  22.   
  23.     // 在这里的时候Entity已经创建了关联的节点,所以只要添加Sprite的渲染就OK了。  
  24.     // 所以当然也要注意,在添加程序组的时候要先添加AddViewSystem,在添加该System。  
  25.     // 不然GameObject都没有创建就执行该代码肯定报错的。  
  26.     protected override void Execute(List<GameEntity> entities)  
  27.     {  
  28.         foreach (GameEntity e in entities)  
  29.         {  
  30.             GameObject go = e.view.gameObject;  
  31.   
  32.             // 先获取SpriteRenderer组件,没有获取到再添加,大家还记得只要改变Sprite的内容就会执行这边的代码吧?  
  33.             SpriteRenderer sr = go.GetComponent<SpriteRenderer>();  
  34.             if (sr == null) sr = go.AddComponent<SpriteRenderer>();  
  35.   
  36.             sr.sprite = Resources.Load<Sprite>(e.sprite.name);  
  37.         }  
  38.     }  
  39. }  


写完一添加System,效果就出来了:


当然没有对Position和Direction进行处理,所以生成的所有GameView都在中间,也就是(0, 0)位置。

 

OK,接下来就是对Position和Direction进行处理,一样注意需要放在AddViewSystem之后添加System:

  1. // RenderPositionSystem.cs  
  2. using System.Collections.Generic;  
  3. using Entitas;  
  4.   
  5. // 处理Position值发生变化后的处理,直接赋值就OK,不多说  
  6. public class RenderPositionSystem : ReactiveSystem<GameEntity>  
  7. {  
  8.     public RenderPositionSystem(Contexts contexts) : base(contexts.game)  
  9.     {  
  10.     }  
  11.   
  12.     protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)  
  13.     {  
  14.         return context.CreateCollector(GameMatcher.Position);  
  15.     }  
  16.   
  17.     protected override bool Filter(GameEntity entity)  
  18.     {  
  19.         return entity.hasPosition && entity.hasView;  
  20.     }  
  21.   
  22.     protected override void Execute(List<GameEntity> entities)  
  23.     {  
  24.         foreach (GameEntity e in entities)  
  25.         {  
  26.             e.view.gameObject.transform.position = e.position.value;  
  27.         }  
  28.     }  
  29. }  
  30.   
  31. // RenderDirectionSystem.cs  
  32. using System.Collections.Generic;  
  33. using Entitas;  
  34. using UnityEngine;  
  35.   
  36. // 该System也一样处理比较直接,不多说  
  37. public class RenderDirectionSystem : ReactiveSystem<GameEntity>  
  38. {  
  39.     readonly GameContext _context;  
  40.   
  41.     public RenderDirectionSystem(Contexts contexts) : base(contexts.game)  
  42.     {  
  43.         _context = contexts.game;  
  44.     }  
  45.   
  46.     protected override ICollector<GameEntity> GetTrigger(IContext<GameEntity> context)  
  47.     {  
  48.         return context.CreateCollector(GameMatcher.Direction);  
  49.     }  
  50.   
  51.     protected override bool Filter(GameEntity entity)  
  52.     {  
  53.         return entity.hasDirection && entity.hasView;  
  54.     }  
  55.   
  56.     protected override void Execute(List<GameEntity> entities)  
  57.     {  
  58.         foreach (GameEntity e in entities)  
  59.         {  
  60.             float ang = e.direction.value;  
  61.             e.view.gameObject.transform.rotation = Quaternion.AngleAxis(ang - 90, Vector3.forward);  
  62.         }  
  63.     }  
  64. }  

好了,添加System再运行:



完美!

 

做到这里是不是有这样的感觉:一个Component对应一个System,Component的内容进行改变时就由System进行处理。如果去掉RenderDirectionSystem,则它的Direction不会起效,所有图片就都会正着显示;如果去掉RenderPositionSystem,则Position就不会起效。

 

嗯,有这样的感觉就说明掌握了Entitas的基本用法,Component和ReactiveSystem相对应进行使用。是其框架的主要思想,但也不仅仅局限于此,接下来的两个System用于完成点击左键,图片移动的功能。

 

首先Entitas的Context本身就是个消息池,或者说是NotificationCenter,所以这边要点击左键发出命令进行移动就特别方便。

 

我们首先来看创建命令的System:

  1. // CommandMoveSystem.cs  
  2. using System.Collections.Generic;  
  3. using Entitas;  
  4.   
  5. // 点击左键后,用于创建移动命令  
  6. public class CommandMoveSystem : ReactiveSystem<InputEntity>  
  7. {  
  8.     readonly IGroup<GameEntity> _movers;  
  9.   
  10.     // 获取拥有Mover标志Entity的组  
  11.     public CommandMoveSystem(Contexts contexts) : base(contexts.input)  
  12.     {  
  13.         _movers = contexts.game.GetGroup(GameMatcher.AllOf(GameMatcher.Mover));  
  14.     }  
  15.   
  16.     // 过滤左键点击,和右键点击那个System一样  
  17.     protected override ICollector<InputEntity> GetTrigger(IContext<InputEntity> context)  
  18.     {  
  19.         return context.CreateCollector(InputMatcher.AllOf(InputMatcher.LeftMouse, InputMatcher.MouseDown));  
  20.     }  
  21.   
  22.     protected override bool Filter(InputEntity entity)  
  23.     {  
  24.         return entity.hasMouseDown;  
  25.     }  
  26.   
  27.     // 在Entity上设置移动命令Move  
  28.     protected override void Execute(List<InputEntity> entities)  
  29.     {  
  30.         foreach (InputEntity e in entities)  
  31.         {  
  32.             GameEntity[] movers = _movers.GetEntities();  
  33.             foreach (GameEntity entity in movers)  
  34.                 entity.ReplaceMove(e.mouseDown.position);  
  35.         }  
  36.     }  
  37. }  

添加System,并运行,添加几个Mover,并在屏幕上点击左键时,就会在Game View的Entity Link中就可以看到Move组件,并会随着点击其值发生变化:



直接看上去没有变化的效果,是因为没有刷新,你可以先切换到其他节点,再切换回原节点就能看到。

 

最后一步是移动:

  1. // MoveSystem.cs  
  2. using Entitas;  
  3. using UnityEngine;  
  4.   
  5. // 根据Move命令,执行移动,实现IExecuteSystem的Execute方法,每帧都会执行,  
  6. // ICleanupSystem实现Cleanup方法,同样每帧执行,但会在所有System的Execute之后  
  7. public class MoveSystem : IExecuteSystem, ICleanupSystem  
  8. {  
  9.     readonly IGroup<GameEntity> _moves;  
  10.     readonly IGroup<GameEntity> _moveCompletes;  
  11.     const float _speed = 4f;  
  12.   
  13.     // 获取有移动目标Move组和完成移动MoveComplete组  
  14.     public MoveSystem(Contexts contexts)  
  15.     {  
  16.         _moves = contexts.game.GetGroup(GameMatcher.Move);  
  17.         _moveCompletes = contexts.game.GetGroup(GameMatcher.MoveComplete);  
  18.     }  
  19.   
  20.     // 拥有目标的Mover每帧执行  
  21.     public void Execute()  
  22.     {  
  23.         foreach (GameEntity e in _moves.GetEntities())  
  24.         {  
  25.             // 计算下一个GameObject的位置,并替换  
  26.             Vector2 dir = e.move.target - e.position.value;  
  27.             Vector2 newPosition = e.position.value   dir.normalized * _speed * Time.deltaTime;  
  28.             e.ReplacePosition(newPosition);  
  29.   
  30.             // 计算下一个方向  
  31.             float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg;  
  32.             e.ReplaceDirection(angle);  
  33.   
  34.             // 如果距离在0.5f之内,则判断为移动完成,移除Move命令,并添加移动完成标志  
  35.             float dist = dir.magnitude;  
  36.             if (dist <= 0.5f)  
  37.             {  
  38.                 e.RemoveMove();  
  39.                 e.isMoveComplete = true;  
  40.             }  
  41.         }  
  42.     }  
  43.   
  44.     // 清除所有MoveComplete,MoveComplete暂时没有作用  
  45.     public void Cleanup()  
  46.     {  
  47.         foreach (GameEntity e in _moveCompletes.GetEntities())  
  48.         {  
  49.             e.isMoveComplete = false;  
  50.         }  
  51.     }  
  52. }  

OK,添加System,并运行,效果就出来了:



点击中键更换图片:

这个功能大家想想看该怎么做,能把这个功能做出来的话Entitas就基本掌握了,没有什么思路的话请看看我的方法,再揣摩揣摩:

  1. // MiddleMouseKeyChangeSpriteSystem.cs  
  2. using UnityEngine;  
  3. using Entitas;  
  4.   
  5. public class MiddleMouseKeyChangeSpriteSystem : IExecuteSystem  
  6. {  
  7.     readonly IGroup<GameEntity> _sprites;  
  8.   
  9.     // 获取所有拥有Sprite的组  
  10.     public MiddleMouseKeyChangeSpriteSystem(Contexts contexts)  
  11.     {  
  12.         _sprites = contexts.game.GetGroup(GameMatcher.Sprite);  
  13.     }  
  14.   
  15.     // 如果按下的中键,则替换  
  16.     public void Execute()  
  17.     {  
  18.         if(Input.GetMouseButtonDown(2))  
  19.         {  
  20.             foreach(var e in _sprites.GetEntities())  
  21.             {  
  22.                 e.sprite.name = "head2";  
  23.             }  
  24.         }  
  25.     }  
  26. }  


添加到System组中,在运行,效果就出来了……那是不可能的。

 

这边讲一个知识点,就是你在调试模式下,在Inspector调整Entity的Component,比如说是Sprite,执行的是e.ReplaceSprite方法。也就是说e.sprite.name = "head2"并不会触发ReactiveSystem,这点需要注意。

 

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

 

再试试吧。

 

最终效果:



来自:http://blog.csdn.net/u012632851/article/details/77867799

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引

0个评论