using scsmmedia;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

// Sci-Fi Ship Controller. Copyright (c) 2018-2023 SCSM Pty Ltd. All rights reserved.
namespace SciFiShipController
{
    /// <summary>
    /// Heads Up Display - typically used from an in-cockpit player view or setup.
    /// The general approach here is to use RectTransforms that are NOT stretched
    /// and are anchored at the centre.
    /// Setup Notes:
    /// HUDPanel should be anchored at four corners of screen (stretched)
    /// Display Reticle panel should be anchored at the centre of HUD
    /// Altitude Panel should be anchored to centre
    /// Altitude Text pivot point should be 0,0 (left bottom corner of textbox)
    /// </summary>
    [AddComponentMenu("Sci-Fi Ship Controller/Misc/Ship Display Module")]
    [HelpURL("http://scsmmedia.com/ssc-documentation")]
    public class ShipDisplayModule : MonoBehaviour
    {
        #region Enumerations

        #endregion

        #region Public Static Variables
        public readonly static string hudPanelName = "HUDPanel";
        public readonly static string targetsPanelName = "TargetsPanel";
        public readonly static string gaugesPanelName = "GaugesPanel";
        public readonly static string attitudePanelName = "AttitudePanel";
        public readonly static string headingPanelName = "HeadingPanel";
        public readonly static string overlayPanelName = "OverlayPanel";
        public readonly static string attitudeScrollName = "AttitudeScroll";
        public readonly static string headingScrollName = "HeadingScroll";
        public readonly static string headingIndicatorName = "HeadingIndicator";

        [System.NonSerialized] public static readonly AnimationCurve easeInOutCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);

        #endregion

        #region Public Variables and Properties - General

        /// <summary>
        /// If enabled, the Initialise() will be called as soon as Start() runs. This should be disabled if you are
        /// instantiating the HUD through code.
        /// </summary>
        public bool initialiseOnStart = false;

        /// <summary>
        /// Show or Hide the HUD when it is first Initialised
        /// </summary>
        public bool isShowOnInitialise = true;

        /// <summary>
        /// Show overlay image on HUD. At runtime call ShowOverlay() or HideOverlay()
        /// </summary>
        public bool showOverlay = true;

        /// <summary>
        /// The head-up display's normalised width of the screen. 1.0 is full width, 0.5 is half width.
        /// At runtime call shipDisplayModule.SetHUDSize(..)
        /// </summary>
        [Range(0.1f, 1f)] public float displayWidth = 0.5f;

        /// <summary>
        /// The head-up display's normalised height of the screen. 1.0 is full height, 0.5 is half height.
        /// At runtime call shipDisplayModule.SetHUDSize(..)
        /// </summary>
        [Range(0.1f, 1f)] public float displayHeight = 0.75f;

        /// <summary>
        /// The head-up display's normalised offset between the left (-1) and the right (1) from the centre (0) of the screen.
        /// At runtime call shipDisplayModule.SetHUDOffset(..)
        /// </summary>
        [Range(-1f, 1f)] public float displayOffsetX = 0f;

        /// <summary>
        /// The head-up display's normalised offset between the bottom (-1) and the top (1) from the centre (0) of the screen.
        /// At runtime call shipDisplayModule.SetHUDOffset(..)
        /// </summary>
        [Range(-1f, 1f)] public float displayOffsetY = 0f;

        /// <summary>
        /// Primary colour of the heads-up display.
        /// At runtime call shipDisplayModule.SetPrimaryColour(..)
        /// </summary>
        public Color32 primaryColour = Color.grey;

        /// <summary>
        /// Automatically hide the screen cursor or mouse pointer after it has been stationary
        /// for a fixed period of time. Automatically show the cursor if the mouse if moved
        /// provided that the Display Reticle is on shown.
        /// </summary>
        public bool autoHideCursor = true;

        /// <summary>
        /// The number of seconds to wait until after the cursor has not moved before hiding it
        /// </summary>
        public float hideCursorTime = 3f;

        /// <summary>
        /// Is the ship display module initialised? Gets set at runtime when Initialise() is called.
        /// </summary>
        public bool IsInitialised { get; private set; }

        /// <summary>
        /// Is the heads-up display shown? To set use ShowHUD() or HideHUD().
        /// IsHUDShown should never be true at runtime if IsInitialised is false
        /// </summary>
        public bool IsHUDShown { get; private set; }

        /// <summary>
        /// Get a reference to the HUD canvas
        /// </summary>
        public Canvas GetCanvas { get { return IsInitialised ? canvas : GetComponent<Canvas>(); } }

        /// <summary>
        /// The overall brightness of the HUD. At runtime use
        /// SetBrightness(value)
        /// </summary>
        [Range(0f, 1f)] public float brightness = 1f;

        /// <summary>
        /// The sort order of the canvas in the scene. Higher numbers are on top.
        /// At runtime call shipDisplayModule.SetCanvasSortOrder(..)
        /// </summary>
        public int canvasSortOrder = 2;

        /// <summary>
        /// The default size the HUD was designed with. This should always
        /// return 1920x1080
        /// </summary>
        public Vector2 ReferenceResolution { get { return refResolution; } }

        /// <summary>
        /// The current scaled resolution of the HUD canvas
        /// </summary>
        public Vector2 CanvasResolution { get { return cvsResolutionFull; } }

        /// <summary>
        /// The current scale factor of the HUD canvas
        /// </summary>
        public Vector3 CanvasScaleFactor { get { return cvsScaleFactor; } }

        /// <summary>
        /// The current actual screen resolution. This may be different from
        /// what the 
        /// </summary>
        public Vector2 ScreenResolution { get { return screenResolution; } }

        #endregion

        #region Public Variables and Properties - Flicker

        /// <summary>
        /// Whenever the HUD is shown, should it flicker on?
        /// </summary>
        public bool isShowHUDWithFlicker = false;

        /// <summary>
        /// Whenever the HUD is hidden, should it flicker off?
        /// </summary>
        public bool isHideHUDWithFlicker = false;

        /// <summary>
        /// The time, in seconds, the effect takes to reach a steady state
        /// </summary>
        [Range(0.01f, 5f)] public float flickerDefaultDuration = 1f;

        /// <summary>
        /// The minimum time, in seconds, that the effect is inactive or off
        /// </summary>
        [Range(0f, 2f)] public float flickerMinInactiveTime = 0.1f;

        /// <summary>
        /// The maximum time, in seconds, that the effect is inactive or off
        /// </summary>
        [Range(0f, 2f)] public float flickerMaxInactiveTime = 0.2f;

        /// <summary>
        /// The minimum time, in seconds, that the effect is active or on
        /// </summary>
        [Range(0f, 2f)] public float flickerMinActiveTime = 0.1f;

        /// <summary>
        /// The maximum time, in seconds, that the effect is active or on
        /// </summary>
        [Range(0f, 2f)] public float flickerMaxActiveTime = 0.2f;

        /// <summary>
        /// Smooth the flickering effect. Higher values give a smoother effect
        /// </summary>
        [Range(0, 5)] public float flickerSmoothing = 3f;

        /// <summary>
        /// The intensity of the effect will randomly change
        /// </summary>
        public bool flickerVariableIntensity = false;

        /// <summary>
        /// The maximum intensity of the effect used during the on cycle when
        /// flickerVariableIntensity is enabled.
        /// This is a multiplier of the starting intensity of the effect.
        /// Value must be between 0.01 and 1.0.
        /// </summary>
        [Range(0.01f, 1f)] public float flickerMaxIntensity = 1f;

        #endregion

        #region Public Variables and Properties - Reticles

        /// <summary>
        /// The guidHash of the currently selected / displayed reticle
        /// </summary>
        public int guidHashActiveDisplayReticle = 0;

        /// <summary>
        /// The list of display reticles that can be used with
        /// this Ship Display. Call ReinitialiseVariables() if
        /// you modify the list at runtime.
        /// </summary>
        public List<DisplayReticle> displayReticleList;

        /// <summary>
        /// Get the current number of display reticles
        /// </summary>
        public int GetNumberDisplayReticles { get { return IsInitialised ? numDisplayReticles : displayReticleList == null ? 0 : displayReticleList.Count; } }

        /// <summary>
        /// [INTERNAL USE ONLY]
        /// </summary>
        public bool isDisplayReticleListExpanded = true;

        /// <summary>
        /// For consistency, is the display reticle currently shown when the
        /// module has already been initialised?
        /// </summary>
        public bool IsDisplayReticleShown { get { return showActiveDisplayReticle; } }

        /// <summary>
        /// Get the Display Reticle position on the screen in Viewport coordinates. x = 0.0-1.0, y = 0.0-1.0. 0,0 is bottom left corner.
        /// </summary>
        public Vector2 DisplayReticleViewportPoint { get { return new Vector2((displayReticleOffsetX + 1f) * 0.5f, (displayReticleOffsetY + 1f) * 0.5f); } }

        /// <summary>
        /// Show the current (active) Display Reticle
        /// on the HUD. At runtime use ShowDisplayReticle()
        /// or HideDisplayReticle().
        /// </summary>
        public bool showActiveDisplayReticle = false;

        /// <summary>
        /// The Display Reticle's normalised offset between the left (-1) and the right (1) from the centre (0) of the screen.
        /// At runtime call shipDisplayModule.SetDisplayReticleOffset(..)
        /// </summary>
        [Range(-1f, 1f)] public float displayReticleOffsetX = 0f;

        /// <summary>
        /// The Display Reticle's normalised offset between the bottom (-1) and the top (1) from the centre (0) of the screen.
        /// At runtime call shipDisplayModule.SetDisplayReticleOffset(..)
        /// </summary>
        [Range(-1f, 1f)] public float displayReticleOffsetY = 0f;

        /// <summary>
        /// Should the Display Reticle follow the cursor or mouse screen position?
        /// </summary>
        public bool lockDisplayReticleToCursor = false;

        /// <summary>
        /// Colour of the active Display Reticle.
        /// At runtime call shipDisplayModule.SetDisplayReticleColour(..)
        /// </summary>
        public Color32 activeDisplayReticleColour = Color.white;

        /// <summary>
        /// This is the main camera that HUD will reference. If left empty, this will
        /// be auto-populated with your first Camera with a tag of MainCamera.
        /// Can be changed at runtime with shipDisplayModule.SetCamera(..).
        /// </summary>
        public Camera mainCamera = null;

        #endregion

        #region Public Variables and Properties - Altitude and Speed

        /// <summary>
        /// The ship in the scene that will supply the data for this HUD
        /// </summary>
        public ShipControlModule sourceShip = null;

        /// <summary>
        /// Show the Altitude indicator on the HUD. At runtime use ShowAltitude() or HideAltitude().
        /// Typically only used when near the surface of a planet. See also groundPlaneHeight.
        /// </summary>
        public bool showAltitude = false;

        /// <summary>
        /// Used to determine altitude when near a planet's surface. On Earth this would typically
        /// be sea-level but it can be used to set an artificial zero-height which may be useful
        /// when flying over the surface of a planet.
        /// </summary>
        public float groundPlaneHeight = 0f;

        /// <summary>
        /// The colour of the Altitude text (number).
        /// At runtime call shipDisplayModule.SetAltitudeTextColour(..)
        /// </summary>
        public Color32 altitudeTextColour = Color.grey;

        /// <summary>
        /// Show the air speed indicator on the HUD. At runtime use ShowAirSpeed() or HideAirSpeed().
        /// </summary>
        public bool showAirspeed = false;

        /// <summary>
        /// The colour of the Air Speed text (number).
        /// At runtime call shipDisplayModule.SetAirSpeedTextColour(..)
        /// </summary>
        public Color32 airspeedTextColour = Color.grey;

        #endregion

        #region Public Variables and Properties - Attitude

        /// <summary>
        /// Show or hide the Attitude.
        /// At runtime use ShowAttitude() or HideAttitude()
        /// </summary>
        public bool showAttitude = false;

        /// <summary>
        /// Primary colour of the scrollable heading
        /// At runtime call shipDisplayModule.SetDisplayAttitudePrimaryColour(..)
        /// </summary>
        public Color attitudePrimaryColour = Color.white;

        #endregion

        #region Public Variables and Properties - Heading

        /// <summary>
        /// Show or hide the Heading or direction as a scrollable ribbon in the UI.
        /// At runtime use ShowHeading() or HideHeading()
        /// </summary>
        public bool showHeading = false;

        /// <summary>
        /// Show or hide the small heading indicator
        /// At runtime use ShowHeadingIndictor() or HideHeadingIndicator()
        /// </summary>
        public bool showHeadingIndicator = true;

        /// <summary>
        /// Primary colour of the scrollable heading
        /// At runtime call shipDisplayModule.SetDisplayHeadingPrimaryColour(..)
        /// </summary>
        public Color headingPrimaryColour = Color.white;

        /// <summary>
        /// The small indicator colour of the scrollable heading
        /// At runtime call shipDisplayModule.SetDisplayHeadingIndicatorColour(..)
        /// </summary>
        public Color headingIndicatorColour = Color.green;

        #endregion

        #region Public Variables and Properties - Gauges

        /// <summary>
        /// The list of display gauges that can be used with
        /// this Ship Display. Call ReinitialiseVariables() if
        /// you modify this list at runtime. Where possible use the API
        /// methods to Add, Delete, or set gauge attributes.
        /// </summary>
        public List<DisplayGauge> displayGaugeList;

        /// <summary>
        /// Get the current number of display gauge
        /// </summary>
        public int GetNumberDisplayGauges { get { return IsInitialised ? numDisplayGauges : displayGaugeList == null ? 0 : displayGaugeList.Count; } }

        /// <summary>
        /// [INTERNAL USE ONLY]
        /// </summary>
        public bool isDisplayGaugeListExpanded = true;

        #endregion

        #region Public Variables and Properties - Messages

        /// <summary>
        /// The list of display messages that can be used with
        /// this Ship Display. Call ReinitialiseVariables() if
        /// you modify this list at runtime. Where possible use the API
        /// methods to Add, Delete, or set messages attributes.
        /// </summary>
        public List<DisplayMessage> displayMessageList;

        /// <summary>
        /// Get the current number of display messages
        /// </summary>
        public int GetNumberDisplayMessages { get { return IsInitialised ? numDisplayMessages : displayMessageList == null ? 0 : displayMessageList.Count; } }

        /// <summary>
        /// [INTERNAL USE ONLY]
        /// </summary>
        public bool isDisplayMessageListExpanded = true;

        #endregion

        #region Public Variables and Properties - Targets

        /// <summary>
        /// The list of display targets that can be used with
        /// this Ship Display. Call ReinitialiseVariables() if
        /// you modify this list at runtime. Where possible use the API
        /// methods to Add, Delete, or set target attributes.
        /// </summary>
        public List<DisplayTarget> displayTargetList;

        /// <summary>
        /// Get the current number of Display Targets.
        /// </summary>
        public int GetNumberDisplayTargets { get { return IsInitialised ? numDisplayTargets : displayTargetList == null ? 0 : displayTargetList.Count; } }

        /// <summary>
        /// Get the size of the Targets viewport as a normalised (0.0-1.0) value of the current screensize
        /// </summary>
        public Vector2 GetTargetsViewportSize { get { return new Vector2(targetsViewWidth, targetsViewHeight); } }

        /// <summary>
        /// Get the offset from the screen centre of the Targets viewport. Values are -1.0 to 1.0
        /// </summary>
        public Vector2 GetTargetsViewportOffset { get { return new Vector2(targetsViewOffsetX, targetsViewOffsetY); } }

        /// <summary>
        /// When DisplayTarget slots have an active RadarItem assigned to them, should the reticles be automatically
        /// moved on the HUD?
        /// </summary>
        public bool autoUpdateTargetPositions = true;

        /// <summary>
        /// The Targets normalised viewable width of the screen.
        /// 1.0 is full width, 0.5 is half width.
        /// At runtime call shipDisplayModule.SetTargetsViewportSize(..)
        /// </summary>
        [Range(0.1f, 1f)] public float targetsViewWidth = 1.0f;

        /// <summary>
        /// The Targets normalised viewable height of the screen.
        /// 1.0 is full height, 0.5 is half height.
        /// At runtime call shipDisplayModule.SetTargetsViewportSize(..)
        /// </summary>
        [Range(0.1f, 1f)] public float targetsViewHeight = 0.5f;

        /// <summary>
        /// The X offset from centre of the screen for the Targets viewport
        /// At runtime call shipDisplayModule.SetTargetsViewportOffset(..)
        /// </summary>
        [Range(-1f, 1f)] public float targetsViewOffsetX = 0f;

        /// <summary>
        /// The Y offset from centre of the screen for the Targets viewport
        /// At runtime call shipDisplayModule.SetTargetsViewportOffset(..)
        /// </summary>
        [Range(-1f, 1f)] public float targetsViewOffsetY = 0.5f;

        /// <summary>
        /// The maximum distance, in metres, that targets can be away from the ship
        /// </summary>
        public float targetingRange = 5000f;

        /// <summary>
        /// [INTERNAL USE ONLY]
        /// </summary>
        public bool isDisplayTargetListExpanded = true;

        #endregion

        #region Public Variables and Properties - FUTURE FEATURES

        /// <summary>
        /// FUTURE
        /// </summary>
        //public bool showArtificialHorizon = false;

        // FUTURE - FPV indicator
        //public bool showFlightPathVector = false;

        // FUTURE - wing's angle relative to airflow
        //public bool showAngleOfAttack = false;

        // FUTURE - target designation indicator
        //public bool showTargetDesignator = false;

        // Other options could be closing (target) velocity, range (to target), selected weapons, amno available etc.

        #endregion

        #region Public Variables and Properties - INTERNAL ONLY

        /// <summary>
        /// [INTERNAL ONLY]
        /// </summary>
        [HideInInspector] public bool allowRepaint = false;

        /// <summary>
        /// [INTERNAL ONLY]
        /// </summary>
        public bool showGeneralSettingsInEditor = false;

        /// <summary>
        /// [INTERNAL ONLY]
        /// </summary>
        public bool showReticleSettingsInEditor = false;

        /// <summary>
        /// [INTERNAL ONLY]
        /// </summary>
        public bool showAltSpeedSettingsInEditor = false;

        /// <summary>
        /// [INTERNAL ONLY]
        /// </summary>
        public bool showAttitudeSettingsInEditor = false;

        /// <summary>
        /// [INTERNAL ONLY]
        /// </summary>
        public bool showFlickerSettingsInEditor = false;

        /// <summary>
        /// [INTERNAL ONLY]
        /// </summary>
        public bool showHeadingSettingsInEditor = false;

        /// <summary>
        /// [INTERNAL ONLY]
        /// </summary>
        public bool showGaugeSettingsInEditor = false;

        /// <summary>
        /// [INTERNAL ONLY]
        /// </summary>
        public bool showMessageSettingsInEditor = false;

        /// <summary>
        /// [INTERNAL ONLY]
        /// </summary>
        public bool showTargetSettingsInEditor = false;

        /// <summary>
        /// [INTERNAL ONLY]
        /// Outside play mode, show a bounding box for where the HUD will be placed
        /// </summary>
        public bool showHUDOutlineInScene = true;

        /// <summary>
        /// [INTERNAL ONLY]
        /// Outside play mode, show a bounding box for where Targets can be displayed
        /// </summary>
        public bool showTargetsOutlineInScene = false;
        /// <summary>
        /// [INTERNAL ONLY]
        /// </summary>
        public bool IsEditorMode { get { return editorMode; } }

        #endregion

        #region Public Delegates

        public delegate void CallbackOnBrightnessChange(float newBrightness);
        public delegate void CallbackOnSizeChange(float newWidth, float newHeight);

        /// <summary>
        /// The name of the custom method that is called immediately
        /// after brightness has changed. Your method must take 1 float
        /// parameter. This should be a lightweight method to avoid
        /// performance issues. It could be used to update your custom
        /// HUD elements.
        /// </summary>
        public CallbackOnBrightnessChange callbackOnBrightnessChange = null;

        /// <summary>
        /// The name of the custom method that is called immediately after the
        /// HUD size has changed. This method must take 2 float parameters. It
        /// should be a lightweight method to avoid performance issues. It could
        /// be used to update your custom HUD elements.
        /// </summary>
        public CallbackOnSizeChange callbackOnSizeChange = null;

        #endregion

        #region Private Variables - General
        private Canvas canvas = null;
        private CanvasScaler canvasScaler = null;

        // Scaled canvas resolution. See CheckHUDResize()
        private Vector2 cvsResolutionFull = Vector2.one;
        private Vector2 cvsResolutionHalf = Vector2.one;
        private Vector2 prevResolutionFull = Vector2.one;
        // Default reference resolution e.g. 1920x1080
        private Vector2 refResolution = Vector2.one;
        private Vector3 cvsScaleFactor = Vector3.one;
        private Vector2 screenResolution = Vector2.one;

        private Transform hudPanel = null;
        private bool isMainCameraAssigned = false;
        private Color tempColour = Color.clear;

        // Used to update HUD from the editor when not in play mode
        private bool editorMode = false;

        private List<Text> tempTextList = null;
        private List<UnityEngine.UI.Image> tempImgList = null;

        #endregion

        #region Private Variables - Attitude
        private RectTransform attitudeRectTfrm = null;
        private RectTransform attitudeScrollPanel = null;
        private UnityEngine.UI.Image attitudeScrollImg = null;
        private UnityEngine.UI.Image attitudeMaskImg = null;
        private bool isAttitudeScrolling = false;
        private Vector3 tempAttitudeOffset = Vector3.zero;
        private Vector3 attitudeInitPosition = Vector3.zero;

        /// <summary>
        /// This gets calculated in InitialiseAttitude(..) at runtime.
        /// </summary>
        private float attitudePixelsPerDegree = 1080f / 180f;

        /// <summary>
        /// The attitude normalised offset between the left (-1) and the right (1) from the centre (0) of the screen.
        /// At runtime call shipDisplayModule.SetAttitudeOffset(..)
        /// </summary>
        [SerializeField, Range(-1f, 1f)] private float attitudeOffsetX = 0f;

        /// <summary>
        /// The attitude normalised offset between the bottom (-1) and the top (1) from the centre (0) of the screen.
        /// At runtime call shipDisplayModule.SetAttitudeOffset(..)
        /// </summary>
        [SerializeField, Range(-1f, 1f)] private float attitudeOffsetY = 0f;

        /// <summary>
        /// The normalised masked width of the scrollable attitude. 1.0 is full width of the screen, 0.5 is half width. 
        /// </summary>
        [SerializeField, Range(0.1f, 1f)] private float attitudeWidth = 0.75f;

        /// <summary>
        /// The normalised masked height of the scrollable attitude. 1.0 is full height of the screen, 0.5 is half height.
        /// </summary>
        [SerializeField, Range(0.1f, 1f)] private float attitudeHeight = 1f;

        /// <summary>
        /// The sprite (texture) that will scroll up/down
        /// </summary>
        [SerializeField] private Sprite attitudeScrollSprite = null;

        /// <summary>
        /// The sprite (texture) that will mask the scrollable attitude sprite
        /// </summary>
        [SerializeField] private Sprite attitudeMaskSprite = null;

        /// <summary>
        /// The number of pixels between the top of the scroll sprite and the first (90) pitch line.
        /// It is assumed that this is the same for between the bottom and the -90 pitch line.
        /// </summary>
        [SerializeField, Range(0f, 1000f)] private float attitudeScrollSpriteBorderWidth = 250f;

        #endregion

        #region Private Variables - Flickering
        /// <summary>
        /// Is the HUD currently flickering and will it end in the Shown state?
        /// This tracks if flickering is currently happening.
        /// </summary>
        private bool isFlickeringEndStateOn = false;
        /// <summary>
        /// Is the HUD currently flickering and will it end in the Hidden state?
        /// This tracks if flickering is currently happening.
        /// </summary>
        private bool isFlickeringEndStateOff = false;

        private float flickerDurationTimer = 0f;
        private float flickerInactiveTimer = 0f;
        private float flickerActiveTimer = 0f;
        private float flickerDuration = 0f;
        private float flickerActiveTime = 0f;
        private float flickerInactiveTime = 0f;
        private bool isFlickerWaiting = false;
        private SSCRandom hudRandom = null;
        private float flickerIntensity = 0f;
        private float flickerStartIntensity = 0f;
        private Queue<float> flickerHistory = null;
        private float flickerHistoryTotal = 0f;

        #endregion

        #region Private Variables - Brightness
        private SSCColour baseReticleColour;
        private SSCColour baseOverlayColour;
        private SSCColour baseAltitudeTextColour;
        private SSCColour baseAirspeedTextColour;
        private SSCColour baseAttitudePrimaryColour;
        private SSCColour baseHeadingPrimaryColour;
        private SSCColour baseHeadingIndicatorColour;
        #endregion

        #region Private Variables - Heading
        private RectTransform headingRectTfrm = null;
        private RectTransform headingScrollPanel = null;
        private RectTransform headingIndicatorPanel = null;
        private UnityEngine.UI.Image headingScrollImg = null;
        private UnityEngine.UI.Image headingMaskImg = null;
        private UnityEngine.UI.Image headingIndicatorImg = null;

        private bool isHeadingScrolling = false;
        private Vector3 headingInitPosition = Vector3.zero;
        private Vector3 tempHeadingOffset = Vector3.zero;

        /// <summary>
        /// This gets calculated in InitialiseHeading(..) at runtime.
        /// </summary>
        private float headingPixelsPerDegree = 1920f / 360f;

        /// <summary>
        /// The heading normalised offset between the left (-1) and the right (1) from the centre (0) of the screen.
        /// At runtime call shipDisplayModule.SetHeadingOffset(..)
        /// </summary>
        [SerializeField, Range(-1f, 1f)] private float headingOffsetX = 0f;

        /// <summary>
        /// The heading normalised offset between the bottom (-1) and the top (1) from the centre (0) of the screen.
        /// At runtime call shipDisplayModule.SetHeadingOffset(..)
        /// </summary>
        [SerializeField, Range(-1f, 1f)] private float headingOffsetY = 0f;

        /// <summary>
        /// The normalised masked width of the scrollable heading. 1.0 is full width of the screen, 0.5 is half width. 
        /// </summary>
        [SerializeField, Range(0.1f, 1f)] private float headingWidth = 0.75f;

        /// <summary>
        /// The normalised masked height of the scrollable heading. 1.0 is full height of the screen, 0.5 is half height.
        /// </summary>
        [SerializeField, Range(0.1f, 1f)] private float headingHeight = 1f;

        /// <summary>
        /// The sprite (texture) that will scroll left/right
        /// </summary>
        [SerializeField] private Sprite headingScrollSprite = null;

        /// <summary>
        /// The sprite (texture) that will mask the scrollable heading sprite
        /// </summary>
        [SerializeField] private Sprite headingMaskSprite = null;

        /// <summary>
        /// The small sprite (texture) that will indicate or point to the heading on the HUD
        /// </summary>
        [SerializeField] private Sprite headingIndicatorSprite = null;

        #endregion

        #region Private Variables - Reticle
        private RectTransform hudRectTfrm = null;
        private Transform reticlePanel = null;
        private float reticleWidth = 1f;
        private float reticleHeight = 1f;
        private DisplayReticle currentDisplayReticle = null;
        private UnityEngine.UI.Image displayReticleImg = null;
        private int numDisplayReticles = 0;
        #endregion

        #region Private Variables - Gauges
        private int numDisplayGauges = 0;
        private RectTransform gaugesRectTfrm = null;
        private Vector3 tempGaugeOffset = Vector3.zero;
        #endregion

        #region Private Variables - Message
        private int numDisplayMessages = 0;       
        private Vector3 tempMessageOffset;
        #endregion

        #region Private Variables - Cursor
        private bool isCursorVisible = true;
        private float cursorTimer = 0f;

        // Switch to using the New Input System if it is available
#if SSC_UIS
        private Vector2 currentMousePosition = Vector2.zero;
        private Vector2 lastMousePosition = Vector2.zero;
#else
        private Vector3 currentMousePosition = Vector3.zero;
        private Vector3 lastMousePosition = Vector3.zero;
#endif
        #endregion

        #region Private Variables - Altitude and Speed
        private UnityEngine.UI.Image primaryOverlayImg = null;
        private RectTransform overlayPanel = null;
        private RectTransform altitudeTextRectTfrm = null;
        private Text altitudeText = null;
        private Vector3 altitudeInitPosition;
        private Vector3 altitudeCurrentPosition;
        private SCSMString altitudeString = null;
        private RectTransform airspeedTextRectTfrm = null;
        private Text airspeedText = null;
        private Vector3 airspeedInitPosition;
        private Vector3 airspeedCurrentPosition;
        private SCSMString airspeedString = null;
        #endregion

        #region Private Variables - Targets
        private int numDisplayTargets = 0;
        private RectTransform targetsRectTfrm = null;
        private Vector3 tempTargetOffset;
        private List<int> tempIncludeFactionList = null;
        private List<int> tempIncludeSquadronList = null;
        private SSCRadar sscRadar = null;
        #endregion

        #region Private Initialisation Methods

        // Start is called before the first frame update
        void Start()
        {
            if (initialiseOnStart) { Initialise(); }
        }

        #endregion

        #region Update Methods

        // Update is called once per frame
        void Update()
        {
            float dTime = Time.deltaTime;

            #region Flickering
            if (isFlickeringEndStateOn || isFlickeringEndStateOff)
            {
                flickerDurationTimer += dTime;
                // Disable flickering
                if (flickerDurationTimer > flickerDuration)
                {
                    flickerHistory.Clear();
                    if (isFlickeringEndStateOn)
                    {
                        isFlickeringEndStateOn = false;
                        // Restore the original brightness
                        SetBrightness(flickerStartIntensity);
                        ShowOrHideHUD(true);
                    }
                    // FlickeringOff
                    else
                    {
                        isFlickeringEndStateOff = false;
                        // Restore the original brightness
                        brightness = flickerStartIntensity;
                        ShowOrHideHUD(false);
                    }                    
                }
                else if (isFlickerWaiting)
                {
                    flickerInactiveTimer += dTime;
                    if (flickerInactiveTimer > flickerInactiveTime)
                    {
                        isFlickerWaiting = false;
                        flickerActiveTimer = 0f;
                        flickerActiveTime = hudRandom != null ? hudRandom.Range(flickerMinActiveTime, flickerMaxActiveTime + 0.001f): 1f;
                        ShowOrHideHUD(true);                       
                    }
                }
                else
                {
                    flickerActiveTimer += dTime;
                    if (flickerActiveTimer > flickerActiveTime)
                    {
                        flickerHistory.Clear();
                        flickerHistory.Enqueue(0f);
                        flickerInactiveTimer = 0f;
                        flickerInactiveTime = hudRandom != null ? hudRandom.Range(flickerMinInactiveTime, flickerMaxInactiveTime + 0.001f) : 1f;
                        isFlickerWaiting = true;
                        ShowOrHideHUD(false);
                    }
                    else if (flickerVariableIntensity)
                    {
                        if (flickerSmoothing > 0)
                        {
                            // Remove the first value from the queue
                            if (flickerHistory.Count > 0 && flickerHistory.Count > flickerSmoothing + 1) { flickerHistoryTotal -= flickerHistory.Dequeue(); }

                            flickerIntensity = hudRandom.Range(0f, flickerStartIntensity * flickerMaxIntensity);
                            flickerHistoryTotal += flickerIntensity;

                            // Add the new value to the end of the queue
                            flickerHistory.Enqueue(flickerIntensity);

                            SetBrightness(flickerHistoryTotal / (float)flickerHistory.Count);
                        }
                        else
                        {
                            // No smoothing
                            flickerIntensity = hudRandom.Range(0f, flickerStartIntensity * flickerMaxIntensity);
                            SetBrightness(flickerIntensity);
                        }
                    }
                }
            }
            #endregion

            #region Check Window Resize
            if (IsHUDShown)
            {
                // Would be nice to not call this so often
                CheckHUDResize(true);
            }
            #endregion

            #region AutoHideCursor and Reticle cursor control
            if (autoHideCursor || (lockDisplayReticleToCursor && showActiveDisplayReticle))
            {
                #if SSC_UIS
                currentMousePosition = UnityEngine.InputSystem.Mouse.current.position.ReadValue();
                #else
                currentMousePosition = Input.mousePosition;
                #endif

                #region Auto-hide Cursor
                if (autoHideCursor)
                {
                    if (isCursorVisible)
                    {
                        cursorTimer += dTime;
                        // If use has move the mouse, reset the timer
                        if (lastMousePosition != currentMousePosition) { lastMousePosition = currentMousePosition; cursorTimer = 0f; }
                        // After hideCursorTime secs, hide it
                        else if (cursorTimer > hideCursorTime) { ShowOrHideCursor(false); }
                    }
                    // Check if mouse has moved (does user wish to click on something?)
                    else if (lastMousePosition != currentMousePosition)
                    {
                        lastMousePosition = currentMousePosition;
                        ShowOrHideCursor(true);
                    }
                }

                #endregion

                #region Local Display Reticle to Cursor
                if (IsInitialised && isMainCameraAssigned && lockDisplayReticleToCursor && showActiveDisplayReticle)
                {
                    // It is safe to switch between Vector3 and Vector2 as each has an expicit
                    // operator which either adds z = 0 or drops the z component.
                    Vector2 viewPoint = mainCamera.ScreenToViewportPoint(currentMousePosition);

                    // Convert 0.0 to 1.0 into -1.0 to 1.0.
                    SetDisplayReticleOffset(viewPoint.x * 2f - 1f, viewPoint.y * 2f - 1f);

                    // v1.2.8+ cursor should be hidden when reticle is following the cursor.
                    if (isCursorVisible && !autoHideCursor && IsHUDShown) { ShowOrHideCursor(false); }
                }
                #endregion
            }
            #endregion

            #region Update Altitude, Speed, Heading

            if ((showAltitude || showAirspeed || isHeadingScrolling || isAttitudeScrolling) && IsHUDShown && sourceShip != null && sourceShip.ShipIsEnabled() && sourceShip.IsInitialised)
            {
                if (showAltitude)
                {
                    /// TODO - round altitude to 100s
                    altitudeString.Set((int)(sourceShip.shipInstance.TransformPosition.y - groundPlaneHeight));
                    if (altitudeText != null) { altitudeText.text = altitudeString.ToString(); }
                }
                if (showAirspeed)
                {
                    // Show in km/h
                    airspeedString.Set((int)(sourceShip.shipInstance.LocalVelocity.z*3.6f));
                    if (airspeedText != null) { airspeedText.text = airspeedString.ToString(); }
                }

                if (isHeadingScrolling)
                {
                    Vector3 _shipFwd = sourceShip.shipInstance.TransformForward;
                    Vector3 _headingPerpendicular = Vector3.Cross(Vector3.forward, _shipFwd);
                    float _headingDir = -Vector3.Dot(_headingPerpendicular, Vector3.up);
                    // Get the forward angle of the ship in WS (ignoring the Up axis)
                    headingScrollImg.transform.position = headingInitPosition + new Vector3(Vector3.Angle(new Vector3(_shipFwd.x, 0f, _shipFwd.z), Vector3.forward) * Mathf.Sign(_headingDir) * headingPixelsPerDegree, 0f, 0f);
                }

                if (isAttitudeScrolling)
                {
                    float _rollAngle = sourceShip.shipInstance.RollAngle;
                    float _rollAngleRAD = -_rollAngle * Mathf.Deg2Rad;
                    float _scrollDistance = -sourceShip.shipInstance.PitchAngle * attitudePixelsPerDegree;
                    float _scrollRadius = _scrollDistance * Mathf.Sqrt(2 * (1 - Mathf.Cos(_rollAngleRAD)));
                    float _scrollX = _scrollRadius * Mathf.Cos(_rollAngleRAD/2f);
                    float _scrollY = _scrollRadius * Mathf.Sin(_rollAngleRAD/2f);

                    if (_rollAngle > 0f)
                    {
                        // Flip values when rolling to the right
                        _scrollX = -_scrollX;
                        _scrollY = -_scrollY;
                    }

                    Vector3 attitudeScrollOffset = new Vector3(_scrollX, _scrollDistance - _scrollY, 0f);

                    // Adjust the position and rotation
                    attitudeScrollImg.transform.position = attitudeInitPosition + attitudeScrollOffset;
                    // Rotate the panel so that the mask rotates at the same time.
                    attitudeRectTfrm.localRotation = Quaternion.Euler(0f, 0f, _rollAngle);

                    // Seems like we can adjust the image WS rotation at the same time, rather than having to do attitudeRectTfrm.localRotation.
                    //attitudeScrollImg.transform.SetPositionAndRotation(attitudeInitPosition + attitudeScrollOffset, Quaternion.Euler(0f, 0f, _rollAngle));

                    //Debug.Log("[DEBUG]  _rollAngle: " + _rollAngle.ToString("0.00") + " _rollAngleRAD: " + _rollAngleRAD.ToString("0.00") +
                    //         " _scrollDistance: " + _scrollDistance.ToString("0.00") + " _scrollRadius: " + _scrollRadius.ToString("0.00") +
                    //         " scrollX: " + _scrollX.ToString("0.00") + " scrollY: " + _scrollY.ToString("0.00"));
                }
            }

            #endregion

            #region Message updates
            if (IsHUDShown)
            {
                // NOTE: It might be cheaper to maintain an int[] of scrollable messages
                for (int dmIdx = 0; dmIdx < numDisplayMessages; dmIdx++)
                {
                    DisplayMessage displayMsg = displayMessageList[dmIdx];

                    #region Fade-in Message
                    if (displayMsg.fadeinTimer > 0f && displayMsg.fadeinDuration > 0f)
                    {
                        displayMsg.fadeinTimer -= dTime;

                        if (displayMsg.fadeinTimer <= 0f)
                        {
                            displayMsg.fadeinTimer = 0f;
                        }

                        SetDisplayMessageTextFade(displayMsg, false);
                        if (displayMsg.showBackground)
                        {
                            SetDisplayMessageBackgroundFade(displayMsg, false);
                        }
                    }
                    #endregion

                    #region Fade-out Message
                    else if (displayMsg.fadeoutTimer > 0f && displayMsg.fadeoutDuration > 0f)
                    {
                        displayMsg.fadeoutTimer -= dTime;

                        if (displayMsg.fadeoutTimer <= 0f)
                        {
                            displayMsg.fadeoutTimer = 0f;
                            displayMsg.showMessage = false;
                        }

                        SetDisplayMessageTextFade(displayMsg, true);
                        if (displayMsg.showBackground)
                        {
                            SetDisplayMessageBackgroundFade(displayMsg, true);
                        }
                    }

                    #endregion

                    if (displayMsg.scrollDirectionInt != DisplayMessage.ScrollDirectionNone)
                    {                     
                        ScrollMessage(displayMessageList[dmIdx]);
                    }
                }
            }

            #endregion

            #region Target Position Updates
            if (autoUpdateTargetPositions && IsHUDShown) { UpdateTargetPositions(); }
            #endregion
        }

        #endregion

        #region Private and Internal Methods - General

        /// <summary>
        /// Show or hide the hardware (mouse) cursor in the game view
        /// NOTE: This will sometimes fail to turn off the cursor in the editor
        /// Game View when it doesn't have focus, but will work fine in a build.
        /// </summary>
        /// <param name="isShown"></param>
        private void ShowOrHideCursor(bool isShown)
        {
            Cursor.visible = isShown;
            isCursorVisible = isShown;
            if (isShown) { cursorTimer = 0f; }
        }

        /// <summary>
        /// Show or Hide the HUD.
        /// IsHUDShown should never be true at runtime if IsInitialised is false
        /// </summary>
        /// <param name="isShown"></param>
        private void ShowOrHideHUD(bool isShown)
        {
            if (IsInitialised)
            {
                IsHUDShown = isShown;
                hudPanel.gameObject.SetActive(isShown);
            }
            // HUD should never be shown at runtime if IsInitialised is false
            else { IsHUDShown = false; }
        }

        private IEnumerator UnlockCursor()
        {
            yield return new WaitForEndOfFrame();
            Cursor.lockState = CursorLockMode.None;
        }

        /// <summary>
        /// Check to see if the window has been resized. Ideally should be
        /// called a max once per frame (less often would be nice).
        /// TODO - trigger a HUD refresh if changed.
        /// </summary>
        private void CheckHUDResize(bool isRefreshIfChanged)
        {
            cvsResolutionFull = GetHUDFullSize(true);
            cvsResolutionHalf.x = cvsResolutionFull.x * 0.5f;
            cvsResolutionHalf.y = cvsResolutionFull.y * 0.5f;

            if (cvsResolutionFull.x != prevResolutionFull.x || cvsResolutionFull.y != prevResolutionFull.y)
            {
                refResolution = GetHUDFullSize(false);

                cvsScaleFactor = canvas == null ? Vector3.one : canvas.transform.localScale;
                screenResolution = new Vector2(Screen.width, Screen.height);

                if (isRefreshIfChanged) { RefreshHUD(); }
                prevResolutionFull.x = cvsResolutionFull.x;
                prevResolutionFull.y = cvsResolutionFull.y;
            }
        }

        /// <summary>
        /// Return full the width and height of the HUD Panel.
        /// If the module hasn't been initialised it will return 1920x1080.
        /// Assume the dev hasn't changed the CanvasScaler reference resolution settings.
        /// When using the HUD canvas current dimensions, always set isScaled = true.
        /// Returns x,y values in pixels.
        /// </summary>
        /// <param name="isScaled"></param>
        /// <returns></returns>
        private Vector2 GetHUDFullSize(bool isScaled)
        {
            // Default SSC scaler resolution is 1920x1080
            if (isScaled)
            {
                if (IsInitialised || canvas != null)
                {
                    // The default [width, height] * inversed scaled [width, height]
                    //return new Vector2(canvas.pixelRect.width / canvas.scaleFactor, canvas.pixelRect.height / canvas.scaleFactor);
                    return ((RectTransform)canvas.transform).sizeDelta;
                }
                // We don't know, so just return the original size...
                else { return new Vector2(1920f, 1080f); }
            }
            else
            {
                if (IsInitialised || canvasScaler != null)
                {
                    return canvasScaler.referenceResolution;
                }
                else { return new Vector2(1920f, 1080f); }
            }
        }

        /// <summary>
        /// Return half the width and height of the HUD Panel.
        /// If the module hasn't been initialised it will return half 1920x1080.
        /// Assuming the dev hasn't changed it, it should always return the same
        /// when initialised.
        /// When using the HUD canvas current dimensions, always set isScaled = true.
        /// Returns x,y values in pixels.
        /// </summary>
        /// <param name="isScaled"></param>
        /// <returns></returns>
        private Vector2 GetHUDHalfSize(bool isScaled)
        {
            // Default scaler resolution is 1920x1080
            if (isScaled)
            {
                if (IsInitialised || canvas != null)
                {
                    // The default [width, height] * inversed scaled [width, height]
                    //return new Vector2(canvas.pixelRect.width / canvas.scaleFactor * 0.5f, canvas.pixelRect.height / canvas.scaleFactor * 0.5f);
                    return ((RectTransform)canvas.transform).sizeDelta * 0.5f;
                }
                // We don't know, so just return the original half size...
                else { return new Vector2(960f, 540f); }
            }
            else
            {
                if (IsInitialised || canvasScaler != null)
                {
                    return new Vector2(canvasScaler.referenceResolution.x * 0.5f, canvasScaler.referenceResolution.y * 0.5f);
                }
                else { return new Vector2(960f, 540f); }
            }
        }

        /// <summary>
        /// [INTERNAL ONLY]
        /// Call CheckHUDResize(false) before calling this method
        /// </summary>
        private void GetHUDPanels()
        {
            if (hudRectTfrm != null)
            {
                hudPanel = hudRectTfrm.transform;
                reticlePanel = SSCUtils.GetChildTransform(hudPanel, "DisplayReticlePanel", this.name);
                overlayPanel = SSCUtils.GetChildRectTransform(hudPanel, overlayPanelName, this.name);

                // For backward compatibility with earlier versions of the HUD1 prefab
                if (overlayPanel == null)
                {
                    overlayPanel = SSCUtils.GetChildRectTransform(hudPanel, "AltitudePanel", this.name);
                    if (overlayPanel != null) { overlayPanel.name = overlayPanelName; }
                }

                altitudeTextRectTfrm = SSCUtils.GetChildRectTransform(overlayPanel, "AltitudeText", this.name);
                airspeedTextRectTfrm = SSCUtils.GetChildRectTransform(overlayPanel, "AirSpeedText", this.name);

                targetsRectTfrm = GetTargetsPanel();
                gaugesRectTfrm = GetGaugesPanel();
                headingRectTfrm = GetHeadingPanel();
            }
        }

        /// <summary>
        /// [INTERNAL ONLY]
        /// </summary>
        private void GetImgComponents()
        {
            if (reticlePanel != null) { displayReticleImg = reticlePanel.GetComponent<Image>(); }
            if (overlayPanel != null) { primaryOverlayImg = overlayPanel.GetComponent<Image>(); }
        }

        /// <summary>
        /// [INTERNAL ONLY]
        /// </summary>
        private void GetTextComponents()
        {
            if (altitudeTextRectTfrm != null) { altitudeText = altitudeTextRectTfrm.GetComponent<Text>(); }
            if (airspeedTextRectTfrm != null) { airspeedText = airspeedTextRectTfrm.GetComponent<Text>(); }
        }

        /// <summary>
        /// [INTERNAL ONLY]
        /// Used to change HUD outside play mode
        /// </summary>
        public void InitialiseEditorEssentials()
        {
            if (canvas == null) { canvas = GetCanvas; }

            hudRectTfrm = SSCUtils.GetChildRectTransform(transform, hudPanelName, this.name);

            if (hudRectTfrm != null)
            {
                // Call before GetHUDPanels()
                CheckHUDResize(false);

                GetHUDPanels();
                GetImgComponents();
                GetTextComponents();

                InitialiseAttitude(true);
                InitialiseHeading(true);
                InitialiseMessages();
                InitialiseTargets();
                editorMode = true;

                ShowOrHideAttitude(showAttitude);
                ShowOrHideHeading(showHeading);
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: ShipDisplayModule.InitialiseEditorEssentials() could not find canvas HUDPanel. Did you start with the sample HUD prefab from Prefabs/Visuals folder?"); }
            #endif
        }

        /// <summary>
        /// The Overlay Panel contains the Overlay image and child objects
        /// Altitude Text and Airspeed Text.
        /// </summary>
        private void ShowOrHideOverlayPanel()
        {
            // Should the Overlay panel be enabled?
            if ((showAltitude || showAirspeed || showOverlay) && !overlayPanel.gameObject.activeSelf)
            {
                overlayPanel.gameObject.SetActive(true);
            }
            else if (!showAltitude && !showAirspeed && !showOverlay && overlayPanel.gameObject.activeSelf)
            {
                overlayPanel.gameObject.SetActive(false);
            }
        }

        /// <summary>
        /// Show or Hide the Overlay image on the HUD. Turn on HUD if required
        /// </summary>
        /// <param name="isShown"></param>
        private void ShowOrHideOverlay(bool isShown)
        {
            if (IsInitialised)
            {
                showOverlay = isShown;

                ShowOrHideOverlayPanel();

                if (primaryOverlayImg != null) { primaryOverlayImg.enabled = isShown; }

                if (showOverlay && !IsHUDShown) { ShowOrHideHUD(true); }
            }
        }

        #endregion

        #region Private and Internal Methods - Brightness

        /// <summary>
        /// Remembers the starting colours of various elements so that they can be
        /// used to apply brightness to those elements at runtime.
        /// Sets the initial brightness.
        /// NOTE: Brightness doesn't work so well if colour has low alpha to begin with.
        /// </summary>
        private void InitialiseBrightness()
        {
            if (displayReticleImg != null) { baseReticleColour = displayReticleImg.color; }
            if (primaryOverlayImg != null) { baseOverlayColour = primaryOverlayImg.color; }
            if (altitudeText != null) { baseAltitudeTextColour = altitudeText.color; }
            if (airspeedText != null) { baseAirspeedTextColour = airspeedText.color; }
            if (attitudeScrollImg != null) { baseAttitudePrimaryColour = attitudeScrollImg.color; }
            if (headingScrollImg != null) { baseHeadingPrimaryColour = headingScrollImg.color; }
            if (headingIndicatorImg != null) { baseHeadingIndicatorColour = headingIndicatorImg.color; }

            SetBrightness(brightness);
        }

        private void SetOverlayBrightness()
        {
            if (primaryOverlayImg != null)
            {
                // Skip more expensive HSV->RGBA and new Color op if possible
                if (brightness == 1f)
                {
                    SSCUtils.Color32toColorNoAlloc(ref primaryColour, ref tempColour);
                    primaryOverlayImg.color = tempColour;
                }
                else
                {
                    primaryOverlayImg.color = baseOverlayColour.GetColorWithBrightness(brightness);
                }
            }
        }

        private void SetReticleBrightness()
        {
            if (displayReticleImg != null)
            {
                // Skip more expensive HSV->RGBA and new Color op if possible
                if (brightness == 1f)
                {
                    SSCUtils.Color32toColorNoAlloc(ref activeDisplayReticleColour, ref tempColour);
                    displayReticleImg.color = tempColour;
                }
                else
                {
                    displayReticleImg.color = baseReticleColour.GetColorWithBrightness(brightness);
                }
            }
        }

        private void SetAltitudeTextBrightness()
        {
            if (altitudeText != null)
            {
                // Skip more expensive HSV->RGBA and new Color op if possible
                if (brightness == 1f)
                {
                    SSCUtils.Color32toColorNoAlloc(ref altitudeTextColour, ref tempColour);
                    altitudeText.color = tempColour;
                }
                else
                {
                    altitudeText.color = baseAltitudeTextColour.GetColorWithBrightness(brightness);
                }
            }
        }

        private void SetAirSpeedTextBrightness()
        {
            if (airspeedText != null)
            {
                // Skip more expensive HSV->RGBA and new Color op if possible
                if (brightness == 1f)
                {
                    SSCUtils.Color32toColorNoAlloc(ref airspeedTextColour, ref tempColour);
                    airspeedText.color = tempColour;
                }
                else
                {
                    airspeedText.color = baseAirspeedTextColour.GetColorWithBrightness(brightness);
                }
            }
        }

        private void SetAttitudeBrightness()
        {
            if (attitudeScrollImg != null)
            {
                // Skip more expensive HSV->RGBA and new Color op if possible
                if (brightness == 1f)
                {
                    attitudeScrollImg.color = attitudePrimaryColour;
                }
                else
                {
                    attitudeScrollImg.color = baseAttitudePrimaryColour.GetColorWithBrightness(brightness);
                }
            }
        }

        private void SetHeadingBrightness()
        {
            if (headingScrollImg != null)
            {
                // Skip more expensive HSV->RGBA and new Color op if possible
                if (brightness == 1f)
                {
                    headingScrollImg.color = headingPrimaryColour;
                    headingIndicatorImg.color = headingIndicatorColour;
                }
                else
                {
                    headingScrollImg.color = baseHeadingPrimaryColour.GetColorWithBrightness(brightness);
                    headingIndicatorImg.color = baseHeadingIndicatorColour.GetColorWithBrightness(brightness);
                }
            }
        }

        private void SetDisplayGaugeForegroundBrightness(DisplayGauge displayGauge)
        {
            if (displayGauge != null)
            {
                if (IsInitialised || editorMode)
                {
                    if (displayGauge.CachedFgImgComponent != null)
                    {
                        if (brightness == 1f) { displayGauge.CachedFgImgComponent.color = displayGauge.foregroundColour; }
                        else { displayGauge.CachedFgImgComponent.color = displayGauge.baseForegroundColour.GetColorWithBrightness(brightness); }
                    }
                    #if UNITY_EDITOR
                    else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeForegroundBrightness - displayGauge.CachedFgImgComponent is null"); }
                    #endif
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeForegroundBrightness - displayGauge is null"); }
            #endif
        }

        private void SetDisplayGaugeBackgroundBrightness(DisplayGauge displayGauge)
        {
            if (displayGauge != null)
            {
                if (IsInitialised || editorMode)
                {
                    if (displayGauge.CachedBgImgComponent != null)
                    {
                        if (brightness == 1f) { displayGauge.CachedBgImgComponent.color = displayGauge.backgroundColour; }
                        else { displayGauge.CachedBgImgComponent.color = displayGauge.baseBackgroundColour.GetColorWithBrightness(brightness); }
                    }
                    #if UNITY_EDITOR
                    else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeBackgroundBrightness - displayGauge.CachedBgImgComponent is null"); }
                    #endif
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeBackgroundBrightness - displayGauge is null"); }
            #endif
        }

        /// <summary>
        /// Adjust the brightness of the gauge text. If it is a numeric gauge with a label,
        /// also adjust the brightness of the label.
        /// </summary>
        /// <param name="displayGauge"></param>
        private void SetDisplayGaugeTextBrightness(DisplayGauge displayGauge)
        {
            if (displayGauge != null)
            {
                if (IsInitialised || editorMode)
                {
                    if (displayGauge.CachedTextComponent != null)
                    {
                        if (brightness == 1f) { displayGauge.CachedTextComponent.color = displayGauge.textColour; }
                        else { displayGauge.CachedTextComponent.color = displayGauge.baseTextColour.GetColorWithBrightness(brightness); }
                    }
                    #if UNITY_EDITOR
                    else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeTextBrightness - displayGauge.CachedTextComponent is null"); }
                    #endif

                    if (displayGauge.CachedLabelTextComponent != null)
                    {
                        if (brightness == 1f) { displayGauge.CachedLabelTextComponent.color = displayGauge.textColour; }
                        else { displayGauge.CachedLabelTextComponent.color = displayGauge.baseTextColour.GetColorWithBrightness(brightness); }
                    }
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeTextBrightness - displayGauge is null"); }
            #endif
        }

        private void SetDisplayMessageBackgroundBrightness(DisplayMessage displayMessage)
        {
            if (displayMessage != null)
            {
                if (IsInitialised || editorMode)
                {
                    if (displayMessage.CachedBgImgComponent != null)
                    {
                        if (brightness == 1f) { displayMessage.CachedBgImgComponent.color = displayMessage.backgroundColour; }
                        else { displayMessage.CachedBgImgComponent.color = displayMessage.baseBackgroundColour.GetColorWithBrightness(brightness); }
                    }
                    #if UNITY_EDITOR
                    else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayMessageBackgroundBrightness - displayMessage.CachedBgImgComponent is null"); }
                    #endif
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayMessageBackgroundBrightness - displayMessage is null"); }
            #endif
        }

        private void SetDisplayMessageBackgroundFade (DisplayMessage displayMessage, bool isFadeOut)
        {
            if (displayMessage != null)
            {
                if (IsInitialised || editorMode)
                {
                    if (displayMessage.CachedBgImgComponent != null)
                    {
                        float fadeValue = easeInOutCurve.Evaluate(isFadeOut ? displayMessage.GetFadeOutValue() : displayMessage.GetFadeInValue());

                        displayMessage.CachedBgImgComponent.color = displayMessage.baseBackgroundColour.GetColorWithFadedBrightness(brightness, fadeValue);
                    }
                    #if UNITY_EDITOR
                    else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayMessageBackgroundFade - displayMessage.CachedBgImgComponent is null"); }
                    #endif
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayMessageBackgroundFade - displayMessage is null"); }
            #endif
        }

        private void SetDisplayMessageTextBrightness(DisplayMessage displayMessage)
        {
            if (displayMessage != null)
            {
                if (IsInitialised || editorMode)
                {
                    if (displayMessage.CachedTextComponent != null)
                    {
                        if (brightness == 1f) { displayMessage.CachedTextComponent.color = displayMessage.textColour; }
                        else { displayMessage.CachedTextComponent.color = displayMessage.baseTextColour.GetColorWithBrightness(brightness); }
                    }
                    #if UNITY_EDITOR
                    else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayMessageTextBrightness - displayMessage.CachedTextComponent is null"); }
                    #endif
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayMessageTextBrightness - displayMessage is null"); }
            #endif
        }

        private void SetDisplayMessageTextFade (DisplayMessage displayMessage, bool isFadeOut)
        {
            if (displayMessage != null)
            {
                if (IsInitialised || editorMode)
                {
                    if (displayMessage.CachedTextComponent != null)
                    {
                        float fadeValue = easeInOutCurve.Evaluate(isFadeOut ? displayMessage.GetFadeOutValue() : displayMessage.GetFadeInValue());

                        //Debug.Log("[DEBUG] msg fadeinTimer: " + displayMessage.fadeinTimer + " fadeValue: " + fadeValue);

                        displayMessage.CachedTextComponent.color = displayMessage.baseTextColour.GetColorWithFadedBrightness(brightness, fadeValue);
                    }
                    #if UNITY_EDITOR
                    else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayMessageTextFade - displayMessage.CachedTextComponent is null"); }
                    #endif
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayMessageTextFade - displayMessage is null"); }
            #endif
        }

        /// <summary>
        /// Set the brightness of all slots (copies) of a DisplayTarget
        /// </summary>
        /// <param name="displayTarget"></param>
        private void SetDisplayTargetBrightness(DisplayTarget displayTarget)
        {
            if (displayTarget != null)
            {
                if (IsInitialised || editorMode)
                {
                    for (int sIdx = 0; sIdx < displayTarget.maxNumberOfTargets; sIdx++)
                    {
                        if (displayTarget.displayTargetSlotList[sIdx].CachedImgComponent != null)
                        {
                            if (brightness == 1f) { displayTarget.displayTargetSlotList[sIdx].CachedImgComponent.color = displayTarget.reticleColour; }
                            else { displayTarget.displayTargetSlotList[sIdx].CachedImgComponent.color = displayTarget.baseReticleColour.GetColorWithBrightness(brightness); }
                        }
                        #if UNITY_EDITOR
                        else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayTargetBrightness - displayTarget slot " + sIdx + " CachedImgComponent is null"); }
                        #endif
                    }
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayTargetBrightness - displayTarget is null"); }
            #endif
        }

        #endregion

        #region Private or Internal Methods - Reticle

        /// <summary>
        /// Show or Hide the reticle. Turn on the HUD if required.
        /// Always hide the cursor (hardware pointer) when the reticle is shown.
        /// </summary>
        /// <param name="isShown"></param>
        private void ShowOrHideReticle(bool isShown)
        {
            if (IsInitialised)
            {
                showActiveDisplayReticle = isShown;

                if (isShown)
                {
                    // Check the active display reticle
                    if (guidHashActiveDisplayReticle == 0)
                    {
                        showActiveDisplayReticle = false;
                        currentDisplayReticle = null;
                        LoadDisplayReticleSprite(currentDisplayReticle);

                        #if UNITY_EDITOR
                        Debug.LogWarning("ShipDisplayModule - could not show the Display Rectile because there was no active rectile");
                        #endif
                    }
                    // If there is no current reticle, or if we need to change the reticle,
                    // look up the hash value and load the sprite (if there is a match)
                    else if (currentDisplayReticle == null || (currentDisplayReticle.guidHash != guidHashActiveDisplayReticle))
                    {
                        currentDisplayReticle = displayReticleList.Find(dr => dr.guidHash == guidHashActiveDisplayReticle);
                        LoadDisplayReticleSprite(currentDisplayReticle);
                    }
                }

                if (showActiveDisplayReticle && lockDisplayReticleToCursor) { Cursor.visible = false; }

                reticlePanel.gameObject.SetActive(showActiveDisplayReticle);

                if (showActiveDisplayReticle && !IsHUDShown) { ShowOrHideHUD(true); }
            }
        }

        /// <summary>
        /// Load the reticle sprite into the UI image on the panel
        /// </summary>
        /// <param name="displayReticle"></param>
        private void LoadDisplayReticleSprite(DisplayReticle displayReticle)
        {
            if (displayReticle != null)
            {
                displayReticleImg.sprite = displayReticle.primarySprite;
            }
            else
            {
                displayReticleImg.sprite = null;
            }
        }

        #endregion

        #region Private or Internal Methods - Overlay, Altitude and Speed

        /// <summary>
        /// Show or Hide the Altitude indicator on the HUD. Turn on HUD if required.
        /// </summary>
        /// <param name="isShown"></param>
        private void ShowOrHideAltitude(bool isShown)
        {
            if (IsInitialised)
            {
                showAltitude = isShown;

                ShowOrHideOverlayPanel();

                if (altitudeText != null) { altitudeText.gameObject.SetActive(showAltitude); }

                if (showAltitude && !IsHUDShown) { ShowOrHideHUD(true); }
            }
        }

        /// <summary>
        /// Show or Hide the Air Speed indicator on the HUD. Turn on HUD if required
        /// </summary>
        /// <param name="isShown"></param>
        private void ShowOrHideAirSpeed(bool isShown)
        {
            if (IsInitialised)
            {
                showAirspeed = isShown;

                ShowOrHideOverlayPanel();

                if (airspeedText != null) { airspeedText.gameObject.SetActive(showAirspeed); }

                if (showAirspeed && !IsHUDShown) { ShowOrHideHUD(true); }
            }
        }

        #endregion

        #region Private or Internal Methods - Attitude

        /// <summary>
        /// Get or Create the scrollable attitude.
        /// The attitude image is masked by the parent mask image.
        /// </summary>
        private void GetOrCreateAttitude()
        {
            if (attitudeRectTfrm == null) { attitudeRectTfrm = GetAttitudePanel(); }

            if (attitudeRectTfrm != null)
            {
                // Find or create the Mask for the scrollable attitude
                if (attitudeMaskImg == null)
                {
                    attitudeMaskImg = attitudeRectTfrm.GetComponent<UnityEngine.UI.Image>();

                    if (attitudeMaskImg == null)
                    {
                        attitudeMaskImg = attitudeRectTfrm.gameObject.AddComponent<UnityEngine.UI.Image>();
                        UnityEngine.UI.Mask attitudeMask = attitudeRectTfrm.gameObject.AddComponent<UnityEngine.UI.Mask>();

                        if (attitudeMaskImg != null)
                        {
                            attitudeMaskImg.raycastTarget = false;
                            attitudeMaskImg.preserveAspect = false;
                        }
                        #if UNITY_EDITOR
                        else { Debug.LogWarning("ERROR: shipDisplayModule.GetOrCreateAttitude() - could not add mask image component to Attitude panel"); }
                        #endif

                        if (attitudeMask != null)
                        {
                            attitudeMask.showMaskGraphic = false;
                        }
                        #if UNITY_EDITOR
                        else { Debug.LogWarning("ERROR: shipDisplayModule.GetOrCreateAttitude() - could not add mask component to Attitude panel"); }
                        #endif
                    }
                }
            }

            // Find or create the scrollable panel and image. This is a child of the attitude panel and mask
            if (attitudeScrollImg == null)
            {
                // The panel width and height are 1.0 which is the full resolution e.g. 1920x1080
                attitudeScrollPanel = SSCUtils.GetOrCreateChildRectTransform(attitudeRectTfrm, cvsResolutionFull, attitudeScrollName,
                                                    0f, 0f, 1f, 1f, 0.5f, 0.5f, 0.5f, 0.5f);

                if (attitudeScrollPanel != null)
                {
                    attitudeScrollImg = attitudeScrollPanel.GetComponent<UnityEngine.UI.Image>();

                    if (attitudeScrollImg == null)
                    {
                        attitudeScrollImg = attitudeScrollPanel.gameObject.AddComponent<UnityEngine.UI.Image>();

                        if (attitudeScrollImg != null)
                        {
                            attitudeScrollImg.raycastTarget = false;
                            attitudeScrollImg.preserveAspect = true;
                        }
                        #if UNITY_EDITOR
                        else { Debug.LogWarning("ERROR: shipDisplayModule.GetOrCreateAttitude() - could not add background to Attitude panel"); }
                        #endif
                    }
                }
            }


            // TODO - Find or create the small indicator panel and image (NOT SURE IF WE NEED THIS YET).
        }

        /// <summary>
        /// Initialise the scrollable Attitude.
        /// </summary>
        /// <param name="isInitialising"></param>
        private void InitialiseAttitude(bool isInitialising)
        {
            GetOrCreateAttitude();

            SetDisplayAttitudeScrollSprite(attitudeScrollSprite);
            SetDisplayAttitudeMaskSprite(attitudeMaskSprite);

            SetDisplayAttitudeOffset(attitudeOffsetX, attitudeOffsetY);
            SetDisplayAttitudeSize(attitudeWidth, attitudeHeight);

            if (attitudeScrollImg != null)
            {
                if (isInitialising)
                {
                    attitudeInitPosition = attitudeScrollImg.transform.position;
                }

                RefreshAttitudeAfterResize();
            }
        }

        /// <summary>
        /// Show or hide the scrollable Attitude display. Turn on HUD if required.
        /// </summary>
        /// <param name="isShown"></param>
        private void ShowOrHideAttitude(bool isShown)
        {
            if (IsInitialised || IsEditorMode)
            {
                showAttitude = isShown;

                if (attitudeRectTfrm != null) { attitudeRectTfrm.gameObject.SetActive(showAttitude); }

                if (attitudeScrollImg != null)
                {
                    attitudeScrollImg.enabled = isShown;
                    isAttitudeScrolling = isShown;
                }
                else
                {
                    isAttitudeScrolling = false;
                }

                if (showAttitude && !IsHUDShown) { ShowOrHideHUD(true); }
            }
        }

        #endregion

        #region Private or Internal Methods - Gauges

        /// <summary>
        /// [INTERNAL USE ONLY]
        /// Create a new Gauge RectTransform under the HUD GaugesPanel in the hierarchy
        /// Add a background Image
        /// Add a (foreground) fillable iamge
        /// Add a text panel
        /// </summary>
        /// <param name="displayGauge"></param>
        /// <returns></returns>
        public RectTransform CreateGaugePanel(DisplayGauge displayGauge)
        {
            RectTransform gaugePanel = null;

            if (gaugesRectTfrm == null) { gaugesRectTfrm = GetGaugesPanel(); }

            if (gaugesRectTfrm != null && displayGauge != null)
            {
                float panelWidth = displayGauge.displayWidth;
                float panelHeight = displayGauge.displayHeight;

                gaugePanel = SSCUtils.GetOrCreateChildRectTransform(gaugesRectTfrm, cvsResolutionFull, "Gauge_" + displayGauge.guidHash, 0f, 0f,
                                                                        panelWidth, panelHeight, 0.5f, 0.5f, 0.5f, 0.5f);

                if (gaugePanel != null)
                {
                    Image bgimgComponent = gaugePanel.gameObject.AddComponent<Image>();
                    if (bgimgComponent != null)
                    {
                        bgimgComponent.raycastTarget = false;
                    }
                    #if UNITY_EDITOR
                    else { Debug.LogWarning("ERROR: shipDisplayModule.CreateGaugePanel() - could not add background to Gauge panel"); }
                    #endif

                    RectTransform gaugeAmtPanel = SSCUtils.GetOrCreateChildRectTransform(gaugePanel, cvsResolutionFull, "GaugeAmount", 0f, 0f,
                                                                        panelWidth, panelHeight, 0.5f, 0.5f, 0.5f, 0.5f);

                    if (gaugeAmtPanel != null)
                    {
                        Image amtimgComponent = gaugeAmtPanel.gameObject.AddComponent<Image>();
                        if (amtimgComponent != null)
                        {
                            amtimgComponent.raycastTarget = false;
                            amtimgComponent.type = Image.Type.Filled;
                            amtimgComponent.fillMethod = (Image.FillMethod)displayGauge.fillMethod;
                        }
                        #if UNITY_EDITOR
                        else { Debug.LogWarning("ERROR: shipDisplayModule.CreateGaugePanel() - could not add fill image to Gauge panel"); }
                        #endif
                    }

                    RectTransform gaugeTxtPanel = SSCUtils.GetOrCreateChildRectTransform(gaugePanel, cvsResolutionFull, "GaugeText", 0f, 0f,
                                                                        panelWidth, panelHeight, 0.5f, 0.5f, 0.5f, 0.5f);

                    if (gaugeTxtPanel != null)
                    {
                        UnityEngine.UI.Text textComponent = gaugeTxtPanel.gameObject.AddComponent<UnityEngine.UI.Text>();
                        if (textComponent != null)
                        {
                            textComponent.raycastTarget = false;
                            textComponent.resizeTextForBestFit = true;
                            // Text is add in InitialiseGauge().
                            textComponent.text = string.Empty;
                            if (Application.isPlaying)
                            {
                                textComponent.font = SSCUtils.GetDefaultFont();
                            }
                        }
                        #if UNITY_EDITOR
                        else { Debug.LogWarning("ERROR: shipDisplayModule.CreateGaugePanel() - could not add text to Gauge panel"); }
                        #endif
                    }

                    if (IsInitialised || editorMode)
                    {
                        // Update the number of gauges. Count the list rather than assume the gauge didn't already exist.
                        numDisplayGauges = displayGaugeList == null ? 0 : displayGaugeList.Count;
                        InitialiseGauge(displayGauge);
                    }
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: ShipDisplayModule.CreateGaugePanel() - displayGauge is null or could not find or create " + gaugesPanelName); }
            #endif

            return gaugePanel;
        }

        /// <summary>
        /// [INTERNAL USE ONLY]
        /// Find the Gauge RectTransform under the HUD in the hierarchy.
        /// </summary>
        /// <param name="guidHash"></param>
        /// <returns></returns>
        private RectTransform GetGaugePanel(int guidHash)
        {
            if (gaugesRectTfrm == null) { gaugesRectTfrm = GetGaugesPanel(); }
            if (gaugesRectTfrm != null)
            {
                return SSCUtils.GetChildRectTransform(gaugesRectTfrm.transform, "Gauge_" + guidHash, this.name);
            }
            else { return null; }
        }

        /// <summary>
        /// Get or create a gauge label. This only applies to numeric gauges with an additional
        /// text component or label.
        /// If it is the wrong gauge type, remove any secondary label.
        /// This will set or update the displayGauge.CachedLabelTextComponent.
        /// </summary>
        /// <param name="guidHash"></param>
        private void GetOrCreateOrRemoveGaugeLabel(DisplayGauge displayGauge)
        {
            UnityEngine.UI.Text textComponent = null;

            if (displayGauge != null)
            {
                int gaugeTypeInt = (int)displayGauge.gaugeType;

                // Is the component already cached?
                if (displayGauge.CachedLabelTextComponent != null)
                {
                    // If this gauge should have a secondary label, set it to the cached component
                    if (gaugeTypeInt == DisplayGauge.DGTypeNumberWithLabel1Int)
                    {
                        textComponent = displayGauge.CachedLabelTextComponent;
                    }
                    else
                    {
                        // A secondary label (Text component) does not apply to this gauge, so remove it.
                        if (Application.isPlaying) { Destroy(displayGauge.CachedLabelTextComponent.gameObject); }
                        else
                        {
                            #if UNITY_EDITOR
                            UnityEditor.Undo.DestroyObjectImmediate(displayGauge.CachedLabelTextComponent.gameObject);
                            #else
                            DestroyImmediate(displayGauge.CachedLabelTextComponent.gameObject);
                            #endif
                        }

                        // Clear the cached component
                        displayGauge.CachedLabelTextComponent = null;
                    }                  
                }
                else if (gaugeTypeInt == DisplayGauge.DGTypeNumberWithLabel1Int)
                {
                    // Not cached, so attempt to find it
                    RectTransform gaugePanel = displayGauge.CachedGaugePanel == null ? GetGaugePanel(displayGauge.guidHash) : displayGauge.CachedGaugePanel;

                    if (gaugePanel != null)
                    {
                        float panelWidth = displayGauge.displayWidth;
                        float panelHeight = displayGauge.displayHeight / 2f;

                        RectTransform gaugeLabelPanel = SSCUtils.GetOrCreateChildRectTransform(gaugePanel, cvsResolutionFull, "GaugeLabel", 0f, 0f,
                                                        panelWidth, panelHeight, 0.5f, 0.5f, 0.5f, 0.5f);

                        if (gaugeLabelPanel != null)
                        {
                            textComponent = gaugeLabelPanel.GetComponent<UnityEngine.UI.Text>();

                            if (textComponent == null)
                            {
                                // Text component doesn't exist, so add and configure it
                                textComponent = gaugeLabelPanel.gameObject.AddComponent<UnityEngine.UI.Text>();

                                if (textComponent != null)
                                {
                                    textComponent.raycastTarget = false;
                                    textComponent.resizeTextForBestFit = true;

                                    textComponent.text = displayGauge.gaugeLabel;
                                    if (Application.isPlaying)
                                    {
                                        textComponent.font = SSCUtils.GetDefaultFont();
                                    }

                                    displayGauge.CachedLabelTextComponent = textComponent;
                                }
                                #if UNITY_EDITOR
                                else { Debug.LogWarning("ERROR: shipDisplayModule.CreateGaugePanel() - could not add text to Gauge panel"); }
                                #endif
                            }
                            else
                            {
                                displayGauge.CachedLabelTextComponent = textComponent;
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Cache the DisplayGauge RectTransform, Image and Text component.
        /// Set the Text colour.
        /// </summary>
        /// <param name="displayGauge"></param>
        private void InitialiseGauge(DisplayGauge displayGauge)
        {
            displayGauge.CachedGaugePanel = GetGaugePanel(displayGauge.guidHash);
            // Cache FillMethod is None to avoid enum lookup at runtime
            displayGauge.isFillMethodNone = displayGauge.fillMethod == DisplayGauge.DGFillMethod.None;

            if (displayGauge.CachedGaugePanel != null)
            {
                displayGauge.CachedBgImgComponent = displayGauge.CachedGaugePanel.GetComponent<Image>();

                if (displayGauge.CachedBgImgComponent != null)
                {
                    // If there is a sprite but it hasn't been loaded into the image, do it now
                    if (displayGauge.backgroundSprite != null && displayGauge.CachedBgImgComponent.sprite == null)
                    {
                        SetDisplayGaugeBackgroundSprite(displayGauge, displayGauge.backgroundSprite);
                    }

                    // Cache the gauge foreground Image component
                    if (tempImgList == null) { tempImgList = new List<Image>(1); }
                    displayGauge.CachedGaugePanel.GetComponentsInChildren(tempImgList);
                    if (tempImgList.Count > 0)
                    {
                        // GetComponentsInChildren will also return the background image which we don't want
                        tempImgList.Remove(displayGauge.CachedBgImgComponent);

                        if (tempImgList.Count > 0)
                        {
                            displayGauge.CachedFgImgComponent = tempImgList[0];

                            // If there is a sprite but it hasn't been loaded into the image, do it now
                            if (displayGauge.foregroundSprite != null && displayGauge.CachedFgImgComponent.sprite == null)
                            {
                                SetDisplayGaugeForegroundSprite(displayGauge, displayGauge.foregroundSprite);
                            }

                            SetDisplayGaugeValue(displayGauge, displayGauge.gaugeValue);
                        }
                    }
                }

                // Cache the gauge Text component
                if (tempTextList == null) { tempTextList = new List<Text>(2); }
                displayGauge.CachedGaugePanel.GetComponentsInChildren(tempTextList);
                if (tempTextList.Count > 0)
                {
                    displayGauge.CachedTextComponent = tempTextList[0];

                    // NOTE: The order may not be guaranteed here so this might or might not be an issue
                    // with numeric gauges.
                    if (tempTextList.Count > 1)
                    {
                        displayGauge.CachedLabelTextComponent = tempTextList[1];
                    }
                }

                // SetDisplayGaugeForegroundColour does not update the baseForegroundColour if the foregroundColour has not changed. So do it in here instead
                displayGauge.baseForegroundColour.Set(displayGauge.foregroundColour.r, displayGauge.foregroundColour.g, displayGauge.foregroundColour.b, displayGauge.foregroundColour.a, true);
                SetDisplayGaugeForegroundBrightness(displayGauge);

                // SetDisplayGaugeBackgroundColour does not update the baseBackgroundColour if the backgroundColour has not changed. So do it in here instead
                displayGauge.baseBackgroundColour.Set(displayGauge.backgroundColour.r, displayGauge.backgroundColour.g, displayGauge.backgroundColour.b, displayGauge.backgroundColour.a, true);
                SetDisplayGaugeBackgroundBrightness(displayGauge);

                // SetDisplayGaugeTextColour does not update the baseTextColour if the textColour has not changed. So do it in here instead
                displayGauge.baseTextColour.Set(displayGauge.textColour.r, displayGauge.textColour.g, displayGauge.textColour.b, displayGauge.textColour.a, true);
                SetDisplayGaugeTextBrightness(displayGauge);

                SetDisplayGaugeText(displayGauge, displayGauge.gaugeString);
                SetDisplayGaugeTextAlignment(displayGauge, displayGauge.textAlignment);
                SetDisplayGaugeTextFontStyle(displayGauge, (UnityEngine.FontStyle)displayGauge.fontStyle);
                SetDisplayGaugeTextFontSize(displayGauge, displayGauge.isBestFit, displayGauge.fontMinSize, displayGauge.fontMaxSize);

                if (displayGauge.CachedLabelTextComponent != null)
                {
                    SetDisplayGaugeLabel(displayGauge, displayGauge.gaugeLabel);
                    SetDisplayGaugeLabelAlignment(displayGauge, displayGauge.labelAlignment);
                }

                SetDisplayGaugeSize(displayGauge, displayGauge.displayWidth, displayGauge.displayHeight);

                SetDisplayGaugeOffset(displayGauge, displayGauge.offsetX, displayGauge.offsetY);

                // By default gauges are initialised off. This is to prevent them suddenly appearing when first created.
                if (Application.isPlaying)
                {
                    displayGauge.CachedGaugePanel.gameObject.SetActive(false);
                }
            }
        }

        /// <summary>
        /// Initialise all the DisplayGauges by caching the RectTransforms.
        /// </summary>
        private void InitialiseGauges()
        {
            int _numDisplayGauges = GetNumberDisplayGauges;
            
            if (_numDisplayGauges > 0)
            {
                RectTransform hudRectTfrm = GetHUDPanel();
                if (hudRectTfrm != null)
                {
                    CheckHUDResize(false);
                    for (int dgIdx = 0; dgIdx < _numDisplayGauges; dgIdx++)
                    {
                        InitialiseGauge(displayGaugeList[dgIdx]);
                    }

                    if (_numDisplayGauges > 0) { RefreshGaugesSortOrder(); }
                }
                #if UNITY_EDITOR
                else { Debug.LogWarning("ShipDisplayModule.InitialiseGauges() - could not find HUD Panel"); }
                #endif
            }
        }

        /// <summary>
        /// Show or Hide the Display Gauge on the HUD. Turn on the HUD if required.
        /// </summary>
        /// <param name="displayGauge"></param>
        /// <param name="isShown"></param>
        private void ShowOrHideGauge(DisplayGauge displayGauge, bool isShown)
        {
            if (IsInitialised)
            {
                if (displayGauge.CachedGaugePanel != null)
                {
                    displayGauge.showGauge = isShown;
                    displayGauge.CachedGaugePanel.gameObject.SetActive(isShown);
                }

                if (isShown && !IsHUDShown) { ShowOrHideHUD(true); }
            }
        }

        /// <summary>
        /// Show or hide all gauges
        /// </summary>
        /// <param name="isShown"></param>
        private void ShowOrHideGauges(bool isShown)
        {
            if (IsInitialised)
            {
                for (int dmIdx = 0; dmIdx < numDisplayGauges; dmIdx++)
                {
                    ShowOrHideGauge(displayGaugeList[dmIdx], isShown);
                }
            }
        }

        /// <summary>
        /// Show or Hide all gauges based on their current settings
        /// </summary>
        private void ShowOrHideGauges()
        {
            if (IsInitialised)
            {
                for (int dmIdx = 0; dmIdx < numDisplayGauges; dmIdx++)
                {
                    DisplayGauge displayGauge = displayGaugeList[dmIdx];
                    ShowOrHideGauge(displayGauge, displayGauge.showGauge);
                }
            }
        }

        #endregion

        #region Private or Internal Methods - Heading

        /// <summary>
        /// Initialise the scrollable heading or compass.
        /// </summary>
        /// <param name="isInitialising"></param>
        private void InitialiseHeading(bool isInitialising)
        {
            GetOrCreateHeading();

            SetDisplayHeadingScrollSprite(headingScrollSprite);
            SetDisplayHeadingMaskSprite(headingMaskSprite);
            SetDisplayHeadingIndicatorSprite(headingIndicatorSprite);

            SetDisplayHeadingOffset(headingOffsetX, headingOffsetY);
            SetDisplayHeadingSize(headingWidth, headingHeight);

            if (isInitialising && headingScrollImg != null)
            {
                headingInitPosition = headingScrollImg.transform.position;

                // The heading img should have 2 sets of 0-360 deg to avoid scrolling partly out of view.
                // The image is autoscaled to the full width of the default HUD screen size.
                headingPixelsPerDegree = cvsResolutionFull.x / 720f;
            }
        }

        /// <summary>
        /// Get or Create the scrollable heading.
        /// The heading image is masked by the parent mask image.
        /// </summary>
        private void GetOrCreateHeading()
        {
            if (headingRectTfrm == null) { headingRectTfrm = GetHeadingPanel(); }

            if (headingRectTfrm != null)
            {
                // Find or create the Mask for the scrollable heading
                if (headingMaskImg == null)
                {
                    headingMaskImg = headingRectTfrm.GetComponent<UnityEngine.UI.Image>();

                    if (headingMaskImg == null)
                    {
                        headingMaskImg = headingRectTfrm.gameObject.AddComponent<UnityEngine.UI.Image>();
                        UnityEngine.UI.Mask headingMask = headingRectTfrm.gameObject.AddComponent<UnityEngine.UI.Mask>();

                        if (headingMaskImg != null)
                        {
                            headingMaskImg.raycastTarget = false;
                            headingMaskImg.preserveAspect = false;
                        }
                        #if UNITY_EDITOR
                        else { Debug.LogWarning("ERROR: shipDisplayModule.GetOrCreateHeading() - could not add mask image component to Heading panel"); }
                        #endif

                        if (headingMask != null)
                        {
                            headingMask.showMaskGraphic = false;
                        }
                        #if UNITY_EDITOR
                        else { Debug.LogWarning("ERROR: shipDisplayModule.GetOrCreateHeading() - could not add mask component to Heading panel"); }
                        #endif
                    }
                }

                // Find or create the scrollable panel and image. This is a child of the heading panel and mask
                if (headingScrollImg == null)
                {
                    // The panel width and height are 1.0 which is the full resolution e.g. 1920x1080
                    headingScrollPanel = SSCUtils.GetOrCreateChildRectTransform(headingRectTfrm, cvsResolutionFull, headingScrollName,
                                                        0f, 0f, 1f, 1f, 0.5f, 0.5f, 0.5f, 0.5f);

                    if (headingScrollPanel != null)
                    {
                        headingScrollImg = headingScrollPanel.GetComponent<UnityEngine.UI.Image>();

                        if (headingScrollImg == null)
                        {
                            headingScrollImg = headingScrollPanel.gameObject.AddComponent<UnityEngine.UI.Image>();

                            if (headingScrollImg != null)
                            {
                                headingScrollImg.raycastTarget = false;
                                headingScrollImg.preserveAspect = true;
                            }
                            #if UNITY_EDITOR
                            else { Debug.LogWarning("ERROR: shipDisplayModule.GetOrCreateHeading() - could not add background to Heading panel"); }
                            #endif
                        }
                    }
                }

                // Find or create the small indicator panel and image.
                if (headingIndicatorImg == null)
                {
                    // The panel width and height are 1.0 which is the full resolution e.g. 1920x1080
                    headingIndicatorPanel = SSCUtils.GetOrCreateChildRectTransform(headingRectTfrm, cvsResolutionFull, headingIndicatorName,
                                                        0f, 0f, 16f / cvsResolutionFull.x, 16f / cvsResolutionFull.y, 0.5f, 0.5f, 0.5f, 0.5f);

                    if (headingIndicatorPanel != null)
                    {
                        headingIndicatorImg = headingIndicatorPanel.GetComponent<UnityEngine.UI.Image>();

                        if (headingIndicatorImg == null)
                        {
                            headingIndicatorImg = headingIndicatorPanel.gameObject.AddComponent<UnityEngine.UI.Image>();

                            if (headingIndicatorImg != null)
                            {
                                headingIndicatorImg.raycastTarget = false;
                                headingIndicatorImg.preserveAspect = true;
                            }
                            #if UNITY_EDITOR
                            else { Debug.LogWarning("ERROR: shipDisplayModule.GetOrCreateHeading() - could not add indicator to Heading panel"); }
                            #endif
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Show or Hide the scrollable Heading on the HUD. Turn on HUD if required.
        /// </summary>
        /// <param name="isShown"></param>
        private void ShowOrHideHeading(bool isShown)
        {
            if (IsInitialised || IsEditorMode)
            {
                showHeading = isShown;

                if (headingRectTfrm != null) { headingRectTfrm.gameObject.SetActive(showHeading); }

                if (headingScrollImg != null)
                {
                    headingScrollImg.enabled = isShown;
                    isHeadingScrolling = isShown;
                }
                else 
                {
                    isHeadingScrolling = false;
                }

                if (showHeading && !IsHUDShown) { ShowOrHideHUD(true); }
            }
        }

        /// <summary>
        /// Show or Hide the small Heading indicator on the HUD.
        /// </summary>
        /// <param name="isShown"></param>
        private void ShowOrHideHeadingIndicator(bool isShown)
        {
            if (IsInitialised || IsEditorMode)
            {
                showHeadingIndicator = isShown;

                if (headingIndicatorImg != null)
                {
                    headingIndicatorImg.enabled = isShown;
                }
            }
        }

        #endregion

        #region Private or Internal Methods - Messages

        /// <summary>
        /// Cache the DisplayMessage RectTransform, Image and Text component.
        /// Set the Text colour.
        /// </summary>
        /// <param name="displayMessage"></param>
        /// <param name="hudRectTfrm"></param>
        private void InitialiseMessage(DisplayMessage displayMessage, RectTransform hudRectTfrm)
        {
            displayMessage.CachedMessagePanel = GetMessagePanel(displayMessage.guidHash, hudRectTfrm);

            displayMessage.scrollDirectionInt = (int)displayMessage.scrollDirection;
            displayMessage.scrollOffsetX = 0f;
            displayMessage.scrollOffsetY = 0f;

            if (displayMessage.CachedMessagePanel != null)
            {
                displayMessage.CachedBgImgComponent = displayMessage.CachedMessagePanel.GetComponent<Image>();

                if (displayMessage.CachedBgImgComponent != null)
                {
                    displayMessage.CachedBgImgComponent.enabled = displayMessage.showBackground;
                }

                // Cache the message Text component
                if (tempTextList == null) { tempTextList = new List<Text>(1); }
                displayMessage.CachedMessagePanel.GetComponentsInChildren(tempTextList);
                if (tempTextList.Count > 0)
                {
                    displayMessage.CachedTextComponent = tempTextList[0];
                }

                // SetDisplayMessageBackgroundColour does not update the baseBackgroundColour if the backgroundColour has not changed. So do it in here instead
                displayMessage.baseBackgroundColour.Set(displayMessage.backgroundColour.r, displayMessage.backgroundColour.g, displayMessage.backgroundColour.b, displayMessage.backgroundColour.a, true);
                SetDisplayMessageBackgroundBrightness(displayMessage);

                // SetDisplayMessageTextColour does not update the baseTextColour if the textColour has not changed. So do it in here instead
                displayMessage.baseTextColour.Set(displayMessage.textColour.r, displayMessage.textColour.g, displayMessage.textColour.b, displayMessage.textColour.a, true);
                SetDisplayMessageTextBrightness(displayMessage);

                SetDisplayMessageText(displayMessage, displayMessage.messageString);
                SetDisplayMessageTextAlignment(displayMessage, displayMessage.textAlignment);
                SetDisplayMessageTextFontSize(displayMessage, displayMessage.isBestFit, displayMessage.fontMinSize, displayMessage.fontMaxSize);
                SetDisplayMessageSize(displayMessage, displayMessage.displayWidth, displayMessage.displayHeight);

                if (displayMessage.scrollDirectionInt != DisplayMessage.ScrollDirectionNone)
                {
                    // scrollWidth/Height should be the same as those used in ScrollMessage(..)
                    // The left/right and top/bottom scroll limits can be ignored OR can consider the width/height of the message.
                    // Fullscreen scrolling = +/- half width of message + half the width of canvas
                    float scrollWidth = displayMessage.isScrollFullscreen ? displayMessage.displayWidth * cvsResolutionHalf.x + cvsResolutionHalf.x : displayMessage.displayWidth * cvsResolutionFull.x;

                    // Fullscreen scrolling = +/- half height of message + half the height of canvas
                    float scrollHeight = displayMessage.isScrollFullscreen ? displayMessage.displayHeight * cvsResolutionHalf.y + cvsResolutionHalf.y : displayMessage.displayHeight * cvsResolutionFull.y;

                    // Start at beginning of scrolling position (e.g. Offscreen left/right)
                    if (displayMessage.scrollDirectionInt == DisplayMessage.ScrollDirectionLR)
                    {
                        displayMessage.scrollOffsetX = -scrollWidth;
                    }
                    else if (displayMessage.scrollDirectionInt == DisplayMessage.ScrollDirectionRL)
                    {
                        displayMessage.scrollOffsetX = scrollWidth;
                    }
                    if (displayMessage.scrollDirectionInt == DisplayMessage.ScrollDirectionBT)
                    {
                        displayMessage.scrollOffsetY = -scrollHeight;
                    }
                    else if (displayMessage.scrollDirectionInt == DisplayMessage.ScrollDirectionTB)
                    {
                        displayMessage.scrollOffsetY = scrollHeight;
                    }
                }
                SetDisplayMessageOffset(displayMessage, displayMessage.offsetX, displayMessage.offsetY);
                
                // By default messages are initialised off. This is to prevent them suddenly appearing when first created.
                if (Application.isPlaying)
                {
                    displayMessage.CachedMessagePanel.gameObject.SetActive(false);
                }
            }
        }

        /// <summary>
        /// Initialise all the DisplayMessages by caching the RectTransforms.
        /// </summary>
        private void InitialiseMessages()
        {
            int _numDisplayMessages = GetNumberDisplayMessages;
            
            if (_numDisplayMessages > 0)
            {
                RectTransform hudRectTfrm = GetHUDPanel();
                if (hudRectTfrm != null)
                {
                    CheckHUDResize(false);
                    for (int dmIdx = 0; dmIdx < _numDisplayMessages; dmIdx++)
                    {
                        InitialiseMessage(displayMessageList[dmIdx], hudRectTfrm);
                    }

                    if (_numDisplayMessages > 0) { RefreshMessagesSortOrder(); }
                }
                #if UNITY_EDITOR
                else { Debug.LogWarning("ShipDisplayModule.InitialiseMessages() - could not find HUD Panel"); }
                #endif
            }
        }

        /// <summary>
        /// Show or Hide the Display Message on the HUD. Turn on the HUD if required.
        /// When fading out a message, it isn't instantly turned off.
        /// Option to override the fade settings and instantly hide or show the message.
        /// </summary>
        /// <param name="displayMessage"></param>
        /// <param name="isShown"></param>
        /// <param name="isOverrideFade"></param>
        private void ShowOrHideMessage(DisplayMessage displayMessage, bool isShown, bool isOverrideFade = false)
        {
            if (IsInitialised)
            {
                if (displayMessage.CachedMessagePanel != null)
                {
                    if (isShown)
                    {
                        if (isOverrideFade)
                        {
                            displayMessage.fadeinTimer = 0f;
                        }
                        // Is a fade-out already in progress?
                        else if (displayMessage.fadeoutDuration > 0f && displayMessage.fadeoutTimer > 0f)
                        {
                            // (1 - Normalise amount faded out) * fade in duration.
                            displayMessage.fadeinTimer = (1f - (displayMessage.fadeoutTimer / displayMessage.fadeoutDuration)) * displayMessage.fadeinDuration;
                        }
                        else
                        {
                            displayMessage.fadeinTimer = displayMessage.fadeinDuration;

                            if (displayMessage.fadeinTimer > 0f)
                            {
                                // Instantly update to avoid flashing the faded in message before Update() is called.
                                SetDisplayMessageTextFade(displayMessage, false);
                                if (displayMessage.showBackground)
                                {
                                    SetDisplayMessageBackgroundFade(displayMessage, false);
                                }
                            }
                        }

                        // Should never be fading out when turning the message on
                        displayMessage.fadeoutTimer = 0f;
                    }
                    else if (isOverrideFade)
                    {
                        displayMessage.fadeoutTimer = 0f;
                    }
                    else if (displayMessage.fadeoutDuration > 0f)
                    {
                        // Is a fade-in already in progress?
                        if (displayMessage.fadeinDuration > 0f && displayMessage.fadeinTimer > 0f)
                        {
                            // (1 - Normalise amount faded in) * fade out duration.
                            displayMessage.fadeoutTimer = (1f - (displayMessage.fadeinTimer / displayMessage.fadeinDuration)) * displayMessage.fadeoutDuration;
                        }
                        else
                        {
                            displayMessage.fadeoutTimer = displayMessage.fadeoutDuration;
                        }

                        // Should never be fading in when turning the message off
                        displayMessage.fadeinTimer = 0f;

                        // Override instantly turning off the message
                        isShown = true;
                    }
                    else
                    {
                        displayMessage.showMessage = false;
                    }

                    displayMessage.showMessage = isShown;

                    displayMessage.CachedMessagePanel.gameObject.SetActive(isShown);
                }

                if (isShown && !IsHUDShown) { ShowOrHideHUD(true); }
            }
        }

        /// <summary>
        /// Show or hide all messages.
        /// Option to override fade settings and instantly show or hide.
        /// </summary>
        /// <param name="isShown"></param>
        /// <param name="isOverrideFade"></param>
        private void ShowOrHideMessages(bool isShown, bool isOverrideFade = false)
        {
            if (IsInitialised)
            {
                for (int dmIdx = 0; dmIdx < numDisplayMessages; dmIdx++)
                {
                    ShowOrHideMessage(displayMessageList[dmIdx], isShown, isOverrideFade);
                }
            }
        }

        /// <summary>
        /// Show or Hide all messages based on their current settings.
        /// This is called by 
        /// </summary>
        private void ShowOrHideMessages()
        {
            if (IsInitialised)
            {
                for (int dmIdx = 0; dmIdx < numDisplayMessages; dmIdx++)
                {
                    DisplayMessage displayMessage = displayMessageList[dmIdx];
                    // Shown messages should fade in if required.
                    // Hidden messages should be instantly hidden.
                    ShowOrHideMessage(displayMessage, displayMessage.showMessage, !displayMessage.showMessage);
                }
            }
        }

        /// <summary>
        /// [INTERNAL USE ONLY]
        /// Find the Message RectTransform under the HUD in the hierarchy.
        /// If hudRectTrfm is null, it will attempt to find it.
        /// </summary>
        /// <param name="guidHash"></param>
        /// <param name="hudRectTrfm"></param>
        /// <returns></returns>
        private RectTransform GetMessagePanel(int guidHash, RectTransform hudRectTrfm)
        {
            if (hudRectTfrm == null) { hudRectTfrm = GetHUDPanel(); }
            if (hudRectTfrm != null)
            {
                return SSCUtils.GetChildRectTransform(hudRectTfrm.transform, "Message_" + guidHash, this.name);
            }
            else { return null; }
        }

        /// <summary>
        /// [INTERNAL USE ONLY]
        /// Create a new Message RectTransform under the HUD in the hierarchy
        /// </summary>
        /// <param name="displayMessage"></param>
        /// <param name="hudRectTfrm"></param>
        /// <returns></returns>
        public RectTransform CreateMessagePanel(DisplayMessage displayMessage, RectTransform hudRectTfrm)
        {
            RectTransform messagePanel = null;

            if (displayMessage != null)
            {
                float panelWidth = displayMessage.displayWidth;
                float panelHeight = displayMessage.displayHeight;

                //Canvas _canvas = GetCanvas;
                //Vector3 _canvasScale = _canvas == null ? Vector3.one : _canvas.transform.localScale;

                if (IsInitialised) { numDisplayMessages++; }
                messagePanel = SSCUtils.GetOrCreateChildRectTransform(hudRectTfrm, cvsResolutionFull, "Message_" + displayMessage.guidHash, 0f, 0f,
                                                                        panelWidth, panelHeight, 0.5f, 0.5f, 0.5f, 0.5f);

                if (messagePanel != null)
                {
                    Image imgComponent = messagePanel.gameObject.AddComponent<Image>();
                    if (imgComponent != null)
                    {
                        imgComponent.raycastTarget = false;
                    }
                    #if UNITY_EDITOR
                    else { Debug.LogWarning("ERROR: shipDisplayModule.CreateMessagePanel() - could not add background to Message panel"); }
                    #endif

                    RectTransform messageTxtPanel = SSCUtils.GetOrCreateChildRectTransform(messagePanel, cvsResolutionFull, "MessageText", 0f, 0f,
                                                                        panelWidth, panelHeight, 0.5f, 0.5f, 0.5f, 0.5f);

                    if (messageTxtPanel != null)
                    {
                        Text textComponent = messageTxtPanel.gameObject.AddComponent<Text>();
                        if (textComponent != null)
                        {
                            textComponent.raycastTarget = false;
                            textComponent.resizeTextForBestFit = true;
                            // Text is addd in InitialiseMessage().
                            textComponent.text = string.Empty;
                            if (Application.isPlaying)
                            {
                                textComponent.font = SSCUtils.GetDefaultFont();
                            }
                        }
                        #if UNITY_EDITOR
                        else { Debug.LogWarning("ERROR: shipDisplayModule.CreateMessagePanel() - could not add text to Message panel"); }
                        #endif
                    }

                    if (IsInitialised || editorMode) { InitialiseMessage(displayMessage, hudRectTfrm); }
                }
                #if UNITY_EDITOR
                else { Debug.LogWarning("ERROR: shipDisplayModule.CreateMessagePanel() - could not create Message panel"); }
                #endif
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: ShipDisplayModule.CreateMessagePanel() - displayMessage is null"); }
            #endif

            return messagePanel;
        }

        /// <summary>
        /// Show or hide the message background by enabling or disabling the image script
        /// </summary>
        /// <param name="displayMessage"></param>
        /// <param name="isShown"></param>
        private void ShowOrHideDisplayMessageBackground(DisplayMessage displayMessage, bool isShown)
        {
            if (IsInitialised || editorMode)
            {
                if (!editorMode) { displayMessage.showBackground = isShown; }
                displayMessage.CachedBgImgComponent.enabled = isShown;
            }
        }

        /// <summary>
        /// Scroll message across the display. Assumes module is initialised, message is not null,
        /// and scrollDirection is not None.
        /// </summary>
        /// <param name="displayMessage"></param>
        private void ScrollMessage(DisplayMessage displayMessage)
        {
            if (displayMessage.showMessage)
            {
                // Determine the distance to scroll
                float scrollFactor = displayMessage.scrollSpeed * Time.deltaTime * canvas.scaleFactor * 0.1f;
                float deltaX = scrollFactor * cvsResolutionFull.x;
                float deltaY = scrollFactor * cvsResolutionFull.y;

                // scrollWidth/Height should be the same as starting positions set in InitialiseMessage(..)
                // The left/right and top/bottom scroll limits can be ignored OR can consider the width/height of the message.
                // Fullscreen scrolling = +/- half width of message + half the width of canvas
                float scrollWidth = displayMessage.isScrollFullscreen ? displayMessage.displayWidth * cvsResolutionHalf.x + cvsResolutionHalf.x : displayMessage.displayWidth * cvsResolutionFull.x;

                // Fullscreen scrolling = +/- half height of message + half the height of canvas
                float scrollHeight = displayMessage.isScrollFullscreen ? displayMessage.displayHeight * cvsResolutionHalf.y + cvsResolutionHalf.y : displayMessage.displayHeight * cvsResolutionFull.y;

                // Calc the localPosition of the message
                tempMessageOffset.x = displayMessage.offsetX * cvsResolutionHalf.x;
                tempMessageOffset.y = displayMessage.offsetY * cvsResolutionHalf.y;

                if (displayMessage.scrollDirectionInt == DisplayMessage.ScrollDirectionLR)
                {
                    if (displayMessage.isScrollFullscreen) { tempMessageOffset.x = 0f; }
                    displayMessage.scrollOffsetX += deltaX;
                    displayMessage.scrollOffsetY = 0f;

                    if (displayMessage.scrollOffsetX > scrollWidth) { displayMessage.scrollOffsetX = -scrollWidth; }
                }
                else if (displayMessage.scrollDirectionInt == DisplayMessage.ScrollDirectionRL)
                {
                    if (displayMessage.isScrollFullscreen) { tempMessageOffset.x = 0f; }
                    displayMessage.scrollOffsetX -= deltaX;
                    displayMessage.scrollOffsetY = 0f;

                    if (displayMessage.scrollOffsetX < -scrollWidth) { displayMessage.scrollOffsetX = scrollWidth; }
                }
                else if (displayMessage.scrollDirectionInt == DisplayMessage.ScrollDirectionBT)
                {
                    if (displayMessage.isScrollFullscreen) { tempMessageOffset.y = 0f; }
                    displayMessage.scrollOffsetX = 0f;
                    displayMessage.scrollOffsetY += deltaY;

                    if (displayMessage.scrollOffsetY > scrollHeight) { displayMessage.scrollOffsetY = -scrollHeight; }
                }
                else // Top to Bottom
                {
                    if (displayMessage.isScrollFullscreen) { tempMessageOffset.y = 0f; }
                    displayMessage.scrollOffsetX = 0f;
                    displayMessage.scrollOffsetY -= deltaY;

                    if (displayMessage.scrollOffsetY < -scrollHeight) { displayMessage.scrollOffsetY = scrollHeight; }
                }

                Transform dmTrfm = displayMessage.CachedMessagePanel.transform;

                tempMessageOffset.x += displayMessage.scrollOffsetX;
                tempMessageOffset.y += displayMessage.scrollOffsetY;
                tempMessageOffset.z = dmTrfm.localPosition.z;
                dmTrfm.localPosition = tempMessageOffset;
            }
        }

        #endregion

        #region Private or Internal Methods - Targets

        /// <summary>
        /// [INTERNAL USE ONLY]
        /// Create a new Target RectTransform under the HUD in the hierarchy for the give slot
        /// </summary>
        /// <param name="displayTarget"></param>
        /// <param name="slotIndex"></param>
        /// <returns></returns>
        public RectTransform CreateTargetPanel(DisplayTarget displayTarget, int slotIndex)
        {
            RectTransform targetPanel = null;

            if (targetsRectTfrm == null) { targetsRectTfrm = GetTargetsPanel(); }

            if (displayTarget != null && targetsRectTfrm != null)
            {
                if (slotIndex >= 0 && slotIndex < DisplayTarget.MAX_DISPLAYTARGET_SLOTS)
                {
                    // Convert into 0.0 to 1.0 value. The width/height values assume default 1920x1080.
                    float panelWidth = displayTarget.width / refResolution.x;
                    float panelHeight = displayTarget.height / refResolution.y;

                    //if (IsInitialised && slotIndex == 0) { numDisplayTargets++; }
                    targetPanel = SSCUtils.GetOrCreateChildRectTransform(targetsRectTfrm, cvsResolutionFull, "Target_" + displayTarget.guidHash + "_" + slotIndex.ToString(), 0f, 0f,
                                                                            panelWidth, panelHeight, 0.5f, 0.5f, 0.5f, 0.5f);

                    if (targetPanel != null)
                    {
                        Image imgComponent = targetPanel.gameObject.AddComponent<Image>();
                        if (imgComponent != null)
                        {
                            imgComponent.raycastTarget = false;
                        }
                        #if UNITY_EDITOR
                        else { Debug.LogWarning("ERROR: shipDisplayModule.CreateTargetPanel() - could not add image to Target panel"); }
                        #endif

                        if ((IsInitialised || editorMode) && !displayTarget.isInitialised) { InitialiseTarget(displayTarget); }
                    }
                    #if UNITY_EDITOR
                    else { Debug.LogWarning("ERROR: shipDisplayModule.CreateTargetPanel() - could not create Target panel"); }
                    #endif
                }
                #if UNITY_EDITOR
                else { Debug.LogWarning("ERROR: shipDisplayModule.CreateTargetPanel() - could not create Target panel. Slot number is outside the range 0 to " + DisplayTarget.MAX_DISPLAYTARGET_SLOTS.ToString()); }
                #endif
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: ShipDisplayModule.CreateTargetPanel() - displayTarget is null or could not find or create " + targetsPanelName); }
            #endif

            return targetPanel;
        }

        /// <summary>
        /// [INTERNAL USE ONLY]
        /// Find the Target RectTransform under the HUD in the hierarchy.
        /// If the parent targetsRectTfrm is null, it will attempt to find or create it.
        /// </summary>
        /// <param name="guidHash"></param>
        /// <param name="slotIndex"></param>
        /// <returns></returns>
        private RectTransform GetTargetPanel(int guidHash, int slotIndex)
        {
            if (targetsRectTfrm == null) { targetsRectTfrm = GetTargetsPanel(); }
            if (targetsRectTfrm != null)
            {
                return SSCUtils.GetChildRectTransform(targetsRectTfrm.transform, "Target_" + guidHash + "_" + slotIndex, this.name);
            }
            else { return null; }
        }

        /// <summary>
        /// [INTERNAL ONLY]
        /// Used by the editor when moving DisplayTargets around in the list.
        /// </summary>
        /// <param name="displayTargetIndex"></param>
        public void RefreshDisplayTargetSlots()
        {
            numDisplayTargets = displayTargetList == null ? 0 : displayTargetList.Count;

            for (int dtIdx = 0; dtIdx < numDisplayTargets; dtIdx++)
            {
                DisplayTarget displayTarget = displayTargetList[dtIdx];

                // Initialise the Display Target slots
                displayTarget.displayTargetSlotList = new List<DisplayTargetSlot>(displayTarget.maxNumberOfTargets);

                for (int slotIdx = 0; slotIdx < displayTarget.maxNumberOfTargets; slotIdx++)
                {
                    DisplayTargetSlot displayTargetSlot = InitialiseTargetSlot(displayTarget, slotIdx);

                    displayTarget.displayTargetSlotList.Add(displayTargetSlot);
                }

                // InitialiseTargetSlot will turn all the reticles off, so update the DisplayTarget property too
                displayTarget.showTarget = false;
            }
        }

        /// <summary>
        /// Initialise the display target
        /// </summary>
        /// <param name="displayTarget"></param>
        /// <param name="hudRectTfrm"></param>
        private void InitialiseTarget(DisplayTarget displayTarget)
        {
            if (displayTarget.maxNumberOfTargets < 0) { displayTarget.maxNumberOfTargets = 1; }
            else if (displayTarget.maxNumberOfTargets > DisplayTarget.MAX_DISPLAYTARGET_SLOTS) { displayTarget.maxNumberOfTargets = DisplayTarget.MAX_DISPLAYTARGET_SLOTS; }

            // Initialise the Display Target slots
            displayTarget.displayTargetSlotList = new List<DisplayTargetSlot>(displayTarget.maxNumberOfTargets);

            for (int slotIdx = 0; slotIdx < displayTarget.maxNumberOfTargets; slotIdx++)
            {
                DisplayTargetSlot displayTargetSlot = InitialiseTargetSlot(displayTarget, slotIdx);

                displayTarget.displayTargetSlotList.Add(displayTargetSlot);
            }

            SetDisplayTargetSize(displayTarget, displayTarget.width, displayTarget.height);

            // SetDisplayTargetReticleColour does not update the baseReticleColour if the reticleColour has not changed. So do it in here instead
            displayTarget.baseReticleColour.Set(displayTarget.reticleColour.r, displayTarget.reticleColour.g, displayTarget.reticleColour.b, displayTarget.reticleColour.a, true);
            SetDisplayTargetBrightness(displayTarget);

            displayTarget.isInitialised = true;
        }

        /// <summary>
        /// Initialise a DisplayTarget Slot. This includes:
        /// 1. caching the panel and image component
        /// 2. Loading the reticle sprite
        /// 3. Setting the offset to 0,0
        /// </summary>
        /// <param name="displayTarget"></param>
        /// <param name="slotIdx"></param>
        /// <returns></returns>
        private DisplayTargetSlot InitialiseTargetSlot(DisplayTarget displayTarget, int slotIdx)
        {
            DisplayTargetSlot displayTargetSlot = new DisplayTargetSlot();

            displayTargetSlot.CachedTargetPanel = GetTargetPanel(displayTarget.guidHash, slotIdx);

            if (displayTargetSlot.CachedTargetPanel != null)
            {
                displayTargetSlot.CachedImgComponent = displayTargetSlot.CachedTargetPanel.GetComponent<Image>();

                // Add the Reticle sprite to the image for this Display Target
                if (displayTargetSlot.CachedImgComponent != null)
                {
                    // TODO - consider getting the sprite only once per DisplayTarget
                    displayTargetSlot.CachedImgComponent.sprite = GetDisplayReticleSprite(displayTarget.guidHashDisplayReticle);
                }

                SetDisplayTargetOffset(displayTargetSlot, 0f, 0f);

                // By default targets are initialised off. This is to prevent them suddenly appearing when first created.
                if (Application.isPlaying)
                {
                    displayTargetSlot.showTargetSlot = false;
                    displayTargetSlot.CachedTargetPanel.gameObject.SetActive(false);
                }
            }

            return displayTargetSlot;
        }

        /// <summary>
        /// Initialise all the DisplayTargets by caching the RectTransforms.
        /// </summary>
        private void InitialiseTargets()
        {
            int _numDisplayTargets = GetNumberDisplayTargets;
            
            if (_numDisplayTargets > 0)
            {
                if (targetsRectTfrm == null) { targetsRectTfrm = GetTargetsPanel(); }
                if (targetsRectTfrm != null)
                {
                    CheckHUDResize(false);

                    for (int dmIdx = 0; dmIdx < _numDisplayTargets; dmIdx++)
                    {
                        InitialiseTarget(displayTargetList[dmIdx]);
                    }

                    if (_numDisplayTargets > 0) { RefreshTargetsSortOrder(); }
                }
                #if UNITY_EDITOR
                else { Debug.LogWarning("ShipDisplayModule.InitialiseTargets() - could not find " + targetsPanelName); }
                #endif
            }
        }

        /// <summary>
        /// Show or Hide DisplayTarget slot on the HUD.
        /// By design, if the HUD is not shown, the Target slot will not be show.
        /// </summary>
        /// <param name="displayTargetSlot"></param>
        /// <param name="isShown"></param>
        private void ShowOrHideTargetSlot(DisplayTargetSlot displayTargetSlot, bool isShown)
        {
            if ((IsInitialised || editorMode) && displayTargetSlot != null && displayTargetSlot.CachedTargetPanel != null)
            {
                displayTargetSlot.showTargetSlot = isShown;
                displayTargetSlot.CachedTargetPanel.gameObject.SetActive(isShown);
            }
        }

        /// <summary>
        /// Show or Hide all slots of a Display Target on the HUD.
        /// By design, if the HUD is not shown, the Targets will not be show.
        /// </summary>
        /// <param name="displayTarget"></param>
        /// <param name="isShown"></param>
        private void ShowOrHideTarget(DisplayTarget displayTarget, bool isShown)
        {
            if (IsInitialised || editorMode)
            {
                displayTarget.showTarget = isShown;

                for (int sIdx = 0; sIdx < displayTarget.maxNumberOfTargets; sIdx++)
                {
                    ShowOrHideTargetSlot(displayTarget.displayTargetSlotList[sIdx], isShown);
                }
            }
        }

        /// <summary>
        /// Show or hide all targets
        /// </summary>
        /// <param name="isShown"></param>
        private void ShowOrHideTargets(bool isShown)
        {
            if (IsInitialised)
            {
                for (int dmIdx = 0; dmIdx < numDisplayTargets; dmIdx++)
                {
                    ShowOrHideTarget(displayTargetList[dmIdx], isShown);
                }
            }
        }

        /// <summary>
        /// Show or Hide all targets based on their current settings
        /// </summary>
        private void ShowOrHideTargets()
        {
            if (IsInitialised)
            {
                for (int dmIdx = 0; dmIdx < numDisplayTargets; dmIdx++)
                {
                    DisplayTarget displayTarget = displayTargetList[dmIdx];
                    ShowOrHideTarget(displayTarget, displayTarget.showTarget);
                }
            }
        }

        /// <summary>
        /// Update the position on the HUD for each of the Display Target slots
        /// that are shown.
        /// </summary>
        private void UpdateTargetPositions()
        {
            if (IsInitialised)
            {
                if (sscRadar == null) { sscRadar = SSCRadar.GetOrCreateRadar(); }

                if (sscRadar != null)
                {
                    // Loop through the display targets
                    for (int dtgtIdx = 0; dtgtIdx < numDisplayTargets; dtgtIdx++)
                    {
                        DisplayTarget displayTarget = displayTargetList[dtgtIdx];

                        if (displayTarget.showTarget)
                        {
                            // Loop through the display target slots
                            for (int slotIdx = 0; slotIdx < displayTarget.maxNumberOfTargets; slotIdx++)
                            {
                                DisplayTargetSlot displayTargetSlot = displayTarget.displayTargetSlotList[slotIdx];

                                if (displayTargetSlot.radarItemKey.radarItemIndex >= 0 && displayTargetSlot.showTargetSlot)
                                {
                                    SSCRadarItem radarItem = sscRadar.GetRadarItem(displayTargetSlot.radarItemKey);

                                    if (radarItem != null)
                                    {
                                        SetDisplayTargetPosition(displayTarget, slotIdx, radarItem.position);
                                    }
                                    //#if UNITY_EDITOR
                                    //else
                                    //{
                                    //    // This could be a timing issue
                                    //    Debug.LogWarning("ShipDisplayModule.UpdateTargetPositions - could not find radar item with index " + displayTargetSlot.radarItemKey.radarItemIndex + " for DisplayTarget " + (dtgtIdx+1).ToString("000") + " slot " + slotIdx);
                                    //}
                                    //#endif
                                }
                            }
                        }
                    }
                }
            }
        }

        #endregion

        #region Public API Methods - General

        /// <summary>
        /// Initialise the ShipDisplayModule. Either set initialiseOnStart to false and call this
        /// method in your code, or set initialiseOnStart to true in the inspector and don't call
        /// this method.
        /// </summary>
        public void Initialise()
        {
            // calling this method twice may incorrectly set the positions of the Altitude and speed panels.
            if (IsInitialised) { return; }

            canvas = GetComponent<Canvas>();
            canvasScaler = GetComponent<CanvasScaler>();

            hudRectTfrm = SSCUtils.GetChildRectTransform(transform, hudPanelName, this.name);

            if (canvas != null && hudRectTfrm != null)
            {
                canvas.sortingOrder = canvasSortOrder;

                // Get the reference resolution from the canvas scaler. If it hasn't been changed this should
                // always be 1920x1080 for SSC.
                //if (canvasScaler != null) { refResolutionFull = canvasScaler.referenceResolution; }
                //else { refResolutionFull.x = 1920f; refResolutionFull.y = 1080f; }

                // use the scaled size rather than the original reference resolution size.
                CheckHUDResize(false);

                GetHUDPanels();

                if (reticlePanel != null && overlayPanel != null && altitudeTextRectTfrm != null && airspeedTextRectTfrm != null)
                {
                    GetImgComponents();
                    GetTextComponents();

                    // Store the starting position of the Altitude text
                    altitudeInitPosition = altitudeTextRectTfrm.localPosition;
                    altitudeCurrentPosition = altitudeInitPosition;   
                    // Initialise the string used to store the Altitude value and reduce GC.
                    altitudeString = new SCSMString(6);
                    altitudeString.SetMaxIntLength(6);

                    // Store the starting position of the Air speed text
                    airspeedInitPosition = airspeedTextRectTfrm.localPosition;
                    airspeedCurrentPosition = airspeedInitPosition;
                    // Initialise the string used to store the Airspeed value and reduce GC.
                    airspeedString = new SCSMString(6);
                    airspeedString.SetMaxIntLength(6);

                    // Create an empty array of integers to use when processing
                    tempIncludeFactionList = new List<int>(20);
                    tempIncludeSquadronList = new List<int>(20);

                    InitialiseAttitude(true);
                    InitialiseHeading(true);

                    ReinitialiseVariables();

                    hudRandom = new SSCRandom();
                    // Set the seed to an arbitary prime number (but it could be anything really)
                    if (hudRandom != null) { hudRandom.SetSeed(691); }
                    flickerHistory = new Queue<float>(5);

                    #if UNITY_EDITOR
                    if (!isMainCameraAssigned) { Debug.LogWarning("ShipDisplayModule could not find a camera with the MainCamera tag set"); }
                    #endif

                    IsInitialised = displayReticleImg != null && isMainCameraAssigned;

                    RefreshHUD();

                    if (isShowHUDWithFlicker && isShowOnInitialise) { FlickerOn(flickerDefaultDuration); }
                    else  { ShowOrHideHUD(isShowOnInitialise); }
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: ShipDisplayModule.Initialise() could not find canvas HUDPanel. Did you start with the sample HUD prefab from Prefabs/Visuals folder?"); }
            #endif
        }

        /// <summary>
        /// Check to see if the ship is the same as the sourceShip currently assigned to the HUD.
        /// If either are null, the method will return false.
        /// </summary>
        /// <param name="shipControlModule"></param>
        /// <returns></returns>
        public bool IsSourceShip(ShipControlModule shipControlModule)
        {
            if (sourceShip == null || shipControlModule == null) { return false; }
            else { return shipControlModule.GetShipId == sourceShip.GetShipId; }
        }

        /// <summary>
        /// Check to see if the ship is the same as the soureShip currently assigned to the HUD.
        /// If either are null, the method will return false.
        /// </summary>
        /// <param name="ship"></param>
        /// <returns></returns>
        public bool IsSourceShip(Ship ship)
        {
            if (sourceShip == null || ship == null) { return false; }
            else { return ship.shipId == sourceShip.GetShipId; }
        }

        /// <summary>
        /// Call this if you modify any of the following at runtime.
        /// 1) displayReticleList
        /// 2) The DisplayReticlePanel size
        /// 3) Add or remove the MainCamera tag from a camera
        /// 4) displayMessageList
        /// 5) displayTargetList
        /// </summary>
        public void ReinitialiseVariables()
        {
            ValidateReticleList();
            numDisplayReticles = displayReticleList == null ? 0 : displayReticleList.Count;

            if (reticlePanel != null)
            {
                RectTransform reticleRectTransform = reticlePanel.GetComponent<RectTransform>();
                if (reticleRectTransform != null)
                {
                    reticleWidth = reticleRectTransform.sizeDelta.x;
                    reticleHeight = reticleRectTransform.sizeDelta.y;
                }
            }

            // If the camera hasn't been assigned, attempt to use the first camera with the MainCamera tag.
            if (mainCamera == null && Camera.main != null) { mainCamera = Camera.main; }

            isMainCameraAssigned = mainCamera != null;

            ValidateGaugeList();
            numDisplayGauges = displayGaugeList == null ? 0 : displayGaugeList.Count;

            ValidateMessageList();
            numDisplayMessages = displayMessageList == null ? 0 : displayMessageList.Count;

            ValidateTargetList();
            numDisplayTargets = displayTargetList == null ? 0 : displayTargetList.Count;
        }

        /// <summary>
        /// Refresh all components of the HUD. Typically called after the screen has been resized.
        /// </summary>
        public void RefreshHUD()
        {
            if (IsInitialised || editorMode)
            {
                // Set the initial position
                SetDisplayReticleOffset(displayReticleOffsetX, displayReticleOffsetY);

                SetHUDSize(displayWidth, displayHeight);
                SetHUDOffset(displayOffsetX, displayOffsetY);

                // Show or Hide initial setup
                ShowOrHideReticle(showActiveDisplayReticle);
                ShowOrHideAltitude(showAltitude);
                SetPrimaryColour(primaryColour);
                SetDisplayReticleColour(activeDisplayReticleColour);
                SetAltitudeTextColour(altitudeTextColour);
                ShowOrHideAirSpeed(showAirspeed);
                SetAirSpeedTextColour(airspeedTextColour);
                ShowOrHideOverlay(showOverlay);
                InitialiseAttitude(false);
                ShowOrHideAttitude(showAttitude);
                InitialiseHeading(false);
                ShowOrHideHeading(showHeading);
                InitialiseMessages();
                ShowOrHideMessages();
                InitialiseTargets();
                ShowOrHideTargets();
                InitialiseGauges();
                ShowOrHideGauges();

                // Set brightness after the colours have been set.
                InitialiseBrightness();
            }
        }

        /// <summary>
        /// Show the heads-up display. Has no effect if IsInitialised is false.
        /// </summary>
        public void ShowHUD()
        {
            // Prob makes sense to start in a non-flickering state.
            StopFlickering();

            if (isShowHUDWithFlicker) { FlickerOn(flickerDefaultDuration); }
            else { ShowOrHideHUD(true); }
        }

        /// <summary>
        /// Hide the heads-up display. Has no effect if IsInitialised is false.
        /// </summary>
        public void HideHUD()
        {
            // Prob makes sense to start in a non-flickering state.
            StopFlickering();

            if (isHideHUDWithFlicker) { FlickerOff(flickerDefaultDuration); }
            else { ShowOrHideHUD(false); }
        }

        /// <summary>
        /// Show the Overlay image on the heads-up display. Has no effect if IsInitialised is false.
        /// </summary>
        public void ShowOverlay()
        {
            ShowOrHideOverlay(true);
        }

        /// <summary>
        /// Hide the Overlay image on the heads-up display. Has no effect if IsInitialised is false.
        /// </summary>
        public void HideOverlay()
        {
            ShowOrHideOverlay(false);
        }

        /// <summary>
        /// Set or assign the main camera used by the heads-up display for calculations.
        /// </summary>
        /// <param name="camera"></param>
        public void SetCamera(Camera camera)
        {
            mainCamera = camera;
            isMainCameraAssigned = camera != null;
        }

        /// <summary>
        /// Set the HUD to use a particular display. Displays or monitors are numbered from 1 to 8.
        /// </summary>
        /// <param name="displayNumber">1 to 8</param>
        public void SetCanvasTargetDisplay (int displayNumber)
        {
            if (IsInitialised && SSCUtils.VerifyTargetDisplay(displayNumber, true))
            {
                Canvas _canvas = GetCanvas;

                if (_canvas != null) { _canvas.targetDisplay = displayNumber - 1; }
            }
        }

        /// <summary>
        /// Set the offset (position) of the HUD.
        /// If the module has been initialised, this will also re-position the HUD.
        /// </summary>
        /// <param name="offsetX">Horizontal offset from centre. Range between -1 and 1</param>
        /// <param name="offsetY">Vertical offset from centre. Range between -1 and 1</param>
        public void SetHUDOffset(float offsetX, float offsetY)
        {
            // Clamp the x,y values -1 to 1.
            if (offsetX < -1f) { displayOffsetX = -1f; }
            else if (offsetX > 1f) { displayOffsetX = 1f; }
            else { if (offsetX != displayOffsetX) { displayOffsetX = offsetX; } }

            if (offsetY < -1f) { displayOffsetY = -1f; }
            else if (offsetY > 1f) { displayOffsetY = 1f; }
            else { if (offsetY != displayOffsetY) { displayOffsetY = offsetY; } }

            if (IsInitialised)
            {
                // Here we use the half original (reference) HUD size as it doesn't need to be scaled in any way.
                // The parent HUD canvas is correctly scaled and sized to the actual monitor or device display.
                overlayPanel.localPosition = new Vector3(offsetX * cvsResolutionFull.x, offsetY * cvsResolutionFull.y, overlayPanel.localPosition.z);
            }
        }

        /// <summary>
        /// Set the size of the HUD overlay image and text.
        /// If the module has been initialised, this will also resize the HUD.
        /// The values are only updated if they are outside the range 0.0 to 1.0 or have changed.
        /// </summary>
        /// <param name="width"></param>
        /// <param name="height"></param>
        public void SetHUDSize(float width, float height)
        {
            // Clamp the x,y values 0.0 to 1.0
            if (width < 0f) { displayWidth = 0f; }
            else if (width > 1f) { displayWidth = 1f; }
            else if (width != displayWidth) { displayWidth = width; }

            if (height < 0f) { displayHeight = 0f; }
            else if (height > 1f) { displayHeight = 1f; }
            else if (height != displayHeight) { displayHeight = height; }

            if (IsInitialised)
            {
                // Here we use the original (reference) HUD size as it doesn't need to be scaled in any way.
                // The parent HUD canvas is correctly scaled and sized to the actual monitor or device display.
                overlayPanel.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, displayWidth * cvsResolutionFull.x);
                overlayPanel.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, displayHeight * cvsResolutionFull.y);

                // altitudeInitPosition is in 1920x1080 sizing. Convert into canvas space with cvsResolutionFull.x / refResolution.x

                // Move the altitude text relative to the resized overlay image
                altitudeCurrentPosition.x = altitudeInitPosition.x * displayWidth * cvsResolutionFull.x / refResolution.x;
                altitudeCurrentPosition.y = altitudeInitPosition.y * displayHeight * cvsResolutionFull.y / refResolution.y;
                altitudeTextRectTfrm.localPosition = altitudeCurrentPosition;

                // Move the airspeed text relative to the resized overlay image
                airspeedCurrentPosition.x = airspeedInitPosition.x * displayWidth * cvsResolutionFull.x / refResolution.x;
                airspeedCurrentPosition.y = airspeedInitPosition.y * displayHeight * cvsResolutionFull.y / refResolution.y;
                airspeedTextRectTfrm.localPosition = airspeedCurrentPosition;

                if (callbackOnSizeChange != null) { callbackOnSizeChange(displayWidth, displayHeight); }
            }
        }

        /// <summary>
        /// Set the primary colour of the heads-up display. Only update the colour if it has actually changed.
        /// If the module has been initialised, this will also re-colour the HUD with the appropriate brightness.
        /// </summary>
        /// <param name="newColour"></param>
        public void SetPrimaryColour(Color32 newColour)
        {
            SSCUtils.UpdateColour(ref newColour, ref primaryColour, ref baseOverlayColour, true);

            if (IsInitialised || editorMode)
            {
                if (primaryOverlayImg != null)
                {
                    SetOverlayBrightness();
                }
            }
        }

        /// <summary>
        /// Set the primary colour of the heads-up display. Only update the colour if it has actually changed.
        /// If the module has been initialised, this will also re-colour the HUD with the appropriate brightness.
        /// </summary>
        /// <param name="newColour"></param>
        public void SetPrimaryColour(Color newColour)
        {
            SSCUtils.UpdateColour(ref newColour, ref primaryColour, ref baseOverlayColour, true);

            if (IsInitialised || editorMode)
            {
                if (primaryOverlayImg != null)
                {
                    if (brightness == 1f) { primaryOverlayImg.color = newColour; }
                    else { primaryOverlayImg.color = baseOverlayColour.GetColorWithBrightness(brightness); }
                }
            }
        }

        /// <summary>
        /// Set the overall brightness of the HUD.
        /// </summary>
        /// <param name="newBrightness">Range 0.0 to 1.0</param>
        public void SetBrightness(float newBrightness)
        {
            // Clamp 0.0 to 1.0
            if (newBrightness < 0f) { brightness = 0f; }
            else if (newBrightness > 1f) { brightness = 1f; }

            brightness = newBrightness;

            if (IsInitialised || editorMode)
            {
                SetOverlayBrightness();
                SetReticleBrightness();
                SetAltitudeTextBrightness();
                SetAirSpeedTextBrightness();
                SetAttitudeBrightness();
                SetHeadingBrightness();

                int _numDisplayMessages = GetNumberDisplayMessages;

                // Set the brightness of all the messages
                for (int dmIdx = 0; dmIdx < _numDisplayMessages; dmIdx++)
                {
                    SetDisplayMessageBackgroundBrightness(displayMessageList[dmIdx]);
                    SetDisplayMessageTextBrightness(displayMessageList[dmIdx]);
                }

                int _numDisplayTargets = GetNumberDisplayTargets;

                // Set the brightness for all the targets
                for (int dtgtIdx = 0; dtgtIdx < _numDisplayTargets; dtgtIdx++)
                {
                    SetDisplayTargetBrightness(displayTargetList[dtgtIdx]);
                }

                int _numDisplayGauges = GetNumberDisplayGauges;

                // Set the brightness for all the gauges
                for (int dgIdx = 0; dgIdx < _numDisplayGauges; dgIdx++)
                {
                    SetDisplayGaugeForegroundBrightness(displayGaugeList[dgIdx]);
                    SetDisplayGaugeBackgroundBrightness(displayGaugeList[dgIdx]);
                    SetDisplayGaugeTextBrightness(displayGaugeList[dgIdx]);
                }
            }

            if (callbackOnBrightnessChange != null) { callbackOnBrightnessChange(brightness); }
        }

        /// <summary>
        /// Set the sort order in the scene of the HUD. Higher values appear on top.
        /// </summary>
        /// <param name="newSortOrder"></param>
        public void SetCanvasSortOrder(int newSortOrder)
        {
            canvasSortOrder = newSortOrder;

            if (IsInitialised || editorMode)
            {
                Canvas _canvas = GetCanvas;
                if (_canvas != null) { _canvas.sortingOrder = newSortOrder; }
            }
        }

        /// <summary>
        /// Turn on or off the HUD. Has no effect if not initialised. See also ShowHUD() and HideHUD()
        /// </summary>
        public void ToggleHUD()
        {
            // Prob makes sense to start in a non-flickering state.
            StopFlickering();

            IsHUDShown = !IsHUDShown;

            if (isShowHUDWithFlicker && IsHUDShown) { FlickerOn(flickerDefaultDuration); }
            else if (isHideHUDWithFlicker && !IsHUDShown) { FlickerOff(flickerDefaultDuration); }
            else { ShowOrHideHUD(IsHUDShown); }
        }

        #endregion

        #region Public API Methods - Panels

        /// <summary>
        /// Get the Attitude Panel
        /// Create it if it does not already exist
        /// </summary>
        /// <returns></returns>
        public RectTransform GetAttitudePanel()
        {
            if (hudRectTfrm == null) { hudRectTfrm = GetHUDPanel(); }

            if (hudRectTfrm != null)
            {
                //if (cvsResolutionFull.x == 0f) { cvsResolutionFull = ReferenceResolution; }

                // Centred panel (like messages)
                return SSCUtils.GetOrCreateChildRectTransform(hudRectTfrm, cvsResolutionFull, attitudePanelName, 0f, 0f, 1f, 1f, 0.5f, 0.5f, 0.5f, 0.5f);
            }
            else { return null; }
        }

        /// <summary>
        /// Get the HUD RectTransform or panel
        /// </summary>
        /// <returns></returns>
        public RectTransform GetHUDPanel()
        {
            return SSCUtils.GetChildRectTransform(transform, hudPanelName, this.name);
        }

        /// <summary>
        /// Get the Heading Panel
        /// Create it if it does not already exist
        /// </summary>
        /// <returns></returns>
        public RectTransform GetHeadingPanel()
        {
            if (hudRectTfrm == null) { hudRectTfrm = GetHUDPanel(); }

            if (hudRectTfrm != null)
            {
                // Stretched panel
                //return SSCUtils.GetOrCreateChildRectTransform(hudRectTfrm, cvsResolutionFull, headingPanelName, 0f, 0f, 1f, 1f, 0f, 0f, 1f, 1f);
                // Centred panel (like messages)
                return SSCUtils.GetOrCreateChildRectTransform(hudRectTfrm, cvsResolutionFull, headingPanelName, 0f, 0f, 1f, 1f, 0.5f, 0.5f, 0.5f, 0.5f);
            }
            else { return null; }
        }

        /// <summary>
        /// Get the parent panel for the Display Targets.
        /// Create it if it does not already exist
        /// </summary>
        /// <returns></returns>
        public RectTransform GetTargetsPanel()
        {
            if (hudRectTfrm == null) { hudRectTfrm = GetHUDPanel(); }

            if (hudRectTfrm != null)
            {
                // Stretched panel
                return SSCUtils.GetOrCreateChildRectTransform(hudRectTfrm, cvsResolutionFull, targetsPanelName, 0f, 0f, 1f, 1f, 0f, 0f, 1f, 1f);
            }
            else { return null; }
        }

        /// <summary>
        /// Get the parent panel for the Display Gauges.
        /// Create it if it does not already exist
        /// </summary>
        /// <returns></returns>
        public RectTransform GetGaugesPanel()
        {
            if (hudRectTfrm == null) { hudRectTfrm = GetHUDPanel(); }

            if (hudRectTfrm != null)
            {
                // Stretched panel
                return SSCUtils.GetOrCreateChildRectTransform(hudRectTfrm, cvsResolutionFull, gaugesPanelName, 0f, 0f, 1f, 1f, 0f, 0f, 1f, 1f);
            }
            else { return null; }
        }

        #endregion

        #region Public API Methods - Cursor

        /// <summary>
        /// Show the hardware (mouse) cursor.
        /// This also restarts the countdown auto-hide
        /// timer if that is enabled.
        /// </summary>
        public void ShowCursor()
        {
            ShowOrHideCursor(true);
        }

        /// <summary>
        /// Hide the hardware (mouse) cursor.
        /// NOTE: This will sometimes fail to turn off the cursor in the editor
        /// Game View when it doesn't have focus, but will work fine in a build.
        /// </summary>
        public void HideCursor()
        {
            ShowOrHideCursor(false);
        }

        /// <summary>
        /// Centre the hardware (mouse) cursor in the centre of the screen.
        /// WARNING: This will wait until the next frame before it returns.
        /// </summary>
        public void CentreCursor()
        {
            Cursor.lockState = CursorLockMode.Locked;
            StartCoroutine(UnlockCursor());
        }

        /// <summary>
        /// Toggle the hardware (mouse) cursor on or off.
        /// NOTE: This will sometimes fail to turn off the cursor in the editor
        /// Game View when it doesn't have focus, but will work fine in a build.
        /// </summary>
        public void ToggleCursor()
        {
            ShowOrHideCursor(!Cursor.visible);
        }

        #endregion

        #region Public API Methods - Display Reticle

        /// <summary>
        /// Returns the guidHash of the Reticle in the list given the index or
        /// zero-based position in the list. Will return 0 if no matching Reticle
        /// is found.
        /// Will return 0 if the module hasn't been initialised.
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        public int GetDisplayReticleGuidHash(int index)
        {
            int guidHash = 0;

            if (IsInitialised || editorMode)
            {
                if (index >= 0 && index < GetNumberDisplayReticles)
                {
                    if (displayReticleList[index] != null) { guidHash = displayReticleList[index].guidHash; }
                }
            }

            return guidHash;
        }

        /// <summary>
        /// Returns the guidHash of the Reticle in the list given the name of the sprite.
        /// Will return 0 if no matching Reticle is found.
        /// WARNING: This will increase GC. Use GetDisplayReticleGuidHash(int index) where possible.
        /// </summary>
        /// <param name="spriteName"></param>
        /// <returns></returns>
        public int GetDisplayReticleGuidHash(string spriteName)
        {
            int guidHash = 0;
            DisplayReticle _tempDR = null;

            if ((IsInitialised || editorMode) && !string.IsNullOrEmpty(spriteName))
            {
                int _numDisplayReticles = GetNumberDisplayReticles;
                string _spriteNameLowerCase = spriteName.ToLower();

                for (int drIdx = 0; drIdx < _numDisplayReticles; drIdx++)
                {
                    _tempDR = displayReticleList[drIdx];
                    if (_tempDR != null && _tempDR.primarySprite != null && _tempDR.primarySprite.name.ToLower() == _spriteNameLowerCase)
                    {
                        guidHash = _tempDR.guidHash;
                        break;
                    }
                }
            }

            return guidHash;
        }

        /// <summary>
        /// Get a DisplayReticle given its guidHash.
        /// See also GetDisplayReticleGuidHash(..).
        /// Will return null if guidHash parameter is 0, it cannot be found
        /// or the module has not been initialised.
        /// </summary>
        /// <param name="guidHash"></param>
        /// <returns></returns>
        public DisplayReticle GetDisplayReticle(int guidHash)
        {
            DisplayReticle displayReticle = null;
            DisplayReticle _tempDR = null;

            if (IsInitialised || editorMode)
            {
                int _numDisplayReticles = GetNumberDisplayReticles;

                for (int drIdx = 0; drIdx < _numDisplayReticles; drIdx++)
                {
                    _tempDR = displayReticleList[drIdx];
                    if (_tempDR != null && _tempDR.guidHash == guidHash)
                    {
                        displayReticle = _tempDR;
                        break;
                    }
                }
            }

            return displayReticle;
        }

        /// <summary>
        /// Get the UI sprite (image) for a DisplayReticle.
        /// See also GetDisplayReticleGuidHash(..).
        /// </summary>
        /// <param name="guidHash"></param>
        /// <returns></returns>
        public Sprite GetDisplayReticleSprite(int guidHash)
        {
            Sprite sprite = null;
            DisplayReticle _tempDR = null;

            if (IsInitialised || editorMode)
            {
                int _numDisplayReticles = GetNumberDisplayReticles;

                for (int drIdx = 0; drIdx < _numDisplayReticles; drIdx++)
                {
                    _tempDR = displayReticleList[drIdx];
                    if (_tempDR != null && _tempDR.guidHash == guidHash)
                    {
                        sprite = _tempDR.primarySprite;
                        break;
                    }
                }
            }

            return sprite;
        }

        /// <summary>
        /// Change the DiplayReticle sprite on the UI panel.
        /// See also GetDisplayReticleGuidHash(..).
        /// NOTE: The module must be initialised.
        /// </summary>
        /// <param name="guidHash"></param>
        public void ChangeDisplayReticle(int guidHash)
        {
            if (IsInitialised || editorMode)
            {
                guidHashActiveDisplayReticle = guidHash;
                currentDisplayReticle = GetDisplayReticle(guidHash);

                LoadDisplayReticleSprite(currentDisplayReticle);
            }
        }

        /// <summary>
        /// Hide or turn off the Display Reticle
        /// </summary>
        public void HideDisplayReticle()
        {
            ShowOrHideReticle(false);
        }

        /// <summary>
        /// Set the offset (position) of the Display Reticle on the HUD.
        /// If the module has been initialised, this will also re-position the Display Reticle.
        /// Same as SetDisplayReticleOffset(offset.x, offset.y)
        /// </summary>
        /// <param name="offset">Horizontal and Vertical offset from centre. Values should be between -1 and 1</param>
        public void SetDisplayReticleOffset(Vector2 offset)
        {
            SetDisplayReticleOffset(offset.x, offset.y);
        }

        /// <summary>
        /// Set the offset (position) of the Display Reticle on the HUD.
        /// If the module has been initialised, this will also re-position the Display Reticle.
        /// </summary>
        /// <param name="offsetX">Horizontal offset from centre. Range between -1 and 1</param>
        /// <param name="offsetY">Vertical offset from centre. Range between -1 and 1</param>
        public void SetDisplayReticleOffset(float offsetX, float offsetY)
        {
            // Verify the x,y values are within range -1 to 1.
            if (offsetX >= -1f && offsetX <= 1f)
            {
                displayReticleOffsetX = offsetX;
            }

            if (offsetY >= -1f && offsetY <= 1f)
            {
                displayReticleOffsetY = offsetY;
            }

            if (IsInitialised || editorMode)
            {
                Vector2 halfHUDSize = GetHUDHalfSize(true);

                if (lockDisplayReticleToCursor)
                {
                    // Centre on mouse pointer more accurately (reticle can go half outside screen dimensions on edges)
                    displayReticleImg.transform.localPosition = new Vector3(offsetX * halfHUDSize.x, offsetY * halfHUDSize.y, displayReticleImg.transform.localPosition.z);
                }
                else
                {
                    // Original default behaviour - reticle remains within screen dimensions.
                    displayReticleImg.transform.localPosition = new Vector3(offsetX * (halfHUDSize.x - (reticleWidth / 2f)), offsetY * (halfHUDSize.y - (reticleHeight / 2f)), displayReticleImg.transform.localPosition.z);
                }
            }
        }

        /// <summary>
        /// Set the active Display Reticle colour. Only update the colour if it has actually changed.
        /// If the module has been initialised, this will also re-colour the reticle with the appropriate brightness.
        /// </summary>
        /// <param name="newColour"></param>
        public void SetDisplayReticleColour(Color32 newColour)
        {
            SSCUtils.UpdateColour(ref newColour, ref activeDisplayReticleColour, ref baseReticleColour, true);

            if (IsInitialised)
            {
                if (displayReticleImg != null)
                {
                    SetReticleBrightness();
                }
            }
        }

        /// <summary>
        /// Set the active Display Reticle colour. Only update the colour if it has actually changed.
        /// If the module has been initialised, this will also re-colour the reticle with the appropriate brightness.
        /// </summary>
        /// <param name="newColour"></param>
        public void SetDisplayReticleColour(Color newColour)
        {
            SSCUtils.UpdateColour(ref newColour, ref activeDisplayReticleColour, ref baseReticleColour, true);

            if (IsInitialised || editorMode)
            {
                if (displayReticleImg != null)
                {
                    if (brightness == 1f) { displayReticleImg.color = newColour; }
                    else { displayReticleImg.color = baseReticleColour.GetColorWithBrightness(brightness); }
                }
            }
        }

        /// <summary>
        /// Show the Display Reticle on the HUD. The HUD will automatically be shown if it is not already visible.
        /// </summary>
        public void ShowDisplayReticle()
        {
            ShowOrHideReticle(true);
        }

        /// <summary>
        /// Show or Hide the Display Reticle on the HUD. The HUD will automatically be shown if required.
        /// </summary>
        public void ToggleDisplayReticle()
        {
            ShowOrHideReticle(!showActiveDisplayReticle);
        }

        /// <summary>
        /// Create a new list if required
        /// </summary>
        public void ValidateReticleList()
        {
            if (displayReticleList == null) { displayReticleList = new List<DisplayReticle>(1); }
        }

        #endregion

        #region Public API Methods - Altitude

        /// <summary>
        /// Attempt to show the Altitude indicator. Turn on HUD if required.
        /// </summary>
        public void ShowAltitude()
        {
            ShowOrHideAltitude(true);
        }

        /// <summary>
        /// Attempt to turn off the Altitude indicator
        /// </summary>
        public void HideAltitude()
        {
            ShowOrHideAltitude(false);
        }

        /// <summary>
        /// Set the altitude text colour on the heads-up display. Only update the colour if it has actually changed.
        /// If the module has been initialised, this will also re-colour the HUD with the appropriate brightness.
        /// </summary>
        /// <param name="newColour"></param>
        public void SetAltitudeTextColour(Color32 newColour)
        {
            SSCUtils.UpdateColour(ref newColour, ref altitudeTextColour, ref baseAltitudeTextColour, true);

            if (IsInitialised || editorMode)
            {
                if (altitudeText != null)
                {
                    SetAltitudeTextBrightness();
                }
            }
        }

        /// <summary>
        /// Set the altitude text colour on the heads-up display. Only update the colour if it has actually changed.
        /// If the module has been initialised, this will also re-colour the HUD with the appropriate brightness.
        /// </summary>
        /// <param name="newColour"></param>
        public void SetAltitudeTextColour(Color newColour)
        {
            SSCUtils.UpdateColour(ref newColour, ref altitudeTextColour, ref baseAltitudeTextColour, true);

            if (IsInitialised || editorMode)
            {
                if (altitudeText != null)
                {
                    if (brightness == 1f) { altitudeText.color = newColour; }
                    else { altitudeText.color = baseAltitudeTextColour.GetColorWithBrightness(brightness); }
                }
            }
        }

        #endregion

        #region Public API Methods - Air Speed

        /// <summary>
        /// Attempt to show the Air speed indicator. Turn on HUD if required.
        /// </summary>
        public void ShowAirSpeed()
        {
            ShowOrHideAirSpeed(true);
        }

        /// <summary>
        /// Attempt to turn off the AirSpeed indicator
        /// </summary>
        public void HideAirSpeed()
        {
            ShowOrHideAirSpeed(false);
        }

        /// <summary>
        /// Set the airspeed text colour on the heads-up display. Only update the colour if it has actually changed.
        /// If the module has been initialised, this will also re-colour the HUD with the appropriate brightness.
        /// </summary>
        /// <param name="newColour"></param>
        public void SetAirSpeedTextColour(Color32 newColour)
        {
            SSCUtils.UpdateColour(ref newColour, ref airspeedTextColour, ref baseAirspeedTextColour, true);

            if (IsInitialised || editorMode)
            {
                if (airspeedText != null)
                {
                    SetAirSpeedTextBrightness();
                }
            }
        }

        /// <summary>
        /// Set the airspeed text colour on the heads-up display. Only update the colour if it has actually changed.
        /// If the module has been initialised, this will also re-colour the HUD with the appropriate brightness.
        /// </summary>
        /// <param name="newColour"></param>
        public void SetAirSpeedTextColour(Color newColour)
        {
            SSCUtils.UpdateColour(ref newColour, ref airspeedTextColour, ref baseAirspeedTextColour, true);

            if (IsInitialised || editorMode)
            {
                if (airspeedText != null)
                {
                    if (brightness == 1f) { airspeedText.color = newColour; }
                    else { airspeedText.color = baseAirspeedTextColour.GetColorWithBrightness(brightness); }
                }
            }
        }

        #endregion

        #region Public API Methods - Attitude

        /// <summary>
        /// Attempt to turn off the Attitude display
        /// </summary>
        public void HideAttitude()
        {
            ShowOrHideAttitude(false);
        }

        /// <summary>
        /// Set the sprite that will mask the Display Attitude scrollable sprite
        /// </summary>
        /// <param name="newSprite"></param>
        public void SetDisplayAttitudeMaskSprite (Sprite newSprite)
        {
            attitudeMaskSprite = newSprite;

            if (attitudeMaskImg != null)
            {
                attitudeMaskImg.sprite = attitudeMaskSprite;
            }
        }

        /// <summary>
        /// This is called when the HUD is resized or some elements of the Attitude control are modified
        /// </summary>
        public void RefreshAttitudeAfterResize()
        {
            // Recalculate the pixels per degree in the pitch ladder
            if (attitudeScrollImg != null && attitudeScrollImg.mainTexture != null)
            {
                // The attitude scroll img should be -90 to 90 deg. Ideally it should have a "border"
                // at the top and bottom of blank (transparent) space to prevent +-90 being right
                // at the top/bottom of the HUD.
                // The image is autoscaled to the full height of the default HUD screen size.

                float imgHeight = attitudeScrollImg.mainTexture.height;

                // If the image
                if (imgHeight < 10f) { imgHeight = 1024f; }

                attitudePixelsPerDegree = cvsResolutionFull.y * ((imgHeight - attitudeScrollSpriteBorderWidth * 2f) / imgHeight) / 180f;
            }
            #if UNITY_EDITOR
            else
            {
                Debug.LogWarning("RefreshAttitudeAfterResize - Attitude scroll sprite has no UI texture");
            }
            #endif
        }

        /// <summary>
        /// Set the Display Attitude primary colour. Only update the colour if it has actually changed.
        /// If the module has been initialised, this will also re-colour the attitude with the appropriate brightness.
        /// </summary>
        /// <param name="newColour"></param>
        public void SetDisplayAttitudePrimaryColour (Color newColour)
        {
            SSCUtils.UpdateColour(ref newColour, ref attitudePrimaryColour, ref baseAttitudePrimaryColour, true);

            SetAttitudeBrightness();
        }

        /// <summary>
        /// Set the Display Attitude scroll sprite.
        /// </summary>
        /// <param name="newSprite"></param>
        public void SetDisplayAttitudeScrollSprite (Sprite newSprite)
        {
            attitudeScrollSprite = newSprite;

            if (attitudeScrollImg != null)
            {
                attitudeScrollImg.sprite = attitudeScrollSprite;

                // Recalculate the pixels per degree for scrolling.
                RefreshAttitudeAfterResize();
            }
        }

        /// <summary>
        /// Set the normalised size of the scrollable Display Attitude.
        /// Currently width is always 1.0
        /// </summary>
        /// <param name="width"></param>
        /// <param name="height"></param>
        public void SetDisplayAttitudeSize (float width, float height)
        {
            // Verify the width,height values are within range 0.1 to 1.
            if (height >= 0.1f && height <= 1f)
            {
                attitudeHeight = height;
            }

            attitudeWidth = 1f;

            if (IsInitialised || editorMode)
            {
                #if UNITY_EDITOR
                if (editorMode)
                {
                    CheckHUDResize(false);
                    UnityEditor.Undo.RecordObject(attitudeRectTfrm.transform, string.Empty);
                }
                #endif

                // Here we use the original (reference) HUD size as it doesn't need to be scaled in any way.
                // The parent HUD canvas is correctly scaled and sized to the actual monitor or device display.
                attitudeRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, attitudeWidth * cvsResolutionFull.x);
                attitudeRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, attitudeHeight * cvsResolutionFull.y);
            }
        }

        /// <summary>
        /// Set the normalised offset (position) of the Display Attitude on the HUD.
        /// If the module has been initialised, this will also re-position the Display Attitude.
        /// </summary>
        /// <param name="offsetX">Horizontal offset from centre. Range between -1 and 1</param>
        /// <param name="offsetY">Vertical offset from centre. Range between -1 and 1</param>
        public void SetDisplayAttitudeOffset (float offsetX, float offsetY)
        {
            // Verify the x,y values are within range -1 to 1.
            if (offsetX >= -1f && offsetX <= 1f)
            {
                attitudeOffsetX = offsetX;
            }

            if (offsetY >= -1f && offsetY <= 1f)
            {
                attitudeOffsetY = offsetY;
            }

            if (IsInitialised || editorMode)
            {
                Transform attTrfm = attitudeRectTfrm.transform;

                #if UNITY_EDITOR
                if (editorMode)
                {
                    CheckHUDResize(false);
                    UnityEditor.Undo.RecordObject(attTrfm, string.Empty);
                }
                #endif

                // Use the scaled HUD size. This works in and out of play mode
                tempAttitudeOffset.x = attitudeOffsetX * cvsResolutionHalf.x;
                tempAttitudeOffset.y = attitudeOffsetY * cvsResolutionHalf.y;
                tempAttitudeOffset.z = attTrfm.localPosition.z;

                attTrfm.localPosition = tempAttitudeOffset;

                // Reset the position of the scrollable image
                if (attitudeScrollPanel != null)
                {
                    attitudeScrollPanel.localPosition = Vector3.zero;
                    attitudeInitPosition = attitudeScrollPanel.transform.position;

                    //if (showAttitudeIndicator && attitudeIndicatorPanel != null)
                    //{
                    //    attitudeIndicatorPanel.localPosition = new Vector3(attitudeScrollPanel.localPosition.x, attitudeScrollPanel.localPosition.y - 20f, attitudeScrollPanel.localPosition.z);
                    //}
                }
            }
        }

        /// <summary>
        /// Set the border width of the Scroll Sprite (texture) in pixels
        /// </summary>
        /// <param name="newBorderWidth"></param>
        public void SetDisplayAttitudeSpriteBorderWidth (float newBorderWidth)
        {
            if (newBorderWidth >= 0f)
            {
                attitudeScrollSpriteBorderWidth = newBorderWidth;

                RefreshAttitudeAfterResize();
            }
        }

        /// <summary>
        /// Attempt to show the scrollable Attitude. Turn on HUD if required.
        /// </summary>
        public void ShowAttitude()
        {
            ShowOrHideAttitude(true);
        }

        #endregion

        #region Public API Methods - Flickering

        /// <summary>
        /// Flicker the HUD on/off. Override the Flicker Default Duration.
        /// Show the HUD when flickering finishes.
        /// </summary>
        /// <param name="duration"></param>
        public void FlickerOn (float duration)
        {
            if (duration > 0f)
            {
                flickerDuration = duration;
                flickerDurationTimer = 0f;
                isFlickerWaiting = false;

                isFlickeringEndStateOn = true;
                // If the HUD is flickering to the Shown state, it cannot also be flickering off.
                isFlickeringEndStateOff = false;

                if (flickerHistory != null) { flickerHistory.Clear(); }

                // Remember the current overall HUD brightness setting
                flickerStartIntensity = brightness;

                if (!flickerVariableIntensity) { flickerIntensity = flickerStartIntensity; }
            }
        }

        /// <summary>
        /// Flicker the HUD on/off. Override the Flicker Default Duration.
        /// Hide the HUD when flickering finishers.
        /// </summary>
        /// <param name="duration"></param>
        public void FlickerOff (float duration)
        {
            if (duration > 0f)
            {
                flickerDuration = duration;
                flickerDurationTimer = 0f;
                isFlickerWaiting = false;

                isFlickeringEndStateOff = true;
                // If the HUD is flickering to the Hidden state, it cannot also be flickering on.
                isFlickeringEndStateOn = false;

                if (flickerHistory != null) { flickerHistory.Clear(); }

                // Remember the current overall HUD brightness setting
                flickerStartIntensity = brightness;

                if (!flickerVariableIntensity) { flickerIntensity = flickerStartIntensity; }
            }
        }


        public void StopFlickering()
        {
            isFlickeringEndStateOn = false;
            isFlickeringEndStateOff = false;
        }

        #endregion

        #region Public API Methods - Gauges

        /// <summary>
        /// Add a new gauge to the HUD. By design, they are not visible at runtime when first added.
        /// </summary>
        /// <param name="gaugeName"></param>
        /// <param name="gaugeText"></param>
        /// <returns></returns>
        public DisplayGauge AddGauge(string gaugeName, string gaugeText)
        {
            DisplayGauge displayGauge = null;

            if (GetHUDPanel() == null)
            {
                #if UNITY_EDITOR
                Debug.LogWarning("ERROR: ShipDisplayModule.AddGauge() - could not find HUDPanel for the HUD. Did you use the prefab from Prefabs/Visuals folder?");
                #endif
            }
            else
            {
                displayGauge = new DisplayGauge();
                if (displayGauge != null)
                {
                    displayGauge.gaugeName = gaugeName;
                    displayGauge.gaugeString = gaugeText;
                    ValidateGaugeList();
                    displayGaugeList.Add(displayGauge);

                    numDisplayGauges = displayGaugeList.Count;

                    RectTransform gaugePanel = CreateGaugePanel(displayGauge);
                    if (gaugePanel != null)
                    {
                        #if UNITY_EDITOR
                        // Make sure we can rollback the creation of the new gauge panel for this slot
                        UnityEditor.Undo.RegisterCreatedObjectUndo(gaugePanel.gameObject, string.Empty);
                        #endif
                    }
                 }
            }

            return displayGauge;
        }

        /// <summary>
        /// Add a gauge to the HUD using a displayGauge instance. Typically, this is used
        /// with CopyDisplayGauge(..).
        /// </summary>
        /// <param name="displayGauge"></param>
        public void AddGauge(DisplayGauge displayGauge)
        {
            if (displayGauge != null)
            {
                if (GetHUDPanel() == null)
                {
                    #if UNITY_EDITOR
                    Debug.LogWarning("ERROR: ShipDisplayModule.AddGauge() - could not find HUDPanel for the HUD. Did you use the prefab from Prefabs/Visuals folder?");
                    #endif
                }
                else
                {
                    ValidateGaugeList();
                    displayGaugeList.Add(displayGauge);

                    numDisplayGauges = displayGaugeList.Count;

                    RectTransform gaugePanel = CreateGaugePanel(displayGauge);
                    if (gaugePanel != null)
                    {
                        #if UNITY_EDITOR
                        // Make sure we can rollback the creation of the new gauge panel for this slot
                        UnityEditor.Undo.RegisterCreatedObjectUndo(gaugePanel.gameObject, string.Empty);
                        #endif
                    }
                }
            }
        }

        /// <summary>
        /// Delete a gauge from the HUD.
        /// NOTE: It is much cheaper to HideDisplayGauge(..) than completely remove it.
        /// </summary>
        /// <param name="guidHash"></param>
        public void DeleteGauge(int guidHash)
        {
            #if UNITY_EDITOR
            bool isRecordUndo = !Application.isPlaying;
            int undoGroup=0;
            #else
            bool isRecordUndo = false;
            #endif

            RectTransform gaugeRectTfrm = GetGaugePanel(guidHash);
            int _dgIndex = GetDisplayGaugeIndex(guidHash);

            // Only record undo if in the editor AND is not playing AND something needs to be recorded
            isRecordUndo = isRecordUndo && (gaugeRectTfrm != null || _dgIndex >= 0);

            #if UNITY_EDITOR
            if (isRecordUndo)
            {
                UnityEditor.Undo.SetCurrentGroupName("Delete Display Gauge " + (_dgIndex + 1).ToString());
                undoGroup = UnityEditor.Undo.GetCurrentGroup();

                if (_dgIndex >= 0)
                {
                    UnityEditor.Undo.RecordObject(this, string.Empty);
                }
            }
            #endif

            // Check that the gauge exists and it is within the list constraints
            // NOTE: The script element must be modified BEFORE the Undo.DestroyObjectImmediate is called
            // in order for Undo to correctly undo both the gauge change AND the destroy of the panel.
            if (_dgIndex >= 0 && _dgIndex < (displayGaugeList == null ? 0 : displayGaugeList.Count))
            {
                displayGaugeList.RemoveAt(_dgIndex);
                numDisplayGauges = displayGaugeList.Count;
            }

            if (gaugeRectTfrm != null)
            {
                if (Application.isPlaying) { Destroy(gaugeRectTfrm.gameObject); }
                else
                {
                    #if UNITY_EDITOR
                    UnityEditor.Undo.DestroyObjectImmediate(gaugeRectTfrm.gameObject);
                    #else
                    DestroyImmediate(gaugeRectTfrm.gameObject);
                    #endif
                }
            }

            #if UNITY_EDITOR
            if (isRecordUndo)
            {               
                UnityEditor.Undo.CollapseUndoOperations(undoGroup);
            }
            #endif
        }

        /// <summary>
        /// Create a copy of an existing DisplayGauge, and give it a new name.
        /// Call AddGauge(newDisplayGauge) to make it useable in the game.
        /// </summary>
        /// <param name="displayGauge"></param>
        /// <param name="NameOfCopy"></param>
        /// <returns></returns>
        public DisplayGauge CopyDisplayGauge(DisplayGauge displayGauge, string NameOfCopy)
        {
            DisplayGauge dgCopy = new DisplayGauge(displayGauge);

            if (dgCopy != null)
            {
                // make it unique
                dgCopy.guidHash = SSCMath.GetHashCodeFromGuid();
                dgCopy.gaugeName = NameOfCopy;
            }
            return dgCopy;
        }

        /// <summary>
        /// Returns the guidHash of the Gauge in the list given the index or zero-based position in the list.
        /// Will return 0 if no matching Gauge is found.
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        public int GetDisplayGaugeGuidHash(int index)
        {
            int guidHash = 0;

            if (index >= 0 && index < GetNumberDisplayGauges)
            {
                if (displayGaugeList[index] != null) { guidHash = displayGaugeList[index].guidHash; }
            }

            return guidHash;
        }

        /// <summary>
        /// Get the zero-based index of the Gauge in the list. Will return -1 if not found.
        /// </summary>
        /// <param name="guidHash"></param>
        /// <returns></returns>
        public int GetDisplayGaugeIndex(int guidHash)
        {
            int index = -1;

            int _numDisplayGauges = GetNumberDisplayGauges;

            for (int dgIdx = 0; dgIdx < _numDisplayGauges; dgIdx++)
            {
                if (displayGaugeList[dgIdx] != null && displayGaugeList[dgIdx].guidHash == guidHash) { index = dgIdx; break; }
            }

            return index;
        }

        /// <summary>
        /// Get a DisplayGauge given its guidHash.
        /// See also GetDisplayGaugeGuidHash(..).
        /// Will return null if guidHash parameter is 0, it cannot be found.
        /// </summary>
        /// <param name="guidHash"></param>
        /// <returns></returns>
        public DisplayGauge GetDisplayGauge(int guidHash)
        {
            DisplayGauge displayGauge = null;
            DisplayGauge _tempDG = null;

            int _numDisplayGauges = GetNumberDisplayGauges;

            for (int dgIdx = 0; dgIdx < _numDisplayGauges; dgIdx++)
            {
                _tempDG = displayGaugeList[dgIdx];
                if (_tempDG != null && _tempDG.guidHash == guidHash)
                {
                    displayGauge = _tempDG;
                    break;
                }
            }

            return displayGauge;
        }

        /// <summary>
        /// Get the display gauge give the description title of the gauge.
        /// WARNING: This will increase Garbage Collection (GC). Where possible
        /// use GetDisplayGauge(guidHash) and/or GetDisplayGaugeGuidHash(index)
        /// </summary>
        /// <param name="displayGaugeName"></param>
        /// <returns></returns>
        public DisplayGauge GetDisplayGauge(string displayGaugeName)
        {
            DisplayGauge displayGauge = null;
            DisplayGauge _tempDG = null;

            if (!string.IsNullOrEmpty(displayGaugeName))
            {
                int _numDisplayGauges = GetNumberDisplayGauges;

                for (int dgIdx = 0; dgIdx < _numDisplayGauges; dgIdx++)
                {
                    _tempDG = displayGaugeList[dgIdx];
                    if (_tempDG != null && !string.IsNullOrEmpty(_tempDG.gaugeName))
                    {
                        if (_tempDG.gaugeName.ToLower() == displayGaugeName.ToLower())
                        {
                            displayGauge = _tempDG;
                            break;
                        }
                    }
                }
            }
            return displayGauge;
        }

        /// <summary>
        /// Hide or turn off the Display Gauge
        /// </summary>
        /// <param name="guidHash"></param>
        public void HideDisplayGauge(int guidHash)
        {
            DisplayGauge displayGauge = GetDisplayGauge(guidHash);
            if (displayGauge != null) { ShowOrHideGauge(displayGauge, false); }
            #if UNITY_EDITOR
            else { Debug.LogWarning("WARNING: ShowDisplayGauge - Could not find gauge with guidHash: " + guidHash); }
            #endif
        }

        /// <summary>
        /// Hide or turn off the Display Gauge
        /// </summary>
        /// <param name="displayGauge"></param>
        public void HideDisplayGauge(DisplayGauge displayGauge)
        {
            if (displayGauge != null) { ShowOrHideGauge(displayGauge, false); }
            #if UNITY_EDITOR
            else { Debug.LogWarning("WARNING: ShowDisplayGauge - Could not find gauge - parameter was null"); }
            #endif
        }

        /// <summary>
        /// Hide or turn off all Display Gauges. ShipDisplayModule must be initialised.
        /// </summary>
        public void HideDisplayGauges()
        {
            ShowOrHideGauges(false);
        }

        /// <summary>
        /// After adding or moving DisplayGauges, they may need to be sorted to
        /// have the correct z-order in on the HUD.
        /// </summary>
        public void RefreshGaugesSortOrder()
        {
            if (gaugesRectTfrm != null)
            {
                // Gauges should begin at index 0.
                int zIndex = -1;

                // Update number of DisplayGauges. This can help issues in the Editor, like moving DislayGauges
                // up or down in the list while in play mode.
                numDisplayGauges = displayGaugeList == null ? 0 : displayGaugeList.Count;

                for (int dtIdx = 0; dtIdx < numDisplayGauges; dtIdx++)
                {
                    DisplayGauge displayGauge = displayGaugeList[dtIdx];

                    if (displayGauge != null)
                    {
                        RectTransform _rt = displayGauge.CachedGaugePanel;

                        if (_rt != null)
                        {
                            _rt.SetSiblingIndex(++zIndex);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Import a json file from disk and return as DisplayGauge
        /// </summary>
        /// <param name="folderPath"></param>
        /// <param name="fileName"></param>
        /// <returns></returns>
        public DisplayGauge ImportGaugeFromJson (string folderPath, string fileName)
        {
            DisplayGauge displayGauge = null;

            if (!string.IsNullOrEmpty(folderPath) && !string.IsNullOrEmpty(fileName))
            {
                try
                {
                    string filePath = System.IO.Path.Combine(folderPath, fileName);
                    if (System.IO.File.Exists(filePath))
                    {
                        string jsonText = System.IO.File.ReadAllText(filePath);

                        displayGauge = new DisplayGauge();
                        int displayGaugeGuidHash = displayGauge.guidHash;

                        JsonUtility.FromJsonOverwrite(jsonText, displayGauge);

                        if (displayGauge != null)
                        {
                            // make hash code unique
                            displayGauge.guidHash = displayGaugeGuidHash;
                        }
                    }
                    #if UNITY_EDITOR
                    else
                    {
                        Debug.LogWarning("ERROR: Import Gauge. Could not find file at " + filePath);
                    }
                    #endif
                }
                catch (System.Exception ex)
                {
                    #if UNITY_EDITOR
                    Debug.LogWarning("ERROR: ShipDisplayModule - could not import gauge from: " + folderPath + " PLEASE REPORT - " + ex.Message);
                    #else
                    // Keep compiler happy
                    if (ex != null) { }
                    #endif
                }
            }

            return displayGauge;
        }

        /// <summary>
        /// Save the Gauge to a json file on disk.
        /// </summary>
        /// <param name="displayGauge"></param>
        /// <param name="filePath"></param>
        public bool SaveGaugeAsJson (DisplayGauge displayGauge, string filePath)
        {
            bool isSuccessful = false;

            if (displayGauge != null && !string.IsNullOrEmpty(filePath))
            {
                try
                {
                    string jsonGaugeData = JsonUtility.ToJson(displayGauge);

                    if (!string.IsNullOrEmpty(jsonGaugeData) && !string.IsNullOrEmpty(filePath))
                    {
                        System.IO.File.WriteAllText(filePath, jsonGaugeData);
                        isSuccessful = true;
                    }
                }
                catch (System.Exception ex)
                {
                    #if UNITY_EDITOR
                    Debug.LogWarning("ERROR: ShipDisplayModule SaveGaugeAsJson - could not export: " + displayGauge.gaugeName + " PLEASE REPORT - " + ex.Message);
                    #else
                    // Keep compiler happy
                    if (ex != null) { }
                    #endif
                }
            }

            return isSuccessful;
        }

        /// <summary>
        /// The foreground colour of the gauge will be determined by the gauge value and the low, medium and high colours.
        /// LowColour = value of 0, MediumColour when value is 0.5, and HighColour when value is 1.0
        /// </summary>
        /// <param name="displayGauge"></param>
        /// <param name="lowColour"></param>
        /// <param name="mediumColour"></param>
        /// <param name="highColour"></param>
        public void SetDisplayGaugeValueAffectsColourOn(DisplayGauge displayGauge, Color lowColour, Color mediumColour, Color highColour)
        {
            if (displayGauge != null)
            {
                SSCUtils.ColortoColorNoAlloc(ref lowColour, ref displayGauge.foregroundLowColour);
                SSCUtils.ColortoColorNoAlloc(ref mediumColour, ref displayGauge.foregroundMediumColour);
                SSCUtils.ColortoColorNoAlloc(ref highColour, ref displayGauge.foregroundHighColour);

                displayGauge.isColourAffectByValue = true;

                if (IsInitialised || editorMode)
                {
                    SetDisplayGaugeValue(displayGauge, displayGauge.gaugeValue);
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeValueAffectsColour - displayGauge is null"); }
            #endif
        }

        /// <summary>
        /// The value of the gauge does not affect the foreground colour.
        /// When turning off this feature the new foreground colour would typically be the old foregroundHighColour.
        /// </summary>
        /// <param name="displayGauge"></param>
        /// <param name="newForegroundColour"></param>
        public void SetDisplayGaugeValueAffectsColourOff(DisplayGauge displayGauge, Color newForegroundColour)
        {
            if (displayGauge != null)
            {
                displayGauge.isColourAffectByValue = false;

                if (IsInitialised || editorMode)
                {
                    SetDisplayGaugeForegroundColour(displayGauge, newForegroundColour);
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeValueAffectsColour - displayGauge is null"); }
            #endif
        }

        /// <summary>
        /// Set the gauge value at which the foreground medium colour should be set.
        /// The default value is 0.5.
        /// </summary>
        /// <param name="displayGauge"></param>
        /// <param name="newValue"></param>
        public void SetDisplayGaugeMediumColourValue (DisplayGauge displayGauge, float newValue)
        {
            if (displayGauge != null)
            {
                if (newValue > 0f && newValue < 1f)
                {
                    displayGauge.mediumColourValue = newValue;
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeMediumColourValue - displayGauge is null"); }
            #endif
        }

        /// <summary>
        /// Set the offset (position) of the Display Gauge on the HUD.
        /// If the module has been initialised, this will also re-position the Display Gauge.
        /// </summary>
        /// <param name="offsetX">Horizontal offset from centre. Range between -1 and 1</param>
        /// <param name="offsetY">Vertical offset from centre. Range between -1 and 1</param>
        public void SetDisplayGaugeOffset(DisplayGauge displayGauge, float offsetX, float offsetY)
        {
            // Verify the x,y values are within range -1 to 1.
            if (offsetX >= -1f && offsetX <= 1f)
            {
                displayGauge.offsetX = offsetX;
            }

            if (offsetY >= -1f && offsetY <= 1f)
            {
                displayGauge.offsetY = offsetY;
            }

            if (IsInitialised || editorMode)
            {
                Transform dgTrfm = displayGauge.CachedGaugePanel.transform;

                #if UNITY_EDITOR
                if (editorMode)
                {
                    CheckHUDResize(false);
                    UnityEditor.Undo.RecordObject(dgTrfm, string.Empty);
                }
                #endif

                // Use the scaled HUD size. This works in and out of play mode
                tempGaugeOffset.x = displayGauge.offsetX * cvsResolutionHalf.x;
                tempGaugeOffset.y = displayGauge.offsetY * cvsResolutionHalf.y;
                tempGaugeOffset.z = dgTrfm.localPosition.z;

                dgTrfm.localPosition = tempGaugeOffset;
            }
        }

        /// <summary>
        /// Set the size of the Gauge Panel.
        /// If the module has been initialised, this will also resize the Gauge Panel.
        /// The values are only updated if they are outside the range 0.0 to 1.0 or have changed.
        /// Also updates the Text direction
        /// </summary>
        /// <param name="displayGauge"></param>
        /// <param name="width">Range between 0.0 and 1.0</param>
        /// <param name="height">Range between 0.0 and 1.0</param>
        public void SetDisplayGaugeSize(DisplayGauge displayGauge, float width, float height)
        {
            // Clamp the x,y values 0.0 to 1.0
            if (width < 0f) { displayGauge.displayWidth = 0f; }
            else if (width > 1f) { displayGauge.displayWidth = 1f; }
            else if (width != displayGauge.displayWidth) { displayGauge.displayWidth = width; }

            if (height < 0f) { displayGauge.displayHeight = 0f; }
            else if (height > 1f) { displayGauge.displayHeight = 1f; }
            else if (height != displayGauge.displayHeight) { displayGauge.displayHeight = height; }

            if (IsInitialised || editorMode)
            {
                #if UNITY_EDITOR
                if (editorMode) { CheckHUDResize(false); }
                #endif

                RectTransform dgRectTfrm = displayGauge.CachedGaugePanel;

                float pixelWidth = displayGauge.displayWidth * cvsResolutionFull.x;
                float pixelHeight = displayGauge.displayHeight * cvsResolutionFull.y;

                if (dgRectTfrm != null)
                {
                    // Here we use the original (reference) HUD size as it doesn't need to be scaled in any way.
                    // The parent HUD canvas is correctly scaled and sized to the actual monitor or device display.
                    dgRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, pixelWidth);
                    dgRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, pixelHeight);
                }

                if (displayGauge.CachedFgImgComponent != null)
                {
                    RectTransform dgFgImgRectTfrm = displayGauge.CachedFgImgComponent.rectTransform;

                    dgFgImgRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, pixelWidth);
                    dgFgImgRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, pixelHeight);
                }

                SetDisplayGaugeTextDirection(displayGauge, displayGauge.textDirection);
            }
        }

        /// <summary>
        /// Set the type or style of the gauge.
        /// </summary>
        /// <param name="displayGauge"></param>
        /// <param name="dgType"></param>
        public void SetDisplayGaugeType (DisplayGauge displayGauge, DisplayGauge.DGType dgType)
        {
            if (displayGauge != null)
            {
                displayGauge.gaugeType = dgType;

                GetOrCreateOrRemoveGaugeLabel(displayGauge);

                if (displayGauge.HasLabel)
                {
                    // Refresh the label
                    SetDisplayGaugeTextBrightness(displayGauge);

                    SetDisplayGaugeTextFontStyle(displayGauge, (UnityEngine.FontStyle)displayGauge.fontStyle);
                    SetDisplayGaugeTextFontSize(displayGauge, displayGauge.isBestFit, displayGauge.fontMinSize, displayGauge.fontMaxSize);

                    SetDisplayGaugeLabelAlignment(displayGauge, displayGauge.labelAlignment);
                }

                // Refresh the Text direction which also does the sizing for gauges with and without labels
                SetDisplayGaugeTextDirection(displayGauge, displayGauge.textDirection);

                SetDisplayGaugeValue(displayGauge, displayGauge.gaugeValue);
            }
        }

        /// <summary>
        /// Update the value or reading of the gauge. If Value Affects Colour (isColourAffectByValue)
        /// is enabled, the foreground colour of the gauge will also be updated.
        /// Values should be in range 0.0 to 1.0.
        /// NOTE: Numeric gauges can increase GC at runtime. Where possible, only call this method when
        /// the value changes.
        /// </summary>
        /// <param name="displayGauge"></param>
        /// <param name="gaugeValue"></param>
        public void SetDisplayGaugeValue (DisplayGauge displayGauge, float gaugeValue)
        {
            if (displayGauge != null)
            {
                // Clamp 0.0 to 1.0
                float _gaugeValue = gaugeValue < 0f ? 0f : gaugeValue > 1f ? 1f : gaugeValue;

                displayGauge.gaugeValue = _gaugeValue;
                UnityEngine.UI.Image fgImg = displayGauge.CachedFgImgComponent;

                if (fgImg != null)
                {
                    fgImg.fillAmount = displayGauge.isFillMethodNone ? 0f : _gaugeValue;

                    if (displayGauge.isColourAffectByValue)
                    {
                        if (_gaugeValue > displayGauge.mediumColourValue)
                        {
                            Color _newColour = Color.Lerp(displayGauge.foregroundMediumColour, displayGauge.foregroundHighColour, SSCMath.Normalise(_gaugeValue,  displayGauge.mediumColourValue, 1f));

                            SSCUtils.UpdateColour(ref _newColour, ref displayGauge.foregroundColour, ref displayGauge.baseForegroundColour, false);
                            SetDisplayGaugeForegroundBrightness(displayGauge);
                        }
                        else
                        {
                            Color _newColour = Color.Lerp(displayGauge.foregroundLowColour, displayGauge.foregroundMediumColour, SSCMath.Normalise(_gaugeValue, 0f, displayGauge.mediumColourValue));

                            SSCUtils.UpdateColour(ref _newColour, ref displayGauge.foregroundColour, ref displayGauge.baseForegroundColour, true);
                            SetDisplayGaugeForegroundBrightness(displayGauge);
                        }
                    }
                }

                int gaugeTypeInt = (int)displayGauge.gaugeType;

                // If this is a numeric gauge, update the text with the numeric value
                if (gaugeTypeInt == DisplayGauge.DGTypeFilledNumber1Int || gaugeTypeInt == DisplayGauge.DGTypeNumberWithLabel1Int)
                {
                    float _displayTxtValue = displayGauge.isNumericPercentage ? _gaugeValue * 100f : _gaugeValue * displayGauge.gaugeMaxValue;

                    displayGauge.gaugeString = SSCUtils.GetNumericString(_displayTxtValue, displayGauge.gaugeDecimalPlaces, displayGauge.isNumericPercentage);

                    // Update the text field for the gauge on the HUD UI
                    Text uiText = displayGauge.CachedTextComponent;

                    if (uiText != null) { uiText.text = displayGauge.gaugeString; }
                }

            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeValue - displayGauge is null"); }
            #endif
        }

        /// <summary>
        /// Update the text of the gauge label. This only applies to numeric gauges with labels.
        /// For non-numeric gauges see SetDisplayGaugeText(..).
        /// </summary>
        /// <param name="displayGauge"></param>
        /// <param name="gaugeLabel"></param>
        public void SetDisplayGaugeLabel (DisplayGauge displayGauge, string gaugeLabel)
        {
            if (displayGauge != null)
            {
                displayGauge.gaugeLabel = gaugeLabel;
                Text uiText = displayGauge.CachedLabelTextComponent;

                if (uiText != null) { uiText.text = gaugeLabel; }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeLabel - displayGauge is null"); }
            #endif
        }

        /// <summary>
        /// Update the position of the label within the gauge panel.
        /// Only applies to numeric gauges with a label.
        /// See also SetDisplayGaugeTextAlignment(..)
        /// </summary>
        /// <param name="displayGauge"></param>
        /// <param name="textAlignment"></param>
        public void SetDisplayGaugeLabelAlignment (DisplayGauge displayGauge, TextAnchor textAlignment)
        {
            if (displayGauge != null)
            {
                if (!editorMode) { displayGauge.labelAlignment = textAlignment; }

                // If this is not a numeric gauge with a label, this will return null.
                Text uiText = displayGauge.CachedLabelTextComponent;

                if (uiText != null) { uiText.alignment = textAlignment; }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeLabelAlignment - displayGauge is null"); }
            #endif
        }

        /// <summary>
        /// Update the text of the gauge. For numeric gauges with labels, see
        /// SetDisplayGaugeLabel(..).
        /// </summary>
        /// <param name="displayGauge"></param>
        /// <param name="gaugeText"></param>
        public void SetDisplayGaugeText (DisplayGauge displayGauge, string gaugeText)
        {
            if (displayGauge != null)
            {
                displayGauge.gaugeString = gaugeText;
                Text uiText = displayGauge.CachedTextComponent;

                if (uiText != null) { uiText.text = gaugeText; }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeText - displayGauge is null"); }
            #endif
        }

        /// <summary>
        /// Update the position of the text within the gauge panel
        /// </summary>
        /// <param name="displayGauge"></param>
        /// <param name="textAlignment"></param>
        public void SetDisplayGaugeTextAlignment(DisplayGauge displayGauge, TextAnchor textAlignment)
        {
            if (displayGauge != null)
            {
                if (!editorMode) { displayGauge.textAlignment = textAlignment; }

                Text uiText = displayGauge.CachedTextComponent;

                if (uiText != null) { uiText.alignment = textAlignment; }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeTextAlignment - displayGauge is null"); }
            #endif
        }

        /// <summary>
        /// Set the font of the DisplayGauge Text component
        /// </summary>
        /// <param name="displayGauge"></param>
        /// <param name="font"></param>
        public void SetDisplayGaugeTextFont(DisplayGauge displayGauge, Font font)
        {
            if (displayGauge != null)
            {
                Text uiText = displayGauge.CachedTextComponent;

                if (uiText != null)
                {
                    uiText.font = font;
                }

                // If this is numeric gauge with a label, also set the label
                if (displayGauge.HasLabel && displayGauge.CachedLabelTextComponent != null)
                {
                    displayGauge.CachedLabelTextComponent.font = font;
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeTextFont - displayGauge is null"); }
            #endif
        }

        /// <summary>
        /// Set the font style of the DisplayGauge Text component
        /// </summary>
        /// <param name="displayGauge"></param>
        /// <param name="font"></param>
        public void SetDisplayGaugeTextFontStyle(DisplayGauge displayGauge, FontStyle fontStyle)
        {
            if (displayGauge != null)
            {
                displayGauge.fontStyle = (DisplayGauge.DGFontStyle)fontStyle;

                Text uiText = displayGauge.CachedTextComponent;

                if (uiText != null)
                {
                    uiText.fontStyle = fontStyle;
                }

                // If this is numeric gauge with a label, also set the label
                if (displayGauge.HasLabel && displayGauge.CachedLabelTextComponent != null)
                {
                    displayGauge.CachedLabelTextComponent.fontStyle = fontStyle;
                }

            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeTextFontStyle - displayGauge is null"); }
            #endif
        }

        /// <summary>
        /// Set the font size of the display gauge text. If isBestFit is false, maxSize is the font size set.
        /// </summary>
        /// <param name="displayGauge"></param>
        /// <param name="isBestFit"></param>
        /// <param name="minSize"></param>
        /// <param name="maxSize"></param>
        public void SetDisplayGaugeTextFontSize(DisplayGauge displayGauge, bool isBestFit, int minSize, int maxSize)
        {
            if (displayGauge != null)
            {
                if (!editorMode)
                {
                    displayGauge.isBestFit = isBestFit;
                    displayGauge.fontMinSize = minSize;
                    displayGauge.fontMaxSize = maxSize;
                }

                Text uiText = displayGauge.CachedTextComponent;

                if (uiText != null)
                {
                    uiText.resizeTextForBestFit = isBestFit;
                    uiText.resizeTextMinSize = minSize;
                    uiText.resizeTextMaxSize = maxSize;
                    uiText.fontSize = maxSize;
                }

                // If this is numeric gauge with a label, also set the label
                if (displayGauge.HasLabel && displayGauge.CachedLabelTextComponent != null)
                {
                    Text uiLabelText = displayGauge.CachedLabelTextComponent;
                    uiLabelText.resizeTextForBestFit = isBestFit;
                    uiLabelText.resizeTextMinSize = minSize;
                    uiLabelText.resizeTextMaxSize = maxSize;
                    uiLabelText.fontSize = maxSize;
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeTextFontSize - displayGauge is null"); }
            #endif
        }

        /// <summary>
        /// Set the Display Gauge text colour. Only update the colour if it has actually changed.
        /// If the module has been initialised, this will also re-colour the gauge text with the appropriate brightness.
        /// </summary>
        /// <param name="newColour"></param>
        public void SetDisplayGaugeTextColour(DisplayGauge displayGauge, Color newColour)
        {
            if (displayGauge != null)
            {
                SSCUtils.UpdateColour(ref newColour, ref displayGauge.textColour, ref displayGauge.baseTextColour, true);

                SetDisplayGaugeTextBrightness(displayGauge);
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeTextColour - displayGauge is null"); }
            #endif
        }

        /// <summary>
        /// Update the direction of the text within the gauge panel
        /// </summary>
        /// <param name="displayGauge"></param>
        /// <param name="textDirection"></param>
        public void SetDisplayGaugeTextDirection(DisplayGauge displayGauge, DisplayGauge.DGTextDirection textDirection)
        {
            if (displayGauge != null)
            {
                if (!editorMode) { displayGauge.textDirection = textDirection; }

                Text uiText = displayGauge.CachedTextComponent;
                Text uiLabelText = displayGauge.CachedLabelTextComponent;

                bool hasLabel = uiLabelText != null && displayGauge.HasLabel;

                if (uiText != null)
                {
                    #if UNITY_EDITOR
                    if (editorMode) { CheckHUDResize(false); }
                    #endif

                    float pixelWidth = displayGauge.displayWidth * cvsResolutionFull.x;
                    float pixelHeight = displayGauge.displayHeight * cvsResolutionFull.y;
                    float textRotation = 0f;

                    RectTransform dgTextRectTfrm = displayGauge.CachedTextComponent.rectTransform;
                    RectTransform dgLabelRectTfrm = hasLabel ? displayGauge.CachedLabelTextComponent.rectTransform : null;

                    if (textDirection == DisplayGauge.DGTextDirection.Horizontal)
                    {
                        if (hasLabel)
                        {
                            pixelHeight *= 0.5f;
                            dgLabelRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, pixelWidth);
                            dgLabelRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, pixelHeight);
                        }

                        dgTextRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, pixelWidth);
                        dgTextRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, pixelHeight);
                    }
                    else
                    {
                        if (textDirection == DisplayGauge.DGTextDirection.BottomTop)
                        {
                            textRotation = 90f;
                        }
                        else { textRotation = 270f; }

                        if (hasLabel)
                        {
                            pixelWidth *= 0.5f;
                            dgLabelRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, pixelHeight);
                            dgLabelRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, pixelWidth);
                        }

                        dgTextRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, pixelHeight);
                        dgTextRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, pixelWidth);
                    }

                    uiText.transform.localRotation = Quaternion.Euler(uiText.transform.localRotation.x, uiText.transform.localRotation.y, textRotation);

                    if (hasLabel)
                    {
                        uiLabelText.transform.localRotation = Quaternion.Euler(uiLabelText.transform.localRotation.x, uiLabelText.transform.localRotation.y, textRotation);

                        if (textDirection == DisplayGauge.DGTextDirection.Horizontal)
                        {
                            uiLabelText.transform.localPosition = new Vector3(0f, pixelHeight / 2f, 0f);
                            uiText.transform.localPosition = new Vector3(0f, -pixelHeight / 2f, 0f);
                        }
                        else if (textDirection == DisplayGauge.DGTextDirection.BottomTop)
                        {
                            uiLabelText.transform.localPosition = new Vector3(-pixelWidth / 2f, 0f, 0f);
                            uiText.transform.localPosition = new Vector3(pixelWidth / 2f, 0f, 0f);
                        }
                        else
                        {
                            uiLabelText.transform.localPosition = new Vector3(pixelWidth / 2f, 0f, 0f);
                            uiText.transform.localPosition = new Vector3(-pixelWidth / 2f, 0f, 0f);
                        }
                    }
                    else
                    {
                        // This will correct the position if the dev has (incorrectly) moved the panel manually in the scene
                        uiText.transform.localPosition = Vector3.zero;
                    }
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeTextRotation - displayGauge is null"); }
            #endif
        }

        /// <summary>
        /// Set the Display Gauge foreground colour. Only update the colour if it has actually changed.
        /// If the module has been initialised, this will also re-colour the gauge foreground with the appropriate brightness.
        /// </summary>
        /// <param name="newColour"></param>
        public void SetDisplayGaugeForegroundColour(DisplayGauge displayGauge, Color newColour)
        {
            if (displayGauge != null)
            {
                SSCUtils.UpdateColour(ref newColour, ref displayGauge.foregroundColour, ref displayGauge.baseForegroundColour, true);

                SetDisplayGaugeForegroundBrightness(displayGauge);
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeForegroundColour - displayGauge is null"); }
            #endif
        }

        /// <summary>
        /// Set the Display Gauge foreground sprite. This is used to render the gauge value by partially filling it.
        /// </summary>
        /// <param name="displayGauge"></param>
        /// <param name="newSprite"></param>
        public void SetDisplayGaugeForegroundSprite(DisplayGauge displayGauge, Sprite newSprite)
        {
            if (displayGauge != null)
            {
                displayGauge.foregroundSprite = newSprite;

                UnityEngine.UI.Image fgImg = displayGauge.CachedFgImgComponent;

                if (fgImg != null)
                {
                    fgImg.sprite = newSprite;
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeForegroundSprite - displayGauge is null"); }
            #endif
        }

        /// <summary>
        /// Set the Display Gauge background colour. Only update the colour if it has actually changed.
        /// If the module has been initialised, this will also re-colour the gauge background with the appropriate brightness.
        /// </summary>
        /// <param name="newColour"></param>
        public void SetDisplayGaugeBackgroundColour(DisplayGauge displayGauge, Color newColour)
        {
            if (displayGauge != null)
            {
                SSCUtils.UpdateColour(ref newColour, ref displayGauge.backgroundColour, ref displayGauge.baseBackgroundColour, true);

                SetDisplayGaugeBackgroundBrightness(displayGauge);
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeBackgroundColour - displayGauge is null"); }
            #endif
        }

        /// <summary>
        /// Set the Display Gauge background sprite. This is used to render the background image of the gauge.
        /// </summary>
        /// <param name="displayGauge"></param>
        /// <param name="newSprite"></param>
        public void SetDisplayGaugeBackgroundSprite(DisplayGauge displayGauge, Sprite newSprite)
        {
            if (displayGauge != null)
            {
                displayGauge.backgroundSprite = newSprite;

                UnityEngine.UI.Image bgImg = displayGauge.CachedBgImgComponent;

                if (bgImg != null)
                {
                    bgImg.sprite = newSprite;
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeBackgroundSprite - displayGauge is null"); }
            #endif
        }

        /// <summary>
        /// Set the Display Gauge fill method. This determines how the gauge is filled
        /// </summary>
        /// <param name="displayGauge"></param>
        /// <param name="fillMethod"></param>
        public void SetDisplayGaugeFillMethod(DisplayGauge displayGauge, DisplayGauge.DGFillMethod fillMethod)
        {
            if (displayGauge != null)
            {
                UnityEngine.UI.Image fgImg = displayGauge.CachedFgImgComponent;

                if (fgImg != null)
                {
                    displayGauge.isFillMethodNone = displayGauge.fillMethod == DisplayGauge.DGFillMethod.None;

                    fgImg.fillMethod = displayGauge.isFillMethodNone ? Image.FillMethod.Horizontal : (UnityEngine.UI.Image.FillMethod)fillMethod;

                    // Refresh the gauge if case changing from/to Fill Method None.
                    SetDisplayGaugeValue(displayGauge, displayGauge.gaugeValue);
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeFillMethod - displayGauge is null"); }
            #endif
        }

        /// <summary>
        /// Sets whether or not the foreground and background sprites keep their original texture aspect ratio.
        /// This can be useful when creating circular gauges.
        /// </summary>
        /// <param name="displayGauge"></param>
        /// <param name="fillMethod"></param>
        public void SetDisplayGaugeKeepAspectRatio(DisplayGauge displayGauge, bool isKeepAspectRatio)
        {
            if (displayGauge != null)
            {
                UnityEngine.UI.Image fgImg = displayGauge.CachedFgImgComponent;
                UnityEngine.UI.Image bgImg = displayGauge.CachedBgImgComponent;

                if (fgImg != null)
                {
                    fgImg.preserveAspect = isKeepAspectRatio;
                }

                if (bgImg != null)
                {
                    bgImg.preserveAspect = isKeepAspectRatio;
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayGaugeKeepAspectRatio - displayGauge is null"); }
            #endif
        }
      
        /// <summary>
        /// Show the Display Gauge on the HUD. The HUD will automatically be shown if it is not already visible.
        /// </summary>
        /// <param name="guidHash"></param>
        public void ShowDisplayGauge(int guidHash)
        {
            DisplayGauge displayGauge = GetDisplayGauge(guidHash);
            if (displayGauge != null) { ShowOrHideGauge(displayGauge, true); }
            #if UNITY_EDITOR
            else { Debug.LogWarning("WARNING: ShowDisplayGauge - Could not find gauge with guidHash: " + guidHash); }
            #endif
        }

        /// <summary>
        /// Show the Display Gauge on the HUD. The HUD will automatically be shown if it is not already visible.
        /// </summary>
        /// <param name="displayGauge"></param>
        public void ShowDisplayGauge(DisplayGauge displayGauge)
        {
            if (displayGauge != null) { ShowOrHideGauge(displayGauge, true); }
            #if UNITY_EDITOR
            else { Debug.LogWarning("WARNING: ShowDisplayGauge - Could not find gauge - parameter was null"); }
            #endif
        }

        /// <summary>
        /// Show or turn on all Display Gauges. ShipDisplayModule must be initialised.
        /// The HUD will automatically be shown if it is not already visible.
        /// </summary>
        public void ShowDisplayGauges()
        {
            ShowOrHideGauges(true);
        }

        /// <summary>
        /// Create a new list if required
        /// </summary>
        public void ValidateGaugeList()
        {
            if (displayGaugeList == null) { displayGaugeList = new List<DisplayGauge>(1); }
        }

        #endregion

        #region Public API Methods - Heading

        /// <summary>
        /// Set the small sprite that will indicate or point to the heading on the HUD
        /// </summary>
        /// <param name="newSprite"></param>
        public void SetDisplayHeadingIndicatorSprite (Sprite newSprite)
        {
            headingIndicatorSprite = newSprite;

            if (headingIndicatorImg != null)
            {
                headingIndicatorImg.sprite = headingIndicatorSprite;
            }
        }

        /// <summary>
        /// Set the sprite that will mask the Display Heading scrollable sprite
        /// </summary>
        /// <param name="newSprite"></param>
        public void SetDisplayHeadingMaskSprite (Sprite newSprite)
        {
            headingMaskSprite = newSprite;

            if (headingMaskImg != null)
            {
                headingMaskImg.sprite = headingMaskSprite;
            }
        }

        /// <summary>
        /// Set the Display Heading small indicator colour. Only update the colour if it has actually changed.
        /// If the module has been initialised, this will also re-colour the heading with the appropriate brightness.
        /// </summary>
        /// <param name="newColour"></param>
        public void SetDisplayHeadingIndicatorColour (Color newColour)
        {
            SSCUtils.UpdateColour(ref newColour, ref headingIndicatorColour, ref baseHeadingIndicatorColour, true);

            SetHeadingBrightness();
        }

        /// <summary>
        /// Set the Display Heading primary colour. Only update the colour if it has actually changed.
        /// If the module has been initialised, this will also re-colour the heading with the appropriate brightness.
        /// </summary>
        /// <param name="newColour"></param>
        public void SetDisplayHeadingPrimaryColour (Color newColour)
        {
            SSCUtils.UpdateColour(ref newColour, ref headingPrimaryColour, ref baseHeadingPrimaryColour, true);

            SetHeadingBrightness();
        }

        /// <summary>
        /// Set the Display Heading scroll sprite.
        /// </summary>
        /// <param name="newSprite"></param>
        public void SetDisplayHeadingScrollSprite (Sprite newSprite)
        {
            headingScrollSprite = newSprite;

            if (headingScrollImg != null)
            {
                headingScrollImg.sprite = headingScrollSprite;
            }
        }

        /// <summary>
        /// Set the normalised size of the scrollable Display Heading.
        /// Currently height is always 1.0
        /// </summary>
        /// <param name="width"></param>
        /// <param name="height"></param>
        public void SetDisplayHeadingSize (float width, float height)
        {
            // Verify the width,height values are within range 0.1 to 1.
            if (width >= 0.1f && width <= 1f)
            {
                headingWidth = width;
            }

            headingHeight = 1f;

            if (IsInitialised || editorMode)
            {
                #if UNITY_EDITOR
                if (editorMode)
                {
                    CheckHUDResize(false);
                    UnityEditor.Undo.RecordObject(headingRectTfrm.transform, string.Empty);
                }
                #endif

                // Here we use the original (reference) HUD size as it doesn't need to be scaled in any way.
                // The parent HUD canvas is correctly scaled and sized to the actual monitor or device display.
                headingRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, headingWidth * cvsResolutionFull.x);
                headingRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, headingHeight * cvsResolutionFull.y);
            }
        }

         /// <summary>
        /// Set the normalised offset (position) of the Display Heading on the HUD.
        /// If the module has been initialised, this will also re-position the Display Heading.
        /// </summary>
        /// <param name="offsetX">Horizontal offset from centre. Range between -1 and 1</param>
        /// <param name="offsetY">Vertical offset from centre. Range between -1 and 1</param>
        public void SetDisplayHeadingOffset (float offsetX, float offsetY)
        {
            // Verify the x,y values are within range -1 to 1.
            if (offsetX >= -1f && offsetX <= 1f)
            {
                headingOffsetX = offsetX;
            }

            if (offsetY >= -1f && offsetY <= 1f)
            {
                headingOffsetY = offsetY;
            }

            if (IsInitialised || editorMode)
            {
                Transform hdgTrfm = headingRectTfrm.transform;

                #if UNITY_EDITOR
                if (editorMode)
                {
                    CheckHUDResize(false);
                    UnityEditor.Undo.RecordObject(hdgTrfm, string.Empty);
                }
                #endif

                // Use the scaled HUD size. This works in and out of play mode
                tempHeadingOffset.x = headingOffsetX * cvsResolutionHalf.x;
                tempHeadingOffset.y = headingOffsetY * cvsResolutionHalf.y;
                tempHeadingOffset.z = hdgTrfm.localPosition.z;

                hdgTrfm.localPosition = tempHeadingOffset;

                // Reset the position of the scrollable image
                if (headingScrollPanel != null)
                {
                    headingScrollPanel.localPosition = Vector3.zero;
                    headingInitPosition = headingScrollPanel.transform.position;

                    if (showHeadingIndicator && headingIndicatorPanel != null)
                    {
                        headingIndicatorPanel.localPosition = new Vector3(headingScrollPanel.localPosition.x, headingScrollPanel.localPosition.y - 20f, headingScrollPanel.localPosition.z);
                    }
                }
            }
        }

        /// <summary>
        /// Attempt to turn off the scrollable Heading
        /// </summary>
        public void HideHeading()
        {
            ShowOrHideHeading(false);
        }

        /// <summary>
        /// Attempt to turn off the Heading indicator
        /// </summary>
        public void HideHeadingIndicator()
        {
            ShowOrHideHeadingIndicator(false);
        }

        /// <summary>
        /// Attempt to show the scrollable Heading. Turn on HUD if required.
        /// </summary>
        public void ShowHeading()
        {
            ShowOrHideHeading(true);
        }

        /// <summary>
        /// Attempt to turn on the Heading indicator
        /// </summary>
        public void ShowHeadingIndicator()
        {
            ShowOrHideHeadingIndicator(true);
        }

        #endregion

        #region Public API Methods - Messages

        /// <summary>
        /// Add a new message to the HUD. By design, they are not visible at runtime when first added.
        /// </summary>
        /// <param name="messageName"></param>
        /// <param name="messageText"></param>
        /// <returns></returns>
        public DisplayMessage AddMessage(string messageName, string messageText)
        {
            DisplayMessage displayMessage = null;

            Canvas canvas = GetCanvas;
            hudRectTfrm = GetHUDPanel();

            if (canvas == null)
            {
                #if UNITY_EDITOR
                Debug.LogWarning("ERROR: ShipDisplayModule.AddMessage() - could not find canvas for the HUD. Did you use the prefab from Prefabs/Visuals folder?");
                #endif
            }
            else if (hudRectTfrm == null)
            {
                #if UNITY_EDITOR
                Debug.LogWarning("ERROR: ShipDisplayModule.AddMessage() - could not find HUDPanel for the HUD. Did you use the prefab from Prefabs/Visuals folder?");
                #endif
            }
            else
            {
                displayMessage = new DisplayMessage();
                if (displayMessage != null)
                {
                    displayMessage.messageName = messageName;
                    displayMessage.messageString = messageText;
                    ValidateMessageList();
                    displayMessageList.Add(displayMessage);

                    CreateMessagePanel(displayMessage, hudRectTfrm);

                    numDisplayMessages = displayMessageList.Count;
                 }
            }

            return displayMessage;
        }

        /// <summary>
        /// Add a message to the HUD using a displayMessage instance. Typically, this is used
        /// with CopyDisplayMessage(..).
        /// </summary>
        /// <param name="displayMessage"></param>
        public void AddMessage(DisplayMessage displayMessage)
        {
            if (displayMessage != null)
            {
                RectTransform hudRectTfrm = SSCUtils.GetChildRectTransform(transform, hudPanelName, this.name);

                if (hudRectTfrm == null)
                {
                    #if UNITY_EDITOR
                    Debug.LogWarning("ERROR: ShipDisplayModule.AddMessage() - could not find HUDPanel for the HUD. Did you use the prefab from Prefabs/Visuals folder?");
                    #endif
                }
                else
                {
                    ValidateMessageList();
                    displayMessageList.Add(displayMessage);

                    CreateMessagePanel(displayMessage, hudRectTfrm);

                    numDisplayMessages = displayMessageList.Count;
                }
            }
        }

        /// <summary>
        /// Delete a message from the HUD.
        /// NOTE: It is much cheaper to HideDisplayMessage(..) than completely remove it.
        /// </summary>
        /// <param name="guidHash"></param>
        public void DeleteMessage(int guidHash)
        {
            #if UNITY_EDITOR
            bool isRecordUndo = !Application.isPlaying;
            int undoGroup=0;
            #else
            bool isRecordUndo = false;
            #endif

            RectTransform msgRectTfrm = GetMessagePanel(guidHash, null);
            int _msgIndex = GetDisplayMessageIndex(guidHash);

            // Only record undo if in the editor AND is not playing AND something needs to be recorded
            isRecordUndo = isRecordUndo && (msgRectTfrm != null || _msgIndex >= 0);

            #if UNITY_EDITOR
            if (isRecordUndo)
            {
                UnityEditor.Undo.SetCurrentGroupName("Delete Display Message " + (_msgIndex + 1).ToString());
                undoGroup = UnityEditor.Undo.GetCurrentGroup();

                if (_msgIndex >= 0)
                {
                    UnityEditor.Undo.RecordObject(this, string.Empty);
                }
            }
            #endif

            // Check that the message exists and it is within the list constraints
            // NOTE: The script element must be modified BEFORE the Undo.DestroyObjectImmediate is called
            // in order for Undo to correctly undo both the message change AND the destroy of the panel.
            if (_msgIndex >= 0 && _msgIndex < (displayMessageList == null ? 0 : displayMessageList.Count))
            {
                displayMessageList.RemoveAt(_msgIndex);
                numDisplayMessages = displayMessageList.Count;
            }

            if (msgRectTfrm != null)
            {
                if (Application.isPlaying) { Destroy(msgRectTfrm.gameObject); }
                else
                {
                    #if UNITY_EDITOR
                    UnityEditor.Undo.DestroyObjectImmediate(msgRectTfrm.gameObject);
                    #else
                    DestroyImmediate(msgRectTfrm.gameObject);
                    #endif
                }
            }

            #if UNITY_EDITOR
            if (isRecordUndo)
            {               
                UnityEditor.Undo.CollapseUndoOperations(undoGroup);
            }
            #endif
        }

        /// <summary>
        /// Create a copy of an existing DisplayMessage, and give it a new name.
        /// Call AddMessage(newDisplayMessage) to make it useable in the game.
        /// </summary>
        /// <param name="displayMessage"></param>
        /// <param name="NameOfCopy"></param>
        /// <returns></returns>
        public DisplayMessage CopyDisplayMessage(DisplayMessage displayMessage, string NameOfCopy)
        {
            DisplayMessage dmCopy = new DisplayMessage(displayMessage);

            if (dmCopy != null)
            {
                // make it unique
                dmCopy.guidHash = SSCMath.GetHashCodeFromGuid();
                dmCopy.messageName = NameOfCopy;
            }
            return dmCopy;
        }

        /// <summary>
        /// Returns the guidHash of the Message in the list given the index or
        /// zero-based position in the list. Will return 0 if no matching Message
        /// is found.
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        public int GetDisplayMessageGuidHash(int index)
        {
            int guidHash = 0;

            if (index >= 0 && index < GetNumberDisplayMessages)
            {
                if (displayMessageList[index] != null) { guidHash = displayMessageList[index].guidHash; }
            }

            return guidHash;
        }

        /// <summary>
        /// Get the zero-based index of the Message in the list. Will return -1 if not found.
        /// </summary>
        /// <param name="guidHash"></param>
        /// <returns></returns>
        public int GetDisplayMessageIndex(int guidHash)
        {
            int index = -1;

            int _numDisplayMessages = GetNumberDisplayMessages;

            for (int dmIdx = 0; dmIdx < _numDisplayMessages; dmIdx++)
            {
                if (displayMessageList[dmIdx] != null && displayMessageList[dmIdx].guidHash == guidHash) { index = dmIdx; break; }
            }

            return index;
        }

        /// <summary>
        /// Get a DisplayMessage given its guidHash.
        /// See also GetDisplayMessageGuidHash(..).
        /// Will return null if guidHash parameter is 0, it cannot be found.
        /// </summary>
        /// <param name="guidHash"></param>
        /// <returns></returns>
        public DisplayMessage GetDisplayMessage(int guidHash)
        {
            DisplayMessage displayMessage = null;
            DisplayMessage _tempDM = null;

            int _numDisplayMessages = GetNumberDisplayMessages;

            for (int dmIdx = 0; dmIdx < _numDisplayMessages; dmIdx++)
            {
                _tempDM = displayMessageList[dmIdx];
                if (_tempDM != null && _tempDM.guidHash == guidHash)
                {
                    displayMessage = _tempDM;
                    break;
                }
            }

            return displayMessage;
        }

        /// <summary>
        /// Get the display message give the description title of the message.
        /// WARNING: This will increase Garbage Collection (GC). Where possible
        /// use GetDisplayMessage(guidHash) and/or GetDisplayMessageGuidHash(index)
        /// </summary>
        /// <param name="displayMessageName"></param>
        /// <returns></returns>
        public DisplayMessage GetDisplayMessage(string displayMessageName)
        {
            DisplayMessage displayMessage = null;
            DisplayMessage _tempDM = null;

            if (!string.IsNullOrEmpty(displayMessageName))
            {
                int _numDisplayMessages = GetNumberDisplayMessages;

                for (int dmIdx = 0; dmIdx < _numDisplayMessages; dmIdx++)
                {
                    _tempDM = displayMessageList[dmIdx];
                    if (_tempDM != null && !string.IsNullOrEmpty(_tempDM.messageName))
                    {
                        if (_tempDM.messageName.ToLower() == displayMessageName.ToLower())
                        {
                            displayMessage = _tempDM;
                            break;
                        }
                    }
                }
            }
            return displayMessage;
        }

        /// <summary>
        /// Is the Display Message currently being shown on the HUD?
        /// </summary>
        /// <param name="displayMessage"></param>
        /// <returns></returns>
        public bool IsDisplayMessageShown (DisplayMessage displayMessage)
        {
            if (displayMessage != null)
            {
                return IsHUDShown && displayMessage.showMessage;
            }
            else { return false; }
        }

        /// <summary>
        /// After adding or moving DisplayMessages, they may need to be sorted to
        /// have the correct z-order in on the HUD.
        /// </summary>
        public void RefreshMessagesSortOrder()
        {
            if (hudPanel != null)
            {
                // Messages should begin after index 1.
                int zIndex = 1;

                // Start messages after Altitude in hierarchy.
                if (overlayPanel != null) { zIndex = overlayPanel.GetSiblingIndex(); }

                int _numDisplayMessages = GetNumberDisplayMessages;

                for (int dmIdx = 0; dmIdx < _numDisplayMessages; dmIdx++)
                {
                    DisplayMessage displayMessage = displayMessageList[dmIdx];

                    if (displayMessage != null)
                    {
                        RectTransform _rt = displayMessage.CachedMessagePanel;

                        if (_rt != null)
                        {
                            _rt.SetSiblingIndex(++zIndex);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Import a json file from disk and return as DisplayMessage
        /// </summary>
        /// <param name="folderPath"></param>
        /// <param name="fileName"></param>
        /// <returns></returns>
        public DisplayMessage ImportMessageFromJson (string folderPath, string fileName)
        {
            DisplayMessage displayMessage = null;

            if (!string.IsNullOrEmpty(folderPath) && !string.IsNullOrEmpty(fileName))
            {
                try
                {
                    string filePath = System.IO.Path.Combine(folderPath, fileName);
                    if (System.IO.File.Exists(filePath))
                    {
                        string jsonText = System.IO.File.ReadAllText(filePath);

                        displayMessage = new DisplayMessage();
                        int displayGaugeGuidHash = displayMessage.guidHash;

                        JsonUtility.FromJsonOverwrite(jsonText, displayMessage);

                        if (displayMessage != null)
                        {
                            // make hash code unique
                            displayMessage.guidHash = displayGaugeGuidHash;
                        }
                    }
                    #if UNITY_EDITOR
                    else
                    {
                        Debug.LogWarning("ERROR: Import Message. Could not find file at " + filePath);
                    }
                    #endif
                }
                catch (System.Exception ex)
                {
                    #if UNITY_EDITOR
                    Debug.LogWarning("ERROR: ShipDisplayModule - could not import message from: " + folderPath + " PLEASE REPORT - " + ex.Message);
                    #else
                    // Keep compiler happy
                    if (ex != null) { }
                    #endif
                }
            }

            return displayMessage;
        }

        /// <summary>
        /// Save the Message to a json file on disk.
        /// </summary>
        /// <param name="displayMessage"></param>
        /// <param name="filePath"></param>
        public bool SaveMessageAsJson (DisplayMessage displayMessage, string filePath)
        {
            bool isSuccessful = false;

            if (displayMessage != null && !string.IsNullOrEmpty(filePath))
            {
                try
                {
                    string jsonMessageData = JsonUtility.ToJson(displayMessage);

                    if (!string.IsNullOrEmpty(jsonMessageData) && !string.IsNullOrEmpty(filePath))
                    {
                        System.IO.File.WriteAllText(filePath, jsonMessageData);
                        isSuccessful = true;
                    }
                }
                catch (System.Exception ex)
                {
                    #if UNITY_EDITOR
                    Debug.LogWarning("ERROR: ShipDisplayModule SaveMessageAsJson - could not export: " + displayMessage.messageName + " PLEASE REPORT - " + ex.Message);
                    #else
                    // Keep compiler happy
                    if (ex != null) { }
                    #endif
                }
            }

            return isSuccessful;
        }

        /// <summary>
        /// Set the offset (position) of the Display Message on the HUD.
        /// If the module has been initialised, this will also re-position the Display Message.
        /// </summary>
        /// <param name="offsetX">Horizontal offset from centre. Range between -1 and 1</param>
        /// <param name="offsetY">Vertical offset from centre. Range between -1 and 1</param>
        public void SetDisplayMessageOffset(DisplayMessage displayMessage, float offsetX, float offsetY)
        {
            // Verify the x,y values are within range -1 to 1.
            if (offsetX >= -1f && offsetX <= 1f)
            {
                displayMessage.offsetX = offsetX;
            }

            if (offsetY >= -1f && offsetY <= 1f)
            {
                displayMessage.offsetY = offsetY;
            }

            if (IsInitialised || editorMode)
            {
                Transform dmTrfm = displayMessage.CachedMessagePanel.transform;

                #if UNITY_EDITOR
                if (editorMode)
                {
                    CheckHUDResize(false);
                    UnityEditor.Undo.RecordObject(dmTrfm, string.Empty);
                }
                #endif

                // Use original refResolution rather than the scaled size of the HUD. This works in and out of play mode
                //tempMessageOffset.x = displayMessage.offsetX * (refResolutionHalf.x - (displayMessage.displayWidth / 2f));
                //tempMessageOffset.y = displayMessage.offsetY * (refResolutionHalf.y - (displayMessage.displayHeight / 2f));

                // Use the scaled HUD size. This works in and out of play mode
                tempMessageOffset.x = displayMessage.offsetX * cvsResolutionHalf.x;
                tempMessageOffset.y = displayMessage.offsetY * cvsResolutionHalf.y;
                tempMessageOffset.z = dmTrfm.localPosition.z;

                dmTrfm.localPosition = tempMessageOffset;
            }
        }

        /// <summary>
        /// Set the size of the Message Panel.
        /// If the module has been initialised, this will also resize the Message Panel.
        /// The values are only updated if they are outside the range 0.0 to 1.0 or have changed.
        /// </summary>
        /// <param name="displayMessage"></param>
        /// <param name="width">Range between 0.0 and 1.0</param>
        /// <param name="height">Range between 0.0 and 1.0</param>
        public void SetDisplayMessageSize(DisplayMessage displayMessage, float width, float height)
        {
            // Clamp the x,y values 0.0 to 1.0
            if (width < 0f) { displayMessage.displayWidth = 0f; }
            else if (width > 1f) { displayMessage.displayWidth = 1f; }
            else if (width != displayMessage.displayWidth) { displayMessage.displayWidth = width; }

            if (height < 0f) { displayMessage.displayHeight = 0f; }
            else if (height > 1f) { displayMessage.displayHeight = 1f; }
            else if (height != displayMessage.displayHeight) { displayMessage.displayHeight = height; }

            if (IsInitialised || editorMode)
            {
                #if UNITY_EDITOR
                if (editorMode) { CheckHUDResize(false); }
                #endif

                RectTransform dmRectTfrm = displayMessage.CachedMessagePanel;    

                if (dmRectTfrm != null)
                {
                    // Here we use the original (reference) HUD size as it doesn't need to be scaled in any way.
                    // The parent HUD canvas is correctly scaled and sized to the actual monitor or device display.
                    dmRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, displayMessage.displayWidth * cvsResolutionFull.x);
                    dmRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, displayMessage.displayHeight * cvsResolutionFull.y);
                }

                if (displayMessage.CachedTextComponent != null)
                {
                    RectTransform dmTextRectTfrm = displayMessage.CachedTextComponent.rectTransform;

                    dmTextRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, displayMessage.displayWidth * cvsResolutionFull.x);
                    dmTextRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, displayMessage.displayHeight * cvsResolutionFull.y);
                }
            }
        }

        /// <summary>
        /// Update the text of the message
        /// </summary>
        /// <param name="displayMessage"></param>
        /// <param name="messageText"></param>
        public void SetDisplayMessageText(DisplayMessage displayMessage, string messageText)
        {
            if (displayMessage != null)
            {
                displayMessage.messageString = messageText;
                Text uiText = displayMessage.CachedTextComponent;

                if (uiText != null) { uiText.text = messageText; }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayMessageText - displayMessage is null"); }
            #endif
        }

        /// <summary>
        /// Update the position of the text within the message panel
        /// </summary>
        /// <param name="displayMessage"></param>
        /// <param name="textAlignment"></param>
        public void SetDisplayMessageTextAlignment(DisplayMessage displayMessage, TextAnchor textAlignment)
        {
            if (displayMessage != null)
            {
                if (!editorMode) { displayMessage.textAlignment = textAlignment; }

                Text uiText = displayMessage.CachedTextComponent;

                if (uiText != null) { uiText.alignment = textAlignment; }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayMessageTextAlignment - displayMessage is null"); }
            #endif
        }

        /// <summary>
        /// Set the font of the DisplayMessage Text component
        /// </summary>
        /// <param name="displayMessage"></param>
        /// <param name="font"></param>
        public void SetDisplayMessageTextFont(DisplayMessage displayMessage, Font font)
        {
            if (displayMessage != null)
            {
                Text uiText = displayMessage.CachedTextComponent;

                if (uiText != null)
                {
                    uiText.font = font;
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayMessageTextFont - displayMessage is null"); }
            #endif
        }

        /// <summary>
        /// Set the font size of the display message text. If isBestFit is false, maxSize is the font size set.
        /// </summary>
        /// <param name="displayMessage"></param>
        /// <param name="isBestFit"></param>
        /// <param name="minSize"></param>
        /// <param name="maxSize"></param>
        public void SetDisplayMessageTextFontSize(DisplayMessage displayMessage, bool isBestFit, int minSize, int maxSize)
        {
            if (displayMessage != null)
            {
                if (!editorMode)
                {
                    displayMessage.isBestFit = isBestFit;
                    displayMessage.fontMinSize = minSize;
                    displayMessage.fontMaxSize = maxSize;
                }

                Text uiText = displayMessage.CachedTextComponent;

                if (uiText != null)
                {
                    uiText.resizeTextForBestFit = isBestFit;
                    uiText.resizeTextMinSize = minSize;
                    uiText.resizeTextMaxSize = maxSize;
                    uiText.fontSize = maxSize;
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayMessageTextFontSize - displayMessage is null"); }
            #endif
        }

        /// <summary>
        /// Set the Display Message background colour. Only update the colour if it has actually changed.
        /// If the module has been initialised, this will also re-colour the message background with the appropriate brightness.
        /// </summary>
        /// <param name="newColour"></param>
        public void SetDisplayMessageBackgroundColour(DisplayMessage displayMessage, Color newColour)
        {
            if (displayMessage != null)
            {
                SSCUtils.UpdateColour(ref newColour, ref displayMessage.backgroundColour, ref displayMessage.baseBackgroundColour, true);

                SetDisplayMessageBackgroundBrightness(displayMessage);
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayMessageBackgroundColour - displayMessage is null"); }
            #endif
        }

        /// <summary>
        /// Set the Display Message scroll direction.
        /// USAGE: SetDisplayMessageScrollDirection(displayMessage, DisplayMessage.ScrollDirectionLR)
        /// </summary>
        /// <param name="displayMessage"></param>
        /// <param name="scrollDirection"></param>
        public void SetDisplayMessageScrollDirection(DisplayMessage displayMessage, int scrollDirection)
        {
            if (displayMessage != null)
            {
                if (scrollDirection >= 0 && scrollDirection < 5)
                {
                    displayMessage.scrollDirectionInt = scrollDirection;
                    displayMessage.scrollDirection = (DisplayMessage.ScrollDirection)scrollDirection;
                    if (scrollDirection == DisplayMessage.ScrollDirectionNone)
                    {
                        displayMessage.scrollOffsetX = 0f;
                        displayMessage.scrollOffsetY = 0f;
                        SetDisplayMessageOffset(displayMessage, displayMessage.offsetX, displayMessage.offsetY);
                    }
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayMessageScrollDirection - displayMessage is null"); }
            #endif
        }

        /// <summary>
        /// Set the Display Message to scroll across or up/down the full screen regardless of the message width and height.
        /// Can also be set directly with displayMessage.isScrollFullscreen = true;
        /// </summary>
        /// <param name="displayMessage"></param>
        /// <param name="isScrollFullscreen"></param>
        public void SetDisplayMessageScrollFullscreen(DisplayMessage displayMessage, bool isScrollFullscreen)
        {
            if (displayMessage != null) { displayMessage.isScrollFullscreen = isScrollFullscreen; }
        }

        /// <summary>
        /// Set the Display Message scroll speed.
        /// Can also be set directly with displayMessage.scrollSpeed = scrollSpeed;
        /// </summary>
        /// <param name="displayMessage"></param>
        /// <param name="scrollSpeed"></param>
        public void SetDisplayMessageScrollSpeed(DisplayMessage displayMessage, float scrollSpeed)
        {
            if (displayMessage != null) { displayMessage.scrollSpeed = scrollSpeed; }
        }

        /// <summary>
        /// Set the Display Message text colour. Only update the colour if it has actually changed.
        /// If the module has been initialised, this will also re-colour the message text with the appropriate brightness.
        /// </summary>
        /// <param name="newColour"></param>
        public void SetDisplayMessageTextColour(DisplayMessage displayMessage, Color newColour)
        {
            if (displayMessage != null)
            {
                SSCUtils.UpdateColour(ref newColour, ref displayMessage.textColour, ref displayMessage.baseTextColour, true);

                SetDisplayMessageTextBrightness(displayMessage);
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayMessageTextColour - displayMessage is null"); }
            #endif
        }

        /// <summary>
        /// Show the Display Message on the HUD. The HUD will automatically be shown if it is not already visible.
        /// </summary>
        /// <param name="guidHash"></param>
        public void ShowDisplayMessage (int guidHash)
        {
            DisplayMessage displayMessage = GetDisplayMessage(guidHash);
            if (displayMessage != null) { ShowOrHideMessage(displayMessage, true, false); }
            #if UNITY_EDITOR
            else { Debug.LogWarning("WARNING: ShowDisplayMessage - Could not find message with guidHash: " + guidHash); }
            #endif
        }

        /// <summary>
        /// Show the Display Message on the HUD. The HUD will automatically be shown if it is not already visible.
        /// </summary>
        /// <param name="displayMessage"></param>
        public void ShowDisplayMessage (DisplayMessage displayMessage)
        {
            if (displayMessage != null) { ShowOrHideMessage(displayMessage, true, false); }
            #if UNITY_EDITOR
            else { Debug.LogWarning("WARNING: ShowDisplayMessage - Could not find message - parameter was null"); }
            #endif
        }

        /// <summary>
        /// Show the Display Message on the HUD instantly by ignoring fade settings. The HUD will automatically be shown if it is not already visible.
        /// </summary>
        /// <param name="guidHash"></param>
        public void ShowDisplayMessageInstant (int guidHash)
        {
            DisplayMessage displayMessage = GetDisplayMessage(guidHash);
            if (displayMessage != null) { ShowOrHideMessage(displayMessage, true, true); }
            #if UNITY_EDITOR
            else { Debug.LogWarning("WARNING: ShowDisplayMessageInstant - Could not find message with guidHash: " + guidHash); }
            #endif
        }

        /// <summary>
        /// Show the Display Message on the HUD instantly by ignoring fade settings. The HUD will automatically be shown if it is not already visible.
        /// </summary>
        /// <param name="displayMessage"></param>
        public void ShowDisplayMessageInstant (DisplayMessage displayMessage)
        {
            if (displayMessage != null) { ShowOrHideMessage(displayMessage, true, true); }
            #if UNITY_EDITOR
            else { Debug.LogWarning("WARNING: ShowDisplayMessageInstant - Could not find message - parameter was null"); }
            #endif
        }

        /// <summary>
        /// Show or turn on all Display Messages. ShipDisplayModule must be initialised.
        /// The HUD will automatically be shown if it is not already visible.
        /// </summary>
        public void ShowDisplayMessages()
        {
            ShowOrHideMessages(true, false);
        }

        /// <summary>
        /// Show the Display Message background on the HUD. The display the actual message, you would
        /// need to call ShowDisplayMessage(..).
        /// </summary>
        /// <param name="displayMessage"></param>
        public void ShowDisplayMessageBackground(DisplayMessage displayMessage)
        {
            if (displayMessage != null) { ShowOrHideDisplayMessageBackground(displayMessage, true); }
            #if UNITY_EDITOR
            else { Debug.LogWarning("WARNING: ShowDisplayMessageBackground - Could not find message - parameter was null"); }
            #endif
        }

        /// <summary>
        /// Hide or turn off the Display Message
        /// </summary>
        /// <param name="guidHash"></param>
        public void HideDisplayMessage(int guidHash)
        {
            DisplayMessage displayMessage = GetDisplayMessage(guidHash);
            if (displayMessage != null) { ShowOrHideMessage(displayMessage, false, false); }
            #if UNITY_EDITOR
            else { Debug.LogWarning("WARNING: ShowDisplayMessage - Could not find message with guidHash: " + guidHash); }
            #endif
        }

        /// <summary>
        /// Hide or turn off the Display Message
        /// </summary>
        /// <param name="displayMessage"></param>
        public void HideDisplayMessage (DisplayMessage displayMessage)
        {
            if (displayMessage != null) { ShowOrHideMessage(displayMessage, false, false); }
            #if UNITY_EDITOR
            else { Debug.LogWarning("WARNING: ShowDisplayMessage - Could not find message - parameter was null"); }
            #endif
        }

        /// <summary>
        /// Hide or turn off the Display Message instantly and ignore any fade-out settings.
        /// </summary>
        /// <param name="guidHash"></param>
        public void HideDisplayMessageInstant (int guidHash)
        {
            DisplayMessage displayMessage = GetDisplayMessage(guidHash);
            if (displayMessage != null) { ShowOrHideMessage(displayMessage, false, true); }
            #if UNITY_EDITOR
            else { Debug.LogWarning("WARNING: ShowDisplayMessageInstant - Could not find message with guidHash: " + guidHash); }
            #endif
        }

        /// <summary>
        /// Hide or turn off the Display Message instantly and ignore any fade-out settings.
        /// </summary>
        /// <param name="displayMessage"></param>
        public void HideDisplayMessageInstant (DisplayMessage displayMessage)
        {
            if (displayMessage != null) { ShowOrHideMessage(displayMessage, false, true); }
            #if UNITY_EDITOR
            else { Debug.LogWarning("WARNING: ShowDisplayMessageInstant - Could not find message - parameter was null"); }
            #endif
        }

        /// <summary>
        /// Hide or turn off all Display Messages.
        /// ShipDisplayModule must be initialised.
        /// </summary>
        public void HideDisplayMessages()
        {
            ShowOrHideMessages(false, false);
        }

        /// <summary>
        /// Hide the Display Message background on the HUD. The hide the actual message, you would
        /// need to call HideDisplayMessage(..).
        /// </summary>
        /// <param name="displayMessage"></param>
        public void HideDisplayMessageBackground(DisplayMessage displayMessage)
        {
            if (displayMessage != null) { ShowOrHideDisplayMessageBackground(displayMessage, false); }
            #if UNITY_EDITOR
            else { Debug.LogWarning("WARNING: HideDisplayMessageBackground - Could not find message - parameter was null"); }
            #endif
        }

        /// <summary>
        /// Create a new list if required
        /// </summary>
        public void ValidateMessageList()
        {
            if (displayMessageList == null) { displayMessageList = new List<DisplayMessage>(1); }
        }

        #endregion

        #region Public API Methods - Targets

        /// <summary>
        /// Add a new Display Target to the HUD. By design, they are not visible at runtime when first added.
        /// </summary>
        /// <param name="guidHashDisplayReticle"></param>
        /// <returns></returns>
        public DisplayTarget AddTarget(int guidHashDisplayReticle)
        {
            DisplayTarget displayTarget = null;

            if (GetHUDPanel() == null)
            {
                #if UNITY_EDITOR
                Debug.LogWarning("ERROR: ShipDisplayModule.AddTarget() - could not find HUDPanel for the HUD. Did you use the prefab from Prefabs/Visuals folder?");
                #endif
            }
            else
            {
                displayTarget = new DisplayTarget();
                if (displayTarget != null)
                {
                    displayTarget.guidHashDisplayReticle = guidHashDisplayReticle;

                    ValidateTargetList();
                    displayTargetList.Add(displayTarget);

                    numDisplayTargets = displayTargetList.Count;

                    RectTransform targetSlotPanel = CreateTargetPanel(displayTarget, 0);
                    if (targetSlotPanel != null)
                    {
                        #if UNITY_EDITOR
                        // Make sure we can rollback the creation of the new target panel for this slot
                        UnityEditor.Undo.RegisterCreatedObjectUndo(targetSlotPanel.gameObject, string.Empty);
                        #endif
                    }
                }
            }
            return displayTarget;
        }

        /// <summary>
        /// Add another DisplayTarget slot to a DisplayTarget. This allows you to display another
        /// copy of the target on the HUD.
        /// If the DisplayTarget has not been initialised, the new slot panel will be added to the
        /// scene but this method will return null.
        /// This automatically updates displayTarget.maxNumberOfTargets.
        /// </summary>
        /// <param name="displayTarget"></param>
        /// <param name="numberToAdd"></param>
        /// <returns></returns>
        public DisplayTargetSlot AddTargetSlots(DisplayTarget displayTarget, int numberToAdd)
        {
            DisplayTargetSlot displayTargetSlot = null;

            if (displayTarget != null)
            {
                if (displayTarget.maxNumberOfTargets + numberToAdd <= DisplayTarget.MAX_DISPLAYTARGET_SLOTS)
                {
                    int numberAdded = 0;

                    // slotIndex is zero-based so start to 1 beyond the end of the existing list
                    for (int slotIndex = displayTarget.maxNumberOfTargets; slotIndex < displayTarget.maxNumberOfTargets + numberToAdd; slotIndex++)
                    {
                        RectTransform targetSlotPanel = CreateTargetPanel(displayTarget, slotIndex);

                        // If the DisplayTarget has already been initialised, this slot will need to be initialised
                        if (targetSlotPanel != null)
                        {
                            numberAdded++;

                            #if UNITY_EDITOR
                            // Make sure we can rollback the creation of the new target panel for this slot
                            UnityEditor.Undo.RegisterCreatedObjectUndo(targetSlotPanel.gameObject, string.Empty);
                            #endif

                            if (displayTarget.isInitialised)
                            {
                                displayTargetSlot = InitialiseTargetSlot(displayTarget, slotIndex);
                                displayTarget.displayTargetSlotList.Add(displayTargetSlot);

                                // Refresh the size of the slot reticle panel
                                #if UNITY_EDITOR
                                if (editorMode) { CheckHUDResize(false); }
                                #endif

                                RectTransform dtRectTfrm = displayTargetSlot.CachedTargetPanel;
                                if (dtRectTfrm != null)
                                {
                                    float pixelWidth = (float)displayTarget.width * screenResolution.x / refResolution.x;
                                    float pixelHeight = (float)displayTarget.height * screenResolution.y / refResolution.y;

                                    // Target size is in pixels - this will result in non-square scaling.
                                    // (Future) provide an option to keep aspect ratio of original 64x64 reticle
                                    dtRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, pixelWidth);
                                    dtRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, pixelHeight);
                                }

                                // Refresh brightness of this slot
                                if (displayTargetSlot.CachedImgComponent != null)
                                {
                                    if (brightness == 1f) { displayTargetSlot.CachedImgComponent.color = displayTarget.reticleColour; }
                                    else { displayTargetSlot.CachedImgComponent.color = displayTarget.baseReticleColour.GetColorWithBrightness(brightness); }
                                }
                            }
                        }
                    }
                    displayTarget.maxNumberOfTargets += numberAdded;
                }
                #if UNITY_EDITOR
                else { Debug.LogWarning("WARNING: shipDisplayModule.AddTargetSlot - not enough empty DisplayTarget slots"); }
                #endif
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.AddTargetSlot - displayTarget is null"); }
            #endif

            return displayTargetSlot;
        }

        /// <summary>
        /// Delete a target from the HUD.
        /// NOTE: It is much cheaper to HideDisplayTarget(..) than completely remove it.
        /// </summary>
        /// <param name="guidHash"></param>
        public void DeleteTarget(int guidHash)
        {
            #if UNITY_EDITOR
            bool isRecordUndo = !Application.isPlaying;
            int undoGroup=0;
            #else
            bool isRecordUndo = false;
            #endif

            DisplayTarget displayTarget = GetDisplayTarget(guidHash);

            List<RectTransform> tgtRectTfrmList = null;

            // Get a list of DisplayTarget slot panels
            if (displayTarget != null)
            {
                tgtRectTfrmList = new List<RectTransform>(displayTarget.maxNumberOfTargets);
                for (int sIdx = 0; sIdx < displayTarget.maxNumberOfTargets; sIdx++)
                {
                    RectTransform tgtRectTfrm = GetTargetPanel(guidHash, sIdx);
                    if (tgtRectTfrm != null)
                    {
                        tgtRectTfrmList.Add(tgtRectTfrm);
                    }
                }
            }

            int _numPanels = tgtRectTfrmList == null ? 0 : tgtRectTfrmList.Count;
            int _tgtIndex = GetDisplayTargetIndex(guidHash);

            // Only record undo if in the editor AND is not playing AND something needs to be recorded
            isRecordUndo = isRecordUndo && (_numPanels > 0 || _tgtIndex >= 0);

            #if UNITY_EDITOR
            if (isRecordUndo)
            {
                UnityEditor.Undo.SetCurrentGroupName("Delete Display Target " + (_tgtIndex + 1).ToString());
                undoGroup = UnityEditor.Undo.GetCurrentGroup();

                if (_tgtIndex >= 0)
                {
                    UnityEditor.Undo.RecordObject(this, string.Empty);
                }
            }
            #endif

            // Check that the display target exists and it is within the list constraints
            // NOTE: The script element must be modified BEFORE the Undo.DestroyObjectImmediate is called
            // in order for Undo to correctly undo both the target change AND the destroy of the panels.
            if (_tgtIndex >= 0 && _tgtIndex < (displayTargetList == null ? 0 : displayTargetList.Count))
            {
                displayTargetList.RemoveAt(_tgtIndex);
                numDisplayTargets = displayTargetList.Count;
            }

            if (_numPanels > 0)
            {
                if (Application.isPlaying)
                {
                    // Destroy all the slot panels
                    for (int sIdx = _numPanels - 1; sIdx >= 0; sIdx--)
                    {
                        Destroy(tgtRectTfrmList[sIdx].gameObject);
                        tgtRectTfrmList.RemoveAt(sIdx);
                    }
                }
                else
                {
                    // Destroy all the slot panels
                    for (int sIdx = _numPanels - 1; sIdx >= 0; sIdx--)
                    {
                        #if UNITY_EDITOR
                        UnityEditor.Undo.DestroyObjectImmediate(tgtRectTfrmList[sIdx].gameObject);
                        #else
                        DestroyImmediate(tgtRectTfrmList[sIdx].gameObject);
                        #endif
                        tgtRectTfrmList.RemoveAt(sIdx);
                    }
                }
            }

            #if UNITY_EDITOR
            if (isRecordUndo)
            {               
                UnityEditor.Undo.CollapseUndoOperations(undoGroup);
            }
            #endif
        }

        /// <summary>
        /// Delete or remove a DisplayTarget slot from the HUD. This is an expensive operation.
        /// It is much cheaper to HideDisplayTargetSlot(..) than completely remove it.
        /// Automatically updates displayTarget.maxNumberOfTargets.
        /// NOTE: You cannot remove slot 0.
        /// </summary>
        /// <param name="guidHash">guidHash of the DisplayTarget</param>
        /// <returns></returns>
        public void DeleteTargetSlots(int guidHash, int numberToDelete)
        {
            DisplayTarget displayTarget = GetDisplayTarget(guidHash);

            if (displayTarget != null)
            {
                int remainingTargets = displayTarget.maxNumberOfTargets - numberToDelete;

                if (remainingTargets > 0)
                {
                    #if UNITY_EDITOR
                    bool isRecordUndo = !Application.isPlaying;
                    int undoGroup=0;
                    #else
                    bool isRecordUndo = false;
                    #endif

                    List<RectTransform> tgtRectTfrmList = null;

                    // Get a list of DisplayTarget slot panels to remove
                    if (displayTarget != null)
                    {
                        tgtRectTfrmList = new List<RectTransform>(displayTarget.maxNumberOfTargets);
                        for (int sIdx = remainingTargets; sIdx < displayTarget.maxNumberOfTargets; sIdx++)
                        {
                            RectTransform tgtRectTfrm = GetTargetPanel(guidHash, sIdx);
                            if (tgtRectTfrm != null)
                            {
                                tgtRectTfrmList.Add(tgtRectTfrm);
                            }
                        }
                    }

                    int _numPanels = tgtRectTfrmList == null ? 0 : tgtRectTfrmList.Count;
                    int _tgtIndex = GetDisplayTargetIndex(guidHash);

                    // Only record undo if in the editor AND is not playing AND something needs to be recorded
                    isRecordUndo = isRecordUndo && (_numPanels > 0 || _tgtIndex >= 0);

                    #if UNITY_EDITOR
                    if (isRecordUndo)
                    {
                        UnityEditor.Undo.SetCurrentGroupName("Delete Display Target " + (_tgtIndex + 1).ToString() + " slots");
                        undoGroup = UnityEditor.Undo.GetCurrentGroup();

                        if (_tgtIndex >= 0)
                        {
                            UnityEditor.Undo.RecordObject(this, string.Empty);
                        }
                    }
                    #endif

                    // NOTE: The script element must be modified BEFORE the Undo.DestroyObjectImmediate is called
                    // in order for Undo to correctly undo both the target change AND the destroy of the panels.
                    if (displayTarget.isInitialised)
                    {
                        for (int sIdx = displayTarget.maxNumberOfTargets - 1; sIdx >= remainingTargets; sIdx--)
                        {
                            if (sIdx < displayTarget.displayTargetSlotList.Count)
                            {
                                displayTarget.displayTargetSlotList[sIdx] = null;
                                displayTarget.displayTargetSlotList.RemoveAt(sIdx);
                            }
                            #if UNITY_EDITOR
                            else { Debug.LogWarning("ERROR: shipDisplayModule.DeleteTargetSlot - tried to remove initialised slot " + sIdx + " but only " + displayTarget.displayTargetSlotList.Count + " exist."); }
                            #endif
                        }
                    }

                    displayTarget.maxNumberOfTargets = remainingTargets;

                    if (_numPanels > 0)
                    {
                        if (Application.isPlaying)
                        {
                            // Destroy the slot panels
                            for (int sIdx = _numPanels - 1; sIdx >= 0; sIdx--)
                            {
                                Destroy(tgtRectTfrmList[sIdx].gameObject);
                                tgtRectTfrmList.RemoveAt(sIdx);
                            }
                        }
                        else
                        {
                            // Destroy the slot panels
                            for (int sIdx = _numPanels - 1; sIdx >= 0; sIdx--)
                            {
                                #if UNITY_EDITOR
                                UnityEditor.Undo.DestroyObjectImmediate(tgtRectTfrmList[sIdx].gameObject);
                                #else
                                DestroyImmediate(tgtRectTfrmList[sIdx].gameObject);
                                #endif
                                tgtRectTfrmList.RemoveAt(sIdx);
                            }
                        }
                    }

                    #if UNITY_EDITOR
                    if (isRecordUndo)
                    {               
                        UnityEditor.Undo.CollapseUndoOperations(undoGroup);
                    }
                    #endif

                }
                #if UNITY_EDITOR
                else { Debug.LogWarning("WARNING: shipDisplayModule.DeleteTargetSlot - cannot delete that many DisplayTarget slots"); }
                #endif
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.DeleteTargetSlot - displayTarget cannot be found with guidHash: " + guidHash); }
            #endif
        }

        /// <summary>
        /// Assign a radar target to a DisplayTarget's slot.
        /// See also GetAssignedDisplayTarget(..)
        /// </summary>
        /// <param name="displayTargetIndex"></param>
        /// <param name="instanceIndex"></param>
        /// <param name="radarItemIndex"></param>
        /// <param name="radarItemSequenceNumber"></param>
        public void AssignDisplayTargetSlot(int displayTargetIndex, int slotIndex, int radarItemIndex, uint radarItemSequenceNumber, bool isAutoShow)
        {
            if (displayTargetIndex >= 0 && displayTargetIndex < GetNumberDisplayTargets)
            {
                DisplayTarget displayTarget = displayTargetList[displayTargetIndex];
                if (displayTarget != null && slotIndex >= 0 && slotIndex < displayTarget.maxNumberOfTargets)
                {
                    DisplayTargetSlot displayTargetSlot = displayTarget.displayTargetSlotList[slotIndex];

                    displayTargetSlot.radarItemKey.radarItemIndex = radarItemIndex;
                    displayTargetSlot.radarItemKey.radarItemSequenceNumber = radarItemSequenceNumber;

                    if (isAutoShow)
                    {
                        // Should the DisplayTarget be shown?
                        if (radarItemIndex >= 0)
                        {
                            // If not already shown, turn it on.
                            if (!displayTargetSlot.showTargetSlot)
                            {
                                ShowOrHideTargetSlot(displayTargetSlot, true);
                                // If one slot is on, mark the displayTarget as shown also.
                                displayTarget.showTarget = true;
                            }
                        }
                        // If should not be shown, but currently is, turn it off
                        else if (displayTargetSlot.showTargetSlot) { ShowOrHideTargetSlot(displayTargetSlot, false); }
                    }
                }
            }
        }

        /// <summary>
        /// Get the radarItemKey for a DisplayTarget's slot.
        /// SSCRadarItemKey.radarItemIndex will be -1 if there is no target.
        /// </summary>
        /// <param name="displayTargetIndex"></param>
        /// <param name="slotIndex"></param>
        /// <returns></returns>
        public SSCRadarItemKey GetAssignedDisplayTarget(int displayTargetIndex, int slotIndex)
        {
            if (displayTargetIndex >= 0 && displayTargetIndex < GetNumberDisplayTargets)
            {
                DisplayTarget displayTarget = displayTargetList[displayTargetIndex];
                if (displayTarget != null && slotIndex >= 0 && slotIndex < displayTarget.maxNumberOfTargets)
                {
                    return displayTarget.displayTargetSlotList[slotIndex].radarItemKey;
                }
                else
                {
                    return new SSCRadarItemKey() { radarItemIndex = -1, radarItemSequenceNumber = 0 };
                }
            }
            else { return new SSCRadarItemKey() { radarItemIndex = -1, radarItemSequenceNumber = 0 }; }
        }

        /// <summary>
        /// Get the SSCRadarItem currently assigned to a DisplayTarget slot (instance).
        /// </summary>
        /// <param name="displayTargetSlot"></param>
        /// <returns></returns>
        public SSCRadarItem GetAssignedDisplayTargetRadarItem (DisplayTargetSlot displayTargetSlot)
        {
            if (displayTargetSlot != null && displayTargetSlot.radarItemKey.radarItemIndex >= 0 && sscRadar != null)
            {
                return sscRadar.GetRadarItem(displayTargetSlot.radarItemKey.radarItemIndex);
            }
            else { return null; }
        }

        /// <summary>
        /// Returns the guidHash of the Target in the list given the index or
        /// zero-based position in the list. Will return 0 if no matching Target
        /// is found.
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        public int GetDisplayTargetGuidHash(int index)
        {
            int guidHash = 0;

            if (index >= 0 && index < GetNumberDisplayTargets)
            {
                if (displayTargetList[index] != null) { guidHash = displayTargetList[index].guidHash; }
            }

            return guidHash;
        }

        /// <summary>
        /// Get the zero-based index of the Target in the list. Will return -1 if not found.
        /// </summary>
        /// <param name="guidHash"></param>
        /// <returns></returns>
        public int GetDisplayTargetIndex(int guidHash)
        {
            int index = -1;

            int _numDisplayTargets = GetNumberDisplayTargets;

            for (int dmIdx = 0; dmIdx < _numDisplayTargets; dmIdx++)
            {
                if (displayTargetList[dmIdx] != null && displayTargetList[dmIdx].guidHash == guidHash) { index = dmIdx; break; }
            }

            return index;
        }

        /// <summary>
        /// Get a DisplayTarget given its guidHash.
        /// See also GetDisplayTargetGuidHash(..).
        /// Will return null if guidHash parameter is 0, it cannot be found.
        /// </summary>
        /// <param name="guidHash"></param>
        /// <returns></returns>
        public DisplayTarget GetDisplayTarget(int guidHash)
        {
            DisplayTarget displayTarget = null;
            DisplayTarget _tempDM = null;

            int _numDisplayTargets = GetNumberDisplayTargets;

            for (int dmIdx = 0; dmIdx < _numDisplayTargets; dmIdx++)
            {
                _tempDM = displayTargetList[dmIdx];
                if (_tempDM != null && _tempDM.guidHash == guidHash)
                {
                    displayTarget = _tempDM;
                    break;
                }
            }

            return displayTarget;
        }

        /// <summary>
        /// Get a DisplayTarget given a zero-based index in the list.
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        public DisplayTarget GetDisplayTargetByIndex(int index)
        {
            if (index >= 0 && index < GetNumberDisplayTargets)
            {
                return displayTargetList[index];
            }
            else { return null; }
        }

        /// <summary>
        /// Get the (sprite) name of the DisplayTarget.
        /// WARNING: This will create GC, so not recommended to be called each frame.
        /// This is typically used for debugging purposes only.
        /// </summary>
        /// <param name="displayTarget"></param>
        /// <returns></returns>
        public string GetDisplayTargetName(DisplayTarget displayTarget)
        {
            if (displayTarget != null && displayReticleList != null)
            {
                int _selectedIdx = displayReticleList.FindIndex(dr => dr.guidHash == displayTarget.guidHashDisplayReticle);

                if (_selectedIdx >= 0)
                {
                    string tempDisplayReticleName = displayReticleList[_selectedIdx].primarySprite == null ? "no texture in reticle" : displayReticleList[_selectedIdx].primarySprite.name;
                    return string.IsNullOrEmpty(tempDisplayReticleName) ? "no texture name" : tempDisplayReticleName;
                }
                else { return "--"; }
            }
            else { return "--"; }
        }

        /// <summary>
        /// Get the (sprite) name of the DisplayTarget.
        /// WARNING: This will create GC, so not recommended to be called each frame.
        /// This is typically used for debugging purposes only.
        /// </summary>
        /// <param name="index"></param>
        /// <returns></returns>
        public string GetDisplayTargetName(int index)
        {
            return GetDisplayTargetName(GetDisplayTargetByIndex(index));            
        }

        /// <summary>
        /// Get a DisplayTargetSlot given the zero-based slot index. This is an instance of a DisplayTarget.
        /// </summary>
        /// <param name="displayTarget"></param>
        /// <param name="slotIndex"></param>
        /// <returns></returns>
        public DisplayTargetSlot GetDisplayTargetSlotByIndex (DisplayTarget displayTarget, int slotIndex)
        {
            if (IsInitialised && displayTarget != null && slotIndex > -1 && slotIndex < displayTarget.maxNumberOfTargets)
            {
                return displayTarget.displayTargetSlotList[slotIndex];
            }
            else
            {
                return null;
            }
        }

        /// <summary>
        /// Show the Display Target slots on the HUD. By design, if the HUD is not shown, the Targets will not be show.
        /// </summary>
        /// <param name="guidHash"></param>
        public void ShowDisplayTarget(int guidHash)
        {
            DisplayTarget displayTarget = GetDisplayTarget(guidHash);
            if (displayTarget != null) { ShowOrHideTarget(displayTarget, true); }
            #if UNITY_EDITOR
            else { Debug.LogWarning("WARNING: ShowDisplayTarget - Could not find displayTarget with guidHash: " + guidHash); }
            #endif
        }

        /// <summary>
        /// Show the Display Target slots on the HUD. By design, if the HUD is not shown, the Targets will not be show.
        /// </summary>
        /// <param name="displayTarget"></param>
        public void ShowDisplayTarget(DisplayTarget displayTarget)
        {
            if (displayTarget != null) { ShowOrHideTarget(displayTarget, true); }
            #if UNITY_EDITOR
            else { Debug.LogWarning("WARNING: ShowDisplayTarget - displayTarget parameter was null"); }
            #endif
        }

        /// <summary>
        /// Show the Display Target slot on the HUD. By design, if the HUD is not shown, the Target in this slot will not be show.
        /// </summary>
        /// <param name="displayTargetSlot"></param>
        public void ShowDisplayTargetSlot(DisplayTargetSlot displayTargetSlot)
        {
            if (displayTargetSlot != null) { ShowOrHideTargetSlot(displayTargetSlot, true); }
            #if UNITY_EDITOR
            else { Debug.LogWarning("WARNING: ShowDisplayTargetSlot - displayTargetSlot parameter was null"); }
            #endif
        }

        /// <summary>
        /// Show or turn on all Display Targets.
        /// ShipDisplayModule must be initialised.
        /// </summary>
        public void ShowDisplayTargets()
        {
            ShowOrHideTargets(true);
        }

        /// <summary>
        /// Hide or turn off all slots the Display Target
        /// </summary>
        /// <param name="guidHash"></param>
        public void HideDisplayTarget(int guidHash)
        {
            DisplayTarget displayTarget = GetDisplayTarget(guidHash);
            if (displayTarget != null) { ShowOrHideTarget(displayTarget, false); }
            #if UNITY_EDITOR
            else { Debug.LogWarning("WARNING: ShowDisplayTarget - Could not find message with guidHash: " + guidHash); }
            #endif
        }

        /// <summary>
        /// Hide or turn off all slots of the Display Target
        /// </summary>
        /// <param name="displayTarget"></param>
        public void HideDisplayTarget(DisplayTarget displayTarget)
        {
            if (displayTarget != null) { ShowOrHideTarget(displayTarget, false); }
            #if UNITY_EDITOR
            else { Debug.LogWarning("WARNING: ShowDisplayTarget - Could not find message - parameter was null"); }
            #endif
        }

        /// <summary>
        /// Hide the Display Target slot on the HUD. By design, if the HUD is not shown, the Target in this slot will not be show.
        /// </summary>
        /// <param name="displayTargetSlot"></param>
        public void HideDisplayTargetSlot(DisplayTargetSlot displayTargetSlot)
        {
            if (displayTargetSlot != null) { ShowOrHideTargetSlot(displayTargetSlot, false); }
            #if UNITY_EDITOR
            else { Debug.LogWarning("WARNING: HideDisplayTargetSlot - displayTargetSlot parameter was null"); }
            #endif
        }

        /// <summary>
        /// Hide or turn off all all slots of all Display Targets.
        /// ShipDisplayModule must be initialised.
        /// </summary>
        public void HideDisplayTargets()
        {
            ShowOrHideTargets(false);
        }

        /// <summary>
        /// Given a radar query, add rules based on the current DisplayTargets.
        /// TODO - WARNING - NEED TO PERFORMANCE TEST THIS
        /// </summary>
        public void PopulateTargetingRadarQuery(SSCRadarQuery sscRadarQuery)
        {
            // Build a common list of factions and squadrons to include
            if (IsInitialised)
            {
                int _numDisplayTargets = GetNumberDisplayTargets;

                if (_numDisplayTargets > 0)
                {
                    tempIncludeFactionList.Clear();
                    tempIncludeSquadronList.Clear();

                    for (int dtIdx = 0; dtIdx < _numDisplayTargets; dtIdx++)
                    {
                        DisplayTarget _tempDT = displayTargetList[dtIdx];

                        if (_tempDT != null)
                        {
                            int _numFactionsToInclude = _tempDT.factionsToInclude == null ? 0 : _tempDT.factionsToInclude.Length;
                            int _numSquadronsToInclude = _tempDT.squadronsToInclude == null ? 0 : _tempDT.squadronsToInclude.Length;

                            // By default there is no exclude
                            sscRadarQuery.factionsToExclude = null;
                            sscRadarQuery.squadronsToExclude = null;

                            // Determine which Factions (if any) to include as a filter
                            for (int fIdx = 0; fIdx < _numFactionsToInclude; fIdx++)
                            {
                                // If faction is not already in list add it.
                                int _factionId = _tempDT.factionsToInclude[fIdx];
                                if (tempIncludeFactionList.FindIndex(f => f == _factionId) < 0)
                                {
                                    tempIncludeFactionList.Add(_factionId);
                                }
                            }

                            // Determine which Squadrons (if any) to include as a filter
                            for (int sqIdx = 0; sqIdx < _numSquadronsToInclude; sqIdx++)
                            {
                                // If squadron is not already in list add it.
                                int _squadronId = _tempDT.squadronsToInclude[sqIdx];
                                if (tempIncludeSquadronList.FindIndex(sq => sq == _squadronId) < 0)
                                {
                                    tempIncludeSquadronList.Add(_squadronId);
                                }
                            }

                            if (tempIncludeFactionList.Count > 0)
                            {
                                // Check if this generates work for GC
                                sscRadarQuery.factionsToInclude = tempIncludeFactionList.ToArray();
                            }
                            else { sscRadarQuery.factionsToInclude = null; }

                            if (tempIncludeSquadronList.Count > 0)
                            {
                                // Check if this generates work for GC
                                sscRadarQuery.squadronsToInclude = tempIncludeSquadronList.ToArray();
                            }
                            else { sscRadarQuery.squadronsToInclude = null; }
                        }
                    }
                }

                sscRadarQuery.range = targetingRange;
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: PopulateTargetingRadarQuery - shipDisplayModule is not initialised"); }
            #endif
        }

        /// <summary>
        /// Set or change the Reticle assigned to a DisplayTarget.
        /// The Reticle must belong to the list of available reticles for the HUD.
        /// </summary>
        /// <param name="guidHash"></param>
        /// <param name="guidHashDisplayReticle"></param>
        public void SetDisplayTargetReticle(int guidHash, int guidHashDisplayReticle)
        {
            if (IsInitialised || editorMode)
            {
                DisplayTarget displayTarget = GetDisplayTarget(guidHash);

                if (displayTarget != null)
                {
                    displayTarget.guidHashDisplayReticle = guidHashDisplayReticle;

                    DisplayReticle _displayReticle = GetDisplayReticle(guidHashDisplayReticle);

                    // Set the reticle for all slots of this DisplayTarget
                    for (int sIdx = 0; sIdx < displayTarget.maxNumberOfTargets; sIdx++)
                    {
                        if (displayTarget.displayTargetSlotList[sIdx].CachedImgComponent != null)
                        {
                            displayTarget.displayTargetSlotList[sIdx].CachedImgComponent.sprite = _displayReticle == null ? null : _displayReticle.primarySprite;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Set the Display Target Reticle colour. Only update the colour if it has actually changed.
        /// If the module has been initialised, this will also re-colour the reticle with the appropriate brightness.
        /// </summary>
        /// <param name="newColour"></param>
        public void SetDisplayTargetReticleColour(DisplayTarget displayTarget, Color newColour)
        {
            if (displayTarget != null)
            {
                SSCUtils.UpdateColour(ref newColour, ref displayTarget.reticleColour, ref displayTarget.baseReticleColour, true);

                SetDisplayTargetBrightness(displayTarget);
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayTargetReticleColour - displayTarget is null"); }
            #endif
        }

        /// <summary>
        /// Set the offset (position) of the Display Target slot on the HUD.
        /// If the module has been initialised, this will also re-position the Display Target.
        /// </summary>
        /// <param name="displayTarget"></param>
        /// <param name="slotIndex"></param>
        /// <param name="offsetX"></param>
        /// <param name="offsetY"></param>
        public void SetDisplayTargetOffset(DisplayTarget displayTarget, int slotIndex, float offsetX, float offsetY)
        {
            if (displayTarget != null)
            {
                int numSlots = displayTarget.displayTargetSlotList == null ? 0 : displayTarget.displayTargetSlotList.Count;

                if (slotIndex >= 0 & slotIndex < numSlots)
                {
                    SetDisplayTargetOffset(displayTarget.displayTargetSlotList[slotIndex], offsetX, offsetY);
                }
                #if UNITY_EDITOR
                else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayTargetOffset - the displayTarget slot is out-of-range (" + slotIndex + ")"); }
                #endif
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayTargetOffset - displayTarget is null"); }
            #endif
        }

        /// <summary>
        /// Set the offset (position) of the Display Target slot on the HUD.
        /// If the module has been initialised, this will also re-position the Display Target.
        /// </summary>
        /// <param name="displayTargetSlot"></param>
        /// <param name="offsetX">Horizontal offset from centre. Range between -1 and 1</param>
        /// <param name="offsetY">Vertical offset from centre. Range between -1 and 1</param>
        public void SetDisplayTargetOffset(DisplayTargetSlot displayTargetSlot, float offsetX, float offsetY)
        {
            if (displayTargetSlot != null)
            {
                // Verify the x,y values are within range -1 to 1.
                if (offsetX < -1f) { displayTargetSlot.offsetX = -1f; }
                else if (offsetX > 1f) { displayTargetSlot.offsetX = 1f; }
                else
                {
                    displayTargetSlot.offsetX = offsetX;
                }

                if (offsetY < -1f) { displayTargetSlot.offsetY = -1f; }
                else if (offsetY > 1f) { displayTargetSlot.offsetY = 1f; }
                else
                {
                    displayTargetSlot.offsetY = offsetY;
                }

                if (IsInitialised || editorMode)
                {
                    Transform dtgtTrfm = displayTargetSlot.CachedTargetPanel.transform;

                    #if UNITY_EDITOR
                    if (editorMode)
                    {
                        CheckHUDResize(false);
                        UnityEditor.Undo.RecordObject(dtgtTrfm, string.Empty);
                    }
                    #endif

                    // Use the scaled HUD size. This works in and out of play mode
                    tempTargetOffset.x = displayTargetSlot.offsetX * cvsResolutionHalf.x;
                    tempTargetOffset.y = displayTargetSlot.offsetY * cvsResolutionHalf.y;
                    tempTargetOffset.z = dtgtTrfm.localPosition.z;

                    dtgtTrfm.localPosition = tempTargetOffset;
                }
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayTargetOffset - displayTargetSlot is null"); }
            #endif
        }

        /// <summary>
        /// Move the DisplayTarget Slot to the correct 2D position on the HUD, based on a 3D world space position.
        /// If the camera has not been automatically or manually assigned, the DisplayTarget will not be moved.
        /// </summary>
        /// <param name="displayTarget"></param>
        /// <param name="slotIndex"></param>
        /// <param name="wsPosition"></param>
        public void SetDisplayTargetPosition(DisplayTarget displayTarget, int slotIndex, Vector3 wsPosition)
        {
            if (isMainCameraAssigned)
            {
                if (displayTarget != null)
                {
                    Vector3 screenPosition = mainCamera.WorldToScreenPoint(wsPosition);

                    // NOTE: cvsResolutionFull.x / cvsScaleFactor.x is NOT the same as Screen.width
                    // x 2 and subtracting 1 moves it into our HUD -1 to 1 space.
                    SetDisplayTargetOffset(displayTarget, slotIndex, 2f * screenPosition.x / cvsResolutionFull.x / cvsScaleFactor.x - 1f, 2f * screenPosition.y / cvsResolutionFull.y / cvsScaleFactor.y - 1f);
                }
                #if UNITY_EDITOR
                else { Debug.LogWarning("ERROR: shipDisplayModule.SetDisplayTargetPosition - displayTarget is null"); }
                #endif
            }
            #if UNITY_EDITOR
            else { Debug.LogWarning("ERROR: ShipDisplayModule.SetDisplayTargetPosition - Could not find mainCamera."); }
            #endif
        }

        /// <summary>
        /// Set the size of the Target Reticle in each slot.
        /// If the module has been initialised, this will also resize the Target Reticle.
        /// The values are only updated if they are outside the range 8 to 256 or have changed.
        /// The size is based on a screen resolution of 1920x1080
        /// </summary>
        /// <param name="displayTarget"></param>
        /// <param name="width">Range between 8 and 256</param>
        /// <param name="height">Range between 8 and 256</param>
        public void SetDisplayTargetSize(DisplayTarget displayTarget, int width, int height)
        {
            // Clamp the x,y values 8 to 256
            if (width < 8) { displayTarget.width = 8; }
            else if (width > 256) { displayTarget.width = 256; }
            else if (width != displayTarget.width) { displayTarget.width = width; }

            if (height < 8) { displayTarget.height = 8; }
            else if (height > 256) { displayTarget.height = 256; }
            else if (height != displayTarget.height) { displayTarget.height = height; }

            if (IsInitialised || editorMode)
            {
                #if UNITY_EDITOR
                if (editorMode) { CheckHUDResize(false); }
                #endif

                float pixelWidth = (float)displayTarget.width * screenResolution.x / refResolution.x;
                float pixelHeight = (float)displayTarget.height * screenResolution.y / refResolution.y;

                // Set the size of all the reticles in the DisplayTarget slots
                // There can be multiple copies or slots of the same DisplayTarget.
                for (int sIdx = 0; sIdx < displayTarget.maxNumberOfTargets; sIdx++)
                {
                    RectTransform dtRectTfrm = displayTarget.displayTargetSlotList[sIdx].CachedTargetPanel;

                    if (dtRectTfrm != null)
                    {
                        // Target size is in pixels - this will result in non-square scaling.
                        // (Future) provide an option to keep aspect ratio of original 64x64 reticle
                        dtRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, pixelWidth);
                        dtRectTfrm.SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, pixelHeight);
                    }
                }
            }
        }

        /// <summary>
        /// Sets the clipped viewable area of the screen that DisplayTargets can be shown. When Targets
        /// are outside this area, they will be hidden.
        /// See also SetTargetsViewportOffset(..).
        /// </summary>
        /// <param name="width">Range between 0.1 and 1.0</param>
        /// <param name="height">Range between 0.1 and 1.0</param>
        public void SetTargetsViewportSize(float width, float height)
        {
            // Currently we just set the values after basic validation. This may change in future versions

            // Perform basic validation
            if (width >= 0.1f && width <= 1.0f) { targetsViewWidth = width; }
            if (height >= 0.1f && height <= 1.0f) { targetsViewHeight = height; }
        }

        /// <summary>
        /// Sets the Targets viewport offset from the centre of the screen.
        /// Values can be between -1.0 and 1.0 with 0,0 depicting the centre of the screen.
        /// See also SetTargetsViewportSize(..).
        /// </summary>
        /// <param name="offsetX">Range between -1.0 and 1.0</param>
        /// <param name="offsetY">Range between -1.0 and 1.0</param>
        public void SetTargetsViewportOffset(float offsetX, float offsetY)
        {
            // Currently we just set the values after basic validation. This may change in future versions

            if (offsetX >= -1f && offsetX <= 1.0f) { targetsViewOffsetX = offsetX; }
            if (offsetY >= -1f && offsetY <= 1.0f) { targetsViewOffsetY = offsetY; }
        }

        /// <summary>
        /// After adding or moving DisplayTargets, they may need to be sorted to
        /// have the correct z-order in on the HUD.
        /// </summary>
        public void RefreshTargetsSortOrder()
        {
            if (targetsRectTfrm != null)
            {
                // Targets should begin at index 0.
                int zIndex = -1;

                // Update number of DisplayTargets. This can help issues in the Editor, like moving DislayTargets
                // up or down in the list while in play mode.
                numDisplayTargets = displayTargetList == null ? 0 : displayTargetList.Count;

                for (int dtIdx = 0; dtIdx < numDisplayTargets; dtIdx++)
                {
                    DisplayTarget displayTarget = displayTargetList[dtIdx];

                    if (displayTarget != null)
                    {
                        //Debug.Log("[DEBUG] dt: " + dtIdx + " maxNum: " + displayTarget.maxNumberOfTargets + " actual: " + displayTarget.displayTargetSlotList.Count);

                        // Loop through all the slots for this DisplayTarget
                        for (int sIdx = 0; sIdx < displayTarget.maxNumberOfTargets; sIdx++)
                        {
                            RectTransform _rt = displayTarget.displayTargetSlotList[sIdx].CachedTargetPanel;

                            if (_rt != null)
                            {
                                _rt.SetSiblingIndex(++zIndex);
                            }
                        }
                    }
                }
            }
        }

         /// <summary>
        /// Create a new list if required
        /// </summary>
        public void ValidateTargetList()
        {
            if (displayTargetList == null) { displayTargetList = new List<DisplayTarget>(1); }
        }

        #endregion
    }
}