question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Using Entitas in generic way, instead of generators

See original GitHub issue

Sorry that’s off topic, what is the advantage of using generators in particular, instead, for example, generic methods or barehands? Why there no any of tutorial, how to use entitas without generators notimetoexplain - use generators? Lack of time to make some, I think.

Today I spend whole day trying to work with Entitas and Generators, but it always broke all my code when I remove some parameters from components, or rename it. And that is annoying. 1 minute to change component, 30 minutes to generate code somehow… And there only TWO COMPONENTS. >___<. How it work when there ten of them, thousands, how it use with other teammates? Yeah, I know about compilers, reflections, mono, roslyn and etc, but it is so hard to work with(especially on mac), I did not even try to use them because of complexity.

I start thinking about two solution, first one, copy components in their generated environment (in different namespace, or in class), and use in system only components from there. It prevents of broke code and interrupt compiling process (in theory). Because it very hard to implement with one evening with my knowledge about entitas, I found second solution, that I’m prefer: Get some generated classes, and make them in generic way. It took about three hours and I make some working solution to start working with Entitas without generator.

Here are a my three hours code, maybe need to add some methods… Context:

namespace Generic {
// IInitializable is Zenject interface
	public class Context<TEntity> : Entitas.Context<TEntity>, IInitializable where TEntity : class, IEntity, new() {
		public readonly LookupTable Table;
		public readonly Matcher<TEntity> Matcher;

		public Context(string name, LookupTable table, Matcher<TEntity> matcher)
			: base(table.Size, 0, new ContextInfo(name, table.Names, table.Components)) {
			Table = table;
			Matcher = matcher;
		}

		public void Initialize() {
#if(!ENTITAS_DISABLE_VISUAL_DEBUGGING && UNITY_EDITOR)
			if (!UnityEngine.Application.isPlaying)
				return;
			var observer = new ContextObserver(this);
			UnityEngine.Object.DontDestroyOnLoad(observer.gameObject);
#endif
		}

		public TEntity Get<T>() where T : IComponent {
			return GetGroup(Matcher.For<T>()).GetSingleEntity();
		}

		public bool Has<T>() where T : IComponent {
			return Get<T>() != null;
		}

		public TEntity Set<T>(T component) where T : IComponent {
			if (Has<T>())
				throw new Exception("Something went wrong with setting " + typeof(T) + " component.");
			TEntity entity = CreateEntity();
			entity.AddComponent(Table.IndexOf<T>(), component);
			return entity;
		}

		public TEntity Replace<T>(T component) where T : IComponent {
			TEntity entity = Get<T>();
			if (entity == null)
				return Set(component);
			entity.ReplaceComponent(Table.IndexOf<T>(), component);
			return entity;
		}

		public void Remove<T>() where T : IComponent {
			DestroyEntity(Get<T>());
		}

	}
}

LookupTable:

namespace Generic {
	public class LookupTable {

		public Type[] Components { get; private set; }
		public string[] Names { get; private set; }

		public int Size {
			get { return Components.Length; }
		}

		public LookupTable(Type[] components) {
			Components = components;
			Names = new string[components.Length];
			for (int i = 0; i < components.Length; i++) {
				Names[i] = components[i].ToString();
			}
		}

		public int IndexOf<T>() where T : IComponent {
			// Todo assert if -1
			return Array.IndexOf(Components, typeof(T));
		}

	}
}

Matcher

namespace Generic {
	public class Matcher<TEntity> where TEntity : class, IEntity, new() {

		private readonly LookupTable _table;
		private readonly Dictionary<Type, IMatcher<TEntity>> _cache = new Dictionary<Type, IMatcher<TEntity>>();

		public Matcher(LookupTable table) {
			_table = table;
		}

		public IMatcher<TEntity> For<T>() where T : IComponent {
			Type type = typeof(T);
			if (_cache.ContainsKey(type))
				return _cache[type];
			var matcher = (Entitas.Matcher<TEntity>) Entitas.Matcher<TEntity>.AllOf(_table.IndexOf<T>());
			matcher.componentNames = _table.Names;
			_cache[type] = matcher;
			return _cache[type];
		}
	}
}

Entity

namespace Generic {
	public class Entity : Entitas.Entity {}
}

How to use

  1. Create a context class:
namespace Generic {
	public class GameContext : Context<Entity> {
		public GameContext(LookupTable table) : base("Game", table, new Matcher<Entity>(table)) {

		}
	}
}
  1. Install it with zenject, or just make by your hands and save in singletone (that’s bad)
namespace Installers {
	public class ContextInstaller : MonoInstaller {
		public override void InstallBindings() {
			GameContext();
		}

		public void GameContext() {
			Type[] components = {
				typeof(DebugMessageComponent),
				typeof(PositionComponent)
			};
			Container.Bind<LookupTable>().FromInstance(new LookupTable(components)).WhenInjectedInto<GameContext>();
			Container.BindInterfacesAndSelfTo<GameContext>().AsSingle();
		}
	}
public class ConextInstallerNoDependency {

		private static GameContext _game;
		public static GameContext Game {
			get {
				if (_game == null) {
					Type[] components = {
						typeof(DebugMessageComponent),
						typeof(PositionComponent)
					};
					_game = new GameContext(new LookupTable(components));
				}
				return _game;
			}
		}

	}
}
  1. Use it
namespace Systems {
	public class DebugMessageSystem : ReactiveSystem<Entity>
	{
		private readonly GameContext _context;

		public DebugMessageSystem(GameContext context) : base(context) {
			_context = context;
		}

		protected override Collector<Entity> GetTrigger(IContext<Entity> context) {
			return context.CreateCollector((context as GameContext).Matcher.For<DebugMessageComponent>());
		}

		protected override bool Filter(Entity entity) {
			return entity.HasComponent(_context.Table.IndexOf<DebugMessageComponent>());
		}

		protected override void Execute(List<Entity> entities)
		{
			foreach (var e in entities) {
				DebugMessageComponent component = (DebugMessageComponent)e.GetComponent(_context.Table.IndexOf<DebugMessageComponent>());
				// This is my bicycle instead of Debug.Log
				// D.Log(component.Message);
				Debug.Log(component.Message);
			}
		}
	}
	public class HelloWorldSystem : IInitializeSystem, IExecuteSystem {
		private readonly GameContext _context;
		// I do not love to new objects in update, that's why I cached it, but this is not necessary 
		private DebugMessageComponent component;

		public HelloWorldSystem(GameContext context) {
			_context = context;
		}

		public void Initialize() {
			component = new DebugMessageComponent {Message = "Test" + Random.Range(0, 1000)};
		}

		public void Execute() {
			component.Message = "Message " + Random.Range(0, 1000);
			_context.Replace(component);
		}
	}
	public class Hello : Feature {
		public Hello(GameContext context) : base("HelloFeature") {
			Add(new DebugMessageSystem(context));
			Add(new HelloWorldSystem(context));
		}
	}
	// This class may be MonoBehaviour, but I use Zenject and prefer simple classes
	public class GameController : ITickable, IInitializable, IDisposable {
		private readonly GameContext _context;
		private Entitas.Systems _systems;

		[Inject] // Zenject attribute for injection
		public GameController(GameContext context) {
			_context = context;
		}

		// No injection
		public GameController() {
			_context = ConextInstallerNoDependency.Game;
		}
  		// OnEnable
		public void Initialize() {
			_systems = new Feature("Systems").Add(new Hello(_context));
			_systems.Initialize();
		}
		// Update
		public void Tick() {
			_systems.Execute();
			_systems.Cleanup();
		}
		// OnDisable
		public void Dispose() { }
	}
}

Gist

Issue Analytics

  • State:closed
  • Created 7 years ago
  • Comments:22 (5 by maintainers)

github_iconTop GitHub Comments

3reactions
sschmidcommented, Mar 19, 2017

That is bad. Maybe you not need a code generator at all, anyway 95% of frame time eaten by graphics. Maybe need to concentrate on building more comfortable workflow with developers, that can save a lot time for making a lot of new features, than on optimization that save 1% of frame time, and cost a hundreds of developers hours

A multiplayer backend doesn’t do anything involving graphics and it’s a question of real money if you need 1000 servers or if you can do the same with 100 servers with an optimized framework

1reaction
Spy-Shiftycommented, Mar 23, 2017

Renaming is quite easy. Use your IDE for that job (Visual Studio or something else). Deleting and Adding Fields can be indeed a problem. But it don’t take hours as you described before. The real problem (at the moment) are blueprints. If you delete a field of a component wich you use in a blueprint, you have to recreate the hole blueprint… (because of the binary serialization). Thats my real problem by the way.

I had my difficulties with the codegen at beginning, too. But now i feel comfortable with it. Sure, there could be some improvements. But speed is very important! I build a game with thousands of “bots” and still I have 75 FPS. That’s inconceivable with the “intuitive” Unity way (Monobehaviour Scripts attached to each bot)

The best way to learn and use entitas is to take a look at the examples and videos. And first think about components and then act! And finally, the simplest solution is the right solution 😉

Read more comments on GitHub >

github_iconTop Results From Across the Web

Using Entitas in generic way, instead of generators #345
Today I spend whole day trying to work with Entitas and Generators, but it always broke all my code when I remove some...
Read more >
startkit/Entitas-CSharp
Great code generator CLI experience with helpful commands like status and fix which will let you modify Entitas.properties interactively; Logo refinements based ...
Read more >
Entities installation and setup | Entities | 0.50.1-preview.2
Entities 0.50 uses the Microsoft Source Generator feature for its code ... You can use the following ways to install the Entities package:....
Read more >
Creating an entity
You can generate entities from a JDL file using the jdl sub-generator, by running jhipster jdl your-jdl-file.jh . If you do not want...
Read more >
[Release] Entitas ECS Automatic Synchronization framework
I've worked in a project that used Entitas but i would not recommend it. The ECS concept is fine, but the API is...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found