New World instance builder

Published April 03, 2016
Advertisement

[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.
In short, too many movable parts and easy to break.

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!
2 likes 3 comments

Comments

DemonDar

Interesting, but what's wrong by passing a list in the constructor?

April 05, 2016 08:15 AM
DemonDar

Interesting, but what's wrong by passing a list in constructor?

April 05, 2016 08:16 AM
TheChubu

A single list doesn't allows for having one order for iteration and another for processing.

What about two lists? One for observers and another for priorities?

Well at that point it becomes annoying, the user has to make two lists separately and pass them to the World instance plus the flag, it becomes easier just using the Builder and let it handle the list/priorities for you inside.

You can see the 'build' step here:

https://github.com/dustContributor/dustArtemis/blob/master/src/com/artemis/World.java#L505

It not only sorts, but it also initializes observers and injects mappers/observers into other observers via Injector. Moreover, it also enforces each observer passed isn't null, so to make sure World receives a (reasonably) functional set of parameters.

The 'init' step might be arbitrarily complex since each subsystem can define their own init method, doing it inside World constructor would be unsafe.

Also the Builder allows me to set some defaults (like the order flags) without having to declare overloaded World constructors with different parameters, and the mentioned 'init' step can be safely hidden away without means for the World user to call it again.

I wanted World constructor to be fairly minimal, and with a compact public interface. Having something else doing the initial heavy lifting allows for that.

April 05, 2016 09:34 PM
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement