1
\$\begingroup\$

I'm coding the combat system for my RTS game, and every time a unit spawns I iterate over all enemies and then attack the closest one. This is achieved using the below block of code:

void FindCurrentTarget()
    {
        //find all potential targets (enemies of this character)
        enemies = GameObject.FindGameObjectsWithTag(attackTag);

        //distance between character and its nearest enemy
        float closestDistance = Mathf.Infinity;
        GameObject potentialTarget = null;
        foreach (GameObject _enemy in enemies)
        {
            //check if there are enemies left to attack and check per enemy if its closest to this character
            if (Vector3.Distance(transform.position, _enemy.transform.position) < closestDistance && _enemy != null)
            {
                //if this enemy is closest to character, set closest distance to distance between character and enemy
                closestDistance = Vector3.Distance(transform.position, _enemy.transform.position);

                //also set current target to closest target (this enemy)
                if ((currentTarget == null) || ((currentTarget != null) && Vector3.Distance(transform.position, currentTarget.transform.position) > 2))
                {
                    potentialTarget = _enemy;
                }
            }
        }
        //set the current target at end of for loop, this is the closest enemy
        currentTarget = potentialTarget;

    }

This works as expected but the problem is that if I deploy 10 units, all 10 units will target the same closest unit and will gang up on the poor guy. These 10 units will then all move to the next guy and this way I have a murder ball of units.

I am thinking of a solution for this but cant think of any, any ideas as a solution would be greatly welcomed.

One solution I thought was to ensure that a target can have a maximum of 3 attackers, and if in the above loop, if it is seen that a target already has 3 attackers attacking it then move on to the next.

For this I added an int _attackers to the Character() script, and then when a new target is found it increments that integer, and if that integer is 3 we move on to the next enemy in the loop.

Below is the modification of the above code to take into account max attackers:

void FindCurrentTarget()
    {
        //find all potential targets (enemies of this character)
        enemies = GameObject.FindGameObjectsWithTag(attackTag);

        //distance between character and its nearest enemy
        float closestDistance = Mathf.Infinity;
        GameObject potentialTarget = null;
        foreach (GameObject _enemy in enemies)
        {
            // if the current enemy already has 3 attackers on him, then ignore and move to next
            if (_enemy.GetComponent<Character>()._attackers >= 3)
                continue;

            //check if there are enemies left to attack and check per enemy if its closest to this character
            if (Vector3.Distance(transform.position, _enemy.transform.position) < closestDistance && _enemy != null)
            {
                //if this enemy is closest to character, set closest distance to distance between character and enemy
                closestDistance = Vector3.Distance(transform.position, _enemy.transform.position);

                //also set current target to closest target (this enemy)
                if ((currentTarget == null) || ((currentTarget != null) && Vector3.Distance(transform.position, currentTarget.transform.position) > 2))
                {
                    potentialTarget = _enemy;
                }
            }
        }
        // in case we have a different target than the one we are changing to then decrement attacker before changing target
        if (currentTarget != null && potentialTarget != null && potentialTarget != currentTarget)
        {
            potentialTarget.GetComponent<Character>()._attackers--;
        }
        // increment the attacker after targetting it
        if (potentialTarget != null)
        {
            potentialTarget.GetComponent<Character>()._attackers++;
        }

        //set the current target at end of for loop, this is the closest enemy
        currentTarget = potentialTarget;


    } 

I am of course also ensuring that the int is decremented when the attacker dies and and am also clamping the value of _attackers between 0 and 4.

But this modification is not working, the nearest enemies are ignored, the targeting which used to be nearest first is now random, sometimes targets are ignored entirely.

When I check in the inspector the targets that are ignored, it shows that the _attackers variable is already at 3. When I debug which of my deployed units is these 3 units, its just a random selection, no longer is the nearest principle respected.

Ive tried all day trying to fix this and I just end up fixing one thing and breaking two. It should be a simple enough concept to implement, but its apparently not :(

\$\endgroup\$
4
  • \$\begingroup\$ Why not break it down in two loops. The first makes a list of all enemies and sorts it by distance. Once you have your distance list, you take the first entry, check how many are already attacking it and grab the next entry when your limit is reached. \$\endgroup\$
    – Zibelas
    Commented Mar 14, 2023 at 23:01
  • \$\begingroup\$ Thanks, but the battleground is constantly changing with new units being deployed regularly, every time a new unit is deployed, I search for new targets for all units. Constantly creating 2 lists, one for distance and associated enemies may be a performance issue? would it not? especially that my game is targetting mobile? \$\endgroup\$ Commented Mar 15, 2023 at 0:35
  • \$\begingroup\$ You don't need to "constantly" create new lists. Create the two lists once, then clear and reuse them each time you do a search. \$\endgroup\$
    – Kevin
    Commented Mar 15, 2023 at 1:44
  • \$\begingroup\$ If you're concerned about performance, you shouldn't be constantly calling GameObject.FindGameObjectsWithTag() or GetComponent(). \$\endgroup\$
    – Kevin
    Commented Mar 15, 2023 at 1:45

2 Answers 2

1
\$\begingroup\$

You have this in your code:

// in case we have a different target than the one we are changing to then decrement attacker before changing target
if (currentTarget != null && potentialTarget != null && potentialTarget != currentTarget) {
    potentialTarget.GetComponent<Character>()._attackers--;
}
// increment the attacker after targetting it
if (potentialTarget != null) {
    potentialTarget.GetComponent<Character>()._attackers++;
}

You are decrementing the number of attackers on potentialTarget by 1 and then incrementing it by 1; the net result is no change.

Perhaps you meant:

// in case we have a different target than the one we are changing to then decrement attacker before changing target
if (currentTarget != null && potentialTarget != null && potentialTarget != currentTarget) {
    currentTarget.GetComponent<Character>()._attackers--; //currentTarget instead of potentialTarget
}

For better performance, you should try to avoid FindGameObjectsWithTag() and GetComponent() as much as possible. It's hard to suggest a specific approach without having a better understanding of how you're using tags, but one possible approach is to search by type:

Character[] characters = GameObject.FindObjectsOfType<Character>();

However, if both allies and enemies can be characters, the array will include units you don't want to look at. Another potential solution is to create a component that is used to track the units that belong to a faction.

\$\endgroup\$
2
  • \$\begingroup\$ Thank you. Yes, you are right, I am using the same component for both player and enemy object, so thats why I am using FindGameObjectsWithTag. My understanding was that as far as finding objects go FindGameObjectsWithTag is the most performant. Also, as regards to GetComponent what other alternative is there? \$\endgroup\$ Commented Mar 15, 2023 at 16:09
  • 1
    \$\begingroup\$ @StuckInPhDNoMore One way to reduce calls to GetComponent() is to cache things by component (e.g. private Character currentTarget, not private GameObject currentTarget). You can also create a singleton component that keeps a cached list of the characters that have been spawned for each faction, but that's outside the scope of comments and would best be addressed in a separate question. \$\endgroup\$
    – Kevin
    Commented Mar 16, 2023 at 0:17
1
\$\begingroup\$

The classic solution is to issue "attacker tokens".

<< Before each attack, enemies must acquire a token for it. After the attack and some cooldown period, the token goes back into the “free” pool.

Changing the number of available tokens for each attack type and tweaking the cooldown periods are an easy way to adjust overall difficulty. >>

https://www.catnapgames.com/2022/08/21/d2-log-070-attack-tokens/

You can Loop through your enemies, and give the closest enemies an Attacker Token. Once they have that token, they can change their State from "Roam" to "Attack".

Then when an enemy is killed, the token can be returned to a Attacker Token Queue... and you can loop through the Queue, and assign the attack token to the closest enemy.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .