Unity官方Dots范例工程学习——Entities101-FireFighters

  大家好,我是阿赵。继续学习Unity的官方Dots例子。
  接下来要看的是FireFighters例子。
  这个例子是一个消防员救火的模拟例子,地面上会产生火源并着火,场景里面有很多个消防员,它们会去搬水救火。
在这里插入图片描述

  接下来按照制作步骤看看:

1、 Step1

  惯例先看SubScene里面有什么:
在这里插入图片描述

  很干净的只有一个Config物体,上面挂了ExecuteAuthoring作为让不同System生效的选项。
  然后上面挂了一个ConfigAuthoring,主要的作用就是记录了各种参数。比如

1. Ponds

  每个边上有多少个池塘
在这里插入图片描述

  池塘指的就是这些可以取水的小方块。

2. Bots

  机器人的数量,应该是指消防员的数量
在这里插入图片描述

  从设置上看,机器人是按队伍分的,然后每个队伍多少人可以指定,移动速度可以指定。

3. Buckets

  水桶的数量,消防员需要拿水桶去接水。
在这里插入图片描述

  水桶在装水的过程中,颜色会变化,大小也会变化。

4. Ground

  这个例子的地面,实际上是通过很多个方块拼在一起的
在这里插入图片描述

  所以需要指定地面生成方块的长宽
在这里插入图片描述

5. Heat

  这是着火时候的效果
在这里插入图片描述

  在燃烧的过程中,是会变颜色的,所以这里可以指定颜色。然后还有燃烧的速度、范围等。

6. Prefabs

  这里是指定了各种预设,比如机器人的预设、水桶的预设、池塘的预设、地面的预设,还有带动的机器人的预设。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

  这些元素就是构成这个Demo的所有演员了。
  Step1主要做的事情就是列出了这些元素,并且生成在场景里面,可以看看SpawnSystem,里面有逐个类别的生成代码,由于现在还没有用到这些实体的功能,所以单纯看SpawnSystem会比较难懂。这些实体的实际功能,在后面用到的时候可以再返回来看。
  由于只是生成而没有任何控制,所以运行的时候Step1是这样的:
在这里插入图片描述

2、 Step2

  第二步主要是做这个着火点的扩散:
在这里插入图片描述

  慢慢扩散之后,会变成一片火海:
在这里插入图片描述

  惯例先看SubScene
在这里插入图片描述

  这种用Execute选项来区分System运行的方法已经是老演员了,通过勾选不同的选项生成不同的组件给实体,用于区分System运行。这里就不再详细说明。
  这里勾选了HeatSystem,证明Step2主要运行这个HeatSystem
  所以打开HeatSystem来看看,里面主要做了2件事

1.火势加剧和蔓延

  把已经带有Heat的实体,通过附近8个格子的Heat的值,再加上自己的燃烧速度值,改变自己的燃烧状态。如果附近8个格子有燃烧,那么渐渐自己的燃烧值也会变大,也就是火势慢慢的扩展了。
  这个过程在代码里面一开始是没有使用JobSystem做,使用的方法是直接HeatSpread_MainThread里面的同步循环。然后这个HeatSpread_MainThread方法没有实际调用,Demo里面使用的是HeatSpread_ParallelJob方法,然后开了可以并行运算作业的继承了接口IJobParallelFor的HeatSpreadJob_Parallel来计算这个过程。

2. 制作火势蔓延的表现

  通过遍历所有的GroundCell,也就是地面的每一个格子实体,然后判断heatBuffer里面的燃烧值,然后做出相应的表现,比如改变颜色,然后改变y轴的高度。
  这个过程是通过继承IJobEntity接口的GroundCellUpdate实现的。

3、 Step3

  这一步主要是制作机器人的移动逻辑的。
在这里插入图片描述

  从子场景的Config可以看出,这里生成机器人的数量比较多,总共有12个队伍,然后每个队伍里面有20个人。
  然后下面的Buckets水桶生成得也比较多,总共有140个。
  接下来需要制作的功能涉及到一些demo里面的规则,需要先说明一下:

1. 每个Team(队伍)的构成

public struct Team : IComponentData
{
    public Entity Filler;
    public Entity Bucket;
    public int NumFiresDoused;
}

  每个队伍都有一个负责装水的实体Filler,这个Filler是在SpawnSystem创建team的成员的时候,把botIdx==0的那个成员实体指定的:

for (int botIdx = 0; botIdx < numBotsPerTeam; botIdx++)
                    {
                        var botEntity = state.EntityManager.Instantiate(config.BotPrefab);

                        var x = rand.NextFloat(0.5f, config.GroundNumColumns - 0.5f);
                        var z = rand.NextFloat(0.5f, config.GroundNumRows - 0.5f);

                        state.EntityManager.SetComponentData(botEntity, LocalTransform.FromPosition(x, 1, z));
                        state.EntityManager.SetComponentData(botEntity, new URPMaterialPropertyBaseColor
                        {
                            Value = teamColor
                        });

                        // designate the filler
                        if (botIdx == 0)
                        {
                            team.Filler = botEntity;
                        }

                        memberBuffer.Add(new TeamMember { Bot = botEntity });
                    }

  然后Team还有一个Bucket水桶实体。具体赋值也是在SpawnSystem创建队伍的时候赋值的:

// spawn teams
{
    int numBotsPerTeam = config.NumPassersPerTeam + 1;
    int douserIdx = (config.NumPassersPerTeam / 2);
    for (int teamIdx = 0; teamIdx < config.NumTeams; teamIdx++)
    {
        var teamEntity = state.EntityManager.CreateEntity();
        var team = new Team
        {
            Bucket = bucketEntities[teamIdx]
        };

2. TeamMember队伍成员

    // all bots in the team (including the Filler and Douser) in order of passing, starting with the Filler
    public struct TeamMember : IBufferElementData
    {
        public Entity Bot;
}

  在一个队伍里面,有很多个成员。这些成员是继承IBufferElementData的元素。里面会记录一个机器人实体,用于后面的各种行为和动画表现。

3. 排队的概念

  在每个teamEntity队伍实体里面,会添加一个RepositionLine。这是一个排队定位的组件。如果队伍已经确定了行为,则需要队伍里面的每个成员按照固定的排队位置进行排队。

4. 每个Bucket水桶的构成

    public struct Bucket : IComponentData
    {
        public float Water;  // 0 = empty, 1 = full

        public Entity CarryingBot;
        public bool IsCarried;
}

  首先水桶通过water参数确定水是否满了
  然后水桶有一个正在携带它的机器人的实体
  当然这个实体也可以没有,因为还有一个是否正在被携带的状态。

  知道了这些规则之后,就可以回来看看Step3用到的3个新的System了

1. BucketSystem

先看最简单的BucketSystem。这个系统是根据当前水桶的状态做出相应的表现:

foreach (var (bucket, trans, color) in
         SystemAPI.Query<RefRW<Bucket>, RefRW<LocalTransform>, RefRW<URPMaterialPropertyBaseColor>>())
{
    // todo we only really need to update the color when the water value changes
    color.ValueRW.Value = math.lerp(config.BucketEmptyColor, config.BucketFullColor, bucket.ValueRO.Water);
    trans.ValueRW.Scale = math.lerp(config.BucketEmptyScale, config.BucketFullScale, bucket.ValueRO.Water);

    if (bucket.ValueRO.IsCarried)
    {
        var botTrans = SystemAPI.GetComponent<LocalTransform>(bucket.ValueRO.CarryingBot);
        trans.ValueRW.Position = botTrans.Position + new float3(0, 1, 0); // place above the bot's head
    }
}

  首先是水桶的颜色和缩放,根据水量的多少直接赋值。
  然后是水桶是否被人拿着,如果是,把水桶的坐标放到拿着水桶的人头上:

if (bucket.ValueRO.IsCarried)
{
    var botTrans = SystemAPI.GetComponent<LocalTransform>(bucket.ValueRO.CarryingBot);
    trans.ValueRW.Position = botTrans.Position + new float3(0, 1, 0); // place above the bot's head
}

2. LineSystem

  LineSystem就是给Team的成员排队用的。
在这里插入图片描述

  每次队伍做一个行为,都会这样排队,对于队伍的Filler来说,如果队伍所属的Bucket水桶没有被携带,那么Filler会把状态改成BotState.MOVE_TO_BUCKET,先去拿水桶。如果水桶已经被携带,那么Filler的状态会改成BotState.MOVE_TO_LINE,将移动到排队前端。

var filler = SystemAPI.GetComponentRW<Bot>(team.ValueRO.Filler);
filler.ValueRW.LinePos = randomPondPos;

var bucket = SystemAPI.GetComponentRW<Bucket>(team.ValueRO.Bucket);
if (bucket.ValueRO.IsCarried)
{
    filler.ValueRW.State = BotState.MOVE_TO_LINE;
}
else
{
    filler.ValueRW.TargetPos = SystemAPI.GetComponent<LocalTransform>(team.ValueRO.Bucket).Position.xz;
    filler.ValueRW.State = BotState.MOVE_TO_BUCKET;
}

其他成员会根据队伍的位置去排队。

for (int i = 1; i <= douserIdx; i++)
{
    var ratio = (float)i / (douserIdx + 1);
    var offset = math.sin(math.lerp(0, math.PI, ratio)) * offsetVec * config.LineMaxOffset;
    var pos = math.lerp(randomPondPos, nearestFirePos, ratio);

    var bot = SystemAPI.GetComponentRW<Bot>(members[i].Bot);
    bot.ValueRW.State = BotState.MOVE_TO_LINE;
    bot.ValueRW.LinePos = pos + offset;

    if (bot.ValueRO.IsDouser)
    {
        bot.ValueRW.TargetPos = nearestFirePos;
    }

    var otherBot = SystemAPI.GetComponentRW<Bot>(members[^i].Bot);
    otherBot.ValueRW.State = BotState.MOVE_TO_LINE;
    otherBot.ValueRW.LinePos = pos - offset;
}

3. BotSystem

  这个System是控制机器人的各种行为。
  机器人的状态有:

    public enum BotState
    {
        IDLE,
        CLAIM_BUCKET,
        MOVE_TO_BUCKET,
        FILL_BUCKET,
        PASS_BUCKET,
        DOUSE_FIRE,
        MOVE_TO_LINE,
        WAIT_IN_LINE,
}

分别是:
IDLE:站立
CLAIM_BUCKET:索取水桶
MOVE_TO_BUCKET:移动到水桶
FILL_BUCKET:给水桶装水
PASS_BUCKET:传递水桶
DOUSE_FIRE:浇水灭火
MOVE_TO_LINE:移动到排队位置
WAIT_IN_LINE:排队等待

  所以在BotSystem里面,虽然看着很复杂,实际上就是根据情况,设置机器人的不同的状态而已。然后再根据机器人的状态,给他们执行相应的行为。

4、 Step4

  到Step3一步,实际上机器人已经会自动执行这个Demo的大部分行为了,只是没有表现,也不能通过UI去控制.
  所以最后一步Step4就补齐这些内容。
在这里插入图片描述

  看看SubScene,这里面的config把UISystem和AnimationSystem都勾上了。

1. UISystem

  这个System明显是控制UI的。
在这里插入图片描述

  在场景里面有一个UIDocument物体,上面挂着UIController脚本。
  在UISystem里面先找到了UIController:

var configEntity = SystemAPI.GetSingletonEntity<Config>();
var configManaged = state.EntityManager.GetComponentObject<ConfigManaged>(configEntity);

if (!initialized)
{
    initialized = true;

    configManaged.UIController = GameObject.FindFirstObjectByType<UIController>();
}

  接下来的两个功能,首先是统计个个队伍浇灭火的数量,所以是遍历了所有Team组件,然后把NumFiresDoused相加得到总的浇灭火数量。

foreach (var (team, entity) in 
         SystemAPI.Query<RefRO<Team>>()
             .WithEntityAccess())
{
    totalFiresDoused += team.ValueRO.NumFiresDoused;

  然后如果按了UI上面的Reposition按钮,将会给实体上的RepositionLine组件设置为true,那么该队伍就会重新去寻找目标了。

if (shouldReposition)
{
    SystemAPI.SetComponentEnabled<RepositionLine>(entity, true);
}    

2. AnimationSystem

  之前在Step3里面,角色都是没有具体的模型形象的,而是用胶囊体来代替机器人的形象:
在这里插入图片描述

  在Step4里面,将会用这个骨骼动画模型来代替胶囊体:
在这里插入图片描述

  在AnimationSystem里面,先进行初始化,把所有Bot组件的实体找出来,给他们添加BotAnimation组件,实例化骨骼模型并记录在BotAnimation组建里面。

if (!isInitialized)
{
    isInitialized = true;

    var configEntity = SystemAPI.GetSingletonEntity<Config>();
    var configManaged = state.EntityManager.GetComponentObject<ConfigManaged>(configEntity);

    var ecb = new EntityCommandBuffer(Allocator.Temp);

    foreach (var (transform, entity) in
             SystemAPI.Query<RefRO<LocalTransform>>()
                 .WithAll<Bot>()
                 .WithEntityAccess())
    {
        var botAnimation = new BotAnimation();
        var go = GameObject.Instantiate(configManaged.BotAnimatedPrefabGO);
        botAnimation.AnimatedGO = go;
        go.transform.localPosition = (Vector3)transform.ValueRO.Position;
        ecb.AddComponent(entity, botAnimation);
        
        // disable rendering
        ecb.RemoveComponent<MaterialMeshInfo>(entity);
    }

    ecb.Playback(state.EntityManager);
}

  然后遍历所有的Bot和BotAnimation组件的实体,根据Bot现在的状态,让BotAnimation里面的骨骼动画模型做相应的动作。

var isMovingId = Animator.StringToHash("IsMoving");

foreach (var (bot, transform, botAnimation) in
         SystemAPI.Query<RefRO<Bot>, RefRO<LocalTransform>, BotAnimation>())
{
    var pos = (Vector3)transform.ValueRO.Position;
    pos.y = 0;
    botAnimation.AnimatedGO.transform.localPosition = pos;
    botAnimation.AnimatedGO.transform.localRotation = (Quaternion)transform.ValueRO.Rotation;

    var animator = botAnimation.AnimatedGO.GetComponent<Animator>();
    animator.SetBool(isMovingId, bot.ValueRO.IsMoving());
}

  到这个时候,所有的效果都做完了,之前的胶囊体模型被替换成了骨骼动画模型了:
在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值