How to Move to Different Points Around a Raycast for RTS

I have a mostly functioning script for RTS style movement, but I have an issue with the movement. I would each unit to move to a separate point around the raycast hit, but I’m not sure how. As it stands, they just kinda all try to head for the same point and get stuck in a loop of all trying to cram into one spot lol. Here’s my script for reference, particularly from line 54.

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

public class BimbusSelect : MonoBehaviour
{
    Ray camRay;
    RaycastHit hit;
    List<BimbusMove> selectedBimbi = new List<BimbusMove>();
    bool isDragging = false;
    Vector3 mousePosition;
    Vector3 mousePos1;
    Vector3 mousePos2;

    public void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            mousePosition = Input.mousePosition;
            var camRay = Camera.main.ScreenPointToRay(Input.mousePosition);
            if (Physics.Raycast(camRay, out hit))
            {
                if (hit.transform.CompareTag("Bimbi"))
                {
                    SelectBimbus(hit.transform.GetComponent<BimbusMove>(), Input.GetKey(KeyCode.LeftShift));
                }
                else
                {
                    isDragging = true;
                }

            }
        }
        if (Input.GetMouseButtonUp(0))
        {
            if (isDragging)
            {
                DeselectBimbus();

                foreach (var selectableObject in FindObjectsOfType<BimbusMove>())
                {
                    if (IsInSelectBox(selectableObject.transform))
                    {
                        SelectBimbus(selectableObject.gameObject.GetComponent<BimbusMove>(), true);
                    }

                }
                isDragging = false;
            }

           
        }

        if (Input.GetMouseButtonDown(1) && selectedBimbi.Count > 0)
        {

                mousePosition = Input.mousePosition;
                var camRay = Camera.main.ScreenPointToRay(Input.mousePosition);
                if (Physics.Raycast(camRay, out hit))
                {
                if (hit.transform.CompareTag("Terrain"))
                {
                    foreach (var selectableObject in selectedBimbi)
                    {
                        selectableObject.MoveUnit(hit.point);
                    }
                }
                else if (hit.transform.CompareTag("Enemy"))
                    {

                    }
                }
           
        }

    }



    private void SelectBimbus(BimbusMove unit, bool isMultiBimbi = false)
    {
        if (!isMultiBimbi)
        {
            DeselectBimbus();
        }
            selectedBimbi.Add(unit);
            unit.SetSelected(true);
    }
    private void DeselectBimbus()
    {
      
        for(int i = 0; i < selectedBimbi.Count; i++)
        {
            selectedBimbi[i].SetSelected(false);
        }
        selectedBimbi.Clear();
    }
    private bool IsInSelectBox(Transform transform)
    {
        if(!isDragging)
        {
            return false;
        }
        var camera = Camera.main;
        var viewportBounds = ScreenHelper.GetViewportBounds(camera, mousePosition, Input.mousePosition);
        return viewportBounds.Contains(camera.WorldToViewportPoint(transform.position));
       
    }

}

I imagine I could do a Random.PointOnUnitSphere? But I’m just not sure how to implement it, also I want to make sure they don’t head for the same random point…

Thanks in advance

Bump

Random.onUnitSphere might not work well for you because it returns a point on a sphere (instead of a circle) … so that’d be awkward to implement. A better solution would be some code that returns a point on the circumference of a circle … so they like stand in a circle around the point, but that could be a bit weird and unnatural to look at.

Perhaps the best (and simplest) solution would simply be to make it so if your units don’t make any progress towards the “target” within a certain amount of time, their logic tells them to stop trying and assume that they have reached their destination? So every second, get how much closer they’ve gotten to the target. If the distance doesn’t improve by at least N units per second, (and they are at most X units from the goal, because otherwise they could give up despite being far away), then they stop trying. OR make it so that once they get within N units of the target, they give up after say 2 seconds.

EDIT: or you could make some kind of auto-arranging script, which certainly isn’t impossible or hard, but it might be difficult to generalise it to different kinds of units.

1 Like

There probably won’t be different kinds of units, the end product will be a village sim… Could you give me a general idea of an auto-arranging script?

Also I’m using a nav agent, but I’m not super familiar with the functions. I imagine I could set a randomized Vector3 and just make it stop within a certain distance of the destination maybe? I made a script for another game that basically set a timer that was the distance between the original point and the destination / 10, but I’m not 100% sure how to implement this in a nav agent… Is there a stop move function built-in?

Turns out you can actually use the internal stoppingDistance variable to make it so they don’t all try and go to exactly the same point:

An auto-arranging script would possibly look the best though. First of all, you need to think about what kind of formation (pattern) you want the units to arrange themselves in. A line? A circle? Rows? Or just looking kind of scattered? Once you’ve figured that you, let P be the Vector3 of the goal position. If you had nine units for example, and you wanted 3x3 rows of units, then you could do some for() loops that assign a different goal position to each unit (so P+(2,0,0), P, P+(-2, 0, 0) etc).

It may even work quite well if you don’t use a pattern, and just assign the units a random distance away from the point as you mentioned, plus throw in the stopping distance; they’re unlikely to all go to the same spot (although there is a risk that they will). You’d need to carefully increase the maximum distance they can randomly stop from the point as the number of units increases though. On top of that you will want to throw in some kind of Raytrace to make sure that you’re not sending your units off of the world or something like that. Even still, with the pattern or random distance way of doing it, you might run into annoying outcomes like for instance if you move your units to the top of a cliff near the edge, half of them will go to the bottom of the cliff whilst half will go to the top, which is not really what you want. Not super sure how you’d fix that, but that might not be a problem for you.

If you want to stop your units maybe you could do selectableObject.MoveUnit(selectableObject.position). I’m not crazy familiar with the Nav stuff.

Personally I would send them all to the same position and do something like this:

void Update()
{
     if(Vector3.Distance(gameObject.position, targetPosition) < 10)
     {
          StartCoroutine(BeginStopping());
     }
}

IEnumerator BeingStopping()
{
      yield return new WaitForSeconds(2.0f);
      gameObject.MoveUnit(gameObject.position);
}

I think that might be the simplest and even best solution :slight_smile: But I think that’s basically what the stoppingDistance variable does (although this way gives you more control over what’s going on).

EDIT: not sure why I’m calling MoveUnit() on a gameObject :wink:

1 Like

I tried the coroutine but I couldn’t to get it to work, I can’t really put it in the update method cause this is on a global selection manager, not each individual creature… I tried this,

                    foreach (var selectableObject in selectedBimbi)
                    {
                        if (!selectableObject.GetComponent <BimbuStuff>().isDead)
                        {
                            Vector3 dest = hit.point;
                            selectableObject.MoveUnit(dest + new Vector3(Random.Range(-5f,5f),0,Random.Range(-5f,5f)));
                        }
                    }
                }

And added 5 to the stopping distance of the navagent. It’s not optimal, but I suppose it works for now. Sometimes they kinda push eachother trying to get to their destination lol

If it helps here’s a lil script that will move units into rows and columns (assuming they don’t push eachother out of formation):

float UNITSPACING = 1.0f;

    void MoveUnits(Vector3 goal) // move units, stopping in rows and columns (well, probably not if they bump into eachother)
    {
        int rows = Mathf.FloorToInt(Mathf.Sqrt(selectedBimbi.Count));
        int columns = Mathf.CeilToInt((float)selectedBimbi.Count / rows); // sometimes this will be a decimal so we need to add one more

        int n = 0;
        float adjustedx = 0.0f;
        float adjustedy = 0.0f;

        for (int y = 0; y < columns; y++)
        {
            if (n >= selectedBimbi.Count) break;

            for (int x = 0; x < rows; x++)
            {
                if (n >= selectedBimbi.Count) break; // otherwise it might keep assigning units that don't exist

                var b = selectedBimbi[x + (y*rows)].GetComponent<BimbuStuff>();

                adjustedx = (x * UNITSPACING) - (rows / 2.0f * UNITSPACING); // basically just set the middle as the centre point
                adjustedy = (y * UNITSPACING) - (columns / 2.0f * UNITSPACING);

                b.MoveUnit(goal + new Vector3(adjustedx, 0, adjustedy); // this function assumes none of them are dead. You can still check if they are, but it will leave holes in the formation

                n++;
            }
        }
    }

And here’s what using a coroutine might look like in the unit controller:

void Update()
    {
        foreach (var selectableObject in allUnits) // some list of all units
        {
            BimbuStuff b = selectableObject.GetComponent<BimbuStuff>();

            if (b.isMoving && !b.isDead && !b.isStopping) // just some new random variable set when the unit has an order to move
            {
                if (Vector3.Distance(gameObject.position, targetPosition) < 10)
                {
                    StartCoroutine(BeginStopping(b));
                }
            }
        }
    }

    IEnumerator BeginStopping(BimbuStuff b)
    {
        b.isStopping = true;
        yield return new WaitForSeconds(2.0f);
        b.MoveUnit(b.gameObject.position);
    }

Probably some epic bugs in there but you get the idea.