大家好,我是阿赵。继续学习Unity官方的DOTS例子工程。
之前花了很多时间看完了HelloCube的基础例子,接下来看一个综合一点的小例子

这个例子的实际效果是通过上下左右键同时控制很多个胶囊体,然后这些胶囊体模型会和场景里面的圆柱形阻挡物产生碰撞阻挡效果。当按回车键,每个胶囊体会随机方向发射一个小球,然后当胶囊体接触到球的时候,按空格键可以把球踢开。

如果胶囊体接近了球时按C键,可以把球捡起来

接下来根据Demo里面的步骤,看看制作的过程:
1、 Step1
第一步比较简单,打开子场景可以看到,里面除了一个作为地面的Plane之外,还有一个Config:

在Config上面挂载了ConfigAuthoring脚本,里面记录了很多参数,这都是准备Bake给实体时记录的参数了。
然后下面的ExecuteAuthoring就是老演员了,其作用就是为了让接下来不同步骤的System脚本不要互相干扰而已。
然后在生成方面,只记录了一个阻挡物的Prefab,这个Prefab上面挂载了ObstacleAuthoring脚本,作用是在Bake的时候给实体加上Obstacle组件:

在System方面,这个例子只有一个System

作用是生成障碍物,直接两重循环,根据数量实例化实体而已:
public void OnUpdate(ref SystemState state)
{
// We only want to spawn obstacles one time. Disabling the system stops subsequent updates.
state.Enabled = false;
// GetSingleton and SetSingleton are conveniences for accessing
// a "singleton" component (a component type that only one entity has).
// If 0 entities or 2 or more entities have the Config component, this GetSingleton() call will throw.
var config = SystemAPI.GetSingleton<Config>();
// For simplicity and consistency, we'll use a fixed random seed value.
var rand = new Random(123);
var scale = config.ObstacleRadius * 2;
// Spawn the obstacles in a grid.
for (int column = 0; column < config.NumColumns; column++)
{
for (int row = 0; row < config.NumRows; row++)
{
// Instantiate copies an entity: a new entity is created with all the same component types
// and component values as the ObstaclePrefab entity.
var obstacle = state.EntityManager.Instantiate(config.ObstaclePrefab);
// Position the new obstacle by setting its LocalTransform component.
state.EntityManager.SetComponentData(obstacle, new LocalTransform
{
Position = new float3
{
x = (column * config.ObstacleGridCellSize) + rand.NextFloat(config.ObstacleOffset),
y = 0,
z = (row * config.ObstacleGridCellSize) + rand.NextFloat(config.ObstacleOffset)
},
Scale = scale,
Rotation = quaternion.identity
});
}
}
}
所以这个Step1步骤的场景运行的时候,只会看到有一定数量的障碍物生成在场景里面:

2、 Step2
先看看Step2的子场景,里面的Config是这样的:

还是之前的ConfigAuthoring,但多指定了Player的预设。

看看挂在Player身上的PlayerAuthoring:
namespace Tutorials.Kickball.Step2
{
// Same pattern as ObstacleAuthoring.cs in Step 1.
public class PlayerAuthoring : MonoBehaviour
{
class Baker : Baker<PlayerAuthoring>
{
public override void Bake(PlayerAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent<Player>(entity);
// Used in Step 5
AddComponent<Carry>(entity);
SetComponentEnabled<Carry>(entity, false);
}
}
}
public struct Player : IComponentData
{
}
// Used in Step 5
public struct Carry : IComponentData, IEnableableComponent
{
// on a ball, this denotes the player carrying the ball; on a player, this denotes the ball being carried
public Entity Target;
}
}
在Bake的时候,除了添加了一个Player的ComponentData,还添加了一个Carry的ComponentData,然后把Carry的Enabled设置为false。
看看Carry的说明,是使用在Step5的,所以暂时是用不到。从注释看,这个Carry如果出现在一个ball身上,那么Target代表的是搬运ball的player,如果出现在player身上,那么Target就代表正在搬运的ball。
由于Step2是加在Player身上的Carry,所以里面的Target就是代表将要搬运的球了。这一步先不管,等Step5再看。
然后在ExecuteAuthoring里面,多勾选了Step2的两个项,其实就等于是多启动了两个System。

1. PlayerSpawnerSystem
这个System是创建多个player模型用的。
创建的方式是先找到Step1创建的所有障碍物圆柱体,然后每个player的坐标的x和z是障碍物的坐标的x和z加上一个PlayerOffset值,y轴是1
foreach (var obstacleTransform in
SystemAPI.Query<RefRO<LocalTransform>>().
WithAll<Obstacle>())
{
// Create a player entity from the prefab.
var player = state.EntityManager.Instantiate(config.PlayerPrefab);
// Set the new player's transform (a position offset from the obstacle).
state.EntityManager.SetComponentData(player, new LocalTransform
{
Position = new float3
{
x = obstacleTransform.ValueRO.Position.x + config.PlayerOffset,
y = 1,
z = obstacleTransform.ValueRO.Position.z + config.PlayerOffset
},
Scale = 1, // If we didn't set Scale and Rotation, they would default to zero (which is bad!)
Rotation = quaternion.identity
});
}
2. PlayerMovementSystem
这个System做了2件事情
(1) 监听input
var horizontal = Input.GetAxis("Horizontal");
var vertical = Input.GetAxis("Vertical");
var input = new float3(horizontal, 0, vertical) * SystemAPI.Time.DeltaTime * config.PlayerSpeed;
(2) 判断下一个坐标是否有障碍物
foreach (var playerTransform in
SystemAPI.Query<RefRW<LocalTransform>>()
.WithAll<Player>())
{
var newPos = playerTransform.ValueRO.Position + input;
// A foreach query nested inside another foreach query.
// For every entity having a LocalTransform and Obstacle component, a read-only reference to
// the LocalTransform is assigned to 'obstacleTransform'.
foreach (var obstacleTransform in
SystemAPI.Query<RefRO<LocalTransform>>()
.WithAll<Obstacle>())
{
// If the new position intersects the player with a wall, don't move the player.
if (math.distancesq(newPos, obstacleTransform.ValueRO.Position) <= minDistSQ)
{
newPos = playerTransform.ValueRO.Position;
break;
}
}
playerTransform.ValueRW.Position = newPos;
}
遍历所有的Player,然后把之前input得到的坐标偏移加上当前Player的坐标,得到如果按照输入下一个点需要到达的坐标,然后判断一下是否有和其他障碍物的距离小于指定的最小距离minDistSQ。如果没有,则可以正常走到这个点,如果有,则player就保持当前的位置。
所以这一步运行的效果是已经可以通过上下左右键控制胶囊体的player,然后和圆柱体的障碍物产生碰撞。

3、 Step3
在Step3的子场景里面,Config再指定了Ball的预设:

Ball的预设上面挂了BallAuthoring脚本:
namespace Tutorials.Kickball.Step3
{
public class BallAuthoring : MonoBehaviour
{
class Baker : Baker<BallAuthoring>
{
public override void Bake(BallAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.Dynamic);
// A single authoring component can add multiple components to the entity.
AddComponent<Ball>(entity);
AddComponent<Velocity>(entity);
// Used in Step 5
AddComponent<Carry>(entity);
SetComponentEnabled<Carry>(entity, false);
}
}
}
// A tag component for ball entities.
public struct Ball : IComponentData
{
}
// A 2d velocity vector for the ball entities.
public struct Velocity : IComponentData
{
public float2 Value;
}
}
Bake的时候,会给实体添加Ball组件,添加之前在Player里面提过的Carry组件,然后还添加一个Velocity组件,用于记录球移动的速度大小和方向数据。
很明显Step3是为了增加球的表现的,所以也增加了2个System

1. BallSpawnerSystem
这个System是用于创建Ball的,当监听到按下回车键的时候,就开始触发生成球的逻辑
if (!Input.GetKeyDown(KeyCode.Return))
{
return;
}
然后遍历每个Player,在每个Player的坐标上面生成一个Ball的模型
foreach (var transform in
SystemAPI.Query<RefRO<LocalTransform>>()
.WithAll<Player>())
{
var ball = state.EntityManager.Instantiate(config.BallPrefab);
state.EntityManager.SetComponentData(ball, new LocalTransform
{
Position = transform.ValueRO.Position,
Rotation = quaternion.identity,
Scale = 1
});
并且随机一个方向给Ball上面的Velocity组件设置一个方向的速度。
state.EntityManager.SetComponentData(ball, new Velocity
{
// NextFloat2Direction() returns a random 2d unit vector.
Value = rand.NextFloat2Direction() * config.BallStartVelocity
});
2. BallMovementSystem
这个System主要遍历所有Ball小球,并做了3件事情
(1) 让Ball沿着Velocity方向移动
var magnitude = math.length(velocity.ValueRO.Value);
var newPosition = ballTransform.ValueRW.Position +
new float3(velocity.ValueRO.Value.x, 0, velocity.ValueRO.Value.y) * dt;
(2) 让速度模拟受到阻力越来越慢
var newMagnitude = math.max(magnitude - decayFactor, 0);
velocity.ValueRW.Value = math.normalizesafe(velocity.ValueRO.Value) * newMagnitude;
(3) 如果Ball遇到障碍物圆柱体,则反弹
foreach (var obstacleTransform in
SystemAPI.Query<RefRO<LocalTransform>>()
.WithAll<Obstacle>())
{
if (math.distancesq(newPosition, obstacleTransform.ValueRO.Position) <= minDistSQ)
{
newPosition = DeflectBall(ballTransform.ValueRO.Position, obstacleTransform.ValueRO.Position,
velocity, magnitude, dt);
// As long as the obstacles are spaced apart, it's impossible
// for one ball to hit two obstacles in a single frame, so we can
// break after detecting collision with one obstacle.
break;
}
}
通过DeflectBall方法来计算球反弹后应该所在的坐标,并且改变球的移动速度方向
private float3 DeflectBall(float3 ballPos, float3 obstaclePos, RefRW<Velocity> velocity, float magnitude, float dt)
{
var obstacleToBallVector = math.normalize((ballPos - obstaclePos).xz);
velocity.ValueRW.Value = math.reflect(math.normalize(velocity.ValueRO.Value), obstacleToBallVector) * magnitude;
return ballPos + new float3(velocity.ValueRO.Value.x, 0, velocity.ValueRO.Value.y) * dt;
}
所以现在的表现是这样:

我们可以按上下左右键移动所有Player,然后按回车键,每个Player都会发射一个小球,小球如果碰到障碍物会反弹,而且速度会越来越慢,最后停下来
4、 Step4
步骤4的子场景里面,Config上的Execute改变了:

主要的改变就是取消了Player和Ball原来旧的Movement,然后使用了两个新的Movement。从这里可以看出,这个步骤主要是修改了Player和Ball的移动方式。
1. NewPlayerMovementSystem
和之前的PlayerMovementSystem相比,主要的改变是使用了IJobEntity来对人物移动的逻辑进行了多线程批处理:
// The implicit query of this IJobEntity matches all entities having LocalTransform and Player components.
[WithAll(typeof(Player))]
[BurstCompile]
public partial struct PlayerMovementJob : IJobEntity
{
[ReadOnly] public NativeArray<LocalTransform> ObstacleTransforms;
public float3 Input;
public float MinDistSQ;
public void Execute(ref LocalTransform transform)
{
var newPos = transform.Position + Input;
foreach (var obstacleTransform in ObstacleTransforms)
{
if (math.distancesq(newPos, obstacleTransform.Position) <= MinDistSQ)
{
newPos = transform.Position;
break;
}
}
transform.Position = newPos;
}
}
2. NewBallMovementSystem
和之前的BallMovementSystem相比,也是使用了IJobEntity来进行多线程批处理球的移动逻辑:
// The implicit query of this IJobEntity matches all entities having LocalTransform, Velocity, and Ball components.
[WithAll(typeof(Ball))]
[WithDisabled(typeof(Carry))] // Relevant in Step 5
[BurstCompile]
public partial struct BallMovementJob : IJobEntity
{
[ReadOnly] public NativeArray<LocalTransform> ObstacleTransforms;
public float DecayFactor;
public float DeltaTime;
public float MinDistToObstacleSQ;
public void Execute(ref LocalTransform transform, ref Velocity velocity)
{
if (velocity.Value.Equals(float2.zero))
{
return;
}
var magnitude = math.length(velocity.Value);
var newPosition = transform.Position +
new float3(velocity.Value.x, 0, velocity.Value.y) * DeltaTime;
foreach (var obstacleTransform in ObstacleTransforms)
{
if (math.distancesq(newPosition, obstacleTransform.Position) <= MinDistToObstacleSQ)
{
newPosition = DeflectBall(transform.Position, obstacleTransform.Position, ref velocity, magnitude, DeltaTime);
break;
}
}
transform.Position = newPosition;
var newMagnitude = math.max(magnitude - DecayFactor, 0);
velocity.Value = math.normalizesafe(velocity.Value) * newMagnitude;
}
其他方面没什么变化,所以现在可以支持更多的Player和Ball同时移动:

5、 Step5
最后一个步骤了,看看子场景里面的Config:

在步骤4的基础上,增加了2个System,从字面意思看,BallCarry是捡球,BallKicking是踢球了。
1. BallCarrySystem
这个System做了几件事:
(1) 让有Target的球跟随着对应Target的Player
之前说过,如果Carry放在Ball实体上,那么Carry里面的Target是代表了带着球的玩家。
所以这里先遍历了所有带有Carry组件并且是Enabled的球,然后找到Carry对应Target的Player,然后把Ball的坐标设置到和Player一样,并且偏移一个CarryOffset值
foreach (var (ballTransform, carrier) in
SystemAPI.Query<RefRW<LocalTransform>, RefRO<Carry>>()
.WithAll<Ball>())
{
var playerTransform = state.EntityManager.GetComponentData<LocalTransform>(carrier.ValueRO.Target);
ballTransform.ValueRW.Position = playerTransform.Position + CarryOffset;
}
由于Carry组件默认的Enabled是设置了false的,所以没有被Player捡到的球是不会执行的。
(2) 判断是否按下C键
if (!Input.GetKeyDown(KeyCode.C))
{
return;
}
如果没有按C键,剩下的逻辑是不会运行的。
(3) 遍历所有玩家判断球的情况
当按下了C键之后,就要遍历所有玩家了。
如果玩家身上的Carry组件的Enabled是true的情况,证明这个Player已经捡起了球,那么需要做的事情就是把球放下
先拿到carry组件,然后拿到球的LocalTransform,把它的Position设置成和当前Player一样:
var carried = state.EntityManager.GetComponentData<Carry>(playerEntity);
var ballTransform = state.EntityManager.GetComponentData<LocalTransform>(carried.Target);
ballTransform.Position = playerTransform.ValueRO.Position;
state.EntityManager.SetComponentData(carried.Target, ballTransform);
然后把Player和Ball的Carry组件的Enabled都设置成false
state.EntityManager.SetComponentEnabled<Carry>(carried.Target, false);
state.EntityManager.SetComponentEnabled<Carry>(playerEntity, false);
最后再把Player和Ball的Carry设置成一个新的Carry组件,这样Target就没了
如果玩家身上本身没有球,也就是Carry的Enabled是false,那么就遍历所有球,看有没有哪个球和当前Player的距离足够小,然后就把球拿起来:
先设置一个新的Velocity给球,让球不会再移动
state.EntityManager.SetComponentData(ballEntity, new Velocity());
然后设置Player和ball的Carry组件的Target为对方:
state.EntityManager.SetComponentData(playerEntity, new Carry { Target = ballEntity });
state.EntityManager.SetComponentData(ballEntity, new Carry { Target = playerEntity });
最后把Player和Ball的Carry组件的Enabled都设置为true:
state.EntityManager.SetComponentEnabled<Carry>(playerEntity, true);
state.EntityManager.SetComponentEnabled<Carry>(ballEntity, true);
2. BallKickingSystem
这个System是为了模拟踢球的效果的,过程也比较简单,当按了空格键之后,遍历所有的Player和Ball,如果距离达到可以踢的范围,则通过Ball的xz坐标减去Player的xz坐标得到一个向量,然后施加给Ball。
// For every player, add an impact velocity to every ball in kicking range.
foreach (var playerTransform in
SystemAPI.Query<RefRO<LocalTransform>>()
.WithAll<Player>())
{
foreach (var (ballTransform, velocity) in
SystemAPI.Query<RefRO<LocalTransform>, RefRW<Velocity>>()
.WithAll<Ball>())
{
float distSQ = math.distancesq(playerTransform.ValueRO.Position, ballTransform.ValueRO.Position);
if (distSQ <= config.BallKickingRangeSQ)
{
var playerToBall = ballTransform.ValueRO.Position.xz - playerTransform.ValueRO.Position.xz;
// Use normalizesafe() in case the ball and player are exactly on top of each other
// (which isn't very likely but not impossible).
velocity.ValueRW.Value += math.normalizesafe(playerToBall) * config.BallKickForce;
}
}
}
由于之前的Carry的作用,如果Ball被Player捡起来了,他们的xz坐标会完全一样,所以得不到一个有效的向量,所以被捡起来的球是不会被踢的。
所以最后,这整个例子就做完了:
- 同时通过上下左右键控制多个角色,角色和障碍物有碰撞效果
- 按回车键会发射小球,然后小球和障碍物产生反弹
- 按c键可以把球捡起来或者放下
- 按空格键可以把球踢飞:


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



