Montag, 3. November 2014

Unity3D: 3D-Mesh LED HUD Healthbar

So, you want to achieve something like that?



Then stick to that tutorial.

I will NOT explain here, how to setup a second camera for the HUD, because you can make it your way. What I will explain here, is how to create a Healthbar (or other-bar) using "LEDs".

First, create a mesh with your LEDs in it. Every LED is an object (the mesh must have several parts in unity), wich is named after your LED-name wich you use later. I call them LED_x, where x is a number. Number has no zeroes before. (Not 01, just 1, and then 10)

You can make that mesh with Unity (empty gameObject with some spheres in it) or with your desired 3D-modeling software. Just remember, the LEDs must have all the same name with a number, defining wich LED on the bar it is. And remember for exporting that it must have several parts in unity. Don't merge the LEDs with the background-mesh...

Set all materials of the LEDs to the shader "VertexLit".

The first script we create has some static functions wich will be used later:

public class Static
{
    public const string TAG_HUDROOT = "HUDroot";

    // finds the gameobject tagged with text along the parents.
    public static Transform FindParentWithTag(Transform owner,string tag)
    {
        if (owner && owner.tag == tag)
            return owner;
        else if (!owner)
        {
            Debug.LogWarning("WARNING: No parent with tag " + tag + " found.");
            return null;
        }
        return FindParentWithTag(owner.transform.parent, tag);
    }

    // sets emmisive color on the renderer, if it has one.
    public static void SetRendererEmissiveColor(Renderer renderer, Color color,bool affectDiffuse=true)
    {
        if (!renderer)
            return;

        //renderer.material.SetColor(0, color);

        // maybe leave the diffuse color alone.
        if (affectDiffuse) 
            renderer.material.color = color;
        renderer.material.SetColor("_Emission", color);
        renderer.material.SetColor("_EmisColor", color);
    }

    // turns of the emissive color on the renderer, if it has one.
    public static void TurnOffRendererEmissiveColor(Renderer renderer,bool affectDiffuse=false)
    {
        if (!renderer)
            return;

        // maybe leave the diffuse color alone.
        if (affectDiffuse)
            renderer.material.color = Color.black;

        renderer.material.SetColor("_Emission", Color.black);
        renderer.material.SetColor("_EmisColor", Color.black);
    }
}

Now, we begin to build up our HUD.

Create a gameObject wich is centered on your HUD-cam (or has the same position or something like that). This is the HUDs root-node.

Tag it with "HUDroot" (create that tag, if it does not exist.)

Now add the script "TargetComponent" to the root node:

public class TargetComponent : MonoBehaviour
{ 
    public bool getCamTarget = true;
    public bool destroyIfNoTarget = true;
    public Transform target;
// Use this for initialization
void Start () {
        // get it every update so it changes when it changes.
        if (getCamTarget)
            GetCamTarget();  
}

// Update is called once per frame
void Update () 
    {
        if (destroyIfNoTarget && !target)
            GameObject.DestroyObject(gameObject);
    }

    public void GetCamTarget()
    {
        Camera mainCam = Camera.main;

        if (!mainCam)
            return;

        CameraController c= mainCam.GetComponent<CameraController>();

        if (!c)
        {
            Debug.LogError("ERROR: No CameraController-Script attached to the main camera.\nPlease attach it.");
            return;
        }

        target = c.target;
    }
}


This script contains the target for the whole HUD. It is mostly the player, but for
some HUD items, it could be e.g. the players target or info about some item.

I will not explain here about the CameraController.
It must have a target, wich is your player. That's all.

Add your mesh (gameObject) to this root node and positionate and rotate it like you desire.

Now add the script LEDBar to your HUDs root node:
public class LEDBar : MonoBehaviour 
{

    private void TESTFUNCTION() {;;}
    /*
    private void TESTFUNCTION()
    {
        // HULL HUD
        int posOrange = (int)(ArrayCount * 0.3333f) + 1; // 1/3 is orange
        // red is just the second LED
        int posRed = (int)(ArrayCount * 0.1f) + 1; // 1/10 is red

        // first green -> full range
        TurnOnLEDs(8, 15, new Color(0.0f, 1.0f, 0.0f));
        // then orange: 1/3 range
        TurnOnLEDs(new Color(1.0f, 1.0f, 0.0f), posRed + 1, posOrange, 8, 15);
        // then red: 1/5 range
        TurnOnLEDs(new Color(1.0f, 0.0f, 0.0f), 1, posRed, 8, 15);
    }
    */


    public bool blackIfOff=false;
    public string arrayName = "LED_";

    private MeshRenderer[] LEDarray;
    // the ship wich the hud targets on.
    protected Transform target;
    // the huds root node.
    protected Transform root;


    protected void INITIALIZE()
    {
        //print("INITIALIZE");
        root = Static.FindParentWithTag(transform, Static.TAG_HUDROOT);
        if (!root)
        {
            Debug.LogError("ERROR: No HUDroot found for " + gameObject.name + "\nPlease tag the root object of your hud with " + Static.TAG_HUDROOT);
            return;
        }

        NotifyLEDs();

        // TEST TEST TEST
        TESTFUNCTION();
        // END TEST

        TargetComponent t = root.GetComponent<TargetComponent>();
        if (!t)
        {
            Debug.LogError("ERROR: No TargetComponent-Script attached to the HUDs root node.\nPlease attach it.\nIt searches for the HUDs target and sets it \"globally\" for the HUD.");
            return;
        }

        target = t.target;
// design flaw? target is null
        if (!target)
            print("WTF?");
    }

// ...so that is called in update...
    protected void FINDTARGET()
    {
        if (!target)
        {
            TargetComponent t = root.GetComponent<TargetComponent>();
            target = t.target;
        }
    }

    // Use this for initialization
    void Start () 
    {
        INITIALIZE();
    }

    // Update is called once per frame
    void Update () 
    {
        FINDTARGET();
    }

    public void NotifyLEDs()
    {
        if (!root)
        {
            Debug.LogError("ERROR: Cannot assign LEDs-Meshes because no HUD-root was found in "+gameObject.name+".\nPlease tag the root object of your HUD with " + Static.TAG_HUDROOT);
            return;
        }

        int c = RealCount;
        if (c <= 0)
        {
            Debug.LogWarning("WARNING: No HUD-Meshes found for " + arrayName + "x in " + gameObject.name+" in "+root.name+"\nHULLBar-Script needs to be in its own GameObject.");
            LEDarray = null;
            return;
        }

        // c > 0
        LEDarray = new MeshRenderer[c];

        var renderers = root.GetComponentsInChildren<MeshRenderer>();
        if (renderers.Length <= 0)
        {
            Debug.LogError("ERROR: @LEDBar.NotifyLEDs: No renderers found in " + root.name + ".");
            return;
        }

        int ii = 1;
        for (int i = 0; i < c; i++)
        {
            ii = i + 1;
            foreach (MeshRenderer r in renderers)
            {
                if(r.gameObject.name==arrayName+ii.ToString())
                    LEDarray[i]=r;
            }
        }

        BlackOut(blackIfOff);
        Debug.Log("HUD Mesh Notify: "+c+" HUD Meshes with name " + arrayName + "x found.");
    }

    // turn on leds first to last based on a range
    public void TurnOnLEDs(Color color,int lastLEDposition, float value, float maxValue)
    {
        //print("TURNON LEDS ALL VALUES");

        if (ArrayCount <= 0)
            return;

        if (lastLEDposition <= 0)
        {
            Debug.LogError("ERROR: LED range <= 0 @ LEDBar.TurnOnLEDs_#1 @" + gameObject.name);
            return;
        }

        // else count steps up and turn on each led < step.
        float LEDstep=0.0f;
        
        if(maxValue!=0)
            LEDstep = maxValue / ArrayCount;

        float rangedMaxValue = lastLEDposition * LEDstep;

        // maybe just turn on all the LEDs
        if (value >= rangedMaxValue)
        {
            //print("value > > > "+lastLEDposition);
            TurnOnLEDs(color, 1, lastLEDposition);
            return;
        }

        //print("value = " + value + " r=" + rangedMaxValue+" l="+lastLEDposition);

        float actual = 0.0f;
        for (int i = 0; i < lastLEDposition; i++)
        {
            // maybe we change index.
            int index = i+1;
            actual = i * LEDstep;
            if (value>0 && value >= actual)
                TurnOnLED(index,color);
        }
    }

    // turn on leds first to last.
    // range starts with 1!
    public void TurnOnLEDs(Color color,int first, int last)
    {
        if (ArrayCount <= 0)
            return;

        if (first <= 1)
            first = 1;

        // how many leds to light?
        int ledRange = last - first + 1;
        if (ledRange <= 0)
        {
            Debug.LogError("ERROR: LED range <= 0 @ LEDBar.TurnOnLEDs_#2 @" + gameObject.name);
            return;
        }

        for (int i = 0; i < ledRange; i++)
        {
            int index=first+i;
            TurnOnLED(index, color);
        }
    }

    // turn on ALL leds based on a range.
    // switching colors here for not messing up with the parameters (int and float above)
    public void TurnOnLEDs(float value, float maxValue,Color color)
    {
        //print("TurnOnLEDs range");
        if(ArrayCount<=0)
            return;
        TurnOnLEDs(color,ArrayCount, value, maxValue);
    }

    // index must start with 1!
    public void TurnOnLED(int index, Color color)
    {
        index -= 1;
        if (index >= ArrayCount)
            return;
        if (LEDarray[index])
            Static.SetRendererEmissiveColor(LEDarray[index], color);
        else
            Debug.LogWarning("WARNING: HUD item #" + (index + 1) + " is not assigned.\nLED name: " + arrayName + "x in " + gameObject.name);
    }


    // turn all the leds off.
    public void BlackOut(bool affectDiffuse = false)
    {
        if (ArrayCount<=0)
            return;
        foreach (MeshRenderer r in LEDarray)
            Static.TurnOffRendererEmissiveColor(r,affectDiffuse);
    }

    public int ArrayCount { get { return LEDarray.Length; } }

    // (re)count the items in the gameObject
    public int RealCount
    {
        // count all leds in the in the root node, with the same name.
        get
        {
            var renderers = root.GetComponentsInChildren<MeshRenderer>();
            if (renderers.Length<=0)
            {
                Debug.LogError("ERROR: @LEDBar.RealCount: No renderers found in "+root.name+".");
                return 0;
            }

            bool done = false;
            bool found = false;
            string arname = "";
            int count = 0;
            
            while (!done)
            {
                int c = count + 1;
                arname = arrayName + c.ToString();

                found = false;
                foreach (MeshRenderer r in renderers)
                {
                    if (r.gameObject.name == arname)
                    {
                        Debug.Log("Registering HUD object "+r.gameObject.name);
                        count++;
                        found = true;
                    }
                }

                if (!found)
                    done = true;
            }
            return count;
        }
    }
}



That's a big one. You can add as much of them to your root node as you want.
The important thing here is "arrayName", where you put the name of your LEDs WITHOUT any number. ("LED_")

You can now use BlackOut, TurnOnLEDs and TurnOnLED as you wish.

BlackOut(makeBlack=false) - turns Off all LEDs.
TurnOnLED(index) - turns on a single LED.
TurnOnLEDs(value, maxValue, Color) - turns on the LEDs based on the value and maxValue.
TurnOnLEDs(Color, first, last) - turns on the LEDs from # first to last.
-> And the other function combines that both.
ArrayCount returns the Length of the LED-array.
(RealCount counts the LEDs in the mesh-construction.)

You can now use that as base class for your HUD, and stuff the values/maxValues-scripting-stuff into the derived class, using the "target"-value.

Example class to add to your HUD:
public class HealthHUD : HUDBar
{
    void Start()
    {
         INITIALIZE();
         // ...
    }
 
     void Update()
    {
        FINDTARGET(); // there is a bug with finding the target in Start. I dunno...

        if (!target)
            return;

        BlackOut(true);  // Set all LEDs to black.

        // HULL HUD

         //Destructable is a class with health values.
        // ...make your own...
        Destructable d = target.GetComponent<Destructable>();
        if (d)
        {
            int posOrange = (int)(ArrayCount * 0.3333f) + 1; // 1/3 is orange
            // red is just the second LED
            float posRed = (int)(ArrayCount * 0.1f) + 1; // 1/10 is red

            // first green -> full range
            TurnOnLEDs(d.currentHealthPoints, d.maxHealthPoints, new Color(0.0f, 1.0f, 0.0f));
            // then orange: 1/3 range
            TurnOnLEDs(new Color(1.0f, 1.0f, 0.0f),posOrange, d.currentHealthPoints, d.maxHealthPoints);
            // then red: 1/10 range
            TurnOnLEDs(new Color(1.0f, 0.0f, 0.0f), (int)posRed, d.currentHealthPoints, d.maxHealthPoints);
        }
    }
}

Hope that helps. It's tested but not this blog entry. If you miss something, tell me.

Keine Kommentare:

Kommentar veröffentlichen