UI Starter

My goal for today is to finish off the compass implementation (for now) and mock in the rest of the features I listed last time. To re-iterate, I want to have these things mocked up before moving on from UI for a little while:

  • Health bar
  • Toolbar
  • Equipted item info
  • XP Bar
  • Character Level
  • Available skill point notification
  • Compass

Compass

Alright now I'm ready to start adding some elements to the compass and have them get updated every frame depending on camera rotation. To start, I noticed while playing with this a little bit that my range is way off and even worse, my little function for calculating if something is in view is also way off, so I fixed those up first! Calculate range:

void UpdateRange()
{
    // Calculate range
    range[0] = center - CompassWidth/2;
    if (range[0] < 0)
    {
        range[0] = 360 + range[0];
    }
    range[1] = range[0] + CompassWidth;
    if (range[1] > 359)
    {
        range[1] = range[1] - 360;
    }
}

Decide if something is within the viewable range:

public bool InRange(int degree)
{
    float mid = degree - range[0];
    if (mid < 0)
    {
        mid += 360;
    }
    return (mid < CompassWidth);
}

From here, we're going to want to add ab object to control elements we're adding to the compass view...

class CompassElement : MonoBehaviour
{
    private int degree;
    private GameObject gameObject;
    private float width;
    private float height;
    Compass compass;
    public void Initialize(GameObject prefab, float Width, float Height, Compass Compass, int Degree)
    {
        if (Degree > 359 || Degree < 0)
        {
            throw new Exception("Must place on a degree from 0 to 359");
        }
        if (prefab == null)
        {
            throw new Exception("prefab cannot be null");
        }
        if (Compass == null)
        {
            throw new Exception("Compass cannot be null");
        }
        // Store values
        degree = Degree;
        width = Width;
        height = Height;
        compass = Compass;
    }

Let's update our compass class to create instances of this to create major/minor compass indicators to make sure our implementation works:

void Start()
{
    ...
    // Add compass indicators
    AddMajorIndicators();
    AddMinorIndicators();
}
void AddMajorIndicators()
{
    var north = this.gameObject.AddComponent<CompassElement>();
    var east  = this.gameObject.AddComponent<CompassElement>();
    var south = this.gameObject.AddComponent<CompassElement>();
    var west  = this.gameObject.AddComponent<CompassElement>();
    north.Initialize(MajorDegreeIndicator, 2, canvasHeight / 2, this, 0);
    east.Initialize(MajorDegreeIndicator,  2, canvasHeight / 2, this, 90);
    south.Initialize(MajorDegreeIndicator, 2, canvasHeight / 2, this, 180);
    west.Initialize(MajorDegreeIndicator,  2, canvasHeight / 2, this, 270);
    CompassElements.Add(north);
    CompassElements.Add(east);
    CompassElements.Add(south);
    CompassElements.Add(west);
}
void AddMinorIndicators()
{
    var north_east = this.gameObject.AddComponent<CompassElement>();
    var south_east = this.gameObject.AddComponent<CompassElement>();
    var south_west = this.gameObject.AddComponent<CompassElement>();
    var north_west = this.gameObject.AddComponent<CompassElement>();
    north_east.Initialize(MinorDegreeIndicator, 2, canvasHeight / 4, this, 45);
    south_east.Initialize(MinorDegreeIndicator, 2, canvasHeight / 4, this, 135);
    south_west.Initialize(MinorDegreeIndicator, 2, canvasHeight / 4, this, 225);
    north_west.Initialize(MinorDegreeIndicator, 2, canvasHeight / 4, this, 315);
    CompassElements.Add(north_east);
    CompassElements.Add(south_east);
    CompassElements.Add(south_west);
    CompassElements.Add(north_west);
}

When we create a compass element, we want to create an instance of the prefab, and move it onto the center of the compass to start

 // Create instance
gameObject = Instantiate(prefab);
gameObject.transform.SetParent(compass.transform);
gameObject.GetComponent<RectTransform>().SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top, compass.canvasHeight/2 - height/2, height);
gameObject.GetComponent<RectTransform>().SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, (compass.canvasWidth/2)-width/2, width);

Then, on every frame, update it's actual location:

private void UpdatePosition()
{
    // Is this element in range?
    if (compass.InRange(degree))
    {
        // Enable if disabled
        if (!gameObject.activeSelf)
        {
            gameObject.SetActive(true);
        }
        // Calculate offset from left of compass in degrees
        int degreeOffset = compass.range[1] - degree;
        if (degreeOffset < 0)
        {
            degreeOffset += 360;
        }
        // Calculate positional offset from left of compass
        float xOffset = degreeOffset * compass.resolution;
        gameObject.GetComponent<RectTransform>().SetInsetAndSizeFromParentEdge(RectTransform.Edge.Right, xOffset-width/2, width);
    }
    else
    {
        // Disable if enabled
        if (gameObject.activeSelf)
        {
            gameObject.SetActive(false);
        }
    }
}
void Update()
{
    UpdatePosition();
}

Everything else

I think the compass is likely the hardest part, so I think the rest will come together pretty quickly. Keep in mind, I'm mostly just trying to mock everything up... I expect the game mechanics I want to implement are going to change a lot once I actually get into the meat of this, so I just want something sketchy to get started with. Most of the remaining UI is linked to properties of the player, so I'm going to take a moment to rework my first person camera, into a player. Here's what I've currently got:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FirstPersonCamera : HexUnit
{
    public float turnSpeed = 4.0f;
    public float moveSpeed = 0.125f;
    public float minTurnAngle = -90.0f;
    public float maxTurnAngle = 90.0f;
    private float rotX;
    public float playerHeight = 15f;
    private bool _Locked = false;
    private HexCell currentCell = null;
    public HexGrid grid;
    private Camera camera = null;
    public bool Locked
    {
        get {
            return _Locked;
        }
        set
        {
            camera.enabled = !value;
            _Locked = value;
        }
    }
    void Awake()
    {
        camera = gameObject.GetComponent(typeof(Camera)) as Camera;
    }
    void Start()
    { 
        SpawnPlayer();
    }
  
    void Update()
    {
        if ( !Locked )
        {
            MouseAiming();
            KeyboardMovement();
            UpdateHexLocation();
            VerticalAdjustment();
        }
    }
   
    void MouseAiming()
    {
        // get the mouse inputs
        float y = Input.GetAxis("Mouse X") * turnSpeed;
        rotX += Input.GetAxis("Mouse Y") * turnSpeed;
        // clamp the vertical rotation
        rotX = Mathf.Clamp(rotX, minTurnAngle, maxTurnAngle);
        // rotate the camera
        transform.eulerAngles = new Vector3(-rotX, transform.eulerAngles.y + y, 0);
    }
    void KeyboardMovement()
    {
        Vector3 dir = new Vector3(0, 0, 0);
        dir.x = Input.GetAxis("Horizontal") * moveSpeed;
        dir.z = Input.GetAxis("Vertical") * moveSpeed;
        transform.Translate(dir);
    }
    void UpdateHexLocation()
    {
        HexCell cell = grid.GetCell(new Ray(transform.position, Vector3.down));
        if (cell != currentCell)
        {
            currentCell = cell;
        }
    }
    void VerticalAdjustment()
    {
        if (currentCell != null)
        {
            transform.position = new Vector3(transform.position.x,currentCell.transform.position.y+playerHeight,transform.position.z);
        }
    }
    void SpawnPlayer()
    {
        // Get spawn point
        currentCell = grid.GetCell(new HexCoordinates(0, 0));
        // Translate player to spawn
        transform.Translate(currentCell.transform.position);
    }
}

I'm going to separate out some details of this into a Player script, and a camera script and start working on a more drawn-own hierarchy... FirstPersonCamera:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FirstPersonCamera : HexUnit
{
    public float turnSpeed = 4.0f;
    public float minTurnAngle = -90.0f;
    public float maxTurnAngle = 90.0f;
    private float rotX;
    private Camera camera;
    private Player player;  
    void Start()
    {
        player = transform.parent.gameObject.GetComponent<Player>();
        camera = gameObject.GetComponent<Camera>();
        SpawnPlayer();
    }
    void Update()
    {
        MouseAiming();
        KeyboardMovement();
        VerticalAdjustment();
    }
    void MouseAiming()
    {
        // get the mouse inputs
        float y = Input.GetAxis("Mouse X") * turnSpeed;
        rotX += Input.GetAxis("Mouse Y") * turnSpeed;
        // clamp the vertical rotation
        rotX = Mathf.Clamp(rotX, minTurnAngle, maxTurnAngle);
        // rotate the camera
        transform.eulerAngles = new Vector3(-rotX, transform.eulerAngles.y + y, 0);
    }
    void KeyboardMovement()
    {
        Vector3 dir = new Vector3(0, 0, 0);
        dir.x = Input.GetAxis("Horizontal") * player.moveSpeed;
        dir.z = Input.GetAxis("Vertical") * player.moveSpeed;
        transform.Translate(dir);
    }
    void VerticalAdjustment()
    {
        if (player.currentCell != null)
        {
            transform.position = new Vector3(transform.position.x,player.currentCell.transform.position.y+player.playerHeight,transform.position.z);
        }
    }
    void SpawnPlayer()
    {
        // Get spawn point
        player.currentCell = player.grid.GetCell(player.SpawnPoint);
        // Translate player to spawn
        transform.Translate(player.currentCell.transform.position);
    }
}

Player:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Player : MonoBehaviour
{
    // Player Properties
    public float playerHeight = 15f;
    public float moveSpeed = 0.125f;
    // Location Data
    public HexCell currentCell = null;
    public HexGrid grid;
    public HexCoordinates SpawnPoint = new HexCoordinates(0, 0);
    private Transform camera_transform;
    void Start()
    {
        camera_transform = transform.Find("Camera");
    }
    // Update is called once per frame
    void Update()
    {
        UpdateHexLocation();
    }
    void UpdateHexLocation()
    {
        HexCell cell = grid.GetCell(new Ray(camera_transform.transform.position, Vector3.down));
        if (cell != currentCell)
        {
            currentCell = cell;
        }
    }
}

I create a gameObject called player, nest a camera under it. Attach the Player script to the Player object, and the FirstPersonCamera script to the camera object. Now, I can just add the properties I need to complete the UI for now:

...
public int health = 100;
public int level = 1;
public int XP = 0;
public int SkillPoints = 0;
public GameObject equipted_in_hand = null;
...

Health Bar

I create an image for the foreground and background of the health bar - a simple image, both of the same initial size positioned onto the bottom-right pane with the foreground being green and the background being white. I then create a Gameobject under the pane called HealthBar, and attach a script to it with some initial public parameters: 2 Then I create a script to position everything initially, and a simple update pattern to shrink and shift the foreground by a ratio of units/health.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class HealthBar : MonoBehaviour
{
    // Load prefabs
    public GameObject foreground_prefab;
    public GameObject background_prefab;
    public Player player;
    /// <summary>
    /// Gameobject containing the Canvas which the compass is rendered onto
    /// </summary>
    public GameObject canvas;
    // internals
    private float canvas_width;
    private float canvas_height;
    private float width;
    private float height;
    private float xOffset;
    private float resolution;
    private int last_health;
    private GameObject foreground;
    private GameObject background;
    // Start is called before the first frame update
    void Start()
    {
        // Calculate initial values
        width = background_prefab.GetComponent<RectTransform>().rect.width;
        height = background_prefab.GetComponent<RectTransform>().rect.height;
        canvas_width = canvas.GetComponent<RectTransform>().rect.width;
        canvas_height = canvas.GetComponent<RectTransform>().rect.height;
        UpdateResolution();
        last_health = player.health;
        // Position HealthBar parent
        this.transform.SetParent(canvas.transform);
        this.GetComponent<RectTransform>().SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top, 0, height);
        this.GetComponent<RectTransform>().SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, 0, width);
        // Load and position prefabs
        background = Instantiate(background_prefab);
        foreground = Instantiate(foreground_prefab);
        background.transform.SetParent(this.transform);
        foreground.transform.SetParent(this.transform);
        xOffset = (canvas_width - width) / 2;
        background.GetComponent<RectTransform>().SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top, height/2, height);
        background.GetComponent<RectTransform>().SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, xOffset, width);
        foreground.GetComponent<RectTransform>().SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top, height/2, height);
        foreground.GetComponent<RectTransform>().SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, xOffset, width);
    }
    void UpdateResolution()
    {
        resolution = width / player.health;
    }
    // Update is called once per frame
    void Update()
    {
        if (player.health != last_health)
        {
            last_health = player.health;
            // Shrink width of foreground and translate by difference
            float new_width = last_health * resolution;
            //foreground.GetComponent<RectTransform>().rect.width = new_width;
            foreground.GetComponent<RectTransform>().SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, (canvas_width - new_width) - xOffset, new_width);
        }
    }
}

And, it works! 3 4 Obviously this isn't very exciting yet, but that's fine we'll circle back later once we work out the core game mechanics.

XP Bar & Character Level

I'm going to do this almost the same way as the health bar for now, just add some text above it with info about character level and XP. First I added a few more properties to the player, and an update function to handle leveling up:

public class Player : MonoBehaviour
{
...
public int level = 1;
public int XP = 0;
public int XP_next_level;
public int SkillPoints = 0;
...
void UpdateNextLevelXp()
{
    XP_next_level = (int)Math.Pow(level,4);
}
void UpdatePlayerLevel()
{
    if (XP >= XP_next_level)
    {
        level++;
        XP = 0;
        UpdateNextLevelXp();
    }
}

Then, the XP bar script:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class XPBar : MonoBehaviour
{
    // Load prefabs
    public GameObject foreground_prefab;
    public GameObject background_prefab;
    public Player player;
    /// <summary>
    /// Gameobject containing the Canvas which the compass is rendered onto
    /// </summary>
    public GameObject canvas;
    private Text LevelDisplay;
    // internals
    private float canvas_width;
    private float canvas_height;
    private float width;
    private float height;
    private float xOffset;
    private float resolution;
    private int last_xp = -1;
    private int last_level = -1;
    private GameObject foreground;
    private GameObject background;
    // Start is called before the first frame update
    void Start()
    {
        // Calculate initial values
        width = background_prefab.GetComponent<RectTransform>().rect.width;
        height = background_prefab.GetComponent<RectTransform>().rect.height;
        canvas_width = canvas.GetComponent<RectTransform>().rect.width;
        canvas_height = canvas.GetComponent<RectTransform>().rect.height;
        // Position XP Bar parent
        this.transform.SetParent(canvas.transform);
        this.GetComponent<RectTransform>().SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top, 0, height);
        this.GetComponent<RectTransform>().SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, 0, width);
        AddPrefabs();
        AddCharacterLevelDisplay();
    }
    void UpdateResolution()
    {
        resolution = width / player.XP_next_level;
    }
    void UpdateXP()
    {
        if (player.XP != last_xp)
        {
            last_xp = player.XP;
            float new_width = last_xp * resolution;
            foreground.GetComponent<RectTransform>().SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, (canvas_width - new_width) - xOffset, new_width);
        }
    }
    void UpdateLevel()
    {
        if (player.level != last_level)
        {
            UpdateResolution();
            last_level = player.level;
            LevelDisplay.text = $"Level {last_level}";
        }
    }
    // Update is called once per frame
    void Update()
    {
        UpdateLevel();
        UpdateXP();
       
    }
    void AddCharacterLevelDisplay()
    {
        // Add text for character level display
        // Create game object to add to canvas
        GameObject LevelDisplayObject = new GameObject("Player Level Display");
        LevelDisplayObject.transform.SetParent(this.transform);
        // Create text component
        LevelDisplay = LevelDisplayObject.AddComponent<Text>();
        LevelDisplay.text = $"Level {player.level}";
        Font ArialFont = (Font)Resources.GetBuiltinResource(typeof(Font), "Arial.ttf");
        LevelDisplay.font = ArialFont;
        LevelDisplay.material = ArialFont.material;
        // Add text component to our game object
        LevelDisplay.transform.SetParent(this.transform);
        // Translate gameObject into position
        LevelDisplayObject.GetComponent<RectTransform>().SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top, 0, 20);
        LevelDisplayObject.GetComponent<RectTransform>().SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, xOffset, width);
        LevelDisplayObject.transform.Translate(new Vector3(0, 0, -10));
    }
    void AddPrefabs()
    {
        background = Instantiate(background_prefab);
        foreground = Instantiate(foreground_prefab);
        background.transform.SetParent(this.transform);
        foreground.transform.SetParent(this.transform);
        xOffset = (canvas_width - width) / 2;
        background.GetComponent<RectTransform>().SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top, height / 2, height);
        background.GetComponent<RectTransform>().SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, xOffset, width);
        foreground.GetComponent<RectTransform>().SetInsetAndSizeFromParentEdge(RectTransform.Edge.Top, height / 2, height);
        foreground.GetComponent<RectTransform>().SetInsetAndSizeFromParentEdge(RectTransform.Edge.Left, xOffset, width);
    }
}

And, it works!

I've decided to leave out the tool bar and equipted item info until I get into game mechanics more - I'm not sure if I'll end up with a skill bar or something of that nature.