【问题标题】:Unity Follow The Leader Behavior团结跟随领导者行为
【发布时间】:2019-08-17 17:00:02
【问题描述】:

我问过这个question two years ago。一直没有成功,直到最近我才放弃了这个想法。

我已经能够半修复/复制机制。但是,所有对象似乎都跳到了下一个位置,其中一些重复了它们的“领导者”位置。

橙色是头部,身体部位是绿色。

下面注释掉的代码中可以看出,我尝试了多种排列方式让孩子们顺利跟随他们的领导者,每个身体部位之间的距离只是圆对撞机的半径。

我的想法是,如果“leader”已经移动了半径的距离,那么follower可以向leader的旧位置移动。这让领导者有时间移动。

但唯一似乎半工作的,是未注释的。

谁能看出问题所在?

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;
        }
    }
}

【问题讨论】:

  • 您是否有理由要通过代码执行此操作?孩子们必须移动到确切的父母位置吗?你有没有看过Joints(Unity one,不是你抽的那个)的用法?
  • @Immorality 我想要一个特定的移动风格,而关节不给我那种风格。

标签: c# unity3d


【解决方案1】:

您可以通过许多不同的方式来处理它,但让我向您展示一种方式。

  • Snake - 移动领导者,在路径中创建新点,管理奴才
  • Path - 所有点的环形缓冲区
  • Minion - 根据与领导者的距离跟随路径

这是一个显示小玩意的示例:

  • 绿色是领导者
  • 红色是路径的头
  • 蓝色是路径的尾巴

蛇是主要逻辑所在。

蛇自动向前移动。当领导者和最后一个点之间的距离大于RADIUS时,我们创建一个新点。然后我们沿着点的路径移动所有的小兵。

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);
            }
        }
    }
}

Path 是一个环形缓冲区,Head() 为您提供添加的最新点,您可以使用Head(index) 获取头部并将其偏移方向(+/-)。 Minions 使用它来获取头部后面的点: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;
    }
}

一个minion包含一个索引,它告诉我们minion在snake中的位置(第一、第二、第三)。我们使用这个索引来获得插值所需的两个点。 path.Head(-0) 会给我们领导的观点。 path.Head(-1) 会给我们第一个奴才的分数。

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;
    }
}

为了简化示例,我删除了很多代码。我希望您了解基本概念,并能够实施您自己的解决方案。

【讨论】:

  • 天啊,你是最棒的,我试着像一个月一样创建这个,你把这段代码完美地工作,只是一些改变和阅读。谢谢你!!!!!!
  • @EduardoSteffens 谢谢,很高兴你发现它有用:)
【解决方案2】:

我尝试在 3D 中重新创建此场景,并获得了与您的行为相同的行为。 首先,您希望使用两倍的半径,否则每个子节点将与父节点的一半重叠,因为它是正在移动的圆的中心。

我使用与您不同的方法来移动孩子。结果是平滑的蛇形运动:

代码很简单:

  • 首先我旋转孩子,使其前轴指向领导者的位置
  • 秒计算从当前位置到所需位置的长度。我减去两倍的半径,否则新位置会导致球体重叠
  • 我使用 translate 函数使用计算出的幅度向前移动游戏对象。

        // 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;
    

结果是平滑、可预测的运动。您甚至可以关闭刚体上的运动学以启用碰撞。

希望对您有所帮助。

【讨论】:

    猜你喜欢
    • 2021-12-09
    • 2018-11-13
    • 1970-01-01
    • 2020-07-26
    • 2018-03-29
    • 1970-01-01
    • 2017-04-02
    • 2021-11-04
    • 1970-01-01
    相关资源
    最近更新 更多