jueves, 7 de enero de 2016

Music for my ears

Cabo Trafalgar will have original music for next version

Thanks Enrique Gomez-Cabrero Fernandez for your gift.
https://soundcloud.com/enrique-gomez-cabrero

martes, 1 de septiembre de 2015

Templated Nifty XML with FreeMarker

I finally managed to add a most missed feature (in my opinion) in nifty-gui, though I had to add it on top of my nifty-flow add-on.

Benefits, quite clear to me, for (more or less) the same screen, let me show you the comparison on code and difference in the paradigm. It also allows you to focus in a single way of working and not needing to learn two ways of making the same job, including style. I hate working on front end.

OLD VERSION, Using Java Builder because there's a dynamic number of lists to render.

public final class SelectKeyboardControlsScreenGenerator implements ScreenGenerator {

    @Autowired    private Nifty nifty;
    @Autowired    private SelectKeyboardControlsScreenController controller;
    /**     * Singleton     */    @Autowired    private GameConfiguration gameConfiguration;

    @Override    public void buildScreen(String screenUniqueId) {
        if (nifty.getScreen(screenUniqueId) != null) {
            nifty.removeScreen(screenUniqueId);
        }
        buildScreenNow(screenUniqueId);
    }

    public void buildScreenNow(String screenUniqueId) {

        Collection<KeyboardCommandStateListener> keyListeners
                = gameConfiguration.getPreGameModel().getByType(KeyboardCommandStateListener.class);

        //sorting commands in alphabetical order of command name        List<KeyboardCommandStateListener> sortedCommands = newArrayList(keyListeners);
        Collections.sort(sortedCommands, new Comparator<KeyboardCommandStateListener>() {
            @Override            public int compare(KeyboardCommandStateListener o1, KeyboardCommandStateListener o2) {
                return o1.toString().compareTo(o2.toString());
            }
        });

        final PanelBuilder outerPanelBuilder = new PanelBuilder("PartitionPanel") {
            {
                height("80%");
                childLayoutHorizontal();
            }
        };

        final PopupBuilder popupBuilder = new PopupBuilder("popup") {
            {
                text("some text");
            }
        };

        List<List<KeyboardCommandStateListener>> partitionedSorted = Lists.partition(sortedCommands, 4);

        int partitionIndex = 0;
        for (List<KeyboardCommandStateListener> currentPartition : partitionedSorted) {

            final PanelBuilder partitionPanelBuilder = new PanelBuilder("Partition" + partitionIndex++ + "Panel") {
                {
                    height("80%");
                    childLayoutVertical();
                }
            };

            for (final KeyboardCommandStateListener currentCommandListener : currentPartition) {
                final PanelBuilder commandNamePanelBuilder = new PanelBuilder(currentCommandListener.toString() + "Panel") {
                    {
                        childLayoutHorizontal();

                        text(new TextBuilder("text") {
                            {
                                text(currentCommandListener.toString());
                                style("nifty-label");
                                alignCenter();
                                valignCenter();
                                height("10%");
                                margin("1%");
                            }
                        });

                        control(new ListBoxBuilder(currentCommandListener.toString()) {
                            {
                                displayItems(4);
                                selectionModeSingle();
                                optionalHorizontalScrollbar();
                                optionalVerticalScrollbar();
                                alignCenter();
                                valignCenter();
                                height("10%");
                                width("10%");
                                margin("1%");
                            }
                        });

                    }
                };

                partitionPanelBuilder.panel(commandNamePanelBuilder);
            }

            outerPanelBuilder.panel(partitionPanelBuilder);
        }

        Screen screen = new ScreenBuilder(screenUniqueId) {
            {
                controller(controller); // Screen properties
                // <layer>                layer(new LayerBuilder("Layer_ID") {
                    {
                        childLayoutVertical(); // layer properties, add more...
                        // <panel>                        panel(outerPanelBuilder);
                        // </panel>                        panel(new PanelBuilder("Panel_ID") {
                            {
                                height("20%");
                                childLayoutHorizontal();
                                control(new ButtonBuilder("PreviousButton", "Back") {
                                    {
                                        alignCenter();
                                        valignCenter();
                                        interactOnClick("back()");
                                    }
                                });

                                control(new ButtonBuilder("NextButton", "Next") {
                                    {
                                        alignCenter();
                                        valignCenter();
                                        interactOnClick("next()");
                                    }
                                });

                                control(new LabelBuilder("RepeatError") {
                                    {
                                        alignRight();
                                        valignCenter();
                                        text("");
                                        width("50%");
                                        color(new Color("#ff0"));
                                    }
                                });

                            }
                        });

                    }
                });
                // </layer>            }
        }.build(nifty);
    }


NEW VERSION, two files, some preparation of data in the Generator

public final class SelectKeyboardControlsScreenGeneratorXML extends FtlTemplateGenerator {

    /**     * Singleton     */    @Autowired    private GameConfiguration gameConfiguration;

    public SelectKeyboardControlsScreenGeneratorXML(Nifty nifty) throws IOException {
        super(nifty, "/mod/common/interface_keyboardselector.xml");
    }

    @Override    protected Map injectProperties() {
        Collection<KeyboardCommandStateListener> keyListeners
                = gameConfiguration.getPreGameModel().getByType(KeyboardCommandStateListener.class);

        //sorting commands in alphabetical order of command name        List<KeyboardCommandStateListener> sortedCommands = newArrayList(keyListeners);
        Collections.sort(sortedCommands, new Comparator<KeyboardCommandStateListener>() {
            @Override            public int compare(KeyboardCommandStateListener o1, KeyboardCommandStateListener o2) {
                return o1.toString().compareTo(o2.toString());
            }
        });

        final List<List<KeyboardCommandStateListener>> partitionedSorted = Lists.partition(sortedCommands, 4);

        return new HashMap() {{ put("partitionedKeyboardCommands", partitionedSorted);}};
    }

    public void setGameConfiguration(GameConfiguration gameConfiguration) {
        this.gameConfiguration = gameConfiguration;
    }
}

PLUS the XML with some extra intelligence

<?xml version="1.0" encoding="UTF-8"?><nifty xmlns="http://nifty-gui.sourceforge.net/nifty.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"        xsi:schemaLocation="http://nifty-gui.sourceforge.net/nifty.xsd http://nifty-gui.sourceforge.net/nifty.xsd">
    <useStyles filename="nifty-default-styles.xml" />
    <useControls filename="nifty-default-controls.xml" />

    <screen id="${screenUniqueId}" controller="com.navid.trafalgar.mod.common.SelectKeyboardControlsScreenController">
        <layer id="background" childLayout="center">
        </layer>
        <layer id="foreground" childLayout="vertical" style="nifty-panel-no-shadow">
            <panel id="panel_top" height="15%" width="75%" align="center" childLayout="center">  
                <text text="Select the keys..." font="Interface/Fonts/Default.fnt" width="100%" height="100%" />
            </panel>
            <panel id="panel_mid" height="70%" width="75%" align="center" childLayout="horizontal" style="nifty-panel-red">

                <panel id="panel_mid_left" width="100%" align="center" childLayout="horizontal">
                    <#list partitionedKeyboardCommands as partition>
                        <panel id="partition_${partition?counter}" width="50%" align="center" childLayout="vertical">
                            <#list partition as keyboardCommands>
                                <panel id="keyboardCommands_${keyboardCommands}" align="center" childLayout="horizontal">
                                    <control id="${keyboardCommands}" name="listBox" displayItems="4" forceSelection="true" horizontal="off" width="30%" align="left" />
                                    <text text="${keyboardCommands}" font="Interface/Fonts/Default.fnt" align="right" />
                                </panel>
                                <text text="" font="Interface/Fonts/Default.fnt" align="right" />
                            </#list>
                        </panel>
                    </#list>

                </panel>

            </panel>
            <panel id="panel_bottom" height="15%" width="75%" align="center" childLayout="horizontal">  
                <panel id="panel_bottom_right" height="50%" width="25%" valign="center" childLayout="center">  
                    <control name="button" label="Back" id="QuitButton" align="center" valign="center"                              visibleToMouse="true" > 
                        <interact onClick="back()"/>
                    </control>
                </panel>
                <panel id="panel_bottom_right3" height="50%" width="25%" valign="center" childLayout="center">  
                    <control name="button" label="Select ship" id="PlayButton" align="center" valign="center"                              visibleToMouse="true" > 
                        <interact onClick="next()"/>
                    </control>
                </panel>
            </panel>
        </layer>
    </screen>
</nifty>

Please notice the use of Freemarker commands like list or .counter.

Already uploaded to version 0.4.0 in https://github.com/albertonavarro/nifty-flow

sábado, 29 de agosto de 2015

Input processing in Cabo Trafalgar (and 2)

Another of the risks to face when creating Cabo Trafalgar is making the inclusion of any new element expensive, in terms of work needed due to interactions with elements already existing in the game.




If you didn't design the way of creating new ships correctly, you might end up adding code per command relative to every possible different input available in the system. Likewise, you don't want to modify your ship's commands if you added a new input to your game.
Keeping the work in O(1) is paramount (yes, complexity can be applied to you as a worker).

My strategy for solving this problem is converting one 2-dimensional array into two 1-dimensional ones. Conceptually like this:

In plain English, what this mean is that I decouple actions from actions consumers.

On one side, all I care when I create a new Ship is that it creates "Commands", these are classes able to execute Ship's functions, they represents what a user might want to do. What a ship might or might not do will be determined by the collection of Commands it returns and nothing else. O(1).

On the other side, all I care when I'm creating a new input adapter is that it's able to use commands, generically. It's a simple interface with an execute, so I can focus in the WiiFit complicated bluetooth bridge or the Remote Controll connection, without needing to thing on the other end of the command. O(1)

This is the design implemented in Cabo Trafalgar with the flexibility and "inexpensiveness" in mind.



The story beings with a set of Command, provided by a particular ship (see AShipModelInteractive in the previous post http://cabo-trafalgar.blogspot.co.uk/2015/08/input-processing-in-cabo-trafalgar.html), these are the functionality our ship offers and we need to map it to inputs.

The first step is asking GeneratorBuilder what CommandGenerators are there available for each Command. A CommandGenerator represents a different input, they must be registered in GeneratorBuilder declaring what subclass of Command they support (so far, all CommandGenerators support all Commands, I haven't found an example of subclassing).

So here we have KeyboardCommandGenerator, registered in the GeneratorBuilder (using "registerBuilder" and declaring itself available for each Command it's asked.

As a user, we asked for CommandGenerators for some commands, and we retrieved a list of CommandGenerators per Command, now if that list was longer than 1, we should decide per command how we're going to use it. In this example, we have only KeyboardCommandGenerator so there's little point to choose, only one option per command.

Now we have a command and a "chosen" CommandGenerator, it's time to associate them by invoking on generateCommandStateListener, it takes the Command now, they keycode comes later.

Now we have a collection of KeyboardCommandStateListener, it's time to associate it with keys, up to the user, and the job's done. Each time that key is pressed, the Command in the Ship will be invoked.

The advantage of this design is the ease of adding more CommandGenerators, as I have a screen for choosing the right CommandGenerator per command, I can mix them in the same game.



Input processing in Cabo Trafalgar

If you haven't read multiplayer-modes-in-cabotrafalgar, you should do it now, as "mode 0 - 3" are explained there, otherwise you won't fully understand this article.

This should be the first of a series of articles to explain how to extend Cabo Trafalgar with more and different controls, modules and ships (these are, not by chance, the three dimensions Cabo Trafalgar relies on).

The aim of this article today is explaining why it's not a good idea to rely in the Jme3 Input Manager directly if you're planning to have several multiplayer modes like Cabo Trafalgar does.

In little words, we need to cover the following cases:


Player's input to local game
Player -> Keyboard Input -> Ship calculations locally -> Render locally


Ghost representing past game by same (local file) or (recordserver online service) another player
Downloaded game -> Ship position is set externally -> Render locally


Remote input to local game (multiplayer mode 1)
Remote player -> Remote input -> Ship calculations locally -> Render locally


Multiplayer (multiplayer mode 2 and 3)
Player -> Keyboard Input -> Transmitted to server
and
Server position calculations for all ships -> Ship position is set externally -> Render locally


Player's game is stored in local file or remote server for future ghost/reference
Player -> Keyboard Input -> Ship calculations locally -> Store to file / server


As you can see, if you want to cover at least two of these modes, you need to detach the following elements and combine them according to your needs:

Renderization (AShipModelZ): What we render is a representation of a ship (and other elements), whose position, rotation and scale is known to us, but not how or where they have been calculated. They can come from:
  1. Remotely calculated ship (ShipModelZGhost): This element takes a stream of data from a server (mode 2 and 3) or a file (mode 0) to know what's the position of the ship every frame.
  2. Locally calculated ship (ShipModelZPlayer): This element calculates following position, rotation and scale from previous + input. Input can be:
    1. Local input (keyboard, wiifit, joystick...) (InputManager -> KeyboardGenerator -> ShipModelZControlProxy)
    2. Remote input (mode 1) (RemoteInputCommandGenerator ->ShipModelZControlProxy)
Also, in mode2 and mode3, there's a need for transmitting user's input. It hasn't been implemented yet, but it would take InputManager -> KeyboardGenerator -> ShipModelZControlProxy -> ShipModelZTransmitter (inexistent yet).

Input will deserve another post itself, as it can be more complicated than it seems :)

In the diagram below, green classes belongs to the api, no real meaningful functionality on them, in yellow, ShipModelZ implementation of them:





viernes, 28 de agosto de 2015

Multiplayer modes in CaboTrafalgar

Multiplayer modes in CaboTrafalgar

Commands and Generators

Some common vocabulary needed for understanding following text.

Commands are the smaller representation of an action that a user can do, and it´s declared and generated by each Ship individually. Some ships will offer "rudder left", "right", "mainsail in", "out", while other can offer the same four plus "move weight right", "left". Commands are, therefore, up to the Ship creator and this game takes them and tries to find who can implement them.

Generators come into play then, once we have some commands, we ask the Generator Manager (or similar) how many generators we have available for each one.
There are some generic Generators like Keyboard, able to map all Commands, also Remote is able to map everything, but we might find other Generators restricted about the type of commands able to deal with.

Once we have Commands and the possible Generators, it´s up to the user map each command with the desired generator (they could be keyboard, remote, and also wiifit, joystick, or mouse, just as example).

Finally, each Command is mapped to the chosen generator through a finest specification, in case of keyboard, associating a key.

Multiplayer Mode 0, Offline competition (DONE)

This was the first multiplayer mode done in CaboTrafalgar, for which I have "recordserver". It stores all verified user´s games and best are shared so other users can see, learn and compete against them.

It offers a ranking with times and users to enhance competition.
Information uploaded comprises position, rotation and commands executed per frame, this would be later (not yet) validated by recordserver, who shares back positions per frame, but erases commands.

This way, another player can´t simply take other person´s record as his, commands are needed and game reexecuted to validate authenticity (not yet again).

In order to make this mode, I needed to introduce a Ghost class able to reposition a Ship every frame, which gave me the idea of the current class atlas (to be explained in other post).

Multiplayer Mode 1, Shared controls on a local game (DONE)

We now offer the option of sharing an arbitrary number of Commands to another player, but all calculations are still made locally (your computer, not any of CaboTrafalgar servers) and the rendering is also local to your sole screen. This is, you must be sharing with somebody in your room.

The transfer is made through a barcode able to be read by any SmartPhone that can open a web application with those shared controls on it (this uses our servers), or you could use our app for that (in progress).

As a result, commands travel from your guest´s device to our servers, only to be transfered back with no alteration to your computer, where they´re interpreted just like your own input. As much this is a local collaboration that these results would be uploadable to RecordServer as they were yours alone.

 

Multiplayer Mode 2, Multiplayer game

Little explanation here, who doesn´t know what´s this about? Game doesn´t happen in your computer anymore, you´re just rendering the movements our server will tell you, like they were Mode0 ghosts, and your input will travel far from your computer like it was a Mode1 SmartPhone.

Of course, not everything is always so simple, and some calculation could be done at your computer for fluidity´s and optimization´s sake, but the principles are the same, we need a server online where the calculations are done, and new speeds and positions are redistributed for clients to render.

 

Multiplayer Mode 3, Shared multiplayer game

This is the goal I aim from the first day, this game was always defined as a collaborative multiplayer naval battle simulator. A captain that delegates functions in other players inside the ship, and with help of other ships, fights another number of players collaborating in other ships.

Game again happens remotely, but controls can be shared remotelly as roles, or locally as controls, as a mix of Mode 1 and 2.



miércoles, 8 de julio de 2015

Nifty-flow code released

I finally managed to separate my nifty-flow from cabotrafalgar, and it's started to be ready to open to other users.

What's nifty-flow?
  • A library that helps you to create series of screens to be played in order.
  • A library that helps you to share screens, as they won't need to know where to point next, screens are no longer pointing to other screens.
  • A library that helps you to reuse the same screen, accesible from different screens, allowing decoupling content and routing.
  • A library that (will) help you to apply template technologies to save time creating dynamic screens avoiding java builders.
  • If you're using Spring or Guice for wiring your instances, we won't complain, it's generic enough to accept instances from any system (as long as you build the bridge wink)
It's build on the top of nifty 1.3.3, and fully compatible with jme3 AFAIK.
Code and example here: https://github.com/albertonavarro/nifty-flow
Feedback is welcome, the project works for me though smile

http://hub.jmonkeyengine.org/t/nifty-meets-flow/33139

domingo, 3 de mayo de 2015

Understanding CaboTrafalgar Physics 1 - Tidal forces in shipModelZ

Index:

(To come)


This is the first entry of a series to explain where we are now with physics in the game, and where we want to be.

Most of the entries will refer this "dinghy", ShipModelZ, in regards of movement and commands.

This post is actually an easy win, and that's the best way to start a path, with a victory in an apparently difficult topic as "Tidal movement".

During the hundredths of seconds a frame lasts, several calculations are made to try to simulate movement realistically, considering real or apparent wind, boat position, sail position, rudder position and weight location. In this calculation, tide is not considered, as we're calculating movement OVER the tide (same way, we don't consider the movement of the earth around the Sun for highway speed calculation).

Once we know what's the tri-dimensional distance we want to move over the sea, we need to add the absolute movement of the water, giving as the final distance we have moved.

Code explanation:

private void updatePosition(float tpf) {
        Vector3f shipOrientation3f = this.getGlobalDirection();
        this.move(shipOrientation3f.x * localSpeed * tpf, 0, shipOrientation3f.z * localSpeed * tpf);
        this.move(getContext().getWater().getMovement(this.getGlobalDirection()).mult(-30 * tpf));
        shipDirection.setValue(shipOrientation3f);
    }

For this model, all movement is calculated in the direction of the keel, no lateral movement has been modelled yet. Direction of the keel is "this.getGlobalDirection()"

Local speed is the speed "ahead", direction defined by the keel, it acts as the modulus of a vector, while the keel is the direction of the vector.

As we're just interested in horizontal movement, we take first and third elements in the vector, times the speed, times tpf (time per frame) to get the final distance we've moved over the sea.
On top of this, we apply the movement of the tide for that particular location (this is a bug, we should be passing position, not orientation, but it doesn't matter, the implementation of water context is not as complex yet for it to matter), times some handy constant times time per framework.

So magic is done, on top of the ship movement, we have tide speed complicating everything.

That's it for today.. :)