Lemur, work in progress posted

So, it’s still a definite work in progress but considering I now use this for my UI 100% (despite missing features), I figure I could post it for the more adventurous to start taking a look at.

Code is here:
http://code.google.com/p/jmonkeyplatform-contributions/source/browse/trunk/Lemur?spec=svn756&r=756

First let me start with the limitations:
-there is no animation system
-the text entry field is pretty basic and the document model needs some work probably. No selection, no cut and paste, etc…
-also, right now the key action generation is still handled right in the text entry component instead of being part of a more global service
-only the most basic UI wrapper components are implemented
-there is some slight oddness with mouse capture over layered GUIs.

…However, these kind of belie the nature of what is actually here.

Overview:
Essentially what lemur is is a whole bunch of separate useful things bundled together into a UI. This means that you can use just some of the parts if you like… or you can use the nice wrapper classes to get a full UI experience… or some combination of those.

If you look at classes like Button or Label, you can see that these are basically just hooking up a style system to a couple controls. You can effectively turn any spatial into a button or turn any node into a label just by adding the right controls (one or two controls in the case of button, one in the case of label) and in the case of label, the right components. (More on components in a second.)

The classes that are the most generally/separably useful are in lemur.event. Here you have a MouseAppState that can handle picking in any JME viewport. Add the app state to your app, give it some viewports, and then add MouseEventControls to any spatials you want to receive events. Tada! GUI. It works in 3D or 2D.

If you add a lemur.core.GuiControl to a Spatial then it becomes a more tangible UI component. To this you add layers of GuiComponents which do the job of implementing the view of the UI element. For example, if you add a QuadBackgroundComponent and a TextComponent then you get a label with a solid rectangular background.

Component Stacks
The methodology behind the GuiControl is that a UI element is made up of a stack of GuiComponents with a specific relationship. Each member of the stack influences the rest of the stack. The preferred size is determined “top down” and the actual size and positioning is determined “bottom up”. Both directions act a bit like a “chain of command” pattern altering the state of the next component. Easier to explain with an example, maybe.

Given a QuadBackgroundComponent and a TextComponent stacked in that order… the GuiControl will ask the TextComponent what it’s preferred size is. It then passes this size on to the QuadBackgroundComponent so that it can contribute any additional size that it would like. Moving from the top component to the bottom, each component gets to operate on the “preferred size”.

Once the preferred size for the stack is determined, a size is chosen. Now the chain of command goes the other way with the first component (the quad background) being passed a position and size. It can setup any of its own geometry using that position and size and then alter the position and size for the next component in line… and so on.

So if the QuadBackgroundComponent had a 5 unit margin, it would add that margin during preferred size determination. When being positioned/sized, it would shrink the size for the next component and set the position in by the appropriate margin. So the text component will be drawn in the proper place.

It’s a pretty powerful pattern and can be trivially used in 2D or 3D. In fact, many of the default UI elements operate in 3D (they have Z) even when rendered as a 2D component.

Main Components
While you could build every UI element by hand that way, it’s not particular convenient. Lemur wraps up some common functionality into some basic UI elements. Onto that it straps a CSS-like style system. You can create Panels, Labels, Buttons, etc… include them right where you want or stick them in containers with layouts, whatever.

For example, here is a container panel with a label and a button:
[java]
Container panel = new Container();
panel.addChild( new Label( “My Label” ) );
panel.addChild( new Button( “My Button” ) );
someNode.attachChild(panel);
[/java]

The addChild() method is used instead of attachChild() so that the container’s layout knows about the children.

Panel, Label, and Button are just subclasses of JME’s Node class. You can do anything to them you could any other Node. It’s just that when they have a GuiControl they have special behavior and will lay themselves out.

Furthermore, setting up all of the right AppStates can be tedious as well… also having to pass AssetManager and other app-related classes clutters the code. Lemur has a GuiGlobals singleton that will set this stuff up appropriately, add the right layer comparator to the renderer, keep a reference to asset manager, etc… It also conveniently provides access to texture loading, improved font loading, etc…

Styles
Every UI element (Label, Button, etc.) has an element ID and a style. The element ID defaults to a class specific ID (Label.ELEMENT_ID, Button.ELEMENT_ID, etc.) and the style default to null (the default style). Either of these things can be changed during creation to allow custom styling.

You can set styles at the element ID level or the overall style level. Some basic containment support is also provided. Here are some examples of setting styles in Java code:
[java]
// Get the global styles
Styles styles = GuiGlobals.getInstance().getStyles();

// Set a default background for all panels of any style
styles.getSelector( Panel.ELEMENT_ID, null ).set( “background”,
new QuadBackgroundComponent(new ColorRGBA(0, 0.25f, 0.25f, 0.5f)) );

// Set a default background for any element of style “glass”
styles.getSelector( “glass” ).set( “background”,
new QuadBackgroundComponent(new ColorRGBA(0, 0.5f, 0.5f, 0.5f)) );

// Set a text color for labels in the glass style
styles.getSelector( Label.ELEMENT_ID, “glass” ).set( “color”, new ColorRGBA(0, 0.5f, 0.5f, 0.5f) );

// Set a text color for any buttons that are subcomponents of a slider element
// …when of the “glass” style
styles.getSelector( Slider.ELEMENT_ID, Button.ELEMENT_ID, “glass” ).set( “color”, new ColorRGBA(0, 0.5f, 0.5f, 0.5f) );
[/java]

Using the styles is then as simple as passing the style name when creating the component. For example:
[java]new Label( “My Label”, “glass”)[/java]

UI elements can also be given custom IDs based on the context of their use within a UI. For example, a label might be a “header”.

In the lemur project there is a BasicDemo that illustrates some of this.

Groovy Style Loader
If you have the groovy-all jar in your classpath then you can take advantage of the style loader. This uses slightly customized groovy script to load styles to avoid the messy Java code like above. Right now it’s setup to read a style file as a class resource. AssetManager bindings could be added, etc… I just haven’t needed it.

Here is an example of reading a styles file… any extension can be used but I use .groovy so my editor does proper syntax highlighting automatically. :slight_smile:
[java]new StyleLoader().loadStyleResource( “Interface/mystyles.groovy” );[/java]

The contents of that file might look like:
[java]import com.simsilica.lemur.*
selector( Button.ELEMENT_ID, “glass” ) {
font=“Interface/myfont.fnt”
color=color(1,1,0)
}
selector( “glass” ) {
background = new QuadBackgroundComponent(color(0, 0.5f, 0.5f, 0.5f))
}
[/java]

General Stuff

There are other mesh-related classes and support classes that probably deserve a deeper description than I can provide here. Some things of note:
-TbtQuad and TbtQuadBackgroundComponent. “tbt” = “three by three” This is a 3x3 quad arrangement where the texture coordinates and split coordinates can be manipulated to provide stretching. For example, the border can be maintained at a fixed size in position and texture coordinates while the rest of the quad can be stretched. Anyone who has used nifty is probably already familiar with this concept.
-MBox - a class like Box example that it supports an arbitrary number of “splits” and also supports selective rendering of the sides. If you need a box that is more than 1 quad on a side then this provides it. This might be useful for point lighting or useful for…
-DMesh - wraps another mesh and uses a Deformation function to deform it. DMesh is an actual mesh based on the original wrapped mesh. Therefore it’s bounds, collisions shapes, etc. are all accurate. It’s like shader based deformation except the mesh is really altered. There are many cases where this is useful.

That’s probably enough or this will start to resemble actual documentation. :wink:

16 Likes

P.S.: Let’s not derail things by discussing my bracing style. I’ll fix it to K&R in some later more final version.

1 Like

For easy reference, a small usage example:
http://code.google.com/p/jmonkeyplatform-contributions/source/browse/trunk/Lemur/src/com/simsilica/lemur/demo/BasicDemo.java?r=756

1 Like

Haha this fits me so very much, i use groovy too!

6 Likes

Freaking cutie.

It’s King Julian!

I like to move it move it, I like to, move it!

Paul: painstakingly relicenses his code, minimally cleans it up, commits it, and posts long thread description about… 3 thumbs up.
Remy: posts picture of cute Lemur… 1 thumbs up.

I think I will spend all of my time on google images now. :slight_smile:

2 Likes

Just to twist the knife in the wound I thumb’ed Rémi’s picture post up. :stuck_out_tongue:

I guess that makes it a tie since I’ve also upvoted your OP. XD

In fairness I only thumbsed up the lemur cos he was on 1000 and I felt like breaking it :stuck_out_tongue:

@pspeed said: Paul: painstakingly relicenses his code, minimally cleans it up, commits it, and posts long thread description about.... 3 thumbs up. Remy: posts picture of cute Lemur... 1 thumbs up.

I think I will spend all of my time on google images now. :slight_smile:

lol, I'm sorry Paul. You can troll my posts when ever you want in return ;)

Hi. Thanks for sharing it. It’s amazingly easy to use.

While studying the demo you provided I found a small glitch :
[java]
@Override
public void simpleUpdate(float tpf) {
if (showStatsRef.update()) {
setDisplayStatView(showStatsRef.get());
}
if (showFpsRef.update()) {
setDisplayFps(showFpsRef.get()); // was setDisplayStatView(showFpsRef.get());
}
// …
[/java]

1 Like
@yang71 said: Hi. Thanks for sharing it. It's amazingly easy to use.

While studying the demo you provided I found a small glitch :
[java]
@Override
public void simpleUpdate(float tpf) {
if (showStatsRef.update()) {
setDisplayStatView(showStatsRef.get());
}
if (showFpsRef.update()) {
setDisplayFps(showFpsRef.get()); // was setDisplayStatView(showFpsRef.get());
}
// …
[/java]

Oops. Thanks. :slight_smile: I’ll try to commit an update soon.

Bumpity

As a note to any of those that are using Lemur for HUD’s I ran into an error that Paul helped me solve. Apparently Lemur has it’s own mouse controller so if you are using it as an HUD it will make the cursor visible and not grabbed to the center of the screen. To fix this just add inputManager.setCursorVisible(false) or the respective form of it into the update loop of your game.

1 Like

It’s an oversight that Lemur turns the cursor on when the app state is initialized. It’s an easy fix to just remove that line… or better just disable the mouse app state and it should turn the cursor off… except that’s not right either.

enable() and disable() need to turn the mouse on and off respectively… then you could disable the state to get the cursor to ago away. That’s good anyway since without you can’t really pick anything anyway. :slight_smile:

I’ll fix it the next time I’m doing Lemur updates.

Also note: for anyone who has a main menu of their game first and doesn’t turn on a moving camera until later, this isn’t an issue.

Just posted some minor updates and one major one: I finally got around to adding my InputMapper stuff.

InputMapper is a more robust way of mapping keys to actions. It fully decouples the adding of actions/listeners to the definition of key/axis mapping. Furthermore, it supports combination of keys so you can define a different mapping for A and Shift+A… or Ctrl+Shift+A

Mapping of inputs to actions/listeners is done through FunctionIds. These are the objects that identify a particular “action” and let the registering of listeners be completely decoupled from the mapping of inputs. This is different to InputManager where you will get an error if you try to register a listener for a mapping that does not exist. You can also register multiple types of inputs to the same function. More on why this is useful in a sec.

The other big thing is that all “functions” can be treated as both analog or “binary”… or both. So you can register ‘W’ to be the positive side of the “Move” function and ‘S’ to be the negative side. Then you can map a joystick axis to “Move”. You can either listen to this like an analog input in which case ‘W’ will trigger a value of 1.0… or a joystick will be some analog value. Or you can register for the binary state change in which case you will get notified about InputState.POSITIVE whether the user presses ‘w’ or pushes the joystick up.

So here is an example of mapping inputs for player movement:
[java]public class PlayerMovementFunctions
{
public static final String GROUP_MOVEMENT = “Movement”;

public static final FunctionId F_Y_LOOK = new FunctionId(GROUP_MOVEMENT, "Y Look"); 
public static final FunctionId F_X_LOOK = new FunctionId(GROUP_MOVEMENT, "X Look");

public static final FunctionId F_MOVE = new FunctionId(GROUP_MOVEMENT, "Move"); 
public static final FunctionId F_STRAFE = new FunctionId(GROUP_MOVEMENT, "Strafe");

public static final FunctionId F_JUMP = new FunctionId(GROUP_MOVEMENT, "Jump");
public static final FunctionId F_RUN = new FunctionId(GROUP_MOVEMENT, "Run");

public static void initializeDefaultMappings( InputMapper inputMapper ) {
    // The joystick axes are backwards on game pads... forward
    // is negative.  So we'll flip it over in the mapping.   
    inputMapper.map( F_MOVE, InputState.NEGATIVE, Axis.JOYSTICK_LEFT_Y );
    inputMapper.map( F_MOVE, KeyInput.KEY_W );
    inputMapper.map( F_MOVE, InputState.NEGATIVE, KeyInput.KEY_S );
    inputMapper.map( F_STRAFE, Axis.JOYSTICK_LEFT_X );
    inputMapper.map( F_STRAFE, KeyInput.KEY_D );
    inputMapper.map( F_STRAFE, InputState.NEGATIVE, KeyInput.KEY_A );

    inputMapper.map( F_X_LOOK, Axis.MOUSE_X );
    inputMapper.map( F_X_LOOK, Axis.JOYSTICK_RIGHT_X );
    inputMapper.map( F_X_LOOK, KeyInput.KEY_RIGHT );
    inputMapper.map( F_X_LOOK, InputState.NEGATIVE, KeyInput.KEY_LEFT );
    
    inputMapper.map( F_Y_LOOK, Axis.MOUSE_Y ); 
    inputMapper.map( F_Y_LOOK, Axis.JOYSTICK_RIGHT_Y ); 
    inputMapper.map( F_Y_LOOK, KeyInput.KEY_UP );
    inputMapper.map( F_Y_LOOK, InputState.NEGATIVE, KeyInput.KEY_DOWN );

    inputMapper.map( F_JUMP, KeyInput.KEY_SPACE );
    inputMapper.map( F_JUMP, Button.JOYSTICK_BUTTON3 );

    inputMapper.map( F_RUN, KeyInput.KEY_LSHIFT );
    inputMapper.map( F_RUN, Button.JOYSTICK_RIGHT1 );
}     

}
[/java]

FunctionIds don’t have to have groups but that lets you easily activate and deactivate entire sets of them.

An example of an analog listener might look like:
[java]
class MyMoveListener implements AnalogFunctionListener {
public void valueActive( FunctionId func, double value, double tpf ) {
if( func == PlayerMovementFunctions.F_MOVE ) {
move( value * tpf );
}
}
}
[/java]

An example of an action listener might look like:
[java]
class MyInputStateListener implements StateFunctionListener {
public void valueChanged( FunctionId func, InputState value, double tpf ) {
boolean on = value == InputState.POSITIVE;

    if( func == PlayerMovementFunctions.F_RUN ) {
        setRunning(on);
    }            
}

}
[/java]

As I hinted earlier, registration of listeners is completely decoupled from registration of input mappings. The things reacting to the functions don’t need to know anything about how they are mapped to inputs. In fact, you can have functions that are not mapped at all until a user configures them or whatever.

Using the InputMapper is easy. If you are using the rest of Lemur, then you can just grab the one from GuiGlobals:
[java]InputMapper mapper = GuiGlobals.getInstance().getInputMapper();[/java]

Or if you just want to manage your own:
[java]InputMapper mapper = new InputMapper(app.getInputManager());[/java]

As an aside…

@nh-99 said: As a note to any of those that are using Lemur for HUD's I ran into an error that Paul helped me solve. Apparently Lemur has it's own mouse controller so if you are using it as an HUD it will make the cursor visible and not grabbed to the center of the screen. To fix this just add inputManager.setCursorVisible(false) or the respective form of it into the update loop of your game.

I fixed this issue in this update. If you disable the MouseAppState then it won’t enable the cursor. It is enabled by default since it defaults to thinking there will be a main menu before the game starts.

Edit: it’s weird that the forum is now gobbling up my paragraph breaks. Awesome.

3 Likes