大家好,我是阿赵。继续学习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());
}
到这个时候,所有的效果都做完了,之前的胶囊体模型被替换成了骨骼动画模型了:

3613

被折叠的 条评论
为什么被折叠?



