Back to Blog

Off-screen Player Indicator in Unity

Enes Efe Tokta

Enes Efe Tokta

Feb 19, 2024 • 7 min read

Unity Tutorial UI System C#

Imagine you're making a space combat game. You need to show the player the location of enemy fighter jets; otherwise, they'll waste time searching for them. They might even get bored and quit the game. You need to find a way to show the player where the enemy planes are.

A popular system solves this problem: the off-screen player indicator. This system uses icons on the canvas to show the player the location of other players. This system significantly improves the player experience and is found in many modern games.

Why Use Off-screen Indicators?

Although it might seem difficult to implement at first glance, in practice it has a very simple and easy development process. However, it's worth noting that this system may vary slightly from game to game. I will explain it to you in the basic and simplest way here, but you can customize it as you wish.

Setting Up Variables

First, let's call the UnityEngine.UI library, which allows us to access Unity's UI components. Then, we'll set up our essential variables:

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

public class OffScreenIndicator : MonoBehaviour
{
    [Header("References")]
    public Camera mainCamera;
    public GameObject markerPrefab;
    public Transform canvas;
    
    [Header("Sprites")]
    public Sprite arrowSprite;
    public Sprite squareSprite;
    
    [Header("Settings")]
    public bool isTargetFollow = true;
    public string targetTag = "Enemy";
    
    private GameObject[] targetObjects;
    private Dictionary markerIcons = new Dictionary();
}

Variable Explanations:

  • mainCamera - Determines whether target objects are inside or outside the player's field of view
  • isTargetFollow - Boolean control to enable/disable the system
  • targetObjects - Array containing all target objects in the scene
  • markerPrefab - Image prefab to instantiate markers in the Canvas
  • markerIcons - Dictionary holding target objects and their corresponding icons
  • arrowSprite - Icon when target is outside the screen
  • squareSprite - Icon when target is inside the screen

Finding Target Objects

We need to create a method called TargetFind. This method will list all objects with the relevant tag:

void TargetFind()
{
    targetObjects = GameObject.FindGameObjectsWithTag(targetTag);
    
    if (targetObjects.Length == 0)
    {
        Debug.LogWarning("No target objects found with tag: " + targetTag);
    }
}

Update Loop Implementation

Now let's implement the main logic inside the Update method. It might be a bit long, but it's very straightforward:

void Update()
{
    if (!isTargetFollow) return;
    
    // Refresh target list
    TargetFind();
    
    // Process each target
    for (int i = 0; i < targetObjects.Length; i++)
    {
        GameObject targetObject = targetObjects[i];
        
        if (targetObject == null || !targetObject.activeInHierarchy)
            continue;
            
        // Check if marker exists for this target
        GameObject markerIcon;
        
        if (markerIcons.TryGetValue(targetObject, out markerIcon))
        {
            // Update existing marker
            TargetFollow(targetObject, markerIcon);
        }
        else
        {
            // Create new marker
            markerIcon = Instantiate(markerPrefab, canvas);
            markerIcons.Add(targetObject, markerIcon);
        }
    }
    
    // Clean up destroyed targets
    List targetsToRemove = new List();
    
    foreach (KeyValuePair pair in markerIcons)
    {
        if (pair.Key == null || !pair.Key.activeInHierarchy)
        {
            Destroy(pair.Value);
            targetsToRemove.Add(pair.Key);
        }
    }
    
    foreach (GameObject target in targetsToRemove)
    {
        markerIcons.Remove(target);
    }
}

How It Works:

  1. Check if isTargetFollow is enabled
  2. Loop through all target objects
  3. For each target, check if a marker already exists in the Dictionary
  4. If it exists, update its position; if not, create a new marker in the Canvas
  5. li>Create a cleanup list for destroyed or inactive targets
  6. Remove markers for deleted targets and clean up the Dictionary

The TargetFollow Method

This is the really important part! This method activates our marker icon by determining whether our target object is within the field of view:

void TargetFollow(GameObject targetObject, GameObject markerIcon)
{
    // Convert world position to screen position
    Vector3 screenPosition = mainCamera.WorldToScreenPoint(targetObject.transform.position);
    
    // Check if target is in view
    bool isInView = screenPosition.z > 0 && 
                    screenPosition.x > 0 && screenPosition.x < Screen.width &&
                    screenPosition.y > 0 && screenPosition.y < Screen.height;
    
    Image markerImage = markerIcon.GetComponent();
    
    if (!isInView)
    {
        // Target is off-screen - use arrow sprite
        markerImage.sprite = arrowSprite;
        markerIcon.SetActive(true);
        
        // Clamp position to screen boundaries
        float xPos = Mathf.Clamp(screenPosition.x, 50, Screen.width - 50);
        float yPos = Mathf.Clamp(screenPosition.y, 50, Screen.height - 50);
        
        Vector3 iconPosition = new Vector3(xPos, yPos, 0);
        markerIcon.transform.position = iconPosition;
        
        // Rotate arrow to point at target
        LookAtTarget(markerIcon, targetObject, true);
    }
    else
    {
        // Target is on-screen - use square sprite
        markerImage.sprite = squareSprite;
        markerIcon.SetActive(true);
        markerIcon.transform.position = screenPosition;
        
        // Reset rotation
        LookAtTarget(markerIcon, targetObject, false);
    }
}

Understanding the Field of View Check

If the object is between 0 and Screen.height on the y-axis and between 0 and Screen.width on the x-axis, the target object is within the field of view. Anything outside these values is naturally outside the visible area.

When the target is outside the view:

  • Assign the arrow sprite
  • Clamp the marker position to stay within Canvas boundaries using Mathf.Clamp
  • Rotate the marker to point at the target

When the target is inside the view:

  • Assign the square sprite
  • Position the marker at the target's screen position
  • Reset the rotation

Rotation Control

The LookAtTarget method performs rotation operations on our marker based on the state of our target object:

void LookAtTarget(GameObject markerIcon, GameObject targetObject, bool isLookAtTarget)
{
    if (isLookAtTarget)
    {
        // Create direction vector from camera to target
        Vector3 direction = (targetObject.transform.position - mainCamera.transform.position).normalized;
        
        // Calculate angle from direction
        float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
        
        // Apply rotation (add 90 to correct sprite orientation)
        markerIcon.transform.rotation = Quaternion.Euler(0, 0, angle + 90);
    }
    else
    {
        // Reset rotation
        markerIcon.transform.rotation = Quaternion.Euler(0, 0, 0);
    }
}

How Rotation Works:

  1. Check the isLookAtTarget parameter
  2. If true, create a normalized vector between the target and camera
  3. Calculate the angle from the x and y components of this vector
  4. Apply the rotation using Quaternion.Euler (adding 90° to align the arrow sprite correctly)
  5. If false, reset the marker's rotation to zero

Complete Implementation

Full Script Available

All this might be confusing at first, but believe me, this system is quite simple once you understand it. Now you have icons that track target objects within the boundaries of the Canvas. If the target object is within the field of view, it will appear as a square icon, while if it is outside the field of view, it will appear as an arrow icon.

If you want to access the complete code file, you can visit my GitHub.

Key Takeaways

  • User Experience: Off-screen indicators significantly improve gameplay by helping players locate important objects
  • Simple Implementation: Despite seeming complex, the system uses basic Unity concepts
  • Customizable: The system can be adapted for different game types and visual styles
  • Performance: Using dictionaries and proper cleanup ensures efficient performance
  • Visual Feedback: Different sprites for on-screen vs off-screen provides clear player feedback

Next Steps

You can enhance this system further by:

  • Adding distance-based scaling (closer targets appear larger)
  • Implementing color coding for different enemy types
  • Adding fade-in/fade-out animations
  • Including a minimap integration
  • Adding sound cues when targets come into view

This off-screen indicator system is a powerful tool in your Unity development arsenal. It's used in countless successful games, from racing games to battle royales, and now you know how to implement it yourself!