4 votes

L'unité suit le comportement du leader

Je l'ai demandé question il y a deux ans . N'ayant jamais eu de succès, j'ai abandonné l'idée jusqu'à récemment.

Depuis, j'ai pu réparer ou reproduire le mécanisme. Cependant, tous les objets semblent sauter à leur position suivante, certains dupliquant la position de leur "leader".

L'orange est la tête, les parties du corps étant vertes.

enter image description here

Comme vous pouvez le voir sur le code commenté ci-dessous J'ai essayé de multiples permutations pour que les enfants suivent leur chef. en douceur la distance entre chaque partie du corps étant simplement le rayon du cercle de collision.

Je pensais que si le " leader "Si le leader a parcouru la distance du rayon, le suiveur peut alors se déplacer vers l'ancienne position du leader. Cela donne au leader le temps de se déplacer.

Mais la seule qui semble fonctionner à moitié est celle qui n'est pas commentée.

Quelqu'un peut-il voir le problème ?

FollowTheLeader.cs

public class FollowTheLeader : MonoBehaviour
{
    [Header("Head")]
    public GameObject bodyPart;
    public int bodyLength = 6;

    [Header("Move Speed")]
    [Range(0.25f, 2.0f)] public float moveMin = 0.5f;
    [Range(0.25f, 2.0f)] public float moveMax = 2.0f;

    [Header("Change Directions")]
    [Range(0.25f, 2.0f)] public float changeMin = 0.5f;
    [Range(0.25f, 2.0f)] public float changeMax = 2.0f;

    [SerializeField]
    private Vector2 oldPosition;
    public Vector2 OldPosition { get => oldPosition; set => oldPosition = value; }

    [SerializeField]
    private Vector2 moveDirection = new Vector2(0, -1);
    public Vector2 MoveDirection { get => moveDirection; set => moveDirection = value; }

    [Header("Child")]
    public int index;
    public bool isChild;
    public FollowTheLeader leader;
    public float leaderDistance;

    private CircleCollider2D m_collider2D;
    private Rigidbody2D body2d;

    private float moveSpeed;
    private float moveTimePassed;
    private float changeDirInterval;

    private void Awake()
    {
        m_collider2D = GetComponent<CircleCollider2D>();
        body2d = GetComponent<Rigidbody2D>();

        AddBodyParts();

        DefineDirection(moveDirection);
    }

    private void AddBodyParts()
    {
        if (isChild || bodyPart == null)
            return;

        //The head will generate its body parts. Each body part will have reference to the one before it.

        FollowTheLeader temp = this;

        for (int i = 1; i <= bodyLength; i++)
        {
            GameObject bp = Instantiate(bodyPart, transform);
            bp.transform.SetParent(null);
            //bp.transform.position = transform.position;
            bp.transform.position = new Vector2(i * m_collider2D.radius, 0);
            bp.name = $"Body {i}";

            FollowTheLeader c = bp.AddComponent<FollowTheLeader>();
            c.isChild = true;
            c.index = i;
            c.OldPosition = bp.transform.position;
            c.leader = temp;

            // cache the parent for the next body part 
            temp = c;
        }
    }

    private void Start()
    {
        OnNewDirection();
    }

    private void FixedUpdate()
    {
        //Store the old postion for the next child
        OldPosition = body2d.position;

        // If child
        if (isChild)
        {
            // Calculate the leaders distance
            leaderDistance = Vector2.Distance(OldPosition, leader.OldPosition);

            // We only want to move if the parent is as far away as the  m_collider2D.radius.
            if (leaderDistance < m_collider2D.radius)
                return;

            // BARELY ANY MOVEMENT
            //body2d.MovePosition(leader.OldPosition.normalized);
            //body2d.MovePosition(leader.OldPosition.normalized * moveSpeed);
            //body2d.MovePosition(leader.OldPosition.normalized * moveSpeed * Time.deltaTime);
            //body2d.MovePosition(leader.OldPosition.normalized * parentDistance * moveSpeed * Time.deltaTime);
            //body2d.MovePosition(leader.OldPosition.normalized * m_collider2D.radius * parentDistance * moveSpeed * Time.deltaTime);

            //FLYS ALL OVER THE PLACE
            //body2d.MovePosition(body2d.position + leader.OldPosition.normalized);
            //body2d.MovePosition(body2d.position + leader.OldPosition.normalized * moveSpeed);
            //body2d.MovePosition(body2d.position + leader.OldPosition.normalized * moveSpeed * Time.deltaTime);
            //body2d.MovePosition(body2d.position + leader.OldPosition.normalized * parentDistance * moveSpeed * Time.deltaTime);
            //body2d.MovePosition(body2d.position + leader.OldPosition.normalized * m_collider2D.radius * moveSpeed * Time.deltaTime);
            //body2d.MovePosition(body2d.position + leader.OldPosition.normalized * m_collider2D.radius * parentDistance * moveSpeed * Time.deltaTime);

            // BARELY ANY MOVEMENT
            //body2d.MovePosition(leader.OldPosition * moveSpeed);
            //body2d.MovePosition(leader.OldPosition * moveSpeed * Time.deltaTime);
            //body2d.MovePosition(leader.OldPosition * parentDistance * moveSpeed * Time.deltaTime);
            //body2d.MovePosition(leader.OldPosition * m_collider2D.radius * parentDistance * moveSpeed * Time.deltaTime);

            //FLYS ALL OVER THE PLACE
            //body2d.MovePosition(body2d.position + leader.OldPosition);
            //body2d.MovePosition(body2d.position + leader.OldPosition * moveSpeed);
            //body2d.MovePosition(body2d.position + leader.OldPosition * moveSpeed * Time.deltaTime);
            //body2d.MovePosition(body2d.position + leader.OldPosition * parentDistance * moveSpeed * Time.deltaTime);
            //body2d.MovePosition(body2d.position + leader.OldPosition * m_collider2D.radius * moveSpeed * Time.deltaTime);
            //body2d.MovePosition(body2d.position + leader.OldPosition * m_collider2D.radius * parentDistance * moveSpeed * Time.deltaTime);

            // KINDA FOLLOWS BUT ALL SEEM TO JUMP INTO THE SAME POSITION AS SEEN IN THE GIF
            body2d.MovePosition(leader.OldPosition);

            return;
        } 

        // HEAD ONLY
        // Countdown to next direction change
        moveTimePassed += Time.deltaTime;

        if (moveTimePassed >= changeDirInterval)
        {
            OnNewDirection();
        }

        // Calculate the next position
        body2d.MovePosition(body2d.position + MoveDirection.normalized * moveSpeed * Time.deltaTime);

    }

    public void OnNewDirection()
    {
        moveTimePassed = 0;
        moveSpeed = Random.Range(moveMin, moveMax);
        changeDirInterval = Random.Range(changeMin, changeMax);

        RandomDirection();
    }

    private void RandomDirection()
    {
        switch (Random.Range(0, 4))
        {
            case 0:
                DefineDirection(Vector2.up);
                break;
            case 1:
                DefineDirection(Vector2.right);
                break;
            case 2:
                DefineDirection(Vector2.down);
                break;
            case 3:
                DefineDirection(Vector2.left);
                break;
            default:
                DefineDirection(Vector2.down);
                break;
        }
    }

    public void DefineDirection(Vector2 direction)
    {
        if (direction.Equals(Vector2.up))
        {
            MoveDirection = Vector2.up;
        }

        if (direction.Equals(Vector2.down))
        {
            MoveDirection = Vector2.down;

        }

        if (direction.Equals(Vector2.left))
        {
            MoveDirection = Vector2.left;
        }

        if (direction.Equals(Vector2.right))
        {
            MoveDirection = Vector2.right;
        }
    }
}

5voto

Iggy Points 1846

Il existe de nombreuses façons d'aborder la question, mais laissez-moi vous en montrer une.

  • Snake - fait avancer le leader, crée de nouveaux points sur le chemin, gère les sbires
  • Path - tampon circulaire avec tous les points
  • Minion - suivre le chemin en fonction de la distance par rapport au leader

Voici un exemple où l'on voit des gadgets :

  • Le vert est le leader
  • Le rouge est la tête du chemin
  • Le bleu est la queue du chemin

snake movement snake movement longer

C'est dans le serpent que se trouve la logique principale.

Le serpent avance automatiquement. Lorsque la distance entre le leader et le dernier point est supérieure à RADIUS nous créons un nouveau point. Nous déplaçons ensuite tous les larbins le long de la trajectoire des points.

public class Snake : MonoBehaviour
{
    public const float RADIUS = 1f; // distance between minions
    public const float MOVE_SPEED = 1f; // movement speed

    public Vector2 dir = Vector2.up; // movement direction
    public float headDist = 0f; // distance from path 'head' to leader (used for lerp-ing between points)
    public Path path = new Path(1); // path points
    public List<Minion> minions = new List<Minion>(); // all minions

    public Minion Leader => minions[0];

    void Awake()
    {
        path.Add(this.transform.position);
        AddMinion(new Knight());
    }

    void AddMinion(Minion minion)
    {
        // Initialize a minion and give it an index (0,1,2) which is used as offset later on
        minion.Init(minions.Count);

        minions.Add(minion);
        minion.MoveOnPath(path, 0f);

        // Resize the capacity of the path if there are more minions in the snake than the path
        if (path.Capacity <= minions.Count) path.Resize();
    }

    void FixedUpdate()
    {
        MoveLeader();
        MoveMinions();
    }

    void MoveLeader()
    {
        // Move the first minion (leader) towards the 'dir'
        Leader.transform.position += ((Vector3)dir) * MOVE_SPEED * Time.deltaTime;

        // Measure the distance between the leader and the 'head' of that path
        Vector2 headToLeader = ((Vector2)Leader.transform.position) - path.Head().pos;

        // Cache the precise distance so we can reuse it when we offset each minion
        headDist = headToLeader.magnitude;

        // When the distance between the leader and the 'head' of the path hits the threshold, spawn a new point in the path
        if (headDist >= RADIUS)
        {
            // In case leader overshot, let's make sure all points are spaced exactly with 'RADIUS'
            float leaderOvershoot = headDist - RADIUS;
            Vector2 pushDir = headToLeader.normalized * leaderOvershoot;

            path.Add(((Vector2)Leader.transform.position) - pushDir);

            // Update head distance as there is a new point we have to measure from now
            headDist = (((Vector2)Leader.transform.position) - path.Head().pos).sqrMagnitude;
        }
    }

    void MoveMinions()
    {
        float headDistUnit = headDist / RADIUS;

        for (int i = 1; i < minions.Count; i++)
        {
            Minion minion = minions[i];

            // Move minion on the path
            minion.MoveOnPath(path, headDistUnit);

            // Extra push to avoid minions stepping on each other
            Vector2 prevToNext = minions[i - 1].transform.position - minion.transform.position;

            float distance = prevToNext.magnitude;
            if (distance < RADIUS)
            {
                float intersection = RADIUS - distance;
                minion.Push(-prevToNext.normalized * RADIUS * intersection);
            }
        }
    }
}

Le chemin est un tampon en anneau, Head() vous donne le point le plus récent qui a été ajouté, vous pouvez utiliser Head(index) pour récupérer la tête et la décaler dans une direction (+/-). Les minions l'utilisent pour aller chercher les points qui sont juste derrière la tête : path.Head(-1) .

public class Path
{
    public Vector2[] Points { get; private set; }
    public int Capacity => Points.Length;

    int head;

    public Path(int capacity)
    {
        head = 0;
        Points = new Vector2[capacity];
    }

    public void Resize()
    {
        Vector2[] temp = new Vector2[Capacity * 2];

        for (int i = 0; i < temp.Length; i++)
        {
            temp[i] = i < Capacity ? Head(i + 1) : Tail();
        }

        head = Capacity - 1;

        Points = temp;
    }

    public void Add(Vector2 pos)
    {
        int prev = Mod(head, Capacity);

        Next();

        int next = Mod(head, Capacity);

        Points[next].pos = pos;
    }

    public Vector2 Head()
    {
        return Points[head];
    }

    public Vector2 Head(int index)
    {
        return Points[Mod(head + index, Capacity)];
    }

    public Vector2 Tail()
    {
        return Points[Mod(head + 1, Capacity)];
    }

    public Vector2 Tail(int index)
    {
        return Points[Mod(head + 1 + index, Capacity)];
    }

    void Next()
    {
        head++;
        head %= Capacity;
    }

    int Mod(int x, int m)
    {
        return (x % m + m) % m;
    }
}

Un minion contient un index, qui nous indique la position du minion dans le serpent (premier, deuxième, troisième). Nous utilisons cet indice pour obtenir les deux points nécessaires à l'interpolation. path.Head(-0) nous donnera le point du leader. path.Head(-1) nous donnera le point du premier sous-fifre.

public class Minion : MonoBehaviour
{
    int index;

    public Init(int index)
    {
        this.index = index;
    }

    // Move the minion along the path
    public void MoveOnPath(Path path, float dist)
    {
        Vector2 prev = path.Head(-index);
        Vector2 next = path.Head(-index + 1);

        // Interpolate the position of the minion between the previous and the next point within the path. 'dist' is the distance between the 'head' of the path and the leader
        this.transform.position = Vector2.Lerp(prev.pos, next.pos, dist);
    }

    // Push the minion to avoid minions stepping on each other
    public void Push(Vector2 dir)
    {
        this.transform.position += (Vector3)dir;
    }
}

J'ai supprimé beaucoup de code pour rendre l'exemple plus simple. J'espère que vous avez compris l'idée de base et que vous serez en mesure de mettre en œuvre votre propre solution.

0voto

Nicklas C. Points 108

J'ai essayé de recréer ce scénario en 3D et j'ai obtenu un peu le même comportement que le vôtre. Tout d'abord, il faut utiliser deux fois le rayon, car sinon chaque enfant chevaucherait la moitié du parent puisque c'est le centre du cercle qui est déplacé.

J'ai utilisé une approche différente de la vôtre pour déplacer les enfants. Le résultat est un mouvement doux, comme un serpent : Children following the leader

Le code est assez simple :

  • Tout d'abord, je fais pivoter l'enfant pour que son axe avant soit orienté vers la position du leader.
  • Ensuite, la longueur entre la position actuelle et la position souhaitée est calculée. Je soustrais deux fois le rayon, car la nouvelle position entraînerait sinon un chevauchement des sphères.
  • J'utilise la fonction translate pour déplacer le GameObject dans la direction avant en utilisant la magnitude calculée.

        // KINDA FOLLOWS BUT ALL SEEM TO JUMP INTO THE SAME POSITION AS SEEN IN THE GIF
        //body2d.MovePosition(leader.OldPosition);
    
        transform.LookAt(leader.transform);
        float length = leaderDistance - (m_collider2D.radius * 2);
        transform.Translate(transform.forward *  length, Space.World);
    
        return;

Le résultat est un mouvement souple et prévisible. Vous pouvez même désactiver la cinématique sur le corps rigide pour permettre les collisions.

J'espère que cela vous a aidé.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X