找回密码
 立即注册
查看: 403|回复: 1

Unity集群移动思路分享

[复制链接]
发表于 2022-3-17 07:59 | 显示全部楼层 |阅读模式
1. 前言

最近项目中需要实现集群移动的效果,网上查了些资料,以此文记录集群效果的实现过程和思路梳理。
代码Git地址:

集群移动的实现效果如下:



2. 集群如何移动


  • 群体



  • 单体



群体移动效果的本质是单体受到周围其他单体对它产生的影响,有了“”的作用,因此有了位移和朝向的改变。



这种“”的产生源于一定的规则,我们称之为集群移动中单体的行为表现(Behavior),常见的行为表现有3种:

  • 聚拢
  • 散开
  • 同向

2.1 聚拢



当单体周围有其他单体时,聚拢的行为让单体向其他单体靠近。

2.2 散开



当单体周围有其他单体靠得太近时,散开的行为让单体朝着远离其他单体的方向移动。

2.3 同向



同向的行为让单体朝着周围其他单体朝向的求和平均方向旋转。

上述每种行为经过计算都会得到一个向量,我们将这个向量定义为单体受到的力的大小和方向,对每一种行为的向量进行加权求和得到单体受到的最终的力。
根据牛顿第二定律:F = ma,可得单体的加速度a(假设质量m为1),加速度产生速度,速度产生位移,而单体下一帧的朝向可以根据当前朝向和力的方向进行插值得到。

3. 代码构想

3.1 单体类(FlockItem)

单体类挂在单体上,在FixedUpdate中实时计算加速度对移动、朝向产生的影响。
3.2 群体类(FlockMgr)

群体类挂在场景的空节点上,用于管理、遍历所有单体,计算单体每一帧受到群体对其产生的力。
3.3 行为类(FlockBehavior)

用于定义不同的行为逻辑,得到单体受到该行为对其产生的力的大小和方向。

4. 准备工作

4.1 场景与素材

本文以2D工程为例,3D项目同样可以实现,只需要把代码中的Vector2改为Vector3即可,transform.up改为transform.forward。
导入一张图片,放到单体物体上可以区分朝向即可,本文使用如下图片,在工程Assets/Textures/flock_item_sprite.png中可以找到:





Texture Type设置为Sprite (2D and UI),Pixels Per Unit设置为512,并单击Apply按钮:



4.2 创建单体Prefab

在场景中创建一个空节点,命名为FlockItem,添加Circle Collider 2D,用于通过物理判断周围半径内的单体,将半径设置为0.3:



在FlockItem节点中创建一个Sprite,将图片flock_item_sprite.png拖进来:



将FlockItem拖到Prefabs目录下,至此,Prefab创建完毕:




删除场景中的FlockItem。

5. 编码实现

5.1 单体类(FlockItem.cs)

创建单体类FlockItem.cs,在类声明前加上RequireComponent,保证Collider2D已经挂在单体上:
using UnityEngine;

// 确保Collider2D被挂到单体上
[RequireComponent(typeof(Collider2D))]
public class FlockItem : MonoBehaviour
{
}

5.1.1 变量声明

声明碰撞器,用于在遍历周围其他单体时,排除自身:
// 碰撞器
Collider2D collider;
public Collider2D Collider { get { return collider; } }

声明单体的质量,用于与力相除,获得加速度:
// 单体质量
public float mass = 1.0f;

声明单体移动、旋转需要的变量:
// 移动速度
Vector2 velocity;
public Vector2 Velocity { get { return velocity; } }

// 移动速度上限
[SerializeField]
float maxSpeed;
public float MaxSpeed { get { return maxSpeed; } }
float sqrMaxSpeed;

// 加速度
Vector2 acceleration;

// 旋转速度
[Range(0, 10)]
public float rotateSpeed;

声明集群相关的参数:
// 判断相邻单体的半径
[SerializeField]
float neighborRadius;
public float NeighborRadius { get { return neighborRadius; } }

// 判断散开的半径
[SerializeField]
float seperateRadius;
public float SeperateRadius { get { return seperateRadius; } }

// 散开因子,用于放大远离其他单体的向量
[SerializeField]
float seperateForceMultiplier;
public float SeperateForceMultiplier { get { return seperateForceMultiplier; } }

// 最大受力
[SerializeField]
float maxForce;

// 受力因子,用于控制力的大小(力的灵敏度)
[SerializeField]
float forceFactor;

5.1.2 变量初始化

在Start方法中,初始化碰撞器和最大速度的平方值,后者用于减小每一帧因计算带来的性能开销:
void Start()
{
    sqrMaxSpeed = maxSpeed * maxSpeed;
    collider = GetComponent<Collider2D>();
}

5.1.3 更新速度、位移、朝向

在FixedUpdate中根据单体的加速度,计算单体的速度、位移、朝向:
void FixedUpdate()
{
    float dt = Time.fixedDeltaTime;

    // >>>>>更新速度<<<<<
    velocity += acceleration * dt;

    // 限制最大速度
    if (velocity.sqrMagnitude > sqrMaxSpeed)
    {
        velocity = velocity.normalized * maxSpeed;
    }

    // >>>>>更新位移<<<<<
    Vector3 move = velocity * dt;
    transform.position += move;

    // >>>>>更新朝向<<<<<
    if (velocity.sqrMagnitude > 0.00001)
    {
        // 使用线性插值计算当前帧的朝向
        Vector3 up = Vector3.Slerp(transform.up, velocity, rotateSpeed * dt);
        transform.up = up;
    }
}

5.1.4 受力接口

给单体提供一个受力的实现接口,传参是行为表现经过计算后得到的最终向量,也就是受到的力的方向大小,经过与单体质量相除,得到单体的加速度:
public void AddFlockForce(Vector2 force)
{
    force *= forceFactor;
    acceleration = Vector2.ClampMagnitude(force, maxForce) / mass;
}

5.2 群体类(FlockMgr.cs)

创建群体类FlockMgr.cs:
using System.Collections.Generic;
using UnityEngine;

public class FlockMgr : MonoBehaviour
{
}

5.2.1 变量声明

因为需要动态创建FlockItem,所以要声明Prefab,同时需要声明一个存储FlockItem的列表:
public FlockItem itemPrefab;
List<FlockItem> items = new List<FlockItem>();

声明FlockItem初始的数量和密度:
[Range(10, 500)]
public int startingCount = 250;
const float ItemDensity = 0.08f;

声明群体的行为表现:
public FlockBehavior behavior;

5.2.2 动态创建FlockItem

在Start方法中动态创建一定数量的FlockItem:
void Start()
{
    for (int i = 0; i < startingCount; i++)
    {
        FlockItem itemAgent = Instantiate(
            itemPrefab,
            Random.insideUnitCircle * startingCount * ItemDensity,
            Quaternion.Euler(Vector3.forward * Random.Range(0f, 360f)),
            transform
            );
        itemAgent.name = "Item " + i;
        items.Add(itemAgent);
    }
}

5.2.3 单体受力计算

在Update方法中遍历每一个FlockItem,通过调用行为类的Force方法计算单体的受力情况,调用AddFlockForce方法,让单体受力,改变移动速度和朝向:
void Update()
{
    foreach (FlockItem item in items)
    {
        List<Transform> neighbors = GetNeighborObjects(item);
        Vector2 force = behavior.Force(item, neighbors);
        item.AddFlockForce(force);
    }
}

5.2.4 周围单体方法实现

在上面的遍历计算中,通过GetNeighborObjects获取了某个单体周围的其他单体,其逻辑实现如下:
List<Transform> GetNeighborObjects(FlockItem item)
{
    float radius = item.NeighborRadius;
    List<Transform> neigbhors = new List<Transform>();
    Collider2D[] colliders = Physics2D.OverlapCircleAll(item.transform.position, radius);

    foreach (Collider2D c in colliders)
    {
        if (c != item.Collider)
        {
            neigbhors.Add(c.transform);
        }
    }

    return neigbhors;
}
该逻辑通过Pyhsics2D.OverlapCircleAll方法得到某个位置为中心,一定半径范围内,存在的挂载了碰撞体的物体,通过遍历比对碰撞物体是否与当前单体一样来排除当前单体。

5.3 行为类(FlockBehavior.cs)

5.3.1 抽象类

声明一个抽象类FlockBehavior.cs,声明一个抽象方法Force,为了方便配置,该类继承了ScriptableObject:
using System.Collections.Generic;
using UnityEngine;

public abstract class FlockBehavior : ScriptableObject
{
    // 当前行为群体对单体产生的力
    public abstract Vector2 Force(FlockItem item, List<Transform> neighbors);
}

5.3.2 聚拢行为类(CohesionBehavior.cs)



定义聚拢行为类CohesionBehavior.cs,使用CreateAssetMenu方法自定义ScriptObject的菜单项。
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(menuName = "Flock/Behavior/Cohesion")]
public class CohesionBehavior : FlockBehavior
{
    Vector2 desiredVelocity;

    public override Vector2 Force(FlockItem item, List<Transform> neighbors)
    {
        int count = neighbors.Count;
        if (count == 0) return Vector2.zero;

        Vector2 force = Vector2.zero;
        Vector2 massCenter = Vector2.zero;

        for (int i = 0; i < count; i++)
        {
            massCenter += (Vector2)neighbors.position;
        }

        massCenter /= (float) count;

        force = massCenter - (Vector2)item.transform.position;

        return force;
    }
}
Force方法的实现逻辑:对附近其他单体位置求和平均,得到中心位置,减去当前单体位置得到位置的偏移量作为聚拢产生的力。

5.3.3 散开行为类(SeperateBehavior.cs)



散开行为类代码如下:
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(menuName = "Flock/Behavior/Seperation")]
public class SeperationBehavior : FlockBehavior
{
    public override Vector2 Force(FlockItem item, List<Transform> neighbors)
    {
        Vector2 force = Vector2.zero;
        Vector3 itemPos = item.transform.position;

        if (neighbors.Count == 0) return force;

        float radius = item.SeperateRadius;

        for (int i = 0; i < neighbors.Count; i++)
        {
            Transform neighborTF = neighbors;
            // 得到远离相邻单体的向量
            Vector2 offNeighbor = (Vector2) (itemPos - neighborTF.position);
            float length = offNeighbor.magnitude;
            offNeighbor /= length;

            // 如果距离过于接近,让远离的向量进一步放大,让单体更快远离
            if (length < radius)
            {
                offNeighbor *= item.SeperateForceMultiplier;
            }

            force += offNeighbor;
        }

        return force;
    }
}
Force方法的实现逻辑:根据距离其他单体的位置偏移向量除以距离得到排斥力的向量,如果间距过小,则将力的强度加大,如此遍历周围其他单体,得到当前行为最终受力的向量。

5.3.4 同向行为类(AlignmentBehavior.cs)



同向行为类代码如下:
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(menuName = "Flock/Behavior/Alignment")]
public class AlignmentBehavior : FlockBehavior
{
    public override Vector2 Force(FlockItem item, List<Transform> neighbors)
    {
        Vector2 force = Vector2.zero;

        int count = neighbors.Count;
        if (count == 0) return force;

        for (int i = 0; i < count; i++)
        {
            Transform neighborTF = neighbors;
            force += (Vector2) neighborTF.up;
        }

        force /= count;

        // 产生的力由向量的差量决定,所以得到目标朝向后,需要减去当前的朝向
        force -= (Vector2) item.transform.up;

        return force;
    }
}
Force方法的实现逻辑:遍历所有周围其他单体,得到它们朝向的平均值,再减去当前单体的朝向就是为了保持同向给当前单体施加的力。

5.3.5 组合行为(CompositeBehavior.cs)

将上述3种行为得到的力进行加权求和就是组合行为的核心逻辑:
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(menuName = "Flock/Behavior/Composite")]
public class CompositeBehavior : FlockBehavior
{
    public FlockBehavior[] behaviors;
    public float[] weights;

    public override Vector2 Force(FlockItem item, List<Transform> context)
    {
        if (weights.Length != behaviors.Length)
        {
            Debug.LogError("Data mismatch in " + name, this);
            return Vector2.zero;
        }

        Vector2 force = Vector2.zero;

        for (int i = 0; i < behaviors.Length; i++)
        {
            float weight = weights;
            Vector2 partialForce = behaviors.Force(item, context) * weight;
            force += partialForce;
        }

        return force;
    }
}

6. 参数配置

6.1 单体参数




6.2 行为参数

Project窗口右键单击Create->Flock->Behavior->Alignment/Cohesion/Speration,分别创建三种行为的ScriptableObject(这三个没有参数需要配置):



然后创建Composite的ScriptableObject,将大小设为3,然后分别拖拽并配置散开、聚拢、同向的权重:


这里的Alignment权重配置为0.01是因为相比其他两个,同向的向量值更大,为了均衡效果,将其配小一点,也可以考虑在同向的行为逻辑中将结果除以10在内部消化均衡。

6.3 群体参数

将FlockItem的Prefab拖到群体的Item Prefab变量中,调整初始单体数量,将组合行为的ScriptableObject拖拽到行为变量中,完成群体参数的配置:


至此参数配置完成,运行游戏可看到文章开头的效果。

7. 扩展

其实行为参数及实现逻辑并不是固定的,具体的实现还需要根据具体的需求来定。除了用“力”+加速度的方式,还可以将行为计算得到的向量直接作为单体的速度来实现。
7.1 其他行为

除了聚拢、散开、同向外,还有其他各种各样的行为可以实现,下面简单列举一些行为及其实现思路。

7.1.1 躲避障碍物



调用Physics.Raycast方法,从单体位置向前方一定距离发出一条射线,检测单体前方是否有障碍物。
若有,则以焦点位置的法线方向,调用Vector3.Reflect(transform.up, hit.normal),求反射方向作为单体受到的力。

7.1.2 范围内移动



当单体移动到集群范围外,则取单体到集群中心的位移作为单体的受力方向,将单体拉回集群圈内。

7.1.3 路径移动



单体先向第一个目标点移动,当到目标点的距离小于一定半径后,将单体的目标点设置为下一个,此时单体和第二个目标点之间的位置偏移作为驱动单体向第二个目标点转向的力,以此类推可让单体按照指定路径平滑地经过。

7.1.4 追逐



群体中的所有单体受到指向目标单体方向的力,以此让群体跟着目标单体。
追逐的行为可用于领头羊,或是一群丧尸围攻人类的效果。

7.1.5 逃避



逃避行为与追逐相反,实现思路与散开类似,当当前单体在目标单体一定范围内时,会受到目标单体指向当前单体方向的排斥力,让当前单体远离目标单体移动。
该行为可用于实现牧羊犬赶羊的效果。

集群行为实现的最终效果,其好坏取决于参数、权重等配置是否合理。若参数没有配置好,很可能会得到类似如下的效果:


所以配置想要的效果,还是需要花时间慢慢调参数的。

8. 参考


  • 油管:Flocking Algorithm in Unity
  • CSDN:Unity人工智能编程精粹学习笔记 实现AI角色的自主移动——操控行为

写在最后


本文内容会同步发在笔者的公众号上,欢迎大家关注交流!
公众号:程序员叨叨叨(ID:i_coder

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
发表于 2022-3-17 08:05 | 显示全部楼层
unity的dots有相关鱼群算法实现,效率很高。这个用物理的实现消耗太大了[飙泪笑]
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-22 16:54 , Processed in 0.086842 second(s), 23 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表