Author Topic: Peanuts - a simple data driven ECS library for C#  (Read 7356 times)

Omnivore

  • Rogueliker
  • ***
  • Posts: 154
  • Karma: +0/-0
    • View Profile
Peanuts - a simple data driven ECS library for C#
« on: February 13, 2015, 04:27:39 AM »
Just finished testing it and getting it up on github.  Documentation is sparse, samples are non-existent until after I incorporate it into my latest roguelike project.  It may, or may not, be of interest, but here it is: https://github.com/Brian61/Peanuts

Enjoy,
Omnivore

reaver

  • Rogueliker
  • ***
  • Posts: 207
  • Karma: +0/-0
    • View Profile
Re: Peanuts - a simple data driven ECS library for C#
« Reply #1 on: February 13, 2015, 06:50:43 AM »
I'd be interested to hear how an ECS system works for a roguelike project, as I'm making this combo myself and I've tailored it quite a bit to work with the turn-based nature of the game. I had a quick look at the project, and your naming of things is not helpful ( Nuts: the abstract base class for all data objects using this library. ??? ) Would you be interested to provide an overview?

Omnivore

  • Rogueliker
  • ***
  • Posts: 154
  • Karma: +0/-0
    • View Profile
Re: Peanuts - a simple data driven ECS library for C#
« Reply #2 on: February 13, 2015, 08:20:45 AM »
I'd be interested to hear how an ECS system works for a roguelike project, as I'm making this combo myself and I've tailored it quite a bit to work with the turn-based nature of the game. I had a quick look at the project, and your naming of things is not helpful ( Nuts: the abstract base class for all data objects using this library. ??? ) Would you be interested to provide an overview?

Sure I'll try a brief overview here.  As far as the naming, I'm sure I could've chosen better, but working with libraries like MonoGames (Xna), there's too much naming confusion to suit me with the standard names.  Anyhow, to begin, here is a list of my names vs 'more or less' standard names.

Bag => Entity (combines holding Entity Id with being a collection of Nut subtypes (Components)).
Nut => Component (actually abstract base class for components).
Process => System (IProcess interface and ProcessBaseAbc useful abstract base class for Processes/Systems)

Vendor => EntityManager in some systems, EntityGroup in others, a collection of Bag instances, primary interface to Bags and root object for serialization of game state (using Json).
Recipe => Assemblage http://t-machine.org/index.php/2009/10/26/entity-systems-are-the-future-of-mmos-part-5/ in Adam Martin's parlance, a blueprint for Bag instances.
RecipeBook => A collection of Recipes, and primary interface to them.  RecipeBook's are only created via loading from Json, using the prototype concept and general format (adapted for ECS) shown at the bottom of http://gameprogrammingpatterns.com/prototype.html.

Mix => A custom Bitset used to implement membership testing using the 'key fits lock' concept http://gamedev.stackexchange.com/questions/31473/role-of-systems-in-entity-systems-architecture.

Overall the architecture of Peanuts is designed to support Mick West's "OBJECT AS A PURE AGGREGATION" model as shown in the section of that title in the article: http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/.  The implementation of Peanut's API is influenced by ideas such as Martin Fowler's Minimal Interface http://martinfowler.com/bliki/MinimalInterface.html, so expect the public API to remain small.

Getting down to brass tacks, the actual implementation of a component using Peanuts would look much like:
Code: [Select]
public sealed class ExampleNut : ShallowNutAbc
 {
        public string AStringProperty { get; set; }
        public int AnIntegerProperty { get; set; }
        // etc - nothing but simple properties except for perhaps temporary data
 }

Creating an instance of our ExampleNut component is only done in the context of creating a Bag (Entity) which is done using the Vendor interface method MakeBag.  It could look something like*:
Code: [Select]
Bag myEntity = vendor.MakeBag(new ExampleNut { AStringProperty = "Hello", AnIntegerProperty = 5});

Of course, that is the exception rather than the rule.  Normally you would create a Bag as a collection of components all at one time using a Recipe.  That looks more like:
Code: [Select]
Bag myEntity = vendor.MakeBag(recipeBook.Get("MyRecipeForOrcs"));

There's a number of things being done behind the scenes in the above, the Bag's Id property is set to a contextually unique integer value when created, and the mix of component types making up the entity is represented by a Mix (bitset) object created at the same time and stored inside the Bag.  Also, all systems registered with the vendor will be notified of the new Bag instance if, and only if, their registered key (a Mix instance) is a subset of the Bag's lock (another Mix instance).

*Note that I currently have too many ways to create entities, once I use this library in my current roguelike, I expect to eliminate at least one of the less useful variants.

Speaking of Processes (Systems or Procedures or whatever in ECS literature), with Peanuts a Process must implement the IProcess interface.  Its a simple interface, only three methods, two of which can be handled entirely by the abstract base class ProcessBase, the other method: Update(long gameTicks, object context =null) is the one called to get anything done.  One of the things ProcessBase automates for you is the maintenance of a valid entity id list.  In code, a Process would look something like:
Code: [Select]
public class MyProcess : ProcessBase
{
       // skip constructor which just passes everything to ProcessBase and gets and caches the Nut subtype integer id's

        public override void Update(long gameTick, object context = null)
        {
              // do setup here
             foreach(var eid in MatchingBagIds())
            {
                 var entity = BagVendor.Get(eid);
                 var componentA = entity.Get(cachedComponentATypeId);
                 var componentB = entity.Get(cachedComponentBTypeId);
                 // process component data
            }
      }
}

That is pretty much it in a nutshell :)

Of course, there are a few things I didn't go into, like the need to make a call to Nut.Initialize() after loading any plugins that define Nut subtypes and prior to creating instances of Vendor/Process/Bag/etc.  Also skipped over the constructor call for Processes that ProcessBase handles, where the Process registers itself with the current Vendor instance and tells it what Nut subtypes (component types) it needs to do any processing. 

I also didn't go into too much detail on the creation of Nut subclasses themselves, so let me point out a few things where that is concerned.  Due to the ability to use any entity as a prototype for creating a new entity, all entities need to support the Clone method (abstracted in the Nut base class).  I recommend using the ShallowNutAbc class as the base class instead of using Nut directly since it implements that interface using shallow copy which is sufficient for most use cases. 

While I recommend using shallow inheritance trees for components (derive from one of the supplied abstract base classes), Peanut does support two or more component classes sharing a single non-abstract base class, however to make this work with the auto registration of components in Nut's static Initialize method, all component classes must be sealed.  I also heavily advise against putting any logic in the component classes and any data member object references *must* be marked with [JsonIgnore] attribute if not avoided altogether.

Well, now I'm past the nut shell and into the nut house :)
Omnivore

reaver

  • Rogueliker
  • ***
  • Posts: 207
  • Karma: +0/-0
    • View Profile
Re: Peanuts - a simple data driven ECS library for C#
« Reply #3 on: February 13, 2015, 09:22:29 AM »
Thanks for the details! You could totally put half or more what you wrote in the project's doc ;)
I started with systems being separate from components, but that scaled badly, as for small-but-many components I ended up with lots of systems, which made development slower.
Also, the 'system' nature as found in ECS descriptions around the web fits a real-time system much more than a turn-based one imo.
I will be interested to see how the integration with the roguelike works for you, will be keeping an eye.

Omnivore

  • Rogueliker
  • ***
  • Posts: 154
  • Karma: +0/-0
    • View Profile
Re: Peanuts - a simple data driven ECS library for C#
« Reply #4 on: February 14, 2015, 04:31:57 AM »
Update: after review and an initial shot at applying Peanuts to my roguelike, I have done a refactoring. 

The major changes are:
1) Moved all static methods (including Initialize) to a new static class named Peanuts.
2) Removed the abstract deep copy implementation and moved the shallow copy into the Nut abstract class.
3) Completely hid component ids from the users of the library.
4) Removed the ability to access Nut subtypes (components) by string name.
5) All access to Nut subtypes (components) is now done via typeof(NutSubclass) and generic Get/TryGet methods.
6) Added an IdGenerator class for Bag ids which can be serialized through a data member of the Peanuts static class.
7) Removed the JsonHelper static class and merged the remnants into the new Peanuts static class.
8) Renamed ProcessBase to Process.

Hopefully the library will be easier to use, easier to document, and easier to maintain as a result of these changes.

You could totally put half or more what you wrote in the project's doc ;)

That's the next step for the library, along with better comments, I need to get some real docs together for it.  The amount of effort I put into that, beyond my own needs, depends on how much interest I see in it.

Also, the 'system' nature as found in ECS descriptions around the web fits a real-time system much more than a turn-based one imo.

Yes and it is unfortunate, especially since I agree with many of the comments in this discussion: http://www.reddit.com/r/gamedev/comments/1tu4v5/is_there_any_reason_to_use_entity_system_approach/.
Oddly enough, when you read the writings of professional game developers or speak with any, you find more than a few that believe, for stand alone real-time games at least, ECS as a whole may be a bad fit for parts of such a game due to performance constraints. 

Somehow a lot of people out there jumped on the 'cache friendly' theoretical ECS implementation which no one has really gotten to work in a general purpose framework.  The real strengths of ECS, in my opinion at least, lies in the relative ease of using data driven design approaches, easier maintenance and easier expansion opportunities.  Of course, then there are MMO's which are a whole different ball game and ECS again begins to be at least somewhat popular in that arena from what I've read.

Anyhow, with Peanuts I'm aiming for simplicity, ease of use, and ease of maintenance.  Seeing that roguelikes have been written in scripting languages like Python, Lua, and Javascript, I don't see performance of a C# ECS implementation being a problem or a goal. 

I started with systems being separate from components, but that scaled badly, as for small-but-many components I ended up with lots of systems, which made development slower.

Well, avoiding that will be the acid test for me.  My roguelike project is, in large part, a compendium of ports of earlier projects and prototypes, ported to C# and integrated with MonoGame, RogueSharp, and Peanuts.  Speaking of which, need to stop typing replies and get back to it! :)

Omnivore

  • Rogueliker
  • ***
  • Posts: 154
  • Karma: +0/-0
    • View Profile
Re: Peanuts - a simple data driven ECS library for C#
« Reply #5 on: February 22, 2015, 04:51:55 AM »
After a week of use, another review and major refactoring. 

1) Peanuts now uses common industry names for Entities and Components. 
2) The entity collection class has been renamed to Group (was Vendor) to more accurately reflect intended usage.
3) The custom bitset class for concise description of component sets has been renamed to TagSet (was Mix).
4) Removed Process/Harvester class.

Items 1 through 3 should be self explanatory.  Basically I just got tired of having to explain the names.  Hopefully the library will now be easier to understand as a result.

The fourth item requires a bit of an explanation.  Initially I had created a Process interface and abstract class to support the ECS 'System' concept.  In use, however, it initially seemed that what I needed instead was an enumerable change tracking system, so I replaced Process with Harvester.  Through further use I discovered that almost all of the tracking was unnecessary, indeed it made things more complex for no real gain (YAGNI... sigh).

In practice I have found, with roguelikes at least, that entities naturally fall into a small number of categories.  Peanuts' Group class has been revamped to support an 'entity group per category' approach and simple enumeration support via implementing IEnumerable is sufficient (especially with LINQ).

In my current roguelike game project, I've created four entity categories (terrain, items, effects, mobiles) each with their own Peanuts Group instance though they share many components.  I represent the state of the current game model using those groups along with five 2d arrays, one being the map state, the others being one array per category of entity instances.  This allows me to use any external library supplied map support (or custom roll my own) and quickly and easily translate from map position to entity (and vice versa). 

For rendering, the group approach corresponds to layers (z-order), so much easier to implement it by first determining the map coordinates (rectangle) within the current view, check the map state for visibility, and render each layer in correct z-order by simply grabbing the entities directly from the map arrays.  In actual code, I do  this all in a system on the modeling side which creates a list of {x, y, image_tag } for consumption by the renderer which translates information into whatever format I'm using for display.

In short, I'm using a Zone class which holds the model state (including map) and pretty much all the systems are implemented as methods in that class. 

Anyhow, unless someone has questions or critique, I'll make this my last post this thread.  I need to someday figure out how to use Github's wiki, together with auto-genned docs from my XML comments in code instead of hijacking someone's forum :)