大家好,我是阿赵。继续学习Unity官方的DOTS例子工程。
接下来继续学习剩下的几个HelloCube下的例子。
9、 CrossQuery
这个例子的表现是分别有黑白两列cube:

当两列cube产生接触或者说是碰撞的时候,cube的颜色会发生变化:

首先第一个习惯还是先把子场景SubScene打开,看看里面放了什么,发现里面就是一个PrefabCollection,里面记录了需要实例化的Prefab叫做Box。
然后看看Box身上挂了什么:

可以看到有3个脚本:
1. URPMaterialPropertyBaseColor
由于项目是用URP做的,所以这里是写了宏需要在#if URP_10_0_0_OR_NEWER才会执行,用于控制颜色
2. DefaultColorAuthoring
用一个变量记录了WhenNotColliding的颜色,从字面看就是没有碰撞时的颜色。
3. VelocityAuthoring
用一个Vector3记录了一个移动的方向。
从这三个脚本可以看出了这个demo需要做的事情有2个,一个是通过向量来移动物体,第二个是控制物体的颜色,在没碰撞时一种颜色,碰撞后是另外一种颜色。

于是通过编写3个System来控制功能:
1. SpawnSystem
这个System是用来生成20个Prefab的实例的:
var prefabCollection = SystemAPI.GetSingleton<PrefabCollection>();
// spawn boxes
state.EntityManager.Instantiate(prefabCollection.Box, 20, Allocator.Temp);
然后再遍历这些实体,前10个给了一个黑色的默认色,并且坐标放在一边,后10个设置成白色,然后坐标放在另外一边,然后两边的移动方向velocity初始值是不一样的:
int i = 0;
foreach (var (velocity, trans, defaultColor, colorProperty) in
SystemAPI.Query<RefRW<Velocity>, RefRW<LocalTransform>,
RefRW<DefaultColor>, RefRW<URPMaterialPropertyBaseColor>>())
{
if (i < 10)
{
// black box on left
velocity.ValueRW.Value = new float3(2, 0, 0);
var verticalOffset = i * 2;
trans.ValueRW.Position = new float3(-3, -8 + verticalOffset, 0);
defaultColor.ValueRW.Value = new float4(0, 0, 0, 1);
colorProperty.ValueRW.Value = new float4(0, 0, 0, 1);
}
else
{
// white box on right
velocity.ValueRW.Value = new float3(-2, 0, 0);
var verticalOffset = (i - 10) * 2;
trans.ValueRW.Position = new float3(3, -8 + verticalOffset, 0);
defaultColor.ValueRW.Value = new float4(1, 1, 1, 1);
colorProperty.ValueRW.Value = new float4(1, 1, 1, 1);
}
i++;
}
2. MoveSystem
MoveSystem的作用是移动之前生成的20个Cube。由于本身两边的Cube移动方向是相对的,然后再通过3秒就给移动方向取反方向一次,让它们一直循环往复的移动。
3. CollisionSystem
这个System要做的事情是把之前生成的实体拿出来,然后逐个去做位置的判断,如果距离小于1的,则认为是碰撞。然后根据是否碰撞,设置不同的颜色。设置颜色的方式也比较奇特,并没有设置一种碰撞的颜色,而只是把绿色通道设置成0.5。

10、 RandomSpawn
这个例子运行的时候会看到很多Cube从顶部生成,从上往下不停的移动,到底部cube会消失。这些Cube围成了一个圆柱形:


在子场景里面通过一个类来记录需要生成的预设已经是基本操作了,就不多说了:

在这个ConfigAuthoring里面定义了2个ComponentData:

主要看看2个System:

其中MovementSystem也是老演员了,作用就是把生成出来的Cube不停的往下移动,然后如果y轴坐标小于0就销毁。
然后看看SpawnSystem,这里也是比较常规的操作了,设置了一个生成等待时间0.05秒,然后每次生成200个对象:

接下来先找到有NewSpawn组件的实体,把它们身上的NewSpawn组件删除。这些带有NewSpawn组件的实体都是上次生成的,为了区分每一批生成的内容。
接下来就是通过一个JobEntity来给这些新生成带有NewSpawn组件的实体做随机位置。
和之前的例子不同的有2点:
1. 这个Job是通过ScheduleParallel调用
Schedule和ScheduleParallel会安排新线程执行,区别在于:
Schedule:为每个Entity生成独立的Job,适合处理与Entity直接关联的逻辑。
ScheduleParallel:按Chunk(Entity原型分配的内存块)划分任务,当Chunk满载后会生成新Chunk,每个Chunk对应一个Job。
执行效率
在多Chunk场景下,ScheduleParallel通过并行处理多个Chunk显著提升效率,通常优于Schedule。
当仅有一个Chunk时,两者效果相同。
2. Job上面带有[WithAll(typeof(NewSpawn))]
这里的意思是必须带有NewSpawn组件的实体才会执行,这也是为什么需要把之前旧的实体身上的NewSpawn移除的原因了。只有当前一次生成的实体才会需要进行随机位置。
11、 FirstPersonController
这个例子运行时会看到地图上有一圈立方体,然后可以通过鼠标移动镜头方向,通过上下左右键移动摄像机,通过空格键模拟跳跃,模拟了一个第一人称游戏的效果。

先看子场景:

包括地面和阻挡的Cube在内,这些都是直接放置在子场景里面的,并没有挂脚本。
然后Player是主角移动时使用的Cube

上面挂了一个ControllerAuthoring脚本,里面记录了鼠标移动时旋转镜头的值、玩家移动的速度还有跳跃的速度。
然后打开看看Bake方法:

这个MonoBehaviour脚本在生成Entity实体时会添加2个ComponentData,一个是传递保存旋转速度、移动速度和跳跃速度的Controller,另外一个应该是记录输入情况的InputState。
接下来看看控制这个过程的3个System:

1. InputSystem
这个System起到的作用就是监听输入:
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
ref var inputState = ref SystemAPI.GetSingletonRW<InputState>().ValueRW;
inputState.Horizontal = Input.GetAxisRaw("Horizontal");
inputState.Vertical = Input.GetAxisRaw("Vertical");
inputState.MouseX = Input.GetAxisRaw("Mouse X");
inputState.MouseY = Input.GetAxisRaw("Mouse Y");
inputState.Space = Input.GetKeyDown(KeyCode.Space);
}
在OnUpdate里面监听Input的输入,然后保存到InputState里面。
2. ControllerSystem
这个System的作用是读取InputState里面的当前输入值,然后给Controller组件的实体进行位移旋转等控制。
由于从Bake给Controller时只是赋值了固定的MouseSensitivity、PlayerSpeed、JumpSpeed,为了达到计算垂直方向的实际速度,Controller加了一个VerticalSpeed,然后除了左右旋转,为了能让镜头产生上下角度,模拟抬头和低头的效果,Controller增加了一个CameraPitch值。
3. CameraSystem
之前在ControllerSystem已经进行了对玩家实体的控制了,这个CameraSystem是为了让摄像机能正常的跟随着主角移动而控制摄像机的。
注意看一个新出现的方法GetComponentLookup,这是一种高效的查找类型,用于在Job中快速访问和修改组件数据。
在通过GetComponentLookup获取了所有的LocalTransform只读组件之后,再通过GetSingletonEntity拿到唯一一个Controller的实体,就可以获得场景里面唯一有Controller组件的玩家实体的LocalTransform了,然后把它的位移和旋转赋值给摄像机,并且通过CameraPitch控制摄像机的RotateX旋转,就模拟了摄像机跟随玩家的效果了。
12、 FixedTimestep
这个例子的表现是在OnUpdate里面不停的生成竖线,上面的一段是用于参考的正常频率,下面的一段可以通过滑块调整OnUpdate的频率:


打开SubScene看看,发现有2个发射器,对应的就是例子里面两种不同频率的模型生成的起点了。

这两个发射器身上挂的脚本不同,一个挂的是DefaultRateSpawnerAuthoring,另外一个挂的是FixedRateSpawnerAuthoring,它们的作用都是一样的,都是记录需要生成的预设,还有一个是生成新物体的坐标。 但为了使用不同的System去控制,所以定义了不同的ComponentData。
接下来看看这里用到的3个System

1. DefaultRateSpawnerSystem

不用给这么多代码迷惑,实际上表达的意思就是在OnUpdate里面每次调用就生成一个新的模型实例,然后设置它的位置而已。由于OnUpdate的调用频率很高,所以就好像形成了一个连贯的带状效果。
2. FixedRateSpawnerSystem

这个FixedRateSpawnerSystem和前一个DefaultRateSpawnerSystem几乎完全一样的,区别只有2个:
(1) [UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
(2) 遍历组件时使用的是FixedRateSpawner
其实主要是第一点起作用,它指定了通过FixedStepSimulationSystemGroup来控制Update的频率。
然而这个频率是怎样控制的呢?好像在System里面没有看到设置?
其实这个是在UI组件上面写了脚本去控制的:

public void OnSliderChange()
{
float fixedFps = GetComponent<Slider>().value;
// WARNING: accessing World.DefaultGameObjectInjectionWorld is a broken pattern in non-trivial projects.
// GameObject interaction with ECS should generally go in the other direction: rather than having
// GameObjects access ECS data and code, ECS systems should access GameObjects.
var fixedSimulationGroup = World.DefaultGameObjectInjectionWorld
?.GetExistingSystemManaged<FixedStepSimulationSystemGroup>();
if (fixedSimulationGroup != null)
{
// The group timestep can be set at runtime:
fixedSimulationGroup.Timestep = 1.0f / fixedFps;
// The current timestep can also be retrieved:
sliderValueText.text = $"{(int)(1.0f / fixedSimulationGroup.Timestep)} updates/sec";
}
}
在滑块改变的时候,这里会获取场景里面的FixedStepSimulationSystemGroup,然后把值设置进去。
剩下的事情就和之前一样了,在OnUpdate调用的时候,生成模型实体,然后设置位置。
3. MoveProjectilesSystem
这个System的作用是把场景里面所有生成了的模型实体进行往右移动,使用了IJobEntity进行多线程批量操作。
13、 CustomTransforms
这个例子的表现是场景里面有一个模型在不停的位移旋转并且放大缩小
[image24]
这个例子虽然看起来和之前的例子很类似,都是位移旋转缩放。但实际它是想告诉我们怎样重写transform组件。·
在子场景里面,圆形和三角形组合在一起,然后都挂上了Transform2DAuthoring脚本
[image25]
在对Transform2DAuthoring进行Bake的时候,注意几点:
1. 创建实体是确保是没有标准的transform组件已经添加
var entity = GetEntity(TransformUsageFlags.ManualOverride);
2. 添加Parent组件
除了给实体添加了自定义的LocalTransform2D组件和自带的LocalToWorld组件,还判断了一下transform本身有没有父级物体,如果有,则再给实体添加一个Parent组件
3. 在声明LocalTransform2D结构体的时候使用了 [WriteGroup(typeof(LocalToWorld))]
这里指定了WriteGroup为LocalToWorld,那么拥有LocalTransform2D组件的实体就不会被标准的transform系统处理。
接下来看System

1. LocalToWorld2DSystem
这个System的处理看起来比较复杂,最终想达到的效果是,组装出一个位移旋转缩放的float4x4矩阵,作为localToWorld本地转换到世界坐标系的矩阵。
这个例子学习的目的不是怎样计算矩阵,而是怎样使用ECS,所以我们的关注点可以拉回来。

这里指定了Update的组和顺序,通过菜单Window-Entities-Systems,可以打开Systems窗口,这里可以看到执行的系统:

这里指定了[UpdateInGroup(typeof(TransformSystemGroup))]所以会在TransformSystemGroup里面执行,然后是[UpdateAfter(typeof(ParentSystem))],将会在ParentSystem后执行。
然后在这个例子里面,打了一个CustomTransforms的程序集,其目的应该是为了允许unsafe的代码:

因为里面使用了unsafe的内存块:

这样做估计目的是为了能让内存块更紧密,方便快速查找。
2. MovementSystem
这个System将给模型进行实际的位移旋转缩放。不过位移和缩放都是单独在这个System计算的,只有旋转是使用了localTransform2D里面的Roation。这些都是基本操作,就不再详细说明。
14、 StateChange
这个例子的表现效果是:

场景里面由很多白色方块组成,当鼠标按下时,会以鼠标位置为中心,出现一个红色的圆范围,范围内的方块都变成红色,并且在自转。
子场景里面的Config物体上面挂了ConfigAuthoring脚本,上面记录了需要实例化的预设、实例化的数量和半径,还有一个模式选项。

在Bake的时候,是给实体添加了3种组件数据的:

接下来看看System:

1. CubeSpawnSystem
这个System的作用就是实例化生成很多的盒子来待用。这个之前的例子已经出现过,没有太多的新东西。
2. InputSystem
这个System的作用是通过主摄像机和射线,找到和new Plane(Vector3.up, 0f)的一个碰撞点,作为点击的中心点传入Hit组件里面
3. SetStateSystem
根据第2步找到的碰撞点和半径,修改场景里面实体的状态,包括颜色和Spin组件里面的IsSpinning
4. SpinSystem
根据第3步设置的状态,给合适的cube旋转起来。
15、 ClosestTarget
这个例子和之前的Jobs101例子差不多,都是生成了很多单位和目标实体,然后每个单位去寻找离自己距离最近的目标:


在子场景里面放置了一个Simulation物体,上面的脚本记录了Unit单位的生成数量和预设,target目标的生成数量和预设。还有一个模式的旋转,是使用普通模式、排序二分法还是KDTree算法模式。

还是那句话,这里是介绍ECS使用的例子,所以重点不是具体算法,而是ECS的使用。所以KDTree的使用只是其中的一个展示而已。
主要还是看看System是怎样使用的。

1. InitializationSystem
这个System用于根据数量生成Unit和Target,并随机位置
2. MovementSystem
这个System使用IJobEntity让实体移动起来
3. TargetingSystem
这个是重点System,根据之前选择的模式,使用了三种不同的方式去查找每个Unit最近的Target,分别是直接IJobEntity循环查找、根据x坐标排序并二分查找(Jobs101最后一个步骤ParallelJob_Sorting所使用的方法)、还有使用IJobChunk的KDTree算法。
具体的算法有时间可以看看具体的实现。
4. DebugLinesSystem
这个是绘制Debug线段的System。在第3步里面给Unit查找到最近的Target,所以在这一步用DebugLine把Unit和对应的Target连接起来。
HelloCube的例子终于全部看完了。例子很多,到了后面讲得越来越简略了,这是因为这些例子的主要作用是介绍ECS的使用,在前面的例子里面,很多使用上的方法都说过了,后面的例子就不再重复说了,只是简单的说一下思路。

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



