网易首页 > 网易号 > 正文 申请入驻

用Unity DOTS制作4万飞剑的太极剑阵!

0
分享至

【USparkle专栏】如果你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息!

这是侑虎科技第1437篇文章,感谢作者炎拳供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:465082844)

作者主页:

https://auniquepig.com/

一、演示效果

由于DOTS相关Package不同版本变动很大,许多老的教程也已经过时,给想要探索的小伙伴制造了不少麻烦。所以我便尝试用DOTS制作了这样一个由42804把飞剑组成的炫酷剑阵,每次点击地板,都会有10000把飞剑飞出大阵攻击目标点后返回。算是致敬了古龙小说中的“剑气纵横三万里 ,一剑光寒十九洲”的光景。本文将制作过程和学习经历分享出来,希望能给同样探索DOTS的小伙伴一些参考。

文章较长,对准备工作没兴趣的小伙伴直接跳到目录6。

二、创建工程

首先我们下载好Unity 2020.3.3f1,选择Universal Render Pipeline创建工程。

DOTS的相关Package并未发布,也无法在Package Manager中搜索到,我们需要手动去下载这几个包。

三、下载DOTS相关包

打开上方菜单栏,点击在左上角+号图标选择Add package from git URL,依次输入:

  • com.unity.entities

  • com.unity.rendering.hybrid

  • com.unity.physics

等 待一会儿包就下载好了,但我们的准备工作还没做完。

四、动态合批设置

由于我们将要生成大量相同材质的飞剑,所以将它们合批处理降低DrawCall是有必要的。

首先点击Edit >ProjectSetting>Quality>查看当前使用的Rendering设置文件,关闭SRP Batcher:

再 新建一个材质,勾选Enable GPU Instancing:

你就 会发现Unity将拥有同一此材质的物体合批渲染了:

值得一提的是,本工程中的飞剑已经被我用Blender手动将顶点数降低到105个了,因为我最后大致要生成4万把飞剑,原本的飞剑模型有上千个顶点,庞大的定点数会导致我的场景近乎卡死,最后测试我的电脑能顶住的最大顶点数大概是10M左右。

五、制作飞剑发光效果

我希望每把飞剑都有一个发出淡淡光晕的效果,在2020.3.3版本的URP工程中,Unity已将Post Processing Package默认置入,所以我们无需像老版本一样去自己下载Post-Processing package包。直接使用就可以了。

首先新建一个材质球,勾选了Emission后扔给飞剑,并在场景中右键新建一个Global Volume:

同时点 击 主相机,勾选Solid Color,选择一个偏黑色的颜色,再将Post Processing勾选上:

我 们的场景中不需要天空盒,所以点击Window>Rendering>Lighting>Environment,将天空盒材 质设置为None:

最后 我希望后处理效果只对指定层级的物体起作用,这里需要Global Volume和物体设置为相同层级,并且在主相机中指定该层级:

最后 新建一个配置文件,点击Add Override,并添加Bloom效果即可。

准备工作做完了,终于要进入实践环节了,本文就不赘述DOTS相关的概念了,网上关于DOTS介绍的文章很多,官方文档对于DOTS阐述也算是详尽,这里我放个链接,对DOTS不了解的小伙伴可以先理解了这些概念后再来实践:

官方文档:

https://docs.unity3d.com/Packages/com.unity.entities@0.17/manual/index.html

六、生成太极图案的飞剑群实体

首先找到一张太极八卦阵的图案,然后掐太极法印! 高呼口诀:

  • 天地无极,乾坤借法!

  • 太乙天尊,急急如律令!

然后就发现图片白色部分都变成透明的了:

接下来 我们需要获取图片中每个像素点的位置,然后等比转换到Unity空间中去,让飞剑去填充这些位置。 所以图片也不能太大,这里我们导入的时候在Unity中设置就好了:

代码如下:

public SpriteRenderer spriteRenderer;
//像素点相对位置
public List posList;
[Header("Drawing")]
//剑阵密度
public int drawDensity ;
//剑阵离散程度
public int disperseMin;
public static GetPixel Instance;
//图片宽高
private int width;
private int height;

void Start()
{
Instance = this;
width = spriteRenderer.sprite.texture.width;
height = spriteRenderer.sprite.texture.height;
Debug.Log("图片宽度" + width + "图片高度" + height);
GetPixelPos();
}

public void GetPixelPos()
{
int halfHeight= height / 2;
int halfWidth = width / 2;
int2 tempPos;
for (int i = 0; i < height; i += drawDensity)
{
for (int j = 0; j < width; j += drawDensity)
{
//获取每个位置像素点的颜色
Color32 c = spriteRenderer.sprite.texture.GetPixel(j, i);
tempPos.y = (j-halfHeight)*disperseMin;
// Debug.Log("RGBA:" + c);
//如果对应位置颜色不为透明,则记录坐标到List中
if (c.a != 0)
{
tempPos.x = (i-halfWidth)* disperseMin;
posList.Add(tempPos);
}
}
}
}

位置拿到了,接下来需要根据位置生成大量飞剑,并将飞剑转换为Entity,我们可以选择给飞剑的预制体添加Convert To Entity脚本的方法进行转换:

但点 开C onvert To Entity查看,发现它也是继承了MonoBehaviour,很明显它不会自己在编辑器中转换好,所以我们需要在运行的过程中将飞剑的预制体转换为Entity,代码如下:

using Unity.Entities;

public class Test : MonoBehaviour
{
//飞剑预制体
public GameObject swordPrefab;
//在 World 中, EntityManager 管理所有实体和组件
private EntityManager _manager;
//blobAssetStore是一个提供缓存的类,缓存能让你对象创建时更快。
private BlobAssetStore _blobAssetStore;
private GameObjectConversionSettings _settings;
//转换后的飞剑实体
private Entity swordEntity;

void Start()
{
//获取该世界的EntityManager
_manager = World.DefaultGameObjectInjectionWorld.EntityManager;
_blobAssetStore = new BlobAssetStore();
_settings = GameObjectConversionSettings.FromWorld(World.DefaultGameObjectInjectionWorld, _blobAssetStore);
//将飞剑预制体转换为Entity
swordEntity = GameObjectConversionUtility.ConvertGameObjectHierarchy(swordPrefab, _settings);
}

private void OnDestroy()
{
_blobAssetStore.Dispose();
}


再按位置生成飞剑Entity,顺便打印出来飞剑的数量:
private void Update()
{
if (Input.GetKeyDown(KeyCode.W))
{
BurstGenerateSword();
}
}
//大量生成飞剑!
public void BurstGenerateSword()

Debug.Log("生成数量:" + GetPixel.Instance.posList.Count);
//遍历位置列表,生成对应数量的飞剑Entity
for (int i = 0; i < GetPixel.Instance.posList.Count; i++)
{
SpawnNewSword(GetPixel.Instance.posList[i]);
}
}

public void SpawnNewSword(float2 pos)
{
Entity newSword = _manager.Instantiate(swordEntity);
//创建一个translation Component实例,并赋值对应坐标
Translation ballTrans = new Translation
{
Value = new float3(pos.x, 0f, pos.y)
};
_manager.AddComponentData(newSword, ballTrans);
}

再按位置生成飞剑Entity,顺便打印出飞剑的数量:

private void Update()
{
if (Input.GetKeyDown(KeyCode.W))
{
BurstGenerateSword();
}
}
//大量生成飞剑!
public void BurstGenerateSword()

Debug.Log("生成数量:" + GetPixel.Instance.posList.Count);
//遍历位置列表,生成对应数量的飞剑Entity
for (int i = 0; i < GetPixel.Instance.posList.Count; i++)
{
SpawnNewSword(GetPixel.Instance.posList[i]);
}
}

public void SpawnNewSword(float2 pos)
{
Entity newSword = _manager.Instantiate(swordEntity);
//创建一个translation Component实例,并赋值对应坐标
Translation ballTrans = new Translation
{
Value = new float3(pos.x, 0f, pos.y)
};
_manager.AddComponentData(newSword, ballTrans);
}

然后运行场景按W,在默认Burst Compliler和Job Threads开启的情况下,可以看到飞剑迅速生成到了场景中央,并且没有一点卡顿:

七、调试Entity

每个En tity是无法显示在Hierarchy视窗中进行调试的,Unity提供了一个工具用来分析,依次点击: Window>Analysis>Entity Debugger,就可以在Entity Debugger中查看每个Entity具体包含的Component,以及对应属性值的变化:

图中我 们可以看到,列表长度为42802,但Entity的数量生成了2倍多,这是因为飞剑的预制体中包含一个子物体,转换过程中EntityManager会将物体的子物体一同转换为Entity:

你也 可以点击F ilter,通过筛选Component的方式来寻找想要查看的Entity,左边的系统列表显示了项目中的系统以及系统运行每个框架所花费的时间。你可以使用为每个系统提供的复选框从列表中打开和关闭系统,来进行调试。

八、剑阵围绕中心旋转

飞剑已经生成了,接下来就需要创建一个System来更新这些Entity,让它们旋转起来。

首先定义一个Component添加到预制体上,这个Component的作用是一个标签:

using Unity.Entities;

[GenerateAuthoringComponent]
public struct SwordTag : IComponentData{}

然后新建一个System,继承SystemBase基类,需要注意的是:JobComponentSystem和ComponentSystem这两个基类将会被摒弃,(Entities 0.17.0 package文档中官方明确已经明确表示),SystemBase将会是System唯一的基类,所以建议尽量使用SystemBase。

接着实现System接口,通过Entitie.ForEach方法筛选出Sword Entity,并更新位置:

using Unity.Entities;
using Unity.Jobs;
using Unity.Transforms;
using Unity.Mathematics;
using UnityEngine;

public class SwordRotateSystem : SystemBase
{
protected override void OnUpdate()
{
float deltaTime = Time.DeltaTime;
float angel = 0.01f;

//获取所有包含Swordtag的Entity,并对他们的translation和rotation组件值进行更改
Entities.
WithAll().
ForEach((ref Translation translation, ref Rotation orientation) =>
{
var rotation = orientation;
//目标点为(0,0,0)
float3 targetPosition = float3.zero;
var targetDir = targetPosition - translation.Value;

//飞剑垂直向下面向中心点
quaternion temp1 = Quaternion.FromToRotation(Vector3.left, targetDir);
orientation.Value = math.slerp(orientation.Value, temp1, deltaTime);

#region 飞剑围绕中心点(0,0,0)旋转

float3 pos = translation.Value;
//旋转轴和旋转角度
quaternion rot = quaternion.AxisAngle(math.up(), angel);
float3 dir = pos - float3.zero;
dir = math.mul(rot, dir);
translation.Value = float3.zero + dir;

#endregion

}).ScheduleParallel();
}
}

Unity ECS会自动在系统中发现继承了SystemBase的类,点击运行场景,飞剑此时已经可以沿中心点旋转了:

九、飞剑出阵和归阵

接下来是整个剑阵最核心的功能,在实现的过程中我也实践了两种思路。

第一种思路是:

using Unity.Entities;
using Unity.Mathematics;

public struct Tag2 : IComponentData
{
//飞剑出阵后,要攻击的目标点坐标
public float3 target;

//替代出阵飞剑进行旋转的enitiy
public Entity TempEntity;

}

1. 创建System1,在System1中遍历不带Tag2的飞剑,抽取10000把飞剑,添加Tag2,同时创建10000个TempEntity替代其继续旋转。Tag2存取位置和对应Tempentity信息。

2. 创建System2 ,在System2中遍历所有带Tag2的飞剑,令其出阵飞向目标点。

3. 在System2中遍历所有带Tag2的飞剑,当System2中的飞剑非常接近目标点,则移除Tag2,添加Tag3。

4. 创建System3,在System3中遍历所有带Tag3的飞剑归阵,追赶上自己对应的Tempentity,之后销毁掉对应的Tempentity。

这个方案有这样2个问题:

1. System之间执行顺序问题

所有继承SystemBase,实现的OnUpdate()方法的System都是在主线程上跑的,因此我们可以很方便地规划System1,2,3的执行顺序,代码如下:

[UpdateAfter(typeof(System1))]
public class System2 : SystemBase
{
protected override void OnUpdate(){}
}

[UpdateAfter(typeof(System2))]
public class System3 : SystemBase
{
protected override void OnUpdate(){}
}

2. EntityCommandBuffer时序问题

在ECS中,所有的Entity都是按块(Chunk)存储的,一个块里的所有实体必定拥有相同数量和类型的组件,一旦某个实体的组件数量或类型改变了,它就不属于当前的块,它会被移到其它块里,这个操作在主线程中运行并没有什么问题。

但我希望能发挥CPU全部的性能来运行这个剑阵,所以需要去分配一些任务到子线程中执行,JobSystem帮我们解决了这个问题,它屏蔽了直接对线程的操作,而是把异步逻辑封装成一个个“Job”,由引擎来调度和分配给合适的线程去执行,官方实例代码在这:

Unity - Scripting API: IJob

https://docs.unity3d.com/ScriptReference/Unity.Jobs.IJob.html

除了上面的方法使用JobSystem,ECS提供SystemBase类中常用的Entities.ForEach方法也可以将数据转换到子线程处理,我们可以使用Schedule()或者ScheduleParallel()来执行Job的Lambda方法:

partial class ApplyVelocitySystem : SystemBase
{
protected override void OnUpdate()
{
Entities
.ForEach((ref Translation translation,
in Velocity velocity) =>
{
translation.Value += velocity.Value;
}).Schedule();
//Run()是立即在主线程上执行
//目前并没有找到官方的Schedule()和ScheduleParallel()之间的区别
//目前看到的一个解释是Schedule()强制每个Job单独一个线程
//而ScheduleParallel()则是在并行线程上处理Job,每个线程处理query中一个或多个chunk
}
}

这时候,假设Job1中删除了某个实体的组件,该实体被移到其它Chunk,而它并行的Job2也在对这个实体进行操作,就会产生冲突(操作不存在的组件或者操作了错误块里的实体),所以Job2必须等待Job1读写数据完毕,这就是硬性同步点(Sync points)。

以下导致结构变化的操作,都会产生硬性的同步点:

  • 创建实体

  • 删除实体

  • 向实体添加组件

  • 从实体中删除组件

  • 更改共享组件中的值

每个同步点都会造成停顿,等待当前World中所有先前安排的作业完成。过多的同步点会让效率大大降低,但以上的操作还是 无法避免的。

为了解决这种问题,Unity提供了EntityCommandBuffer(实体命令缓冲区,简称ECB)来解决这个问题。

ECB可以将导致结构性更改的命令排入队列,存储在ECB中的命令可以在一帧中晚些时候回放执行。当回放ECB时,这会将跨帧分布的多个同步点减少到单个同步点。下面这个案例是使用系统自带的ECB,这样可以最大程度的减少同步点:

struct Lifetime : IComponentData
{
public byte Value;
}

class LifetimeSystem : SystemBase
{
EndSimulationEntityCommandBufferSystem m_EndSimulationEcbSystem;
protected override void OnCreate()
{
base.OnCreate();
// 从World中获取ECS系统并且存起来
m_EndSimulationEcbSystem = World
.GetOrCreateSystem();
}

protected override void OnUpdate()
{
// 请求一个ECS并且转换成可并行的
var ecb = m_EndSimulationEcbSystem.CreateCommandBuffer().AsParallelWriter();
Entities
.ForEach((Entity entity, int entityInQueryIndex, ref Lifetime lifetime) =>
{
// 检测entity的lifetime,如果为0则销毁它
if (lifetime.Value == 0)
{
// 将entityInQueryIndex传给操作,这样ECS回放时能保证正确的顺序
ecb.DestroyEntity(entityInQueryIndex, entity);
}
else
{
lifetime.Value -= 1;
}
}).ScheduleParallel();

// 保证ECB system依赖当前这个Job
m_EndSimulationEcbSystem.AddJobHandleForProducer(this.Dependency);
}
}

这样,飞剑添加Component,创建临时的Entity的操作好像都没啥问题了,但实际运行起来时会发现飞剑并没有飞回原定的位置,明显比原先剑阵慢了一帧:

前面 也说到了,EntityCommandBuffer中的命令不会立即执行,而是会在下一帧被En tityCommandBufferSystem使用,有一帧延迟。 我们生成替代出阵飞剑的Entity,在下一帧才会真正的生成并跟随剑阵旋转,所以飞剑最后和Entity同步的位置也自然是慢了一帧。 目前的解决办法是多计算一帧的距离,但这样做既麻烦又有误差,所以我们换一种简单的思路。

对与EntityCommandBuffer时序问题有疑惑的小伙伴可以看这篇文章:

Unity DOTS编码实践:EntityCommandBuffer

https://zhuanlan.zhihu.com/p/328218005

第二种思路是:

生成TempEntity和飞剑在八卦阵中,为飞剑添加Tag1组件,记录相同位置的TempEntity。

using Unity.Entities;
using Unity.Mathematics;

public struct Tag1 : IComponentData
{
//对应的TempEntity
public Entity TempEntity;
}

1. 创建System1,遍历所有TempEntity,使它们沿中心旋转。

2. 创建System2,遍历所有无Tag2组件的飞剑,并且每帧和对应TempEntity位置同步。

3. (System2中执行)若发生点击事件,则抽取10000把飞剑添加Tag2,Tag2记录目标点位置。

using Unity.Entities;
using Unity.Mathematics;

public struct Tag2 : IComponentData
{
//目标点位置
public float3 targetpos;
}

主要代码如下:

System1:

using Unity.Entities;
using Unity.Jobs;
using Unity.Transforms;
using Unity.Mathematics;

public struct TempEntityTag : IComponentData{}

public class TempEntityRotateSystem : SystemBase
{
protected override void OnUpdate()
{
float deltaTime =Time.DeltaTime;
float angel = 0.01f;

Entities.
WithAll().
ForEach(( ref Translation translation, in Target target) =>
{
#region TempEntity围绕剑阵中心点旋转

float3 pos = translation.Value;
//旋转轴和旋转角度
quaternion rot = quaternion.AxisAngle(math.up(), angel);
float3 dir = pos - target.Tpos;
dir = math.mul(rot, dir);
translation.Value = target.Tpos + dir;

#endregion
}).ScheduleParallel();
}
}

System2:

using Unity.Entities;
using Unity.Jobs;
using Unity.Transforms;
using Unity.Mathematics;
using UnityEngine;
using Unity.Collections;
using Unity.Physics.Systems;
using Unity.Physics;

[UpdateAfter(typeof(TempEntityRotateSystem))]
public class SwordRotateSystem : SystemBase
{
EndSimulationEntityCommandBufferSystem m_EndSimulationEcbSystem;

protected override void OnCreate()
{
base.OnCreate();
// 从World中获取ECS系统并且存起来
m_EndSimulationEcbSystem = World
.GetOrCreateSystem();
}

protected override void OnUpdate()
{
bool isGo = false;
float3 hitpos = float3.zero;
float deltaTime = Time.DeltaTime;

// 请求一个ECS并且转换成可并行的
var ecb = m_EndSimulationEcbSystem.CreateCommandBuffer().AsParallelWriter();
if (Input.GetMouseButtonDown(0))
{
//获取物理世界
BuildPhysicsWorld physicsWorld = World.DefaultGameObjectInjectionWorld.GetExistingSystem();
NativeArray rigidBodies = new NativeArray(1, Allocator.TempJob);
NativeArray rayHitPos = new NativeArray(1, Allocator.TempJob);
//获取射线发射位置
UnityEngine.Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastJobHandle raycastJonHande = new RaycastJobHandle()
{
mStartPos = ray.origin,
mEndPos = ray.direction * 10000,
physicsWorld = physicsWorld.PhysicsWorld,
Bodies = rigidBodies,
rayHitpos = rayHitPos
};
//需要依赖当前Job
JobHandle jobHandle = raycastJonHande.Schedule(this.Dependency);
jobHandle.Complete();

if (rigidBodies[0].Entity != null)
{
Debug.Log("目标坐标:" + rayHitPos[0]);
Debug.Log("射线击中目标" + rigidBodies[0].Entity);
hitpos = rayHitPos[0];
isGo = true;
}
rigidBodies.Dispose();
rayHitPos.Dispose();
}

Entities.
WithAll().
WithNone().
ForEach((Entity entity, int entityInQueryIndex, ref Translation translation, ref Rotation orientation, ref Target target) =>
{
#region 飞剑群出击!
if (isGo && entityInQueryIndex < 10000)
{
GoTag tag = new GoTag
{
targetPos = hitpos,
TempEntity = target.targetTempentity,
originPos = translation.Value,
isBack = false
};
// 将entityInQueryIndex传给操作,这样ECS回放时能保证正确的顺序
ecb.AddComponent(entityInQueryIndex, entity, tag);
}
#endregion

if (!HasComponent(target.targetTempentity))
{
return;
}

var rotation = orientation;
float3 targetPosition = target.Tpos;
var targetDir = targetPosition - translation.Value;

//飞剑垂直向下面向中心点
quaternion temp1 = Quaternion.FromToRotation(Vector3.left, targetDir);
orientation.Value = temp1;
LocalToWorld tempEntityPos = GetComponent(target.targetTempentity);
translation.Value = tempEntityPos.Position;

}).ScheduleParallel();

// 保证ECB system依赖当前这个Job
m_EndSimulationEcbSystem.AddJobHandleForProducer(this.Dependency);
}

//发射射线Job
public struct RaycastJobHandle : IJob
{
public NativeArray Bodies;
public NativeArray rayHitpos;
public float3 mStartPos;
public float3 mEndPos;
public PhysicsWorld physicsWorld;

public void Execute()
{
//创建输入
RaycastInput raycastInput = new RaycastInput()
{
Start = mStartPos,
End = mEndPos * 100,
//声明碰撞过滤器,用来过滤某些层级下的物体是否进行射线检测
Filter = new CollisionFilter() { BelongsTo = ~0u, CollidesWith = ~0u, GroupIndex = 0, }
};
Unity.Physics.RaycastHit raycastHit = new Unity.Physics.RaycastHit();

// 发射射线去检测Entity实体
if (physicsWorld.CollisionWorld.CastRay(raycastInput, out raycastHit))
{
//拿到我们射线击中的entity
Bodies[0] = physicsWorld.Bodies[raycastHit.RigidBodyIndex];
//拿到击中点的位置信息
rayHitpos[0] = raycastHit.Position;
}
}
}
}

System3:

using Unity.Entities;
using Unity.Jobs;
using Unity.Transforms;
using Unity.Mathematics;
using UnityEngine;

[UpdateAfter(typeof(SwordRotateSystem))]
public class GroupSystem : SystemBase
{
EndSimulationEntityCommandBufferSystem m_EndSimulationEcbSystem;

protected override void OnCreate()
{
base.OnCreate();
// 从World中获取ECS系统并且存起来
m_EndSimulationEcbSystem = World
.GetOrCreateSystem();
}

protected override void OnUpdate()
{
// 请求一个ECS并且转换成可并行的
var ecb = m_EndSimulationEcbSystem.CreateCommandBuffer().AsParallelWriter();
float deltaTime = Time.DeltaTime;
float angel = 0.01f;

Entities
.WithName("Group").
ForEach((Entity entity, int entityInQueryIndex, ref Translation translation, ref Rotation orientation, ref GoTag goTag, ref Target target) =>
{
var rotation = orientation;
float3 targetPosition = goTag.targetPos;
float distance = math.distance(targetPosition, translation.Value);
LocalToWorld targetTransform = GetComponent(goTag.TempEntity);

//距离目标点位置小于30,则返回剑阵
if (distance < 30f)
{
if (goTag.TempEntity != null)
{
goTag.isBack = true;
}
}

//追上自己对应的Tempentity
if (goTag.isBack)
{
float3 newPos = targetTransform.Position;
var a = newPos - translation.Value;
//飞剑剑头指向目标点
quaternion b = Quaternion.FromToRotation(Vector3.down, a);
orientation.Value = b;

float d1 = math.distance(translation.Value, newPos);
translation.Value += math.normalizesafe(a);
float d2 = math.distance(translation.Value, newPos);
float c = math.distance(newPos, float3.zero) / 100f;
float d = d1 - d2;

if (d1 >10+c)
{
int loop = (int )((10 + c) / d);
for (int i = 0; i < loop; i++)
{
translation.Value += math.normalizesafe(a);
}
}
else
{
target.Tpos = float3.zero;
translation.Value = targetTransform.Position;
float distance3 = math.distance(newPos, translation.Value);
ecb.RemoveComponent(entityInQueryIndex, entity, ComponentType.ReadWrite());
}

return;
}

#region 飞向目标点

var targetDir = targetPosition - translation.Value;
quaternion temp1 = Quaternion.FromToRotation(Vector3.down, targetDir);
orientation.Value = temp1;
float3 distancePos = goTag.targetPos - goTag.originPos;
translation.Value += distancePos * deltaTime * target.randomSpeed / 5f;
#endregion

}).ScheduleParallel();

// 保证ECB system依赖当前这个Job
m_EndSimulationEcbSystem.AddJobHandleForProducer(this.Dependency);

然后在主线程中生成飞剑和TempEntity就可以了,需要注意的是,场景中的地面也需要转换为Entity,并且添加上Physic相关组件。其他的代码就不放了,感兴趣的小伙伴可以下载工程试一下,我放在文章末尾了。最后我们来看下俯视角效果:

我的 电脑是i5-7500四核+GTX1050,场景中有12万8千个实体,正常运行能跑85fps,一万把飞剑出阵也能保持在60左右:

十、参考

文章到这就结束了,回顾下来,踩的坑非常多,希望这篇文章能给小伙伴提供帮助,也期待大佬指正!

对于GameObject转换为Entity流程感兴趣的同学,可以移步这篇文章:

砂糖酱:Dots代码阅读笔记——ConvertToEntity的一生

https://zhuanlan.zhihu.com/p/157919373

官方文档

https://docs.unity3d.com/Packages/com.unity.entities@0.1/manual/index.html

工程地址:Unity-ECS-TaiJi-Swords-Array

https://github.com/ydwj/Unity-ECS-TaiJi-Swords-Array

文末,再次感谢 炎拳 的分享,作者主页:https://auniquepig.com/,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:465082844)

近期精彩回顾

特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。

Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.

相关推荐
热点推荐
张柏芝悼念谢贤,多平台账号头像已变全黑;家属遵从谢贤遗愿,‌不举办大型公开追悼会‌,丧葬流程全部从简,其大部分遗产留给两个孙子

张柏芝悼念谢贤,多平台账号头像已变全黑;家属遵从谢贤遗愿,‌不举办大型公开追悼会‌,丧葬流程全部从简,其大部分遗产留给两个孙子

鲁中晨报
2026-07-20 18:53:10
某些媒体又来侮辱网友智商了!

某些媒体又来侮辱网友智商了!

清书先生
2026-07-20 18:35:43
加维:只问了句因凡蒂诺没接电话吗,帕雷德斯就失去了理智

加维:只问了句因凡蒂诺没接电话吗,帕雷德斯就失去了理智

懂球帝
2026-07-20 17:05:06
89岁谢贤去世!已交代好陪葬品和遗产,唯独放不下小林青霞母子

89岁谢贤去世!已交代好陪葬品和遗产,唯独放不下小林青霞母子

追踪之点
2026-07-20 20:56:00
89岁谢贤去世,与儿子谢霆锋最后合影曝光,曝去世细节及临终遗愿

89岁谢贤去世,与儿子谢霆锋最后合影曝光,曝去世细节及临终遗愿

180视角
2026-07-20 16:48:59
谢贤九成遗产留给张柏芝谢霆锋两个儿子,朋友林青霞保留部分财产

谢贤九成遗产留给张柏芝谢霆锋两个儿子,朋友林青霞保留部分财产

韩小娱
2026-07-20 17:32:27
史上第11人!30岁名将同年夺欧冠+世界杯冠军 他出战50场西班牙0败

史上第11人!30岁名将同年夺欧冠+世界杯冠军 他出战50场西班牙0败

我爱英超
2026-07-20 06:15:10
特朗普怎么拉都不走,“咔嚓”…

特朗普怎么拉都不走,“咔嚓”…

观察者网
2026-07-20 07:49:05
幸好西班牙赢了!如果阿根廷点球大战赢西班牙,世界杯决赛将蒙羞?

幸好西班牙赢了!如果阿根廷点球大战赢西班牙,世界杯决赛将蒙羞?

陈錈爱体育
2026-07-20 06:47:47
谢贤遗产分配揭开张柏芝三子真实身份,林青霞再次出现遗嘱之内

谢贤遗产分配揭开张柏芝三子真实身份,林青霞再次出现遗嘱之内

蜜桔娱乐
2026-07-20 19:45:43
才25岁啊!两天前她还笑得特别灿烂,谁能想到两天后人就没了

才25岁啊!两天前她还笑得特别灿烂,谁能想到两天后人就没了

皮蛋儿电影
2026-07-20 14:54:38
量化多杀多?科技股继续跌停潮,不宜悲观、情绪宣泄接近尾声

量化多杀多?科技股继续跌停潮,不宜悲观、情绪宣泄接近尾声

看财经show
2026-07-20 17:12:45
“阿根廷 脏”热搜爆了!决赛犯规25次,本届世界杯犯规超100次创纪录;西班牙领奖时,阿根廷球员集体背过身,遭痛批“毫无体育精神”

“阿根廷 脏”热搜爆了!决赛犯规25次,本届世界杯犯规超100次创纪录;西班牙领奖时,阿根廷球员集体背过身,遭痛批“毫无体育精神”

极目新闻
2026-07-20 11:23:58
百亿富豪密春雷再爆雷!欠钱只是冰山一角,连老婆董卿也救不了他

百亿富豪密春雷再爆雷!欠钱只是冰山一角,连老婆董卿也救不了他

料峭春寒洞
2026-07-19 13:43:29
《功夫女足》破12亿,演技评分出炉,热巴张小斐排倒数,第1碾压

《功夫女足》破12亿,演技评分出炉,热巴张小斐排倒数,第1碾压

揽星河的笔记
2026-07-19 22:43:35
世界杯刚夺冠,西班牙队就收获不利消息,27岁主力恐退出国家队

世界杯刚夺冠,西班牙队就收获不利消息,27岁主力恐退出国家队

篮球圈里的那些事
2026-07-20 15:06:43
中老年“黄金血压值”已公布!不是120/80,而是这数越接近越放心

中老年“黄金血压值”已公布!不是120/80,而是这数越接近越放心

观星赏月
2026-07-20 16:57:27
第21轮!伊朗出现政变,穆杰塔巴跑了?革命卫队够损,点名约旦

第21轮!伊朗出现政变,穆杰塔巴跑了?革命卫队够损,点名约旦

小小科普员
2026-07-20 17:07:55
重大胜利!伊朗逮住美军“大鱼”!

重大胜利!伊朗逮住美军“大鱼”!

故事终将光明磊落
2026-07-20 11:59:13
WAIC机器人搭长城积木火爆展区,这才是VLA+WM的最佳用法

WAIC机器人搭长城积木火爆展区,这才是VLA+WM的最佳用法

机器之心Pro
2026-07-17 11:46:57
2026-07-20 23:20:49
侑虎科技UWA incentive-icons
侑虎科技UWA
游戏/VR性能优化平台
1600文章数 987关注度
往期回顾 全部

科技要闻

网易科技"未来大奖2026上半年AI榜单"揭晓

头条要闻

致82人死亡矿难发生后 山西多部门已有10余名官员落马

头条要闻

致82人死亡矿难发生后 山西多部门已有10余名官员落马

体育要闻

65岁肌肉男,世界杯最年长冠军主帅

娱乐要闻

谢霆锋发文确认父亲谢贤去世 享年89岁

财经要闻

AI开始挤泡沫

汽车要闻

综合续航超1600km 2027款星途ES上市置换价16.99万起

态度原创

旅游
亲子
艺术
房产
公开课

旅游要闻

那达慕大会把外国记者看感动了:非常好看 真的很开心

亲子要闻

携程梁建章:呼吁多方关注多孩家庭的出游需求

艺术要闻

北京CBD压轴之作!Z1a地块项目正式开工!

房产要闻

北师附、人大附、西侨、丘海…招生、划片最新变化来了!

公开课

李玫瑾:为什么性格比能力更重要?

无障碍浏览 进入关怀版