jueves, 13 de agosto de 2015

Nifty-flow explanation

My first open-source collaboration!


When I started "Cabo Trafalgar", it took me three tries to find the technology that made it possible. I started with Irrlicht, and I also took a look to another library I cannot recall anymore. Only when I discovered that the language that paid my bills was really able to render native 3d and really nice water I adventured on JMonkeyEngine with Java.

Once you start with JME, you don't have many options for creating rich graphic interfaces, and the one that highlights is, needless to say, nifty-gui. Easy to use (I find easier to make examples using JME3 than raw nifty-gui libraries, that easy!) and good documentation and examples. But no doubt it was my webby background I absolutely missed some features:
  •  When you're defining a screen, you're pretty much defining its connections, in my words, "flow of screens". That actually reduces by a lot the possible re-usability of screens in other parts of your game, or other games whatsoever.
  • You can create screens by static xml, or by dynamic java, but web developers have long gone over these restrictions, and we have like a dozen template languages to mix code and static text. At the end of the day, xml for screen is nicer and easier to interpret than crude java code (no offense).
  • Remember, when enumerate, always use 3, 7 or 10 bullet dots, force it if necessary :P
At this point, I had little options, I could have dropped my project, but I didn't feel like doing it again. I could have renegotiated my use cases to make it more cumbersome to manage repeated screens in different parts of the game, or... I could actually build what I need on top of nifty-gui.

For first time I was actually solving my own problems instead of waiting for somebody else to make that for me, and it didn't take me long to realize that it was actually possible.

The use case:

For my game, I needed the user to walk one or two screens, one after the other, until find the "hall", a screen that contains every mode the player can play. In my case, I have a "CounterClock" game and a "Windtunnel" game, both accessible from the same "Menu" screen.

Once you choose "CounterClock", user must "Select profile", then "Select ship", then "Select map" and finally "Select controls", then play.
Once you choose "Windtunnel", user must "Select ship", then "Select controls", then play.

It's easy to spot that I really wish to reuse some screens, and the flow of screens will be linear most of the times in as many examples I can think.

Implementation:

Just take the base of Struts, you have a central controller that will receive every interaction with the user. This controller has, somewhere, the capacity to decide what are the user's options given the current status, and using the current input, calculate the next screen to render.

That exact idea made me build the RedirectorScreenController, all it does is redirecting the user to the next calculated screen, from the ScreenFlowManager. My screens will always onNext and onPrevious actually direct to this RedirectorScreenController and it's associated empty screen.

Next, as I'm talking about reusability, I need to differentiate between "screen definition" and "screen instantiation", and bear in mind that the relationship will be 1:N.
My implementation defines a "screen definition" (See ScreenDefinition) as:
  1. a unique name
  2. a way of getting the controller live instance (unique in the system, so far)
  3. a way of getting the screen constructor (a class that actually knows whether the screen is xml or java and executes it)
My implementation defines a "screen instantiation" (See Screen) as:
  1. the flow it belongs to
  2. a name, unique within the flow
  3. the live instance of the controller associated to this screen.
  4. the live instance of the generator associated to this screen.
Every screen has a uniqueScreenId, made from the flow name and the screen name.
In case the flow name is not set, we can safely assume we need to search that screen in our current flow (local search).

Finally, this theoretical system was actually becoming alive, we just need to define flows.
  1. A name for the flow, unique
  2. A sequence of screen definition names
  3. An optional screenUniqueId parent
We need an initial flow, that's the flow without a parent, we can only have one, the ball is rolling!

If we declare more flows, we need to "hang" them from a concrete existing screen, either from the root flow, or another flow.

We can always query the ScreenFlowManager (See ScreenFlowManagerImpl for implementation details) for our options, it'll tell us whether we can continue forward, and if other flows are available from this screen.

Internally, a JGrapht (in-memory graph database) instance have all the information to know where are you, what are your options and what's your next state from current state and input.

User input

Maybe it's a bit annoying, but there is at least another element to cover. How do we communicate the ScreenFlowManager what's our next move?

I only managed to solve this problem by "injecting" the same ScreenFlowManager into the controllers (not even automagically) and allow a "setNextScreenHint" method to tell the flow manager what's your intention.

Instance resolution

I gently let this for the end.
Do you remember I mentioned "a way of getting the controller live instance" when I was talking about ScreenDefinition?. I was ambiguous because this library is quite technology agnostic, so much, that you have to provide a way of telling me how to get your live instances.

The easier example, LiveInstanceResolutor, it's an object you feed with "instance name" and "instance", you give it to me, and every time you mention the instance name, nifty-flow will take the instance from the resolutor. It's a f*****g map.

StaticScreenGeneratorResolutor will take your static xml directly, that work is done already.

Nifty-gui provides, however, other resolutor, DefaultInstanceResolutor, nothing more than a "resolutor of resolutors" so you can use several at the same time if you wanted. Find in the example below how you can assign a prefix to later use in your Screen Definitions.

The origin of this mechanism is because I cannot live without Spring, so every time I needed an instance, I'm actually invoking a resolutor that is digging inside the BeanFactory for the right bean, autowired, resourced, initialised and ready to use. This implementation is not provided to avoid Spring dependencies, I'm sorry ;)

How's that looking so far?

public interface ScreenFlowManager {
    String NEXT = "next";
    String PREV = "prev";
    String POP = "pop";

    void addScreenDefinition(ScreenDefinition screenDefinition) throws InstanceResolutionException;

    void addFlowDefinition(String flowName, final Optional<String> screenNameFrom, List<String> flowDefinition);

    String nextScreen();

    void setNextScreenHint(String nextScreenHint);

    Collection<String> getChildren();
}

I wouldn't complain much, 5 method, all them explained above. Easy to use!

Working example (please find the entire code here)

public void simpleInitApp() {
    NiftyJmeDisplay niftyDisplay = new NiftyJmeDisplay(
            assetManager, inputManager, audioRenderer, guiViewPort);
    Nifty nifty = niftyDisplay.getNifty();
    guiViewPort.addProcessor(niftyDisplay);
    flyCam.setDragToRotate(true);

    nifty.loadStyleFile("nifty-default-styles.xml");
    nifty.loadControlFile("nifty-default-controls.xml");

    DefaultInstanceResolutor defaultInstanceResolutor = new DefaultInstanceResolutor();
    ScreenFlowManager screenFlowManager = new ScreenFlowManagerImpl(nifty, defaultInstanceResolutor);

    LiveInstanceResolutor liveInstanceResolutor = new LiveInstanceResolutor();
    defaultInstanceResolutor.addResolutor("static", new StaticScreenGeneratorResolutor(nifty));
    defaultInstanceResolutor.addResolutor("live", liveInstanceResolutor);

    RootScreenController screenController = new RootScreenController().setScreenFlowManager(screenFlowManager).setApplication(this);
    ScreenController screenController2 = new Controller2(screenFlowManager);
    ScreenController screenController4 = new Controller4(screenFlowManager);
    liveInstanceResolutor.addController("root", screenController);
    liveInstanceResolutor.addGenerator("root", new RootScreenGenerator(nifty, screenController, screenFlowManager));
    liveInstanceResolutor.addController("controller1", new Controller1(screenFlowManager));
    liveInstanceResolutor.addController("controller2", screenController2);
    liveInstanceResolutor.addController("controller3", new Controller3(screenFlowManager));
    liveInstanceResolutor.addController("controller4", new Controller4(screenFlowManager));

    liveInstanceResolutor.addGenerator("generator2", new Generator2(nifty, screenController2));
    liveInstanceResolutor.addGenerator("generator4", new Generator4(nifty, screenController4));

    try {
        screenFlowManager.addScreenDefinition(new ScreenDefinition("root", "live:root", "live:root"));
        screenFlowManager.addScreenDefinition(new ScreenDefinition("screen1", "live:controller1", "static:/screen.xml"));
        screenFlowManager.addScreenDefinition(new ScreenDefinition("screen2", "live:controller2", "live:generator2"));
        screenFlowManager.addScreenDefinition(new ScreenDefinition("screen3", "live:controller1", "static:/screen.xml"));
        screenFlowManager.addScreenDefinition(new ScreenDefinition("screen4", "live:controller4", "live:generator4"));

        screenFlowManager.addFlowDefinition("root", Optional.<String>absent(), newArrayList("root")); 
        screenFlowManager.addFlowDefinition("screenFlow1", of("root:root"), newArrayList("screen1", "screen2", "screen3", "screen4"));
        screenFlowManager.addFlowDefinition("screenFlow2", of("root:root"), newArrayList("screen1", "screen4"));

        nifty.addScreen("redirector", new ScreenBuilder("start", new RedirectorScreenController().setScreenFlowManager(screenFlowManager)).build(nifty));
        nifty.gotoScreen("redirector");

    } catch (InstanceResolutionException e) {
        e.printStackTrace();
    }


}

In few words:
  1. Create and feed your resolutors with the information you'll need from the screen definitions.
  2. Create your Screen Definitions, I have defined 5 screens, two of them from the same XML and other three generated from java code. Here lays the potential of nifty-flow, the static screens would be reusable even across projects, without knowing what screen to link forwards and backwards.
  3. Create your Screen Flows, I have a root one with one screen, then from that screen, another two reusing some screens.
  4. Add redirector (with that name) as "start" and let the magic happen.
 Final words

There's some more work to do. I implicitely create NEXT and PREV links between screens, but I'd rather have implemented a mechanism to push every movement to a stack, and a POP would always return to the previous screen, this would allow us to jump to the middle of another flow and being able to come back (PREV is "statically" linked to the same screen, while POP could be any invoker).

From the features I was missing in nifty-gui, I still keep missing some help templating XML files, instead of making complex screen from java only, that's something I'd thrill to implement, but god knows I don't have the time now.

Hope you enjoy of this library

Alber

No hay comentarios:

Publicar un comentario

Nota: solo los miembros de este blog pueden publicar comentarios.