Selecting object and movements - C# scripts

Hello everyone,
I’m looking for guidance on a C# issue.
I’m trying to learn Unity (second day of study) and I’m stuck with a behavior that I don’t understand.
I have a scene with a terrain and 3 actors.
I imported some scripts to apply camera movement RTS-style and I’m trying to follow some youtube tutorials on navigation to move units around, however I saw that in the tutorial the guy reference to tags to see if an object is selectable, is ground or whatever. I don’t think using tags would be a good idea (can forget to add tags, could change tags later down the line, etc.), so I came up with a script to give some generic properties to GameObjects that I will interact with.
I attached the scripts to the terrain (marking it as “not selectable”) and other game objects (“selectable”).
However, I have 2 problems:

  1. the script instantiate 1 new GameObject each left click on a “new” (not the previously selected) object, and
  2. if i left click the terrain I got
RTSCameraController.LeftClick () (at Assets/Scripts/RTSCameraController.cs:105)```

here the code:
Camera Controller:

```csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Camera))]

public class RTSCameraController : MonoBehaviour
{
    #region Camera
    public float ScreenEdgeBorderThickness = 5.0f; // distance from screen edge. Used for mouse movement

    [Header("Camera Mode")]
    [Space]
    public bool RTSMode = true;
    public bool FlyCameraMode = false;

    [Header("Movement Speeds")]
    [Space]
    public float minPanSpeed;
    public float maxPanSpeed;
    public float secToMaxSpeed; //seconds taken to reach max speed;
    public float zoomSpeed;

    [Header("Movement Limits")]
    [Space]
    public bool enableMovementLimits;
    public Vector2 heightLimit;
    public Vector2 lenghtLimit;
    public Vector2 widthLimit;
    private Vector2 zoomLimit;

    private float panSpeed;
    private Vector3 initialPos;
    private Vector3 panMovement;
    private Vector3 pos;
    private Quaternion rot;
    private bool rotationActive = false;
    private Vector3 lastMousePosition;
    private Quaternion initialRot;
    private float panIncrease = 0.0f;

    [Header("Rotation")]
    [Space]
    public bool rotationEnabled;
    public float rotateSpeed;

    #endregion

    private GameObject lastSelected;
    private GameObject currentSelected;


    // Use this for initialization
    void Start()
    {
        initialPos = transform.position;
        initialRot = transform.rotation;
        zoomLimit.x = 15;
        zoomLimit.y = 65;
        lastSelected = new GameObject();
        currentSelected = new GameObject();
    }


    void Update()
    {

        # region Camera Mode
        //check that ony one mode is choosen
        if (RTSMode == true) FlyCameraMode = false;
        if (FlyCameraMode == true) RTSMode = false;
        #endregion

        Move();

        #region Zoom

        Camera.main.fieldOfView -= Input.mouseScrollDelta.y * zoomSpeed;
        Camera.main.fieldOfView = Mathf.Clamp(Camera.main.fieldOfView, zoomLimit.x, zoomLimit.y);

        #endregion

        Rotate();

        if (Input.GetMouseButtonDown(0))
        {
            LeftClick();
        }

    }

    public void LeftClick()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;

        if (Physics.Raycast(ray, out hit, 100))
        {
            currentSelected = hit.collider.gameObject;
           
            if (currentSelected.GetComponent<GenericObjectInfo>() != null)
            {
                if (currentSelected.name != lastSelected.name)
                {
                    if (!currentSelected.GetComponent<GenericObjectInfo>().IsSelectable())
                    {
                        lastSelected.GetComponent<GenericObjectInfo>().SetSelected(false);
                        lastSelected = new GameObject();
                    }else if(lastSelected.GetComponent<GenericObjectInfo>() == null)
                    {
                        currentSelected.GetComponent<GenericObjectInfo>().SetSelected(true);
                        lastSelected = currentSelected;
                    }else
                        {
                            lastSelected.GetComponent<GenericObjectInfo>().SetSelected(false);
                            currentSelected.GetComponent<GenericObjectInfo>().SetSelected(true);
                            lastSelected = currentSelected;
                        }
                }
            }else if (lastSelected.name != null)
            {
                lastSelected.GetComponent<GenericObjectInfo>().SetSelected(false);
                lastSelected = new GameObject();
            }



       
        }


    }

    private void Move()
    {
        #region Movement

        panMovement = Vector3.zero;

        if (Input.GetKey(KeyCode.W) || Input.mousePosition.y >= Screen.height - ScreenEdgeBorderThickness)
        {
            panMovement += Vector3.forward * panSpeed * Time.deltaTime;
        }
        if (Input.GetKey(KeyCode.S) || Input.mousePosition.y <= ScreenEdgeBorderThickness)
        {
            panMovement -= Vector3.forward * panSpeed * Time.deltaTime;
        }
        if (Input.GetKey(KeyCode.A) || Input.mousePosition.x <= ScreenEdgeBorderThickness)
        {
            panMovement += Vector3.left * panSpeed * Time.deltaTime;
        }
        if (Input.GetKey(KeyCode.D) || Input.mousePosition.x >= Screen.width - ScreenEdgeBorderThickness)
        {
            panMovement += Vector3.right * panSpeed * Time.deltaTime;
            //pos.x += panSpeed * Time.deltaTime;
        }
        if (Input.GetKey(KeyCode.Q))
        {
            panMovement += Vector3.up * panSpeed * Time.deltaTime;
        }
        if (Input.GetKey(KeyCode.E))
        {
            panMovement += Vector3.down * panSpeed * Time.deltaTime;
        }

        if (RTSMode) transform.Translate(panMovement, Space.World);
        else if (FlyCameraMode) transform.Translate(panMovement, Space.Self);


        //increase pan speed
        if (Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.S)
            || Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.D)
            || Input.GetKey(KeyCode.E) || Input.GetKey(KeyCode.Q)
            || Input.mousePosition.y >= Screen.height - ScreenEdgeBorderThickness
            || Input.mousePosition.y <= ScreenEdgeBorderThickness
            || Input.mousePosition.x <= ScreenEdgeBorderThickness
            || Input.mousePosition.x >= Screen.width - ScreenEdgeBorderThickness)
        {
            panIncrease += Time.deltaTime / secToMaxSpeed;
            panSpeed = Mathf.Lerp(minPanSpeed, maxPanSpeed, panIncrease);
        }
        else
        {
            panIncrease = 0;
            panSpeed = minPanSpeed;
        }

        #endregion

        #region boundaries

        if (enableMovementLimits == true)
        {
            //movement limits
            pos = transform.position;
            pos.y = Mathf.Clamp(pos.y, heightLimit.x, heightLimit.y);
            pos.z = Mathf.Clamp(pos.z, lenghtLimit.x, lenghtLimit.y);
            pos.x = Mathf.Clamp(pos.x, widthLimit.x, widthLimit.y);
            transform.position = pos;
        }



        #endregion

    }

    private void Rotate()
    {
        #region mouse rotation

        if (rotationEnabled)
        {
            // Mouse Rotation
            if (Input.GetMouseButton(2))
            {
                rotationActive = true;
                Vector3 mouseDelta;
                if (lastMousePosition.x >= 0 &&
                    lastMousePosition.y >= 0 &&
                    lastMousePosition.x <= Screen.width &&
                    lastMousePosition.y <= Screen.height)
                    mouseDelta = Input.mousePosition - lastMousePosition;
                else
                {
                    mouseDelta = Vector3.zero;
                }
                var rotation = Vector3.up * Time.deltaTime * rotateSpeed * mouseDelta.x;
                rotation += Vector3.left * Time.deltaTime * rotateSpeed * mouseDelta.y;

                transform.Rotate(rotation, Space.World);

                // Make sure z rotation stays locked
                rotation = transform.rotation.eulerAngles;
                rotation.z = 0;
                transform.rotation = Quaternion.Euler(rotation);
            }

            if (Input.GetMouseButtonUp(0))
            {
                rotationActive = false;
                if (RTSMode) transform.rotation = Quaternion.Slerp(transform.rotation, initialRot, 0.5f * Time.time);
            }

            lastMousePosition = Input.mousePosition;

        }


        #endregion
    }




}

and the GenericInformation I attach to Objects:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;

public class GenericObjectInfo : MonoBehaviour
{
    public bool isSelectable = false;
    public bool isSelected = false;
    public string objectName;
    private NavMeshAgent agent;
    public bool isMovable = false;
    private Transform currentTarget;

    // Start is called before the first frame update
    void Start()
    {
        if (isSelectable)
        {
            agent = GetComponent<NavMeshAgent>();
        }
    }

    // Update is called once per frame
    void Update()
    {

        if (currentTarget != null)
        {
            agent.destination = currentTarget.position;
        }

        if(Input.GetMouseButton(1) && isSelected)
        {
            MoveUnit();
        }


    }

  
    public void MoveUnit()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        if(Physics.Raycast(ray, out RaycastHit hit, 100)){
            agent.destination = hit.point;
            Debug.Log("Moving to"+hit.point);
        }
               

    }


    public bool IsSelectable ()
    {
        return isSelectable;
    }

    public void SetSelectable (bool var)
    {
        isSelectable = var;
    }

    public void MoveUnit (Vector3 dest)
    {
        currentTarget = null;
        agent.destination = dest;
    }

    public bool IsMovable()
    {
        return isMovable;
    }

    public void SetMovable(bool var)
    {
        isMovable = var;
    }

    public void SetSelected(bool selectionToggle)
    {
        isSelected = selectionToggle;
        Debug.Log("this " + this.name + "has been SetSelected to" + selectionToggle);
    }
    public void SetNewTarget(Transform enemy)
    {
        currentTarget = enemy;
    }


}

First off, cache your GetComponent<> result, and store that in lastSelected/currentSelected instead of having those reference the GameObject. That will make your code both faster and more legible. In fact, if GenericObjectInfo is going to be on nearly everything in your game, it’s probably best to make nearly every reference to an object in your whole game a GenericObjectInfo reference and not a GameObject.

For your first problem, lines 108 and 123 both create new GameObjects. I’m not sure why those lines exist. Perhaps you want to set them to null instead?

As for the NullReference, I’m not sure how that’s happening at that particular line, given that line 101 does a null check on that object. Maybe there’s a subtle typo or something that’s not apparent to me (which is how code readability matters big time, see above) that hopefully someone else will notice, because it looks like that should be impossible for that line to generate a NRE.

Two sidenotes:
One, I’m totally onboard with not using tags. Unity’s tag system is so arbitrarily limited (just one tag? seriously?) as to be worthless.
Two, in the long run you’ll probably be better off if you use inheritance with GenericObjectInfo. Have “Terrain” inherit from it, which returns false for things like being clickable; have “Unit” inherit from it with movement/pathfinding logic added; etc. Otherwise, GenericObjectInfo is going to become a very cluttered class.

1 Like

Thank you for the quick answer, it’s really helpful, a couple of considerations:

  1. for what I’m trying to achieve it’s probably better to store the full object currentSelected/lastSelected, instead of only the GetComponent<>, as I will probably need other components to interact with the world later down the line. I updated the script with your suggestion to put the lines 108 and 123 to null, actually is exactly what I was thinking, but I’m a bit rusty with development and forgot about the possibility to have null objects at all.
  2. I’m with you on caching the GetComponent<> for the sake of readability and use them in the ifs, however I don’t know how to reference the GenericObjectInfo class from within another script but without much success (I think it’s a lack of knowledge of the correct syntax, as using doesn’t work). Is there any generic object I could create to store those GetComponent<> results?

Thanks again :slight_smile:

You can use GetComponent just as easily from a GenericObjectInfo (or any component) as you can with a reference to the GameObject:

private GenericObjectInfo currentSelected;
...
if (Physics.Raycast(ray, out hit, 100))
{
currentSelected = hit.collider.GetComponent<GenericObjectInfo>();
//you can still get the GameObject:
GameObject thatGO = currentSelected.gameObject;
//...or any component also attached to the GameObject:
MeshRenderer thatMR = currentSelected.GetComponent<MeshRenderer>();

Broadly speaking, it’s good practice to make the reference that you store for any given object be the one that’s most specific to the particular use case.

So this is basically all about just getting your reference from one place to another. Once you have the value of currentSelected, that’s just a pointer to the object, and you can pass that around to wherever you need to. More to the point, you can pass around a GenericObjectInfo reference with exactly the same ease as you can pass around a GameObject reference - there is literally no difference in that sense.

1 Like

Thank you for the snippet of the code. The line that wasn’t working for me was line 1, that’s why I wasn’t referencing the GenericObjectInfo :slight_smile:
However, upon saving & restarting I gave it another go and it worked!

now the code looks like this:

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

[RequireComponent(typeof(Camera))]

public class RTSCameraController : MonoBehaviour
{
    #region Camera
    public float ScreenEdgeBorderThickness = 5.0f; // distance from screen edge. Used for mouse movement

    [Header("Camera Mode")]
    [Space]
    public bool RTSMode = true;
    public bool FlyCameraMode = false;

    [Header("Movement Speeds")]
    [Space]
    public float minPanSpeed;
    public float maxPanSpeed;
    public float secToMaxSpeed; //seconds taken to reach max speed;
    public float zoomSpeed;

    [Header("Movement Limits")]
    [Space]
    public bool enableMovementLimits;
    public Vector2 heightLimit;
    public Vector2 lenghtLimit;
    public Vector2 widthLimit;
    private Vector2 zoomLimit;

    private float panSpeed;
    private Vector3 initialPos;
    private Vector3 panMovement;
    private Vector3 pos;
    private Quaternion rot;
    private bool rotationActive = false;
    private Vector3 lastMousePosition;
    private Quaternion initialRot;
    private float panIncrease = 0.0f;

    [Header("Rotation")]
    [Space]
    public bool rotationEnabled;
    public float rotateSpeed;

    #endregion

    private GenericObjectInfo lastSelected;
    private GenericObjectInfo currentSelected;

    // Use this for initialization
    void Start()
    {
        initialPos = transform.position;
        initialRot = transform.rotation;
        zoomLimit.x = 15;
        zoomLimit.y = 65;
        lastSelected = null;
        currentSelected = null;
    }


    void Update()
    {

        # region Camera Mode
        //check that ony one mode is choosen
        if (RTSMode == true) FlyCameraMode = false;
        if (FlyCameraMode == true) RTSMode = false;
        #endregion

        Move();

        #region Zoom

        Camera.main.fieldOfView -= Input.mouseScrollDelta.y * zoomSpeed;
        Camera.main.fieldOfView = Mathf.Clamp(Camera.main.fieldOfView, zoomLimit.x, zoomLimit.y);

        #endregion

        Rotate();

        if (Input.GetMouseButtonDown(0))
        {
            LeftClick();
        }

    }

    public void LeftClick()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;

        if (Physics.Raycast(ray, out hit, 1000))
        {
            currentSelected = hit.collider.GetComponent<GenericObjectInfo>();
            Debug.Log("Ray hit");

            if (currentSelected != null)
            {
                Debug.Log("Current Selected not NULL");

                if (lastSelected == null || currentSelected.gameObject.name != lastSelected.gameObject.name)
                {
                    Debug.Log("Last Selected NULL OR current != last");

                    if (!currentSelected.IsSelectable())
                    {
                        Debug.Log("Current is not selectable");
                        lastSelected = null;
                    }else if(lastSelected == null)
                    {
                        Debug.Log("Last was NULL, setting Current True");
                        currentSelected.SetSelected(true);
                        lastSelected = currentSelected;
                    }else
                        {
                            Debug.Log("Last != NULL, setting Current True and last False");
                            lastSelected.SetSelected(false);
                            currentSelected.SetSelected(true);
                            lastSelected = currentSelected;
                        }
                }
            }else if (lastSelected != null)
            {
                Debug.Log("Last != NULL, Current NULL, setting last false and cleaning");
                lastSelected.SetSelected(false);
                lastSelected = null;
            }
            else
            {
                Debug.Log("Current Selected is NULL");
            }





        }


    }

    private void Move()
    {
        #region Movement

        panMovement = Vector3.zero;

        if (Input.GetKey(KeyCode.W) || Input.mousePosition.y >= Screen.height - ScreenEdgeBorderThickness)
        {
            panMovement += Vector3.forward * panSpeed * Time.deltaTime;
        }
        if (Input.GetKey(KeyCode.S) || Input.mousePosition.y <= ScreenEdgeBorderThickness)
        {
            panMovement -= Vector3.forward * panSpeed * Time.deltaTime;
        }
        if (Input.GetKey(KeyCode.A) || Input.mousePosition.x <= ScreenEdgeBorderThickness)
        {
            panMovement += Vector3.left * panSpeed * Time.deltaTime;
        }
        if (Input.GetKey(KeyCode.D) || Input.mousePosition.x >= Screen.width - ScreenEdgeBorderThickness)
        {
            panMovement += Vector3.right * panSpeed * Time.deltaTime;
            //pos.x += panSpeed * Time.deltaTime;
        }
        if (Input.GetKey(KeyCode.Q))
        {
            panMovement += Vector3.up * panSpeed * Time.deltaTime;
        }
        if (Input.GetKey(KeyCode.E))
        {
            panMovement += Vector3.down * panSpeed * Time.deltaTime;
        }

        if (RTSMode) transform.Translate(panMovement, Space.World);
        else if (FlyCameraMode) transform.Translate(panMovement, Space.Self);


        //increase pan speed
        if (Input.GetKey(KeyCode.W) || Input.GetKey(KeyCode.S)
            || Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.D)
            || Input.GetKey(KeyCode.E) || Input.GetKey(KeyCode.Q)
            || Input.mousePosition.y >= Screen.height - ScreenEdgeBorderThickness
            || Input.mousePosition.y <= ScreenEdgeBorderThickness
            || Input.mousePosition.x <= ScreenEdgeBorderThickness
            || Input.mousePosition.x >= Screen.width - ScreenEdgeBorderThickness)
        {
            panIncrease += Time.deltaTime / secToMaxSpeed;
            panSpeed = Mathf.Lerp(minPanSpeed, maxPanSpeed, panIncrease);
        }
        else
        {
            panIncrease = 0;
            panSpeed = minPanSpeed;
        }

        #endregion

        #region boundaries

        if (enableMovementLimits == true)
        {
            //movement limits
            pos = transform.position;
            pos.y = Mathf.Clamp(pos.y, heightLimit.x, heightLimit.y);
            pos.z = Mathf.Clamp(pos.z, lenghtLimit.x, lenghtLimit.y);
            pos.x = Mathf.Clamp(pos.x, widthLimit.x, widthLimit.y);
            transform.position = pos;
        }



        #endregion

    }

    private void Rotate()
    {
        #region mouse rotation

        if (rotationEnabled)
        {
            // Mouse Rotation
            if (Input.GetMouseButton(2))
            {
                rotationActive = true;
                Vector3 mouseDelta;
                if (lastMousePosition.x >= 0 &&
                    lastMousePosition.y >= 0 &&
                    lastMousePosition.x <= Screen.width &&
                    lastMousePosition.y <= Screen.height)
                    mouseDelta = Input.mousePosition - lastMousePosition;
                else
                {
                    mouseDelta = Vector3.zero;
                }
                var rotation = Vector3.up * Time.deltaTime * rotateSpeed * mouseDelta.x;
                rotation += Vector3.left * Time.deltaTime * rotateSpeed * mouseDelta.y;

                transform.Rotate(rotation, Space.World);

                // Make sure z rotation stays locked
                rotation = transform.rotation.eulerAngles;
                rotation.z = 0;
                transform.rotation = Quaternion.Euler(rotation);
            }

            if (Input.GetMouseButtonUp(0))
            {
                rotationActive = false;
                if (RTSMode) transform.rotation = Quaternion.Slerp(transform.rotation, initialRot, 0.5f * Time.time);
            }

            lastMousePosition = Input.mousePosition;

        }


        #endregion
    }




}

I’m still having issues with selection/deselection, however I think these are logic related and I can debug them myself, thanks a lot!