using System.Collections;
using System.Collections.Generic;
using UnityEngine;
// Sci-Fi Ship Controller. Copyright (c) 2018-2023 SCSM Pty Ltd. All rights reserved.
namespace SciFiShipController
{
///
/// A centralised futuristic radar system based on Automatic Dependent Surveillance - Broadcast (ADS-B).
/// Instead of the radar system doing a sweep of the environment, craft which need situational
/// awareness broadcast (send) data to the central radar system.
/// This system will be the Primary Radar System (PRS).
/// This implementation requires all craft to query the radar system (incoming broadcasts are private
/// and don't get sent to every craft - the radar system receives private msgs from ships).
///
// [AddComponentMenu("Sci-Fi Ship Controller/Managers/Radar")]
[HelpURL("http://scsmmedia.com/ssc-documentation")]
public class SSCRadar : MonoBehaviour
{
#region Static Read-only valiables
public static readonly int NEUTRAL_FACTION = 0;
#endregion
#region Enumerations
public enum RadarScreenLocale : int
{
TopLeft = 0,
TopCenter = 1,
TopRight = 2,
MiddleLeft = 3,
MiddleCenter = 4,
MiddleRight = 5,
BottomLeft = 6,
BottomCenter = 7,
BottomRight = 8,
Custom = 99
};
#endregion
#region Public variables and properties
///
/// If enabled, the GetOrCreateRadar() will be called as soon as Start() runs. If there is a UI (mini-map) configured,
/// it will automatically be made visible. This should be disabled if you are instantiating the SSCRadar through code
/// and using the SSCRadar API methods.
///
public bool initialiseOnStart = false;
///
/// [READONLY] Has the radar been initialised?
///
public bool IsInitialised { get { return isInitialised; } }
public int poolInitialSize = 100;
///
/// [INTERNAL USE ONLY]
///
public int poolIncrementSize = 10;
///
/// [INTERNAL USE ONLY]
///
public bool allowRepaint = false;
public bool generalShowInEditor = false;
public bool visualsShowInEditor = false;
public bool movementShowInEditor = false;
public RadarScreenLocale screenLocale = RadarScreenLocale.TopLeft;
public Vector2 screenLocaleCustomXY = Vector2.zero;
// Normalised width as 0.0-1.0 as a proportion of the screen space.
// e.g. 0.2 would be 20% of the screen width.
[Range(0.1f, 1f)] public float radarDisplayWidthN = 0.2f;
///
/// The sort order of the canvas in the scene. Higher numbers are on top.
/// At runtime call SetCanvasSortOrder(..)
///
public int canvasSortOrder = 1;
///
/// When the built-in UI is used, this is the colour of the outer rim
/// of the mini-map display along with any decals.
///
public Color32 overlayColour = Color.white;
///
/// When the built-in UI is used, this is the background colour of the
/// mini-map.
///
public Color32 backgroundColour = Color.clear;
///
/// When the built-in UI is used, this is the colour of any blip that are considered
/// as friendly. Determined by the factionId when available
///
public Color32 blipFriendColour = Color.green;
///
/// When the built-in UI is used, this is the colour of any blip that are considered
/// as hostile. Determined by the factionId when available
///
public Color32 blipFoeColour = Color.red;
///
/// When the built-in UI is used, this is the colour of any blip that are considered
/// as neutral. Determined by the factionId when available.
///
public Color32 blipNeutralColour = Color.white;
///
/// If changing this at runtime, call RefreshRadarImageStatus().
///
public UnityEngine.UI.RawImage radarImage = null;
///
/// The number of results returned in the last query.
///
public int ResultCount { get; private set; }
///
/// Uses 3D distances to determine range when querying the radar data.
///
public bool is3DQueryEnabled = true;
///
/// The sort order of the results. None is the fastest option and has
/// the lowest performance impact.
///
public SSCRadarQuery.QuerySortOrder querySortOrder;
///
/// [READONLY] The direction the on-screen UI display is facing
///
public Quaternion DisplayRotation { get { return displayUIRotation; } }
///
/// [INTERNAL ONLY] Instead call SetDisplay(..) or GetRadarResults(..)
/// Minimum range is 10 metres
///
public float displayRange = 100f;
///
/// [INTERNAL ONLY] Use FollowShip(..) instead.
/// The centre of the radar will move around with this ship.
///
public ShipControlModule shipToFollow = null;
///
/// [INTERNAL ONLY] Use FollowGameObject(..) instead.
/// The centre of the radar will move around with this gameobject
///
public GameObject gameobjectToFollow = null;
///
/// [INTERNAL ONLY]
/// The centre of the radar
///
public Vector3 centrePosition = Vector3.zero;
#endregion
#region Public Delegates
public delegate void CallbackOnDrawBlip(Texture2D tex, Quaternion displayRotation, int factionId, SSCRadarBlip sscRadarBlip);
///
/// The name of the custom method that is called when a blip is to be
/// draw on the radar display. Your method must take 4 parameters -
/// Texture2D, Quaternion, Int, and SSCRadarBlip. Your custom method should
/// "paint" the blip onto the texture by modifying the pixels.
///
public CallbackOnDrawBlip callbackOnDrawBlip = null;
#endregion
#region Private variables
private static SSCRadar currentRadar = null;
private bool isInitialised = false;
private List sscRadarItemList = null;
private int numRadarItems = 0;
private int currentPoolSize = 0;
private BitArray radarItemBitArray = null;
private bool isRadarImageAvailable = false;
private bool isShowRadarImage = false;
// The width of the RawImage texture if in use
private int displayUITexWidth = 10;
private int displayUITexHeight = 10;
private int displayUITexCentreX = 5;
private int displayUITexCentreY = 5;
private Quaternion displayUIRotation = Quaternion.identity;
private Vector3 defaultDisplayFwdDirection = Vector3.forward;
// Used when our UI is enabled
private bool isFollowShip = false;
private bool isFollowGameObject = false;
private const int displayRimWidth = 4;
private Color32[] uiRimPixels;
private Color32[] uiInnerPixels;
private SSCRadarQuery sscRadarQuery;
private List sscRadarResultsList;
#endregion
#region Initialisation Methods
///
/// Called after Awake() just before the scene is rendered
///
private void Start()
{
if (initialiseOnStart)
{
GetOrCreateRadar();
// If the UI is available, display it
if (isInitialised && isRadarImageAvailable)
{
ShowUI();
}
}
}
///
/// [INTERNAL ONLY] Instead use SSCRadar.GetOrCreateRadar()
///
private void Initialise()
{
if (sscRadarItemList == null)
{
if (poolInitialSize < 1) { sscRadarItemList = new List(10); }
else { sscRadarItemList = new List(poolInitialSize); }
}
else { sscRadarItemList.Clear(); }
// Reset the number items in the last query
ResultCount = 0;
FillPool();
RefreshRadarImageStatus();
isFollowShip = shipToFollow != null;
if (isFollowShip)
{
isFollowGameObject = false;
}
else
{
isFollowGameObject = gameobjectToFollow != null;
}
// By default Radar UI is not visible in the scene
if (isRadarImageAvailable)
{
ScreenResized();
HideUI();
}
isInitialised = true;
SetCanvasSortOrder(canvasSortOrder);
}
#endregion
#region Update Methods
// Update is called once per frame
void Update()
{
if (isShowRadarImage && isInitialised)
{
if (isFollowShip)
{
if (shipToFollow != null && shipToFollow.IsInitialised)
{
centrePosition = shipToFollow.shipInstance.TransformPosition;
}
}
else if (isFollowGameObject)
{
if (gameobjectToFollow != null) { centrePosition = gameobjectToFollow.transform.position; }
}
// Run the query
sscRadarQuery.centrePosition = centrePosition;
sscRadarQuery.range = displayRange;
sscRadarQuery.is3DQueryEnabled = is3DQueryEnabled;
sscRadarQuery.querySortOrder = querySortOrder;
sscRadarQuery.factionId = SSCRadarQuery.IGNOREFACTION;
GetRadarResults(sscRadarQuery, sscRadarResultsList);
DisplayResults(false);
}
}
#endregion
#region Private Member Methods
///
/// Fill the pool with empty radar items. These are added to the end
/// of the existing pool up to the current capacity.
///
private void FillPool()
{
int capacity = sscRadarItemList == null ? 0 : sscRadarItemList.Capacity;
numRadarItems = sscRadarItemList == null ? 0 : sscRadarItemList.Count;
int numNewItems = capacity - numRadarItems;
if (radarItemBitArray == null) { radarItemBitArray = new BitArray(capacity, true); }
// Make sure the bit array can hold enough data
if (radarItemBitArray != null && radarItemBitArray.Length < capacity) { radarItemBitArray.Length = capacity; }
for (int itemIdx = capacity - numNewItems; itemIdx < capacity; itemIdx++)
{
// Create a new (empty) slot
sscRadarItemList.Add(new SSCRadarItem());
// Mark this slot as empty
radarItemBitArray[itemIdx] = true;
}
// Cache number of items
numRadarItems = sscRadarItemList == null ? 0 : sscRadarItemList.Count;
currentPoolSize = numRadarItems;
}
///
/// If required, increase the size of the pool
///
private void ExpandPool()
{
if (isInitialised)
{
currentPoolSize = sscRadarItemList == null ? 0 : sscRadarItemList.Count;
int capacity = sscRadarItemList == null ? 0 : sscRadarItemList.Capacity;
// If there is less than poolIncrementSize left at the end of the pool for expansion,
// add some more capacity
if (capacity - currentPoolSize < poolIncrementSize)
{
sscRadarItemList.Capacity += poolIncrementSize;
}
FillPool();
}
}
///
/// Set a slot in the pool of radarItems as being empty or not.
///
///
///
private void SetBitmap(int itemIndex, bool isEmpty)
{
if (isInitialised && itemIndex >= 0 && itemIndex < currentPoolSize)
{
radarItemBitArray[itemIndex] = isEmpty;
}
}
///
/// Draw the background Radar UI, then add the resultant blips
///
/// Force overlay inner and outer redraw
private void DisplayResults(bool redraw = false)
{
int numResults = ResultCount;
int outerRadius = (int)(displayUITexWidth / 2f);
Texture2D radarTex = radarImage.texture as Texture2D;
// If the canvas image has been resized but doesn't match
// the width of the texture, refresh it.
if (displayUITexWidth > radarTex.width)
{
RefreshRadarImageStatus();
return;
}
// Cache the texture pixels to improve performance and prevent GC in each frame
DrawCircle(radarTex, ref uiRimPixels, displayUITexCentreX, displayUITexCentreY, outerRadius, overlayColour, true, redraw);
DrawCircle(radarTex, ref uiInnerPixels, displayUITexCentreX, displayUITexCentreY, outerRadius - displayRimWidth, backgroundColour, true, redraw);
bool isCallbackEnabled = callbackOnDrawBlip != null;
int factionId = 0; // neutral
// Get the direction the central ship or gameobject is facing
if (isFollowShip && shipToFollow != null)
{
if (shipToFollow.shipInstance != null)
{
displayUIRotation = Quaternion.Euler(0f, shipToFollow.shipInstance.TransformInverseRotation.eulerAngles.y, 0f);
factionId = shipToFollow.shipInstance.factionId;
}
else
{
// Ship may be in the process of being destroyed
FollowShip(null);
displayUIRotation = Quaternion.LookRotation(defaultDisplayFwdDirection);
}
}
else if (isFollowGameObject && gameobjectToFollow != null)
{
displayUIRotation = Quaternion.Euler(0f, Quaternion.Inverse(gameobjectToFollow.transform.rotation).eulerAngles.y, 0f);
}
else { displayUIRotation = Quaternion.LookRotation(defaultDisplayFwdDirection); }
// Populate the UI display with blips
for (int blipIdx = 0; blipIdx < numResults; blipIdx++)
{
if (isCallbackEnabled) { callbackOnDrawBlip(radarTex, displayUIRotation, factionId, sscRadarResultsList[blipIdx]); }
else { DrawBlip(radarTex, displayUIRotation, factionId, sscRadarResultsList[blipIdx]); }
}
// Only apply once all operations have finished
radarTex.Apply();
radarImage.texture = radarTex;
}
///
/// Draw blips onto the radar UI display texture.
/// The factionId is the factionId of the item or place running the query. Items with the same factionId
/// will be displayed in the friend colour, items with factionId = 0 will be neutral, while all other items
/// will be considered hostile (foe blip colour).
///
///
///
///
///
private void DrawBlip(Texture2D tex, Quaternion displayRotation, int factionId, SSCRadarBlip sscRadarBlip)
{
Vector3 rotatedPosition = displayRotation * (sscRadarBlip.wsPosition - sscRadarQuery.centrePosition);
int blipOffsetX = (int)((rotatedPosition.x / sscRadarQuery.range) * displayUITexWidth / 2f);
int blipOffsetZ = (int)((rotatedPosition.z / sscRadarQuery.range) * displayUITexHeight / 2f);
Color blipColour = blipNeutralColour;
//int itemTypeInt = (int)sscRadarBlip.radarItemType;
// If the item running the query is neutral, all others are assumed friendly...
// If the blip is not neutral, must be friend or foe
if (factionId != 0 && sscRadarBlip.factionId != 0)
{
// Is this radar item in the same faction or alliance as the radar system
if (sscRadarBlip.factionId == factionId) { blipColour = blipFriendColour; }
else { blipColour = blipFoeColour; }
}
int blipSize = (int)sscRadarBlip.blipSize;
// Draw a 3x3, 4x4, 5x5 blip etc
for (int x = blipOffsetX + displayUITexCentreX - blipSize; x < blipOffsetX + displayUITexCentreX + blipSize; x++)
{
for (int y = blipOffsetZ + displayUITexCentreY - blipSize; y < blipOffsetZ + displayUITexCentreY + blipSize; y++)
{
// Clip with display rim width indent
if (x > displayRimWidth + 1 && x < displayUITexWidth - displayRimWidth - 2 && y > displayRimWidth + 1 && y < displayUITexHeight - displayRimWidth - 2)
{
tex.SetPixel(x, y, blipColour);
}
}
}
}
#endregion
#region Internal Methods
///
/// This method uses SetPixels which may be slower on some devices. It has very minimal GC.
/// If you pass in a Color32 as a parameter, it will be converted to a Color struct.
///
///
///
///
///
///
///
///
public void DrawCircle(Texture2D tex, int centreX, int centreY, int circleRadius, Color pixelColour, bool isFilled, bool apply)
{
for (int x = -circleRadius; x < circleRadius; x++)
{
int dist = (int)Mathf.Ceil(Mathf.Sqrt(circleRadius * circleRadius - x * x));
if (isFilled)
{
for (int y = -dist; y < dist; y++)
{
tex.SetPixel(x + centreX, y + centreY, pixelColour);
}
}
else
{
tex.SetPixel(x + centreX, -dist + centreY, pixelColour);
tex.SetPixel(x + centreX, dist + centreY, pixelColour);
if (x < -circleRadius + 4 || x > circleRadius - 6)
{
for (int i = 0; i < 16 && i < circleRadius / 6f; i++)
{
tex.SetPixel(x + centreX, -dist + i + centreY, pixelColour);
tex.SetPixel(x + centreX, dist - i + centreY, pixelColour);
}
}
}
}
if (apply) { tex.Apply(); }
}
///
/// Uses a cached copy of the pixels for each circle that needs to be regularly drawn. This is useful when wanting
/// to update a Texture2D in the UI very regularly (like each frame). The first time it runs for a particular circle
/// it will create GC but after that there is no GC unless redraw = true.
/// IMPORTANT: After calling DrawCircle(), call tex.Apply() after you have finished drawing to the texture.
///
///
///
///
///
///
///
///
///
public void DrawCircle(Texture2D tex, ref Color32[] pixels, int centreX, int centreY, int circleRadius, Color32 pixelColour, bool isFilled, bool redraw)
{
if (redraw || pixels == null)
{
pixels = tex.GetPixels32();
// Draw the circle
for (int x = -circleRadius; x < circleRadius; x++)
{
// NOTE: Mathf.Ceil is almost twice as slow as Mathf.Sqrt
int dist = (int)Mathf.Ceil(Mathf.Sqrt(circleRadius * circleRadius - x * x));
if (isFilled)
{
for (int y = -dist; y < dist; y++)
{
pixels[(y + centreY) * tex.width + x + centreX] = pixelColour;
}
}
else
{
pixels[(-dist + centreY) * tex.width + x + centreX] = pixelColour;
pixels[(dist + centreY) * tex.width + x + centreX] = pixelColour;
if (x < -circleRadius + 4 || x > circleRadius - 6)
{
for (int i = 0; i < 16 && i < circleRadius / 6f; i++)
{
pixels[(-dist + i + centreY) * tex.width + x + centreX] = pixelColour;
pixels[(dist - i + centreY) * tex.width + x + centreX] = pixelColour;
}
}
}
}
}
tex.SetPixels32(pixels);
}
///
/// [INTERNAL ONLY]
/// Get the anchor points and correct rect transform offset for the radar display screen locale. e.g. BottomLeft, TopRight etc.
///
///
///
///
///
///
///
public void GetMinimapScreenLocation(int screenLocaleInt, Vector2 canvasSize, Vector2 panelSize, ref Vector2 anchorMin, ref Vector2 anchorMax, ref Vector2 panelOffset)
{
float indentX = canvasSize.x * 0.01f;
float indentY = canvasSize.y * 0.01f;
switch (screenLocaleInt)
{
case (int)RadarScreenLocale.BottomLeft:
anchorMin.x = 0f; anchorMin.y = 0f; anchorMax.x = 0f; anchorMax.y = 0f;
panelOffset.x += indentX;
panelOffset.y += indentY;
break;
case (int)RadarScreenLocale.BottomCenter:
anchorMin.x = 0.5f; anchorMin.y = 0f; anchorMax.x = 0.5f; anchorMax.y = 0f;
panelOffset.x += (canvasSize.x * 0.5f) - (panelSize.x * 0.5f);
panelOffset.y += indentY;
break;
case (int)RadarScreenLocale.BottomRight:
anchorMin.x = 1f; anchorMin.y = 0f; anchorMax.x = 1f; anchorMax.y = 0f;
panelOffset.x += canvasSize.x - panelSize.x - indentX;
panelOffset.y += indentY;
break;
case (int)RadarScreenLocale.TopRight:
anchorMin.x = 0f; anchorMin.y = 0f; anchorMax.x = 1f; anchorMax.y = 1f;
panelOffset.x += canvasSize.x - panelSize.x - indentX;
panelOffset.y += canvasSize.y - panelSize.y - indentY;
break;
case (int)RadarScreenLocale.TopCenter:
anchorMin.x = 0.5f; anchorMin.y = 0f; anchorMax.x = 0.5f; anchorMax.y = 0f;
panelOffset.x += (canvasSize.x * 0.5f) - (panelSize.x * 0.5f);
panelOffset.y += canvasSize.y - panelSize.y - indentY;
break;
case (int)RadarScreenLocale.TopLeft:
anchorMin.x = 0f; anchorMin.y = 1f; anchorMax.x = 0f; anchorMax.y = 1f;
panelOffset.x += indentX;
panelOffset.y += canvasSize.y - panelSize.y - indentY;
break;
case (int)RadarScreenLocale.MiddleLeft:
anchorMin.x = 0f; anchorMin.y = 0.5f; anchorMax.x = 0; anchorMax.y = 0.5f;
panelOffset.x += indentX;
panelOffset.y += (canvasSize.y * 0.5f) - (panelSize.y * 0.5f);
break;
case (int)RadarScreenLocale.MiddleCenter:
anchorMin.x = 0f; anchorMin.y = 0.5f; anchorMax.x = 0f; anchorMax.y = 0.5f;
panelOffset.x += (canvasSize.x * 0.5f) - (panelSize.x * 0.5f);
panelOffset.y += (canvasSize.y * 0.5f) - (panelSize.y * 0.5f);
break;
case (int)RadarScreenLocale.MiddleRight:
anchorMin.x = 1f; anchorMin.y = 0.5f; anchorMax.x = 1f; anchorMax.y = 0.5f;
panelOffset.x += canvasSize.x - panelSize.x - indentX;
panelOffset.y += (canvasSize.y * 0.5f) - (panelSize.y * 0.5f);
break;
default:
// custom
anchorMin.x = 0f; anchorMin.y = 0f; anchorMax.x = 0f; anchorMax.y = 0f;
panelOffset.x = screenLocaleCustomXY.x;
panelOffset.y = screenLocaleCustomXY.y;
break;
}
}
///
/// [INTERNAL ONLY]
/// Get the Radar canvas if it exists, or create a new one.
/// NOTE: When creating a new canvas before Unity 2019.3, the sizeDelta returns
/// the incorrect canvas size. The correct value isn't returned until the next frame.
/// At runtime does NOT create an instance of the EventSystem - you would need to
/// do this some other way...
///
///
///
///
///
public void GetorCreateRadarCanvas(out GameObject radarCanvasGO, out Canvas radarCanvas, out Vector2 canvasSize, out Vector3 canvasScale)
{
radarCanvasGO = null;
canvasSize = Vector2.zero;
canvasScale = Vector3.one;
radarCanvas = SSCUtils.FindCanvas("SSCRadarCanvas");
// If SSCRadarCanvas doesn't exist, create it
if (radarCanvas == null)
{
radarCanvasGO = new GameObject("SSCRadarCanvas");
radarCanvasGO.layer = 5;
radarCanvasGO.AddComponent