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:
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!
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.