How to set up a world map mouse over highlighting effect using C# in Uinty 3D

The main idea:
The ability to mouse over an item in a game and have a highlighter placed at that position. (clicking or selecting the item will be covered at a different time)

The outcome: (Method 2 shown)

Art assets from the video can be found here: mapflaghighlighter

Method 1:
This uses a Coroutine on game start to create a ray from the mouse position and compares it to a Unity Tag name. Where the highlighter can then be placed. The highlighter only moves from its current position when the ray hits a different positions.

/// This is the good enough way:
/// where you dont need an array, only to make sure you set the Tag of all the items in Unity.
IEnumerator GetPointsM1()
{
    while (true)
    {
        yield return new WaitForSeconds(waitTime);
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, 100))
        {
            //Debug.DrawLine(ray.origin, hit.point);
            if( hit.transform.tag == "Flag")
            {
                highlighterGO.transform.position = hit.transform.position;
            }
        }
    }
}

Method 2:
This uses GameObject.findgameobjectswithtag() to get an array of all the points (named Flag here) on the map and more importantly their position. This then can be iterated through and using some vector math we can check the mouse position versus the positions of the closest "flag" and "jump" the highlighter to that position if it is closer.

/// Mostly same as above
/// This method calculates the position of the mouse in relation to the "Flag" and 
/// if the cursor is closer to a flag it will "jump" to that position.
IEnumerator GetPointsM2()
{
    GameObject[] flags = GameObject.FindGameObjectsWithTag("Flag");/// this line could be created in the Start method or at some other point if destinations are unlocked.
    while (true)
    {
        yield return new WaitForSeconds(waitTime);
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, 100))
        {
            //Debug.DrawLine(ray.origin, hit.point);
            float distHolder;
            float shortestDist = float.PositiveInfinity;
            for (int i = 0; i < flags.Length; i++)
            {
                distHolder = Calcdist(flags[i].transform.position, hit.point);
                //Debug.Log("DistHolder: " + distHolder + " shortestDist: " + shortestDist + " Hitpoint: "+ hit.point);
                if (shortestDist > distHolder)
                {
                    shortestDist = distHolder;
                    highlighterGO.transform.position = flags[i].transform.position;
                }
            }
        }
    }
}
// distance equation
float Calcdist(Vector3 v1, Vector3 v2)
{
    float x = v1.x - v2.x;
    float y = v1.y - v2.y;
    float z = v1.z - v2.z;
    return Mathf.Sqrt(x * x + y * y + z * z);
}

Other options that might work:
Unity has a build mouse over option when creating a UI that will highlight where the mouse is but this only applies to a ButtonScript and is more suited for a UI. There is also the OnMouseOver function but that would need to be attached to each GameObject (every Flag in this case) which could be more complicated. Using as described here it is all done on one script.

Where to go from here:
This can be used in other applications such as selecting players on a battle field or any time you want something to happen when the mouse is over something.

Why a Coroutine?
Yes this code could be used in the Update method but it does not need to be called 30 time a second. Calling this code 4 time a second will yield the same user experience.

Let me know what you think. What would you do differently?
I hope this was helpful!
-James

Full Script:
using System.Collections;
using UnityEngine;

public class FindPointOnMapScript : MonoBehaviour
{
    public float waitTime = 0.25f;
    public GameObject highlighterGO;
    // Use this for initialization
    void Start ()
    {
        StartCoroutine(GetPointsM2());
    }
    private void OnDisable()
    {
        StopAllCoroutines();
    }
    /// This is the good enought way:
    /// where you dont need an array only to make sure you Tag all the items.    IEnumerator GetPointsM1()
    {
        while (true)
        {
            yield return new WaitForSeconds(waitTime);
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, 100))
            {
                //Debug.DrawLine(ray.origin, hit.point);
                if( hit.transform.tag == "Flag")
                {
                    highlighterGO.transform.position = hit.transform.position;
                }
            }
        }
    }
    /// Mostly same as above
    /// This method calculates the position of the mouse in relation to the "Flag" and 
    /// if the cursor is closer to a flag it will "jump" to that position.    IEnumerator GetPointsM2()
    {
        GameObject[] flags = GameObject.FindGameObjectsWithTag("Flag");/// this line could be created in the Start method or at some other point if destinations are unlocked.
        while (true)
        {
            yield return new WaitForSeconds(waitTime);
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            RaycastHit hit;
            if (Physics.Raycast(ray, out hit, 100))
            {
                //Debug.DrawLine(ray.origin, hit.point);
                float distHolder;
                float shortestDist = float.PositiveInfinity;
                for (int i = 0; i < flags.Length; i++)
                {
                    distHolder = Calcdist(flags[i].transform.position, hit.point);
                    //Debug.Log("DistHolder: " + distHolder + " shortestDist: " + shortestDist + " Hitpoint: "+ hit.point);
                    if (shortestDist > distHolder)
                    {
                        shortestDist = distHolder;
                        highlighterGO.transform.position = flags[i].transform.position;
                    }
                }
            }
        }
    }
    // distance equation
    float Calcdist(Vector3 v1, Vector3 v2)
    {
        float x = v1.x - v2.x;
        float y = v1.y - v2.y;
        float z = v1.z - v2.z;
        return Mathf.Sqrt(x * x + y * y + z * z);
    }
}

Comments

Popular posts from this blog

How to create slowly revealed text in a Unity 3d game

How different data structures can be used in game coding: List, Queue, and Stack

Adding randomness to your game through code using Unity 3d