Jump to content

  • Log In with Google      Sign In   
  • Create Account






dustArtemis, a fork of Artemis Entity System framework

Posted by TheChubu, 14 June 2014 · 603 views

java ecs artemis dustArtemis entity-component system
Hi! This is the first entry of a journal dedicated to dustArtemis, my fork of Artemis Entity System framework, that I've been using for the pseudo-engine project I started this year.

Attached Image

dustArtemis


dustArtemis is a fork of Artemis Entity System, which is a BSD-licenced small Java framework for setting up Entities, Components and Systems.

Artemis

Artemis is structured with a few main classes:
  • World: Here all your entities, systems and components live. You define through a World instance what Systems will be iterated on at each "tick" and in which order.
  • EntitySystem: This is where your logic goes into. They iterate over entities, but only those which meet the system's requirements.
  • Entity: This is your game entity, its just an ID and works as a container for components.
  • Component: This is your data holder.
The Interesting Bits

Iterating over a few EntitySystems calling process() on each sounds simple enough, the interesting part is how they pick Entities they're interested in.

When an Entity is added to the World, the EntitySystems present must see if they're "interested" in the Entity, their interest is defined by which Components the Entity has, this is done through the Aspect class.

The Aspect class is defined by 3 sets of Component types:
  • allSet: A set of Components of which the Entity must have all for it to be processed by the system.
  • oneSet: A set of Components of which the Entity must have at least one for it to be processed by the system.
  • exclusionSet: A set of Components of which the Entity must have none of them for it to be processed by the system.
EntitySystems have one Aspect they're interested in, and they match it against the Entity's components to know if the Entity is of interest for them.

An Aspect might be made out of any combination of the three sets, so you can have for example an EntitySystem that culls non-visible entities, it could be interested in entities which have a 'Spatial' component, one of either 'BoundingBox' or 'BoundingSphere' components and no 'Camera' component. Or maybe you just have an EntitySystem that calculates frame time, its interested in no entities, so it just has an empty Aspect. Its pretty flexible.

In any case, original Artemis site has a pretty good examples on how to start with it, the spaceship warriors demo project is nice for testing things out and seeing Artemis in action. Spaceship Warriors should work with dustArtemis with few, if any, changes.

I fork so you don't have to

The reason why I forked Artemis is because it had a few bugs that haven't been ever fixed (unless you look for other forks, like artemis-odb), and because I wanted to see if I could make some improvements to it, for example, converting it to The One True Brace Style.

More seriously, I didn't wanted complete changes in how it worked, just tweaks here and there. Mostly I removed quite a few classes and code sections that were redundant or not used (ie, some math utils, some inner classes that were generalized into a single class, etc), expanded Artemis backbone collection, the Bag, considerably so it can be used outside Artemis itself (adding general Java 8 goodness too), added comments, removed warnings, optimized a few things, etc.

I've seen a few forks with quite a few changes like adding an event system to it, bytecode weaving, and so on, but I prefer to maintain it simple, no extra build steps nor dependencies, so you only need Java 8, a repo clone and you're good to go.

Disclaimer

I just opened the issue tracker, I have no idea how it works.
I've been using Bitbucktet repositories for a while, but I don't know how to use the more "advanced" features (merges, pull requests, etc).

Links

dustArtemis repository (more details in the readme!)

https://bitbucket.org/dustContributor/dustartemis

original Artemis Entity System framework:

http://gamadu.com/artemis/

And of course, thanks a lot to Arni Arent, Artemis Entity System's original author Posted Image




I've thought about replacing UUID:s with a long/int in artemis-odb, but the problem arises when you need to deal with saving/loading state. I guess one could keep track of used unique ids by saving them in a bitset - should yield better performance, haven't played around with it though.

 

You ought to grab the unit tests from artemis-odb btw; they might help in uncovering more bugs - most were written after running into one.

To be honest, I haven't figured out serialization yet either.

 

I do know that 128 bit UUIDs aren't that popular to "uniquify" game world entities. For example, all Elder Scrolls games (at least for Morrowind, Oblivion and Skyrim) use integer IDs for their world entities (anything from the static rock in the wilderness to the rusty sword you just dropped), so I don't think 128 bit UUIDs are the solution.

 

I'm not exactly sure of what problem you're talking about, but I suppose that is the problem of what IDs to assign to new entities after you have loaded the game state. You must serialize the Entities with the ID they had, but when you load them, you cant start the ID counter from zero.

 

You could just start counting from whatever value the counter was left in the saved state (say, max Entity ID in the saved state is X number, so set the counter to start counting from X+1). Entity IDs will keep increasing forever, someday after two billion entities are created, wrap around and after another two billion entities are created, they'd start to overlap the oldest Entities "alive" (0 and above).

 

If that isn't enough (seriously, 4 billion IDs, it will be enough for 98% of the use cases out there), you could pseudo solve it with an "IDAllocator".

 

Have an object that stores the range of IDs that are free (clean state -> from Integer.MIN_VALUE to Integer.MAX_VALUE), then, for each new ID request, modify the free range or, if necessary, split it.

 

Say that we request our first ID, free range would be [Integer.MIN_VALUE,-1], [1,Integer.MAX_VALUE]. And so on. You don't keep track of what IDs are used, just keep track of the range of IDs that are free.

 

Say that we load a game state that has IDs from 20 to 80 used, and from 100 to 200 used. Your free ranges would be [Integer.MIN_VALUE, 19], [81,99] and [201,Integer.MAX_VALUE].

 

You'd keep a "free list" of IDs like if it were a memory allocator, with the ranges of free IDs available (stored in [start,end] int pairs). That way you'll never "lose" an ID, if in the current state it isn't being used, it will be in the "free" range of IDs (ie, a list of ID ranges that aren't used).

 

There is a risk of "ID fragmentation", ie, having tons of little free ranges in the free range list, which would be bad for memory usage. Just be sure to occupy the smallest free ranges first (ie, order the free range list by size) and you'll be fine, that way you can keep track of all the IDs that aren't used with just a couple of int[2] in a list.

 

Hell, the IdPool could be implemented like that without using the IntStack now that I think of it...

BTW, I have no idea how to use those unit tests :D

I'm not sure ID fragmentation is actually an issue is it? That would only be an issue if your architecture required continuous lists of IDs to represent, say, groups of objects which would be a weird way to design. If the only requirement is for a single ID at a time, fragmentation is not an issue at all in the way it is with memory.

Yeah, as I said, it can be easily fixed by sorting by size of free range and use whatever range is smallest each time you "reserve" an ID.

 

The issue was with the structure to record which ranges are free. I put as an example a List of int[2], in index 0 you'd have the range start, in index 1 the range end. You'd have in the List as many int[2] as free ranges you have.

 

Say that you don't sort the free range list, and split the free ranges a couple of thousand times, you might end up with tons of tiny int[2] allocations in a list and possibly lots of range merges when returning IDs to the pool, which is slow (ordered list remove(), shifts all elements in the list). Add to that JVM's overhead (12 bytes or so for arrays IIRC), and it might add up.

PARTNERS