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(); radarCanvas = radarCanvasGO.GetComponent(); radarCanvas.renderMode = RenderMode.ScreenSpaceOverlay; UnityEngine.UI.CanvasScaler canvasScaler = radarCanvasGO.AddComponent(); if (canvasScaler != null) { canvasScaler.uiScaleMode = UnityEngine.UI.CanvasScaler.ScaleMode.ScaleWithScreenSize; canvasScaler.screenMatchMode = UnityEngine.UI.CanvasScaler.ScreenMatchMode.MatchWidthOrHeight; canvasScaler.referenceResolution = new Vector2(1920f, 1080f); canvasScaler.matchWidthOrHeight = 0.5f; } } #if UNITY_EDITOR // Add an Event System if it doesn't already exist. // NOTE: If it is disabled in scene, a new one isn't added. UnityEditor.EditorApplication.ExecuteMenuItem("GameObject/UI/Event System"); #endif if (radarCanvas != null) { radarCanvasGO = radarCanvas.gameObject; canvasSize = radarCanvasGO.GetComponent().sizeDelta; canvasScale = radarCanvasGO.GetComponent().localScale; } } #endregion #region Public API Static Methods /// /// Returns the current SSCRadar instance for this scene. If one does not already exist, a new one is created. /// USAGE: SSCRadar sscRadar = SSCRadar.GetOrCreateRadar(); /// /// public static SSCRadar GetOrCreateRadar() { // Check whether we have already found a radar instance for this scene if (currentRadar == null) { // Otherwise, check whether this scene already has a radar system currentRadar = GameObject.FindObjectOfType(); if (currentRadar == null) { // If this scene does not already have a manager, create one GameObject newRadarGameObject = new GameObject("SSC Radar"); newRadarGameObject.transform.position = Vector3.zero; newRadarGameObject.transform.parent = null; currentRadar = newRadarGameObject.AddComponent(); } } if (currentRadar != null) { // Initialise the radar if it hasn't already been initialised if (!currentRadar.isInitialised) { currentRadar.Initialise(); } } #if UNITY_EDITOR // If currentRadar is still null, log a warning to the console else { Debug.LogWarning("SSCRadar GetOrCreateRadar() Warning: Could not find or create radar, so returned null."); } #endif return currentRadar; } #endregion #region Public API Member Methods - CORE /// /// Attempt to add a gameobject to the radar system. If added, it will be immediately visible /// to radar and the RadarId (itemIndex) will be return. A value of -1 indicates it wasn't added. /// NOTE: To add a ship to radar, use shipControlModule.EnableRadar(). For Locations use /// sscManager.EnableRadar(location). /// /// /// typically the position of the gameobject /// /// /// The unique hash to identify this gameObject. If it is 0 we will set it to gameObjectToAdd.GetHashCode() /// The relative size it appears to be on the radar mini-map. Acceptable values 1 to 5 /// public int EnableRadar(GameObject gameObjectToAdd, Vector3 position, int factionId, int squadronId, int guidHash, int blipSize) { // Not assigned in the radar system int radarItemIndex = -1; if (gameObjectToAdd != null && isInitialised) { SSCRadarItem sscRadarItem = new SSCRadarItem(); sscRadarItem.radarItemType = SSCRadarItem.RadarItemType.GameObject; sscRadarItem.itemGameObject = gameObjectToAdd; sscRadarItem.isVisibleToRadar = true; sscRadarItem.guidHash = guidHash == 0 ? gameObjectToAdd.GetHashCode() : guidHash; sscRadarItem.position = position; sscRadarItem.factionId = factionId; sscRadarItem.squadronId = squadronId; sscRadarItem.blipSize = blipSize > 0 && blipSize < 6 ? (byte)blipSize : (byte)1; radarItemIndex = AddItem(sscRadarItem); } return radarItemIndex; } /// /// The item will no longer be visible in the radar system. /// If you want to change the visibility to other radar /// consumers, consider changing the radar item data rather /// than disabling the radar and (later) calling EnableRadar again. /// /// This is the value returned from sscRadar.EnableRadar(gameObject...) public void DisableRadar(int itemIndex) { if (isInitialised) { RemoveItem(itemIndex); } } /// /// Add an item to be tracked by radar. Items are not visible to radar initially, instead /// they are only made visible (if set to be so) for ships in ShipControlModule.Update(), /// or sscManager.EnableRadar(..) for Locations or sscRadar.EnableRadar(..) for gameobjects. /// /// /// public int AddItem(SSCRadarItem sscRadarItemInput) { int itemIndex = -1; // Find first available empty slots in sscRadarItemList using the bitmap for (int i = 0; i < currentPoolSize; i++) { // Is this an empty slot? if (radarItemBitArray[i]) { itemIndex = i; break; } } // If the current pool has been exhausted, add some more capacity if (itemIndex < 0) { int originalPoolSize = currentPoolSize; ExpandPool(); // If the pool has been expanded, allocate the next (empty) slot that was added if (currentPoolSize > originalPoolSize) { itemIndex = originalPoolSize; } } if (itemIndex >= 0) { // Remove this from the available empty slots SetBitmap(itemIndex, false); if (sscRadarItemInput == null) { // This should never be null. SSCRadarItem sscRadarItem = sscRadarItemList[itemIndex]; sscRadarItem.SetClassDefaults(); sscRadarItem.isVisibleToRadar = false; } else { SSCRadarItem sscRadarItem = sscRadarItemList[itemIndex]; // Set rarely changing fields sscRadarItem.radarItemType = sscRadarItemInput.radarItemType; sscRadarItem.itemGameObject = sscRadarItemInput.itemGameObject; sscRadarItem.shipControlModule = sscRadarItemInput.shipControlModule; sscRadarItem.guidHash = sscRadarItemInput.guidHash; sscRadarItem.blipSize = sscRadarItemInput.blipSize; sscRadarItem.IncrementSequenceNumber(); // Set frequently changing fields UpdateItem(itemIndex, new SSCRadarPacket(sscRadarItemInput)); } } return itemIndex; } /// /// Remove a single item from the Radar /// /// public void RemoveItem(int itemIndex) { // Return item to available pool by setting isEmpty to true SetBitmap(itemIndex, true); } /// /// Remove all items from the Radar /// public void RemoveItemsAll() { if (isInitialised) { // All slots should be empty (true) radarItemBitArray.SetAll(true); } } /// /// Get an active (assigned) radar item. /// For ships and locations, the itemIndex is stored as RadarId. e.g. shipControlModule.shipInstance.RadarId /// /// /// public SSCRadarItem GetRadarItem(int itemIndex) { // Is the itemIndex within the valid range? if (isInitialised && itemIndex >= 0 && itemIndex < currentPoolSize) { // Check to see if the slot is assigned if (!radarItemBitArray[itemIndex]) { return sscRadarItemList[itemIndex]; } else { return null; } } else { return null; } } /// /// Get an active (assigned) radar item given the itemIndex AND its sequenceNumber. /// The sequence number is used to validate that this is the correct radarItem. /// For ships and locations, the itemIndex is stored as RadarId. e.g. shipControlModule.shipInstance.RadarId /// /// /// /// public SSCRadarItem GetRadarItem(int itemIndex, uint sequenceNumber) { // Is the itemIndex within the valid range? if (isInitialised && itemIndex >= 0 && itemIndex < currentPoolSize) { // Check to see if the slot is assigned and it matches the sequence number if (!radarItemBitArray[itemIndex] && sscRadarItemList[itemIndex].itemSequenceNumber == sequenceNumber) { return sscRadarItemList[itemIndex]; } else { return null; } } else { return null; } } /// /// Get an active (assigned) radar item given the SSCRadarItemKey. /// For ships and locations, the sscRadarItemKey.radarItemIndex is stored as RadarId. e.g. shipControlModule.shipInstance.RadarId /// /// /// public SSCRadarItem GetRadarItem(SSCRadarItemKey sscRadarItemKey) { // Is the itemIndex within the valid range? if (isInitialised && sscRadarItemKey.radarItemIndex >= 0 && sscRadarItemKey.radarItemIndex < currentPoolSize) { // Check to see if the slot is assigned and it matches the sequence number if (!radarItemBitArray[sscRadarItemKey.radarItemIndex] && sscRadarItemList[sscRadarItemKey.radarItemIndex].itemSequenceNumber == sscRadarItemKey.radarItemSequenceNumber) { return sscRadarItemList[sscRadarItemKey.radarItemIndex]; } else { return null; } } else { return null; } } /// /// Location radarItemTypes store a unique guidHash. /// GameObject radarItemTypes by default store a Unity hash code. /// Ship Damage Regions radarItemTypes store the damage region unique guidHash. /// For these three types, when the itemIndex (or RadarId) is unknown, this method /// can be used to retrieve it. /// If there are no matches, -1 is returned. /// /// /// public int GetRadarItemIndexByHash(int guidHash) { int itemIndex = -1; // Unset guidHash is 0, so ignore this. if (guidHash != 0) { for (int iIdx = 0; iIdx < currentPoolSize; iIdx++) { // Check to see if the slot is assigned AND the hashes match if (!radarItemBitArray[iIdx] && sscRadarItemList[iIdx].guidHash == guidHash) { itemIndex = iIdx; break; } } } return itemIndex; } /// /// Does this item appear in Radar queries? If the itemIndex is invalid, this method /// will always return false. /// NOTE: For performance reasons, it doesn't check if this is non-empty radar item. /// For ships and locations, the itemIndex is stored as RadarId. e.g. shipControlModule.shipInstance.RadarId /// /// public bool GetVisibility (int itemIndex) { // Is the itemIndex within the valid range? if (isInitialised && itemIndex >= 0 && itemIndex < currentPoolSize) { return sscRadarItemList[itemIndex].isVisibleToRadar; } else { return false; } } /// /// Set if this item should appear in Radar queries? /// NOTE: For performance reasons, it doesn't check if this is non-empty radar item. /// For ships and locations, the itemIndex is stored as RadarId. e.g. shipControlModule.shipInstance.RadarId /// /// /// public void SetVisibility(int itemIndex, bool isVisibleToRadar) { // Is the itemIndex within the valid range? if (isInitialised && itemIndex >= 0 && itemIndex < currentPoolSize) { sscRadarItemList[itemIndex].isVisibleToRadar = isVisibleToRadar; } } /// /// Set a new world space position for a radar item. See also UpdateItem(..). /// NOTE: For performance reasons, it doesn't check if this is non-empty radar item. /// For ships and locations, the itemIndex is stored as RadarId. e.g. locationData.RadarId /// /// /// public void SetItemPosition(int itemIndex, Vector3 wsPosition) { // Is the itemIndex within the valid range? if (isInitialised && itemIndex >= 0 && itemIndex < currentPoolSize) { sscRadarItemList[itemIndex].position = wsPosition; } } /// /// Set the type of the radarItem. Typically, this will only be performed once for /// each object (e.g. a ship or ground installation) /// NOTE: For performance reasons, it doesn't check if this is non-empty radar item. /// For ships, the itemIndex is stored as RadarId. e.g. shipControlModule.shipInstance.RadarId /// For surfaceTurretModule, the itemIndex is stored as RadarId /// /// /// public void SetItemType(int itemIndex, SSCRadarItem.RadarItemType radarItemType) { if (isInitialised && itemIndex >= 0 && itemIndex < currentPoolSize) { sscRadarItemList[itemIndex].radarItemType = radarItemType; } } /// /// Set the blip size of the radarItem. Typically this is set automatically when a ship or /// Location is enabled for radar. However, this lets the blip size be overridden at runtime. /// NOTE: For performance reasons, it doesn't check if this is non-empty radar item. /// For ships, the itemIndex is stored as RadarId. e.g. shipControlModule.shipInstance.RadarId /// For surfaceTurretModule, the itemIndex is stored as RadarId /// /// /// public void SetBlipSize (int itemIndex, byte blipSize) { if (isInitialised && itemIndex >= 0 && itemIndex < currentPoolSize) { if (blipSize < 1) { sscRadarItemList[itemIndex].blipSize = 1; } if (blipSize > 5) { sscRadarItemList[itemIndex].blipSize = 5; } else { sscRadarItemList[itemIndex].blipSize = blipSize; } } } /// /// Set the factionId of the radarItem. /// NOTE: For performance reasons, it doesn't check if this is non-empty radar item. /// For ships, the itemIndex is stored as RadarId. e.g. shipControlModule.shipInstance.RadarId /// For surfaceTurretModule, the itemIndex is stored as RadarId /// /// /// public void SetFactionId (int itemIndex, int factionId) { if (isInitialised && itemIndex >= 0 && itemIndex < currentPoolSize) { sscRadarItemList[itemIndex].factionId = factionId; } } /// /// Set the squadronId of the radarItem. /// NOTE: For performance reasons, it doesn't check if this is non-empty radar item. /// For ships, the itemIndex is stored as RadarId. e.g. shipControlModule.shipInstance.RadarId /// For surfaceTurretModule, the itemIndex is stored as RadarId /// /// /// public void SetSquardronId (int itemIndex, int squadronId) { if (isInitialised && itemIndex >= 0 && itemIndex < currentPoolSize) { sscRadarItemList[itemIndex].squadronId = squadronId; } } /// /// Send information about a ship or object to the radar system. /// NOTE: For performance reasons, it doesn't check if this is non-empty radar item. /// /// /// public void UpdateItem(int itemIndex, SSCRadarPacket sscRadarPacket) { // Is the itemIndex within the valid range? if (isInitialised && itemIndex >= 0 && itemIndex < currentPoolSize && sscRadarPacket != null) { // This should never be null. SSCRadarItem sscRadarItem = sscRadarItemList[itemIndex]; // Update the radar system with the latest data from the source sscRadarItem.position = sscRadarPacket.position; sscRadarItem.velocity = sscRadarPacket.velocity; sscRadarItem.isVisibleToRadar = sscRadarPacket.isVisibleToRadar; sscRadarItem.factionId = sscRadarPacket.factionId; sscRadarItem.squadronId = sscRadarPacket.squadronId; } } #endregion #region Public API Member Methods - UI /// /// Redraw the results /// public void RefreshResults() { if (isShowRadarImage && isInitialised) { DisplayResults(true); } } /// /// Set the world space position of the radar system. Use when /// you want the radar system to "move" with gameobject. /// /// /// public void SetDisplay(GameObject centre, float range) { gameobjectToFollow = centre; displayRange = range; } /// /// Set the world space position of the radar system. Use when /// you want the radar system to be fixed to a given location or /// will move infrequently. /// /// /// public void SetDisplay(Vector3 centre, float range) { gameobjectToFollow = null; centrePosition = centre; displayRange = range; } /// /// If the UI has been configured in the Editor under Visuals, /// show the radar display on the screen. /// public void ShowUI() { // Only show the UI if it is available isShowRadarImage = isRadarImageAvailable; if (isShowRadarImage) { radarImage.gameObject.SetActive(true); } } /// /// If the UI has been configured in the Editor under Visuals, /// hide the radar display. /// public void HideUI() { isShowRadarImage = false; if (isRadarImageAvailable) { radarImage.gameObject.SetActive(false); } } /// /// The centre of the radar will follow the ship. /// Currently this only works when the built-in UI is configured in the /// editor under Visuals. The same can be achieve via the API /// when calling GetRadarResults(..) and passing in the ship /// TransformPosition. /// /// public void FollowShip (ShipControlModule shipControlModule) { shipToFollow = shipControlModule; isFollowShip = shipToFollow != null; if (isFollowShip) { gameobjectToFollow = null; isFollowGameObject = false; } } /// /// The centre of the radar will follow the gameobject. /// Currently this only works when the built-in UI is configured in the /// editor under Visuals. The same can be achieve via the API /// when calling GetRadarResults(..) and passing in the tranform /// position. /// /// public void FollowGameObject(GameObject gameobject) { gameobjectToFollow = gameobject; isFollowGameObject = gameobjectToFollow != null; if (isFollowGameObject) { shipToFollow = null; isFollowShip = false; } } /// /// Set the sort order in the scene of the radar mini-map. Higher values appear on top. /// /// public void SetCanvasSortOrder(int newSortOrder) { canvasSortOrder = newSortOrder; if (IsInitialised) { Canvas _canvas = radarImage == null ? null : radarImage.transform.parent.GetComponent(); if (_canvas != null) { _canvas.sortingOrder = newSortOrder; } } } /// /// Set the radar mini-map to use a particular display. Displays or monitors are numbered from 1 to 8. /// /// 1 to 8 public void SetCanvasTargetDisplay (int displayNumber) { if (IsInitialised && SSCUtils.VerifyTargetDisplay(displayNumber, true)) { Canvas _canvas = radarImage == null ? null : radarImage.transform.parent.GetComponent(); if (_canvas != null) { _canvas.targetDisplay = displayNumber - 1; } } } /// /// Call at runtime if radarImage is swapped or made null /// public void RefreshRadarImageStatus() { isRadarImageAvailable = radarImage != null && radarImage.texture != null; if (isRadarImageAvailable) { sscRadarQuery = new SSCRadarQuery(); sscRadarResultsList = new List(20); // There are currently no results. ResultCount = 0; displayUITexWidth = radarImage.texture.width; displayUITexHeight = radarImage.texture.height; displayUITexCentreX = Mathf.CeilToInt(displayUITexWidth * 0.5f); displayUITexCentreY = Mathf.CeilToInt(displayUITexHeight * 0.5f); } else { isShowRadarImage = false; } } /// /// When the screen is resize, we may need to refresh things. /// NOTE: Panel offset x/y seem to be incorrect... /// public void ScreenResized() { if (isRadarImageAvailable) { Canvas radarCanvas = SSCUtils.FindCanvas("SSCRadarCanvas"); if (radarCanvas != null) { GameObject radarCanvasGO = radarCanvas.gameObject; Vector2 canvasSize = radarCanvasGO.GetComponent().sizeDelta; Vector3 canvasScale = radarCanvasGO.GetComponent().localScale; Vector2 anchorMin = Vector2.zero; Vector2 anchorMax = Vector2.zero; Vector2 panelOffset = Vector2.zero; float panelWidth = Mathf.Ceil(radarDisplayWidthN * canvasSize.x), panelHeight = panelWidth; GetMinimapScreenLocation((int)screenLocale, canvasSize, new Vector2(panelWidth, panelHeight), ref anchorMin, ref anchorMax, ref panelOffset); int texWidth = Mathf.CeilToInt(panelWidth); int texHeight = Mathf.CeilToInt(panelHeight); Texture2D radarTex = (Texture2D)radarImage.texture; if (radarTex.width != texWidth || radarTex.height != texHeight) { #if !UNITY_2021_2_OR_NEWER radarTex.Resize(texWidth, texHeight); #else radarTex.Reinitialize(texWidth, texHeight); #endif } SSCUtils.UpdateCanvasPanel(radarImage.rectTransform, panelOffset.x, panelOffset.y, panelWidth, panelHeight, anchorMin.x, anchorMin.y, anchorMax.x, anchorMax.y, canvasScale); SSCUtils.FillTexture(radarTex, Color.clear, false); } } } #endregion #region Public API Member Methods - Query /// /// Send a query to the radar system and populate a list of matching items. /// The queryResultsList must be a non-null, empty list. /// A query will never return items with isVisibleToRadar = false. /// TODO - consider items that are close in 2D but far in 3D space /// FILTERING ORDER: /// - Range /// - Factions to Exclude /// - FactionId /// - Squadrons to Exclude /// - Factions to Include /// - Squadrons to Include /// /// /// /// public bool GetRadarResults (SSCRadarQuery sscRadarQuery, List queryResultsList) { if (sscRadarQuery != null && queryResultsList != null && sscRadarQuery.range > 0f) { // Loop through the list of items and see which ones match the query queryResultsList.Clear(); float sqrRange = sscRadarQuery.range * sscRadarQuery.range; float sqrDistance2D = 0f, sqrDistance3D = 0f; float deltaX, deltaY, deltaZ; int factionId, squadronId; // factionsToInclude is only considered if sscRadarQuery.factionId is not set. int numFactionsToInclude = sscRadarQuery.factionId != SSCRadarQuery.IGNOREFACTION || sscRadarQuery.factionsToInclude == null ? 0 : sscRadarQuery.factionsToInclude.Length; int numFactionsToExclude = sscRadarQuery.factionsToExclude == null ? 0 : sscRadarQuery.factionsToExclude.Length; int numSquadronsToInclude = sscRadarQuery.squadronsToInclude == null ? 0 : sscRadarQuery.squadronsToInclude.Length; int numSquadronsToExclude = sscRadarQuery.squadronsToExclude == null ? 0 : sscRadarQuery.squadronsToExclude.Length; for (int itemIndex = 0; itemIndex < currentPoolSize; itemIndex++) { // Make sure slot isn't empty if (!radarItemBitArray[itemIndex]) { SSCRadarItem sscRadarItem = sscRadarItemList[itemIndex]; if (sscRadarItem.isVisibleToRadar) { deltaX = sscRadarItem.position.x - sscRadarQuery.centrePosition.x; deltaY = sscRadarItem.position.y - sscRadarQuery.centrePosition.y; deltaZ = sscRadarItem.position.z - sscRadarQuery.centrePosition.z; sqrDistance2D = (deltaX * deltaX) + (deltaZ * deltaZ); sqrDistance3D = sqrDistance2D + (deltaY * deltaY); // Is this item within the range of the radar? if ((sscRadarQuery.is3DQueryEnabled && sqrDistance3D <= sqrRange) || (!sscRadarQuery.is3DQueryEnabled && sqrDistance2D <= sqrRange)) { // Moving the sscRadarItem outside the Array.IndexOf/Exists() reduces GC factionId = sscRadarItem.factionId; squadronId = sscRadarItem.squadronId; // Avoid GC by using Array.IndexOf( ) > -1 rather than Array.Exists(..) // Skip anything in the array of factions to exclude if (numFactionsToExclude > 0 && System.Array.IndexOf(sscRadarQuery.factionsToExclude, factionId) > -1) { continue; } // Does the faction match the query? if (sscRadarQuery.factionId != SSCRadarQuery.IGNOREFACTION && sscRadarQuery.factionId != factionId) { continue; } // Skip anything in the array of squadrons to exclude if (numSquadronsToExclude > 0 && System.Array.IndexOf(sscRadarQuery.squadronsToExclude, squadronId) > -1) { continue; } // Skip any factions not in the array of factions to include if (numFactionsToInclude > 0 && System.Array.IndexOf(sscRadarQuery.factionsToInclude, factionId) < 0) { continue; } // Skip any squadron not in the array of squadrons to include if (numSquadronsToInclude > 0 && System.Array.IndexOf(sscRadarQuery.squadronsToInclude, squadronId) < 0) { continue; } // Blips are structs rather than classes. These are created on the stack rather than in heap memory. // Although, these are in a List... but generate no garbage and don't impact GC. SSCRadarBlip blip = new SSCRadarBlip(); blip.wsPosition = sscRadarItem.position; blip.radarItemType = sscRadarItem.radarItemType; blip.distanceSqr2D = sqrDistance2D; blip.distanceSqr3D = sqrDistance3D; blip.itemGameObject = sscRadarItem.itemGameObject; blip.shipControlModule = sscRadarItem.shipControlModule; blip.factionId = sscRadarItem.factionId; blip.squadronId = sscRadarItem.squadronId; blip.blipSize = sscRadarItem.blipSize; blip.radarItemIndex = itemIndex; blip.radarItemSequenceNumber = sscRadarItem.itemSequenceNumber; // Locations, GameObjects and Ship DamageRegions store a unique identifier to assist with lookups. // For other types, the guidHash is 0. blip.guidHash = sscRadarItem.guidHash; //int radarItemTypeInt = (int)sscRadarItem.radarItemType; // If this is a Location, set the guidHash so that Locations can be looked up using sscManager.GetLocation(blip.guidHash) // GameObjects can store gameObject.GetHashCode() in this field. //if (radarItemTypeInt == SSCRadarItem.RadarItemTypeLocationInt || radarItemTypeInt == SSCRadarItem.RadarItemTypeGameObjectInt) //{ // blip.guidHash = sscRadarItem.guidHash; //} queryResultsList.Add(blip); } } } } } else { return false; } int querySortOrderInt = (int)sscRadarQuery.querySortOrder; // Quickly bypass if there is no sort order set. if (querySortOrderInt > 0) { if (querySortOrderInt == SSCRadarQuery.querySortOrderDistanceAsc2DInt) { queryResultsList.Sort(delegate (SSCRadarBlip blip1, SSCRadarBlip blip2) { // If x > y return 1, if x < y return -1, if x == y return 0 (this is equivalent to x.CompareTo(y)) // NOTE: CompareTo is slightly faster than if statements to return 1, -1 or 0 - but can impact GC // Ascending order if (blip1.distanceSqr2D > blip2.distanceSqr2D) { return 1; } else if (blip1.distanceSqr2D < blip2.distanceSqr2D) { return -1; } // With floats, x == y is less likely, so do last. else { return 0; } } ); } else if (querySortOrderInt == SSCRadarQuery.querySortOrderDistanceAsc3DInt) { queryResultsList.Sort(delegate (SSCRadarBlip blip1, SSCRadarBlip blip2) { // Ascending order if (blip1.distanceSqr3D > blip2.distanceSqr3D) { return 1; } else if (blip1.distanceSqr3D < blip2.distanceSqr3D) { return -1; } else { return 0; } } ); } else if (querySortOrderInt == SSCRadarQuery.querySortOrderDistanceDesc2DInt) { queryResultsList.Sort(delegate (SSCRadarBlip blip1, SSCRadarBlip blip2) { // Descending order if (blip1.distanceSqr2D < blip2.distanceSqr2D) { return 1; } else if (blip1.distanceSqr2D > blip2.distanceSqr2D) { return -1; } else { return 0; } } ); } else if (querySortOrderInt == SSCRadarQuery.querySortOrderDistanceDesc3DInt) { queryResultsList.Sort(delegate (SSCRadarBlip blip1, SSCRadarBlip blip2) { // Descending order if (blip1.distanceSqr3D < blip2.distanceSqr3D) { return 1; } else if (blip1.distanceSqr3D > blip2.distanceSqr3D) { return -1; } else { return 0; } } ); } } ResultCount = queryResultsList == null ? 0 : queryResultsList.Count; return true; } /// /// Get the next blip in the supplied list, that is within the camera's viewing area. The startIndex /// is the zero-based index to begin searching the supplied list. /// If a match is found, the zero-based index of that blip will be returned, else -1 will be returned. /// NOTE: Currently assumes the camera is full screen. /// /// /// /// /// This is usually the screen resolution /// public int GetNextBlipInView(List blipList, int startIndex, Camera camera, Vector2 viewSize) { int blipIndex = -1; int numBlips = blipList == null ? 0 : blipList.Count; for (int blpIdx = startIndex; blpIdx < numBlips; blpIdx++) { if (SSCUtils.PointViewDirection(camera, blipList[blpIdx].wsPosition, viewSize) == SSCUtils.ViewDirection.InFront) { blipIndex = blpIdx; break; } } return blipIndex; } /// /// Get the next blip in the supplied list, that is in front of the camera within a custom 2D viewport. /// The startIndex is the zero-based index to begin searching the supplied list. /// If a match is found, the zero-based index of that blip will be returned, else -1 will be returned. /// /// /// /// /// This is usually the screen resolution /// The width and height as 0.0-1.0 values of the full viewSize /// -1.0 to 1.0 with 0,0 as the centre of the screen /// public int GetNextBlipinScreenViewPort(List blipList, int startIndex, Camera camera, Vector2 viewSize, Vector2 viewportSize, Vector2 viewportOffset) { int blipIndex = -1; int numBlips = blipList == null ? 0 : blipList.Count; for (int blpIdx = startIndex; blpIdx < numBlips; blpIdx++) { if (SSCUtils.IsPointInScreenViewport(camera, blipList[blpIdx].wsPosition, viewSize, viewportSize, viewportOffset)) { blipIndex = blpIdx; break; } } return blipIndex; } /// /// Is the blip within the camera's viewing area? /// NOTE: Currently assumes the camera is full screen. /// /// /// /// This is usually the screen resolution /// public bool IsBlipInView(SSCRadarBlip radarBlip, Camera camera, Vector2 viewSize) { return SSCUtils.PointViewDirection(camera, radarBlip.wsPosition, viewSize) == SSCUtils.ViewDirection.InFront; } /// /// Is the blip in front of the camera within a custom 2D viewport. /// Offset is -1.0 to 1.0 with 0,0 as the centre of the screen. /// /// /// /// This is usually the screen resolution /// The width and height as 0.0-1.0 values of the full viewSize /// -1.0 to 1.0 with 0,0 as the centre of the screen /// public bool IsBlipInScreenViewPort(SSCRadarBlip radarBlip, Camera camera, Vector2 viewSize, Vector2 viewportSize, Vector2 viewportOffset) { return SSCUtils.IsPointInScreenViewport(camera, radarBlip.wsPosition, viewSize, viewportSize, viewportOffset); } /// /// Does the radar blip contain information about a GameObject? /// /// /// public bool IsGameObjectBlip(SSCRadarBlip radarBlip) { return radarBlip.itemGameObject != null && radarBlip.radarItemType == SSCRadarItem.RadarItemType.GameObject; } /// /// Does the radar blip contain information about a Location? /// /// /// public bool IsLocationBlip(SSCRadarBlip radarBlip) { return radarBlip.guidHash != 0 && radarBlip.radarItemType == SSCRadarItem.RadarItemType.Location; } /// /// Does the radar blip contain information about a ship? /// /// /// public bool IsShipBlip(SSCRadarBlip radarBlip) { return radarBlip.shipControlModule != null && (radarBlip.radarItemType == SSCRadarItem.RadarItemType.AIShip || radarBlip.radarItemType == SSCRadarItem.RadarItemType.PlayerShip); } /// /// Does the radar blip contain information about a ship damage region? /// /// /// public bool IsShipDamageRegionBlip(SSCRadarBlip radarBlip) { return radarBlip.guidHash != 0 && radarBlip.shipControlModule != null && radarBlip.radarItemType == SSCRadarItem.RadarItemType.ShipDamageRegion; } #endregion } }