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

用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-07 19:35:54
中方对印度已读不回,美国也说大实话!这一次,莫迪再牛气也没用

中方对印度已读不回,美国也说大实话!这一次,莫迪再牛气也没用

面包夹知识
2026-07-07 14:30:01
多名院士研究发现:吃一口桃子,就等于给血脂添一次污,真假?

多名院士研究发现:吃一口桃子,就等于给血脂添一次污,真假?

侯医生谈健康
2026-06-28 10:40:09
每年数万人因胰腺炎而死!医生:夏季宁可喝冰水,也别吃这4物

每年数万人因胰腺炎而死!医生:夏季宁可喝冰水,也别吃这4物

垚垚分享健康
2026-07-05 15:19:48
陈积明任浙江工业大学校长

陈积明任浙江工业大学校长

新浪财经
2026-07-08 19:52:04
18万起!吉利新车官宣:7月6日,重磅发布

18万起!吉利新车官宣:7月6日,重磅发布

科技堡垒
2026-07-06 11:14:10
台湾地区前领导人马英九:我不希望台湾成为第二个香港!

台湾地区前领导人马英九:我不希望台湾成为第二个香港!

阿腩讲娱乐
2026-04-13 04:13:20
世体:因队内11人仍参加世界杯,马竞西甲首轮战马拉加或延期

世体:因队内11人仍参加世界杯,马竞西甲首轮战马拉加或延期

懂球帝
2026-07-08 18:22:49
“关注点错了!”高三妈妈晒女儿成年礼服,被劝减肥才是头等大事

“关注点错了!”高三妈妈晒女儿成年礼服,被劝减肥才是头等大事

林林先生
2026-07-07 09:05:03
世界杯8强全出炉:欧洲占6席!阿根廷成南美独苗 1/4决赛对阵确定

世界杯8强全出炉:欧洲占6席!阿根廷成南美独苗 1/4决赛对阵确定

我爱英超
2026-07-08 06:52:54
以色列猖狂至斯?伊朗未服,再干土鸡!

以色列猖狂至斯?伊朗未服,再干土鸡!

汉唐智库
2026-07-08 02:31:17
大结局要来了,中方全面禁运,美国不救高市早苗只能抛售美债自救

大结局要来了,中方全面禁运,美国不救高市早苗只能抛售美债自救

亿通电子游戏
2026-07-08 14:29:01
19岁身价8000万欧!内马尔接班人浮出水面,天赋不输亚马尔

19岁身价8000万欧!内马尔接班人浮出水面,天赋不输亚马尔

篮球圈里的那些事
2026-07-07 17:53:04
图赫尔疯了!35 岁老将挤掉天才进世界杯,基恩 5 年前的话应验了

图赫尔疯了!35 岁老将挤掉天才进世界杯,基恩 5 年前的话应验了

奶盖熊本熊
2026-05-23 03:30:37
“碳水脸”引争议,网友:令人不适的网络词!

“碳水脸”引争议,网友:令人不适的网络词!

中国日报
2026-05-21 15:28:00
一旦开战,中国3000架飞机根本上不了场?俄专家揭露真相

一旦开战,中国3000架飞机根本上不了场?俄专家揭露真相

青山夜谈
2026-06-16 17:16:03
自然界脑科学证实:雌性对某个雄性疯狂上瘾,压根不是情感在作祟,而是他偶然间点燃了她大脑深处一套隐秘的成瘾机制

自然界脑科学证实:雌性对某个雄性疯狂上瘾,压根不是情感在作祟,而是他偶然间点燃了她大脑深处一套隐秘的成瘾机制

心理观察局
2026-06-24 07:27:05
宾馆里的约定:那不是房间,是时间为我们停靠的临时港口

宾馆里的约定:那不是房间,是时间为我们停靠的临时港口

疾跑的小蜗牛
2026-07-07 19:58:59
克洛普:我已经和沃勒尔谈过,如果我接手德国队他会很有帮助

克洛普:我已经和沃勒尔谈过,如果我接手德国队他会很有帮助

懂球帝
2026-07-08 06:21:08
澳大利亚昆士兰州,女子恋爱谈崩分手后,立马与前男友儿子谈恋爱

澳大利亚昆士兰州,女子恋爱谈崩分手后,立马与前男友儿子谈恋爱

岁月有情1314
2026-06-15 09:59:00
2026-07-09 05:11:00
侑虎科技UWA incentive-icons
侑虎科技UWA
游戏/VR性能优化平台
1596文章数 987关注度
往期回顾 全部

科技要闻

独家对话小红书IPO举报风波当事人

头条要闻

广西贵港3000名师生等待救援 老师:人几乎都要被冲走

头条要闻

广西贵港3000名师生等待救援 老师:人几乎都要被冲走

体育要闻

阿根廷绝境重生,离不开这个罗!

娱乐要闻

鹿晗出轨?邓超出轨绯闻又被扒出

财经要闻

新能源车"外挂电池"乱象:暗藏致命风险

汽车要闻

MB.EA纯电架构/售价33.98万 全新奔驰纯电GLC鎏金版上市

态度原创

本地
家居
教育
数码
公开课

本地新闻

万斤西瓜免费吃,来河南顶“瓜瓜”

家居要闻

2026建博会(广州) 公装联探展交流活动

教育要闻

深圳理工大学与多所高校的新探索:拟逐步取消大学英语

数码要闻

1399元!小米发布米家无线洗地机5:污渍毛发一拖即净

公开课

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

无障碍浏览 进入关怀版