Dynamic, multi-layered HUD - Code included

I got bored this morning and threw this together in a few hours. It’s a multi-layered HUD controller. You can add things to each layer as you wish, and it includes a class for items that are temporarily on the screen, such as warnings, hit markers, etc.

It works by having a set number of layers, which has to be odd.
The centermost layer stays stationary on the screen and does not move around. It follows the camera as the camera is.
Layers less than the centermost layer move behind the camera, and are constantly interpolating towards where the camera is looking.
Layers more than the centermost layer move ahead of the camera, and are also interpolating towards where the camera is looking.

It’s pretty straightforward, and relatively easy to implement. I wrote it as an AppState, so you can attach it and detach is as you wish.

The code:
[java]
package src;

import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import java.util.ArrayList;

/**
*

  • @author Brendan Lyon
    */
    public class DynamicHUD extends AbstractAppState {

    private int numOfLayers;
    private Node[] layers;
    private ArrayList<DynamicHUDTempElement> tempElements;
    private Quaternion[] hudRotation;
    private float localCatchUpSpeed = 5.0F;
    private float swing = 50.0F;

    public DynamicHUD(int numOfLayers)
    {
    this.numOfLayers = numOfLayers;
    layers = new Node[numOfLayers];
    tempElements = new ArrayList();
    hudRotation = new Quaternion[numOfLayers];

     for (int x = 0; x &lt; numOfLayers; x++) {
         layers[x] = new Node();
         hudRotation[x] = new Quaternion();
     }
    

    }

    public void addToLayer(int layer, Spatial objToAdd)
    {
    assert (layer > 0);
    assert (layer < numOfLayers);
    assert (objToAdd != null);

     System.out.println(layer + ", " + objToAdd);
    
     layers[layer].attachChild(objToAdd);
    

    }

    /**

    • Remove an item from the HUD
    • @param remove The item to remove.
    • @return True if removed, or false if the spatial wasn’t found in any of
    • the layers.
      */
      public boolean removeFromHUD(Spatial remove)
      {
      for (Node node : layers) {
      if (node.hasChild(remove)) {
      node.detachChild(remove);
      return true;
      }
      }
      return false;
      }

    /**

    • Adds a temporary element to the screen, like a warning, hit marker, or
    • other things.
    • @param tempElement The element to add
    • @param ttl The time the element will stay on screen (in seconds)
      */
      public void addTemporaryElement(DynamicHUDTempElement tempElement, float ttl)
      {
      tempElement.setTimeToLive(ttl);
      tempElements.add(tempElement);
      layers[numOfLayers].attachChild(tempElement);
      }

    /**

    • Sets the speed at which the HUD catches up with the true rotation of the
    • camera. Higher values = faster interpolation.
    • Default value: 5.0F
    • @param lcus the new local catch up speed.
      */
      public void setLocalCatchUpSpeed(float lcus)
      {
      localCatchUpSpeed = lcus;
      }

    /**

    • Sets the HUD’s swing value. Higher values makes the lowest and highest
    • layers move further from the center. Lower values makes the lowest and
    • highest panels stay tighter to the center.
    • Default value: 50.0F
    • @param swing
      */
      public void setHUDSwing(float swing)
      {
      this.swing = swing;
      }

    public class DynamicHUDTempElement extends Node {

     private float ttl;
     private float timeAlive;
    
     public void setTimeToLive(float ttl)
     {
         this.ttl = ttl;
     }
    
     @Override
     public void updateLogicalState(float tpf)
     {
         timeAlive += tpf;
         if (timeAlive &gt;= ttl) {
             removeFromParent();
             return;
         }
    
         super.updateLogicalState(tpf);
     }
    

    }
    /AppState stuff/
    private SimpleApplication app;

    @Override
    public void initialize(AppStateManager asm, Application app2)
    {
    super.initialize(asm, app2);
    this.app = (Main) app2;

     for (Node layer : layers) {
         this.app.getGuiNode().attachChild(layer);
     }
    

    }

    @Override
    public void update(float tpf)
    {
    //update layer positions
    int layerNum = 0;
    for (Node layer : layers) {
    float interpBaseValue = (float) layerNum - (numOfLayers / 2);
    float catchUpSpeed;
    if (interpBaseValue != 0) {
    catchUpSpeed = (interpBaseValue * localCatchUpSpeed) * tpf;
    } else {
    catchUpSpeed = 1.0F;
    }

         boolean isNegative = catchUpSpeed &lt; 0.0F;
         if (isNegative) {
             catchUpSpeed *= -1.0F;
         }
    
         hudRotation[layerNum].nlerp(app.getCamera().getRotation(), catchUpSpeed &gt; 1.0F ? 1.0F : catchUpSpeed);
         Vector3f dir = hudRotation[layerNum].mult(Vector3f.UNIT_Z);
         float x = dir.dot(app.getCamera().getLeft());
         float y = dir.dot(app.getCamera().getUp());
    
         layer.setLocalTranslation(x * (isNegative ? -swing : swing), y * (isNegative ? swing : -swing), layerNum);
    
         layerNum++;
     }
    

    }

    @Override
    public void setEnabled(boolean enabled) {
    if(enabled != isEnabled()) {
    super.setEnabled(enabled);
    if(enabled) {
    for(Node layer : layers) {
    app.getGuiNode().attachChild(layer);
    }
    } else {
    for(Node layer : layers) {
    layer.removeFromParent();
    }
    }
    }
    }
    }
    [/java]

2 Likes

Screenhots or it didn’t happen :stuck_out_tongue:

3 Likes

I updated it a bit. Instead of nlerping a quaternion per layer, I just nlerp’d the same quaternion and multiplied further layers by larger numbers. Perfect effect (actually much better than before), and much less overhead. :smiley:

New code:
[java]
package src;

import com.jme3.app.Application;
import com.jme3.app.SimpleApplication;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.math.FastMath;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Node;
import com.jme3.scene.Spatial;
import java.util.ArrayList;

/**
*

  • @author Brendan Lyon
    */
    public class DynamicHUD extends AbstractAppState {

    private int numOfLayers;
    private Node[] layers;
    private ArrayList<DynamicHUDTempElement> tempElements;
    private Quaternion hudRotation = new Quaternion();
    private float localCatchUpSpeed = 5.0F;
    private float swing = 50.0F;

    public DynamicHUD(int numOfLayers)
    {
    this.numOfLayers = numOfLayers;
    layers = new Node[numOfLayers];
    tempElements = new ArrayList();

     for (int x = 0; x &lt; numOfLayers; x++) {
         layers[x] = new Node();
     }
    

    }

    public void addToLayer(int layer, Spatial objToAdd)
    {
    assert (layer > 0);
    assert (layer < numOfLayers);
    assert (objToAdd != null);

     layers[layer].attachChild(objToAdd);
    

    }

    /**

    • Remove an item from the HUD
    • @param remove The item to remove.
    • @return True if removed, or false if the spatial wasn’t found in any of
    • the layers.
      */
      public boolean removeFromHUD(Spatial remove)
      {
      for (Node node : layers) {
      if (node.hasChild(remove)) {
      node.detachChild(remove);
      return true;
      }
      }
      return false;
      }

    /**

    • Adds a temporary element to the screen, like a warning, hit marker, or
    • other things.
    • @param tempElement The element to add
    • @param ttl The time the element will stay on screen (in seconds)
      */
      public void addTemporaryElement(DynamicHUDTempElement tempElement, float ttl)
      {
      tempElement.setTimeToLive(ttl);
      tempElements.add(tempElement);
      layers[numOfLayers].attachChild(tempElement);
      }

    /**

    • Sets the speed at which the HUD catches up with the true rotation of the
    • camera. Higher values = faster interpolation.
    • Default value: 5.0F
    • @param lcus the new local catch up speed.
      */
      public void setLocalCatchUpSpeed(float lcus)
      {
      localCatchUpSpeed = lcus;
      }

    /**

    • Sets the HUD’s swing value. Higher values makes the lowest and highest
    • layers move further from the center. Lower values makes the lowest and
    • highest panels stay tighter to the center.
    • Default value: 50.0F
    • @param swing
      */
      public void setHUDSwing(float swing)
      {
      this.swing = swing;
      }

    public class DynamicHUDTempElement extends Node {

     private float ttl;
     private float timeAlive;
    
     public void setTimeToLive(float ttl)
     {
         this.ttl = ttl;
     }
    
     @Override
     public void updateLogicalState(float tpf)
     {
         timeAlive += tpf;
         if (timeAlive &gt;= ttl) {
             removeFromParent();
             return;
         }
    
         super.updateLogicalState(tpf);
     }
    

    }
    /AppState stuff/
    private SimpleApplication app;

    @Override
    public void initialize(AppStateManager asm, Application app2)
    {
    super.initialize(asm, app2);
    this.app = (Main) app2;

     for (Node layer : layers) {
         this.app.getGuiNode().attachChild(layer);
     }
    

    }

    @Override
    public void update(float tpf)
    {
    //update layer positions
    float catchUp = tpf * localCatchUpSpeed;
    catchUp = catchUp > 1.0F ? 1.0F : catchUp;
    hudRotation.nlerp(app.getCamera().getRotation(), catchUp);

     int layerNum = 0;
     for (Node layer : layers) {
         float interpBaseValue = (float) layerNum - (numOfLayers / 2);
         
         boolean isNegative = interpBaseValue &lt; 0;
         
         Vector3f dir = hudRotation.mult(Vector3f.UNIT_Z);
         float x = dir.dot(app.getCamera().getLeft());
         float y = dir.dot(app.getCamera().getUp());
    
         layer.setLocalTranslation(x * (isNegative ? -swing : swing) * FastMath.abs(interpBaseValue), y * (isNegative ? swing : -swing) * FastMath.abs(interpBaseValue), layerNum);
    
         layerNum++;
     }
    

    }

    @Override
    public void setEnabled(boolean enabled) {
    if(enabled != isEnabled()) {
    super.setEnabled(enabled);
    if(enabled) {
    for(Node layer : layers) {
    app.getGuiNode().attachChild(layer);
    }
    } else {
    for(Node layer : layers) {
    layer.removeFromParent();
    }
    }
    }
    }
    }[/java]

And finally, screenshots. @erlend_sh, your proofs are here.

I can’t find the button to post an image… helps?