How do you surround the enemy evenly?

  • A* version: [5.2.4]
  • Unity version: [2024.3.40]

Sometimes players are well surrounded, but sometimes only one side.
image

        _rvoController.locked = distanceToPlayer <= enemyDataSO.attackRadius && angleToPlayer <= enemyDataSO.attackAngle / 2;

Locked when the enemy is able to attack.

Maybe it’s when the speed is small.
or
Maybe it happens when it’s all already clumped together.
Maybe not, but I think it happens a little better in both cases.

How can I solve it?

Also, is there a way to make it evenly surrounded without going to one side?

@aron_granberg

rvocontroller & richai setting

Would you be able to get any video footage of this so I can see a bit better how they’re moving? Also out of curiosity, did you go with RichAI for any specific reason?

I don’t know how to program ECS, so I chose RichAI.

I noticed that the current logic is twisted. animator.deltaposition.x shouldn’t have been multiplied by Time.deltaTime, but I was only moving to _rvocontroller.calculateMovementDelta (transform.position, Time.deltaTime). I fixed it and it’s moving too fast. How do I solve it? I want to get it to work with root motion.

private void OnAnimatorMove()
    {
        if (!IsAnimatorMove) return;

        // var desiredSpeed = (animator.deltaPosition / Time.deltaTime).magnitude;
        // var newSpeed = Mathf.Lerp(_lastSpeed, desiredSpeed, 0.5f);
        // AI.maxSpeed = newSpeed;
        // _lastSpeed = newSpeed;

        Vector3 nextPosition = transform.position;
        Quaternion nextRotation = transform.rotation;

        AI.MovementUpdate(Time.deltaTime, out nextPosition, out nextRotation);

        if (!_isSpecialAttack)
        {
            nextRotation = animator.rootRotation;
        }

        nextPosition.x = transform.position.x + (animator.deltaPosition.x);
        nextPosition.z = transform.position.z + (animator.deltaPosition.z);
        // nextPosition.x += animator.deltaPosition.x * Time.deltaTime;
        // nextPosition.z += animator.deltaPosition.z * Time.deltaTime;

        transform.rotation = nextRotation;
        // Vector3 velocity = animator.deltaPosition / Time.deltaTime;
        // velocity.y = nextPosition.y;

        if (_rvoController.enabled && !_rvoController.locked)
        {
            var delta = _rvoController.CalculateMovementDelta(transform.position, Time.deltaTime);
            nextPosition += delta;

            // _rvoController.priorityMultiplier = delta.magnitude;
            // print(delta);
        }
        // else if (_rvoController.enabled)
        // {
        //     _rvoController.SetTarget(transform.position, 0f, AI.maxSpeed, transform.position);
        // }

        // _rvoController.velocity = animator.velocity; //+ _rvoController.CalculateMovementDelta(transform.position, Time.deltaTime);
        AI.maxSpeed = (animator.deltaPosition / Time.deltaTime).magnitude;

        // transform.position = nextPosition;
        AI.FinalizeMovement(nextPosition, nextRotation);
    }
private void OnAnimatorMove()
    {
        if (!IsAnimatorMove) return;

        // var desiredSpeed = (animator.deltaPosition / Time.deltaTime).magnitude;
        // var newSpeed = Mathf.Lerp(_lastSpeed, desiredSpeed, 0.5f);
        // AI.maxSpeed = newSpeed;
        // _lastSpeed = newSpeed;

        Vector3 nextPosition = transform.position;
        Quaternion nextRotation = transform.rotation;

        AI.MovementUpdate(Time.deltaTime, out nextPosition, out nextRotation);

        if (!_isSpecialAttack)
        {
            nextRotation = animator.rootRotation;
        }

        nextPosition.x = transform.position.x + (animator.deltaPosition.x);
        nextPosition.z = transform.position.z + (animator.deltaPosition.z);
        // nextPosition.x += animator.deltaPosition.x * Time.deltaTime;
        // nextPosition.z += animator.deltaPosition.z * Time.deltaTime;

        transform.rotation = nextRotation;
        // Vector3 velocity = animator.deltaPosition / Time.deltaTime;
        // velocity.y = nextPosition.y;

        if (_rvoController.enabled && !_rvoController.locked)
        {
            _rvoController.SetTarget(nextPosition, 0F, AI.maxSpeed, Vector3.positiveInfinity);
            var delta = _rvoController.CalculateMovementDelta(AI.position, Time.deltaTime);
            nextPosition += delta;

            // _rvoController.priorityMultiplier = delta.magnitude;
            // print(delta);
        }

        // _rvoController.velocity = (nextPosition - AI.position);
        // _rvoController.velocity = animator.velocity; //+ _rvoController.CalculateMovementDelta(transform.position, Time.deltaTime);
        // AI.maxSpeed = (animator.deltaPosition / Time.deltaTime).magnitude;

        // transform.position = nextPosition;
        AI.FinalizeMovement(nextPosition, nextRotation);
    }

I implemented local avoidance when moving in this way, but I don’t consider locked.

  Vector3 directionToPlayer = player.position - transform.position;
        directionToPlayer.y = 0f;
        float angleToPlayer = Vector3.Angle(transform.forward, directionToPlayer);
        float distanceToPlayer = directionToPlayer.magnitude;
        _rvoController.locked = distanceToPlayer <= enemyDataSO.attackRadius && angleToPlayer <= enemyDataSO.attackAngle / 2;

If you turn on locked, it overlaps, and if you don’t, it pushes each other. How can I fix it?

EDIT : It doesn’t fully overlap when locked, but it still pushes.

protected override void Start()
    {
        base.Start();

        StartCoroutine(loopingSFX());

        AI.canMove = false;
        AI.updatePosition = true;
        AI.updateRotation = false;
    }

 private void OnAnimatorMove()
    {
        if (!IsAnimatorMove) return;

        Vector3 nextPosition = transform.position;
        Quaternion nextRotation = transform.rotation;

        AI.MovementUpdate(Time.deltaTime, out nextPosition, out nextRotation);

        if (!_isSpecialAttack)
        {
            nextRotation = animator.rootRotation;
        }

        nextPosition.x = transform.position.x + (animator.deltaPosition.x);
        nextPosition.z = transform.position.z + (animator.deltaPosition.z);

        transform.rotation = nextRotation;

        if (_rvoController.enabled)
        {
            if (_rvoController.locked || AI.isStopped)
            {
                // _rvoController.SetTarget(transform.position, AI.maxSpeed, AI.maxSpeed, new Vector3(float.NaN, float.NaN, float.NaN));

            }
            else
            {
                _rvoController.SetTarget(nextPosition, 0F, AI.maxSpeed, new Vector3(float.NaN, float.NaN, float.NaN));
                // _rvoController.SetObstacleQuery(_Rvo)
            }

            var delta = _rvoController.CalculateMovementDelta(transform.position, Time.deltaTime);
            nextPosition += delta;
            // _rvoController.priorityMultiplier = delta.magnitude;
            // print(delta);
        }


        AI.maxSpeed = (animator.deltaPosition / Time.deltaTime).magnitude;
        AI.FinalizeMovement(nextPosition, nextRotation);
    }


It’s the latest code. I keep trying out different things, but it’s still lagging.

@aron_granberg

Previously, when I thought it was moving in root motion, “_rvoCcontroller.CalcateMovementDelta” actually got pretty much everything moving, and “nextPosition.x = transform.position.x + (animator.deltaPosition.x *Time.deltaTime)” barely moved. It seems like it keeps getting pushed, maybe because it’s moving as much as the real animator deltaPosition.

All I want is for the enemies to surround themselves without pushing each other.
I’ve only been doing this side for a few days now, but I can’t see a solution. Please reply.

before code :

private void OnAnimatorMove()
    {
        if (!IsAnimatorMove) return;

        Vector3 nextPosition = transform.position;
        Quaternion nextRotation = transform.rotation;

        AI.MovementUpdate(Time.deltaTime, out nextPosition, out nextRotation);

        if (!_isSpecialAttack)
        {
            nextRotation = animator.rootRotation;
        }
        // nextPosition.x = animator.rootPosition.x;
        // nextPosition.z = animator.rootPosition.z;
        nextPosition.x = transform.position.x + (animator.deltaPosition.x * Time.deltaTime);
        nextPosition.z = transform.position.z + (animator.deltaPosition.z * Time.deltaTime);

        transform.rotation = nextRotation;
        // Vector3 velocity = animator.deltaPosition / Time.deltaTime;
        // velocity.y = nextPosition.y;

        var delta = _rvoController.CalculateMovementDelta(transform.position, Time.deltaTime);
        nextPosition += delta;

        // transform.position = nextPosition;
        AI.FinalizeMovement(nextPosition, nextRotation);
    }
private void OnAnimatorMove()
    {
        if (!IsAnimatorMove) return;

        Vector3 nextPosition = transform.position;
        Quaternion nextRotation = transform.rotation;

        AI.MovementUpdate(Time.deltaTime, out nextPosition, out nextRotation);

        if (!_isSpecialAttack)
        {
            nextRotation = animator.rootRotation;
        }

        var dir = (AI.steeringTarget - transform.position);
        bool t = Physics.SphereCast(transform.position + Vector3.up + dir.normalized * 0.35f, 1f, dir.normalized, out RaycastHit hit, 0.5f, 1 << LayerMask.NameToLayer("Enemy"));
        // Debug.DrawRay(transform.position + Vector3.up + dir.normalized * 0.35f, dir * 50f, Color.red);
        bool test = false;
        if (t)
        {
            test = hit.transform.GetComponent<NewGhoul>() ? hit.transform.GetComponent<NewGhoul>().AI.isStopped : false;
        }
        if (test)
        {
            // print($"true {name}");
            nextPosition.x = transform.position.x + (animator.deltaPosition.x * Time.deltaTime);
            nextPosition.z = transform.position.z + (animator.deltaPosition.z * Time.deltaTime);
        }
        else
        {
            nextPosition.x = transform.position.x + (animator.deltaPosition.x);
            nextPosition.z = transform.position.z + (animator.deltaPosition.z);
        }

        // RichAI ai = AI as RichAI;
        // var distanceToEndOfPath = (nextPosition - transform.position).magnitude;

        // var speedLimitFactor = 1f;
        // Vector2 velocity2D = MovementUtilities.ClampVelocity(
        //     new Vector2(animator.deltaPosition.x, animator.deltaPosition.z), AI.maxSpeed, speedLimitFactor, ai.slowWhenNotFacingTarget && ai.enableRotation, ai.preventMovingBackwards, transform.forward);

        transform.rotation = nextRotation;

        if (_rvoController.enabled)
        {
            // _rvoController.SetTarget(nextPosition, velocity2D.magnitude, AI.maxSpeed, ai.endOfPath);

            // if (_rvoController.locked || AI.isStopped)
            // {
            //     _rvoController.SetTarget(transform.position, 0f, 0f, new Vector3(float.NaN, float.NaN, float.NaN));

            // }
            // else
            {
                _rvoController.SetTarget(nextPosition, 0F, AI.maxSpeed, nextPosition);
                // _rvoController.SetObstacleQuery(_Rvo)
            }

            var delta = _rvoController.CalculateMovementDelta(transform.position, Time.deltaTime);
            nextPosition += delta;
            // _rvoController.priorityMultiplier = delta.magnitude;
            // print(delta);
        }



        AI.maxSpeed = (animator.deltaPosition / Time.deltaTime).magnitude;
        AI.FinalizeMovement(nextPosition, nextRotation);
    }

When you do this temporarily, it doesn’t get pushed too much, but it’s not surrounded and there are many problems. Is there a good way?

If necessary, I can send you a project including that scene. Please let me know.

protected override void Start()
    {
        base.Start();

        AI.canMove = false;
        AI.updatePosition = true;
        AI.updateRotation = false;
    }

void Update()
    {
        if (_canMove && !_isGroggy)
        {
            TargetSetDestinationAndRotate();
            AI.isStopped = false;
            // AI.updateRotation = true;
            // AI.updatePosition = true;
        }
        else
        {
            AI.isStopped = true;
            // AI.updateRotation = false;
            // AI.updatePosition = false;
        }

        // _navmeshCut.enabled = AI.isStopped;
        // AI.canMove = false;
        Vector3 directionToPlayer = player.position - transform.position;
        directionToPlayer.y = 0f;
        float angleToPlayer = Vector3.Angle(transform.forward, directionToPlayer);
        float distanceToPlayer = directionToPlayer.magnitude;

        // AI.isStopped = distanceToPlayer <= enemyDataSO.attackRadius && angleToPlayer <= enemyDataSO.attackAngle / 2;
        _rvoController.locked = distanceToPlayer <= enemyDataSO.attackRadius && angleToPlayer <= enemyDataSO.attackAngle / 2;
        // _rvoController.priority = distanceToPlayer <= enemyDataSO.attackRadius && angleToPlayer <= enemyDataSO.attackAngle / 2 ? 1f : 0.5f;
        // _rvoController.flowFollowingStrength = distanceToPlayer <= enemyDataSO.attackRadius && angleToPlayer <= enemyDataSO.attackAngle / 2 ? 0f : 1f;
        // AI.maxSpeed = (animator.deltaPosition / Time.deltaTime).magnitude;
    }

private void OnAnimatorMove()
    {
        AnimatorStateInfo stateInfo = animator.GetCurrentAnimatorStateInfo(0);

        if (!IsAnimatorMove || stateInfo.IsName("Zombie Biting")) return;

        Vector3 nextPosition = transform.position;
        Quaternion nextRotation = transform.rotation;

        AI.MovementUpdate(Time.deltaTime, out nextPosition, out nextRotation);

        if (!_isSpecialAttack)
        {
            nextRotation = animator.rootRotation;
        }

        nextPosition.x = transform.position.x + (animator.deltaPosition.x);
        nextPosition.z = transform.position.z + (animator.deltaPosition.z);

        transform.rotation = nextRotation;
        AI.maxSpeed = (animator.deltaPosition / Time.deltaTime).magnitude;

        // _rvoController.SetTarget(nextPosition, velocity2D.magnitude, AI.maxSpeed, ai.endOfPath);

        // if (_rvoController.locked || AI.isStopped)
        // {
        //     _rvoController.SetTarget(transform.position, 0f, 0f, new Vector3(float.NaN, float.NaN, float.NaN));

        // }
        // else
        {
            _rvoController.SetTarget(AI.destination, 0F, AI.maxSpeed, new Vector3(float.NaN, float.NaN, float.NaN));
        }
        // print(transform.position + " / " + AI.steeringTarget + " " + name);

        if (!_rvoController.locked)
        {
            var delta = _rvoController.CalculateMovementDelta(transform.position, Time.deltaTime);
            nextPosition += delta;

            AI.FinalizeMovement(nextPosition, nextRotation);
        }

    }

In this way, the enemy’s push is gone, but there is still no siege.

Please excuse the response time as I’m typically unavailable over the weekend.

So do you want them to neither overlap or push each other? What’s your desired interaction between agents that bump into each other? From the original post, if you want them to surround the player, you may want to bump up their Radius a tad bit.

Not fully getting what you mean by no siege here, could you explain that a bit?

I do want to note here that you don’t need to do any ECS yourself with FollowerEntity. ECS is just the basis for how it works under the hood. For the most part you interact with it not too much differently than how you use RichAI. It’s not necessary to switch to FollowerEntity or anything, but it’s worth noting for the future :slight_smile:

Originally, I was going to do it using root motion, so I think it’s a little different to use RichAI and Follower Entity.

As for the siege, the enemy wanted to surround and attack the player.

So I’ve used AI.canmove = true to fix speed only using the speed of root motion and not overlap using Rvoccontroller.

Nevertheless, the enemies can’t surround the player properly.

I can’t speak English, so please understand.

Not a problem :+1:

I think this is the main question I still have. If you could let me know exactly what you’re wanting the enemies to do I think I could help here :slight_smile: It sounds like you want them to surround the player using RVO and pathfinding to get to the player, then when they are close, you want them to only use root motion, but without overlapping and without pushing each other?

Yes. That’s right. I want to have enemies approach the player, surround them, and attack them. Don’t let the enemies push them too far without overlapping each other. Root motion is currently only applied in special situations.

Of course, it would have been nice if I could integrate the movement of root motion and rvoccontroller without using AI.canmove, but I didn’t find a solution.
So, we decided to apply root motion only in special case

Okay now I’m with you :slight_smile: I get it more now. Can you send me a screenshot of your player’s components as well as your agent’s components? I want to recreate this on my end.

I have another question for you: are they pushing the player even when they’re not attacking? Or only when they attack with root motion?

The current character does not have an rvo controller, it moves to ECM2. (custom rigid?)

Enemy :



Default setting :

AI.canMove = true;
AI.updatePosition = true;
AI.updateRotation = false;

 void Update()
    {
        //other code

        AI.canMove = _canMove;
        AI.maxSpeed = animator.deltaPosition.magnitude / Time.deltaTime;

    }

private void OnAnimatorMove()
    {
        if (// not need) return;

        Vector3 nextPosition = transform.position;
        Quaternion nextRotation = transform.rotation;

        if (!_isSpecialAttack)
        {
            nextRotation = animator.rootRotation;
        }

        if (// hit or attack)
        {
            nextPosition.x = transform.position.x + (animator.deltaPosition.x * 1.5f);
            nextPosition.z = transform.position.z + (animator.deltaPosition.z * 1.5f);
            transform.rotation = animator.rootRotation;
            transform.position = nextPosition;
            _rvoController.SetTarget(transform.position, AI.maxSpeed, AI.maxSpeed, new Vector3(float.NaN, float.NaN, float.NaN));
            // AI.canMove = true;
        }

    }

Fixed it to make enemies move with AI by default. Also, if enemies are attacked, they use the value of root motion to push them away.
I’m sorry for the delay in my response.

I’m wondering if this is a scenario where you may want to write your own modifier to nudge enemies to circle around their target if they can’t get to them directly. @aron_granberg what do you think of this? Is that idea overkill?

I hope that’s how it works, but I don’t know how to squeeze it. I thought that would happen with RVOCcontroller.

Can you explain what you mean here? I might be able to help :slight_smile:

I’m wondering if this is a scenario where you may want to write your own modifier to nudge enemies to circle around their target if they can’t get to them directly.

I don’t have a clue how to squeeze this part you told me.

Aron wrote some documentation on how to write your own modifiers here. I will admit though this may not be extremely straightforward, but I don’t think there’s a drop in solution to having them surround the player more evenly. Writing a modifier will require you to kind of think how you want their paths to work pretty granularly.

That said, this also gives you the most options. If you’re having troubles getting started, here’s an idea off the top of my head: You could make a modifier where the closer the enemy is to the player, the more their paths attempt to 1) stay away from existing paths of nearby agents and 2) get the position of agents around the player, and use something (possibly Vector3.Dot?) to find a position that’s the most “exposed” and alter their path to go to that point instead of directly to the player— this is all pretty obtuse but as far as I’m aware, this is the kind of thing that would be difficult to get through existing settings.

After all, it’s hard to get that result with the RVO Controller alone. I didn’t fully understand what you said, but I’ll refer to it.

@aron_granberg
Do you have any samples that other users or Aron implemented for the above part?