[font=arial]In this update I'll talk about the new interface for creating World instances, the World.Builder![/font]
[font=arial]dustArtemis[/font]
dustArtemis[font=arial] is a fork of [/font]Artemis Entity System[font=arial], which is a BSD-licenced small Java framework for setting up Entities, Components and Systems.[/font]
[font=arial]Old World[/font]
[font=arial]World instances were created on the spot before, like this:[/font]
World world = new World();// System, and 'enabled' flag.world.addObserver(new PhysicsSystem(), true);world.addObserver(new GraphicsSystem(), true);world.addObserver(new SoundSystem(), true);// Then justwhile(true) { world.process();}
Fairly straightforward.Thing is, this class was quite fragile:
- You could add and remove systems whenever you wanted, so you had to check on both the list of observers and the map of observer types inside.
- You could add an observer of the same type of an existing observer, in which case you'd need to evict the existing one, then add it.
- World.initialize() was single use only, you could add an observer, initialize the world, then add another observer, and break it, since the last observer wouldn't get the related component mappers injected, nor would have its 'init' method called.
- You couldn't control the iteration order. If you removed an observer then re-added it, it'd always get processed last.
New World
I decided that I wanted a very specific initialization step for the World instance, moreover, now I had a very specific need: I wanted to initialize observers in a specific order, and I wanted to process observers in a specific albeit different order. The issue presented itself when I wanted to initialize the renderer before a few systems that depended on it, but I wanted to actually render after those systems were processed in the game loop.
I also decided World would be immutable. You configured it, got your instance, and that's it. No observer tracking inside nor any sort of checks at runtime, just one initialization step and you're done.
Ended up using the Builder pattern to create an additional mutable object that held all the data the World needed, which implemented a "build" step that created your immutable World instance. Interface looks like this now:
World world = World.builder() .observer(new PhysicsSystem(), 1) .observer(new GraphicsSystem(), 3) .observer(new SoundSystem(), 2) .initializeByOrder(true) .build();
Enabled/disabled state is handled by the observer itself, and now you can specify an order number.You got two flags, 'initializeByOrder' and 'processByOrder', if any is set to true, your observers get initialized/processed by the provided order respectively. What happens if you set it to false? They get initialized/processed by order of appearance. That way you can have one order for processing and a different order for initialization. Hooray!
The 'build' step just sorts the observers as needed, initializes them, then creates the World instance passing the sorted observer array, which will indicate the processing order. The builder makes sure each observer passed isn't null, so to avoid any other checks later. The obtained World instance is immutable, ie, you cant add/remove observers, nor set the 'data' field, which brings me to...
Data passing
Another feature I wanted is arbitrary data passing. Each system has a World instance, and you *can* extend that World instance to add whatever you needed, but it becomes annoying to use when you have to downcast it like "((WorldSubClass)this.world).myMethod()" each time you have to use it inside an EntityObserver, since observers only know about "World", not any subclasses of it. So instead I added an additional (nullable) field:
World.builder().data(new SharedWorldData());
'data' is an object field, so you can put whatever you want in it. This also deprecates the old "delta" field in World, if you want to keep track of delta times, make your own data objects that does it.You can use it like this:
// Inside some EntityObserverSharedWorldData data = this.world.data();float delta = data.delta;WindowSettings settings = data.windowSettings();
'data()' is a generic method, it casts to "T" inside. So in theory you could do "SomeOtherClass data = this.world.data()" and it would compile, but you'd get a ClassCastException at runtime if data isn't of SomeOtherClass type. Its a fair trade-off I think.Another nice thing also is that that 'data' object can be used as a context, to say, share data among systems, create your own event system, whatever you need. I currently use it to share the resource manager, a window object reference and time deltas between frames for example.
Well, thats the new World Builder, I'll describe the new Injector later, cya!
Interesting, but what's wrong by passing a list in the constructor?