发射1w个子弹和Mono世界交互——影子Dots碰撞系统
影子 DOTS 碰撞系统
它的目标很明确:
- 子弹仍然是海量 DOTS 实体
- 场景里的可碰撞目标仍然可以来自 Mono 世界
- 但碰撞检测本身尽量放回 DOTS 世界完成
- 命中结果再通知回 Mono 世界
这套方案,本质上是为了解决一个很现实的问题:
海量 DOTS 子弹,不适合让 Mono 世界反过来逐个追踪。
尤其是像“1 万把飞剑剑雨”这种场景,如果每帧都在 Mono 世界对所有 DOTS 子弹做射线、SphereCast 或桥接查询,哪怕功能上能跑,性能和数据搬运成本都会很快暴露出来。
所以我们最后选的方向不是:
- Mono 世界每帧追踪 DOTS 子弹
而是:
- Mono 世界把“可被命中的碰撞体”同步一份到 DOTS 世界
- DOTS 子弹直接和 DOTS 里的“影子碰撞体”发生命中
- 命中后再把结果通知回对应的 Mono 物体
这就是“影子 DOTS 碰撞系统”的核心思想。
一、问题背景
最开始,这个项目里的子弹是 DOTS 生成的海量飞剑。
它们的移动、初始化、生命周期管理都在 ECS 里完成。
一开始的销毁逻辑很简单,是这种近似规则:
- 飞剑落到某个 GroundY
- 或者寿命结束
- 或者超出最远距离
这种做法很便宜,但它不是“真正碰到场景物体才销毁”。
如果想让飞剑真正碰到场景里的 Collider 才消失,最直觉的想法通常有两种:
- 在 Mono 世界里,每帧拿 DOTS 剑的位置做射线检测
- 在 DOTS 世界里,直接接 Unity Physics
我们都试过。
Mono 桥接版一开始功能能跑,但很快暴露出问题:
- 每帧要从 ECS 拿大量子弹数据
- 再转成 Mono 世界查询输入
- 再把命中结果写回 DOTS
这类“跨世界桥接”在子弹量大时成本很明显。
后来我们也试过 Unity Physics 的逐剑 CastRay 方案。
方向本身没错,但实现方式如果还是“每把剑逐个查”,在 1 万把剑的规模下,成本依然会很重。
所以最终我们落地的是第三种思路:
既然子弹本来就在 DOTS 世界,那就把碰撞目标也影子化到 DOTS 世界。
二、什么是影子碰撞体
所谓“影子碰撞体”,就是:
Mono 世界里原本有一个真实的 Collider,
我们在 DOTS 世界里再创建一个与它对应的碰撞体实体。
这个影子实体会同步原物体的关键信息,比如:
- 世界位置
- 旋转
- 缩放
- 碰撞盒尺寸
- 对应 Mono 物体的实例标识
这样一来,DOTS 子弹在做碰撞检测时,面对的就不再是 Mono 世界的 Collider,
而是 DOTS 世界里的一组影子碰撞体。
命中之后,再通过影子碰撞体保存的映射关系,把事件通知回原始 Mono 物体。
你可以把它理解成:
- Mono 世界负责“目标来源”
- DOTS 世界负责“命中计算”
- Mono 世界负责“业务响应”
三、系统结构
这套方案最终分成了三层。
1. Mono 侧代理层
我们新增了一个代理组件:
- TestDotsShadowColliderProxy
它挂在 Mono 世界的可碰撞物体上,职责是:
- 检查自己身上的 BoxCollider
- 在 DOTS 世界创建一个对应的影子碰撞体实体
- 每帧同步自己的变换和碰撞盒尺寸
- 在销毁或禁用时注销对应的 DOTS 实体
- 当 DOTS 世界通知命中时,回调到当前这个 Mono 物体
这一步的关键意义在于:
我们桥接的对象,不再是 1 万把子弹,而是少量可命中的环境物体。
这比“Mono 世界每帧追踪所有 DOTS 子弹”便宜得多。
2. DOTS 侧影子碰撞体
在 DOTS 世界里,我们给每个影子目标创建了真正的实体,并挂上:
- 影子碰撞元数据
- LocalTransform
- PhysicsCollider
- PhysicsWorldIndex
这样它就能进入 DOTS 的物理查询世界。
影子碰撞体上还会记录一个很关键的字段:
- ColliderInstanceId
这个值用来把 DOTS 世界命中的影子实体,再映射回 Mono 世界真正的物体。
换句话说:
DOTS 世界只知道“命中了哪个影子实体”,
而代理层负责把它翻译成“命中了 Mono 世界里的哪个物体”。
3. DOTS 子弹命中系统
飞剑本身仍然是 DOTS 子弹实体。
每把剑都维护:
- PreviousPosition
- 当前 LocalTransform.Position
- 速度
- 年龄
- 生命周期
- 是否已经命中
这样每一帧,子弹都有一条明确的运动路径:
- 从上一帧位置
- 到当前帧位置
命中系统的任务,就是判断这段路径是否打到了 DOTS 世界里的影子碰撞体。
在最终实现里,这套查询放到了 DOTS 世界中执行,命中后会:
- 把剑位置修正到命中点
- 给子弹打上 Hit 标记
- 创建一个命中事件实体
- 后续由通知系统把这次命中回传给 Mono 侧
四、命中通知是怎么回到 Mono 世界的
为了把 DOTS 世界的命中结果交回 Mono 物体,我们加了一层事件回传:
- TestDotsShadowHitEvent
- TestDotsShadowHitNotifier
流程是这样的:
- DOTS 命中系统检测到飞剑打中了某个影子碰撞体
- 创建一条 TestDotsShadowHitEvent
- 事件里带上:
- 命中目标的 ColliderInstanceId
- 命中点
- TestDotsShadowHitNotifier 读取这些事件
- 再通过 TestDotsShadowColliderProxy.TryNotifyHit(...)
找回对应的 Mono 物体 - Mono 侧执行 OnShadowHit(...)
当前我们先做的是最简单的验证版:
- 打印命中物体名称
- 打印命中点


// 文件内逻辑关系总览: // 1. TestDotsBulletHellDemoBootstrap // - Start() 调用 CreateBootstrap(),确保当前场景存在这个引导对象。 // - Update() 在首次运行阶段调用 TryInitialize(),完成演示场景和 DOTS 配置初始化。 // - OnGUI() 显示操作提示与当前子弹数量。 // // 2. TestDotsDemoConfig // - 保存 DOTS 子弹演示的全局配置,例如预制体、数量、速度、生命周期、发射位置等。 // // 3. TestDotsBulletSpawnSystem // - 读取鼠标输入。 // - 左键批量生成扇形弹幕,右键批量生成放射状弹幕。 // // 4. TestDotsBulletMoveSystem // - 每帧并行推进所有子弹的位置,并累计子弹年龄。 // // 5. TestDotsBulletCleanupSystem // - 在移动之后清理超时或超范围的子弹。 // // 整体流程: // Bootstrap 初始化 -> SpawnSystem 生成子弹 -> MoveSystem 移动子弹 -> CleanupSystem 销毁失效子弹 using Unity.Burst; using Unity.Collections; using Unity.Entities; using Unity.Jobs; using Unity.Mathematics; using Unity.Rendering; using Unity.Transforms; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.SceneManagement; using UnityPhysics = Unity.Physics; // 整个弹幕演示的全局配置组件。 // 这个组件通常作为单例存在,供多个系统共享读取。 public struct TestDotsDemoConfig : IComponentData { public Entity BulletPrefab; public int SpawnCount; public float BulletSpeed; public float BulletLifetime; public float BulletScale; public float MaxDistance; public float GroundY; public float3 RainSpawnCenter; public float2 RainSpawnHalfExtents; } // 每颗子弹的运行时数据组件。 // 记录速度、已存活时间和最大生命周期。 public struct TestDotsBullet : IComponentData { public float3 PreviousPosition; public float3 Velocity; public float Age; public float Lifetime; } public struct TestDotsBulletSpawnInit : IComponentData, IEnableableComponent { public int Index; public int Count; } public struct TestDotsBulletHit : IComponentData { public byte Value; } public struct TestDotsShadowBoxCollider : IComponentData { public int ColliderInstanceId; public float3 Center; public float3 HalfExtents; public quaternion Rotation; } public struct TestDotsShadowHitEvent : IComponentData { public int ColliderInstanceId; public float3 HitPoint; } // 负责搭建和初始化 DOTS 弹幕测试场景的引导类。 // 这里主要处理非 DOTS 的场景展示,以及 DOTS 的初始配置创建。 public class TestDotsBulletHellDemoBootstrap : MonoBehaviour { private const string DemoRootName = "TestDots Demo Root"; private const string BootstrapName = "TestDots DOTS Bootstrap"; private const string SceneName = "TestDots"; private const string BulletVisualResourcePath = "SK_Sword"; private Material _bulletMaterial; private Mesh _bulletMesh; private bool _initialized; private bool _hasBulletQuery; private EntityQuery _bulletQuery; // 进入场景后,先确保有一个 Bootstrap 对象负责后续初始化。 private void Start() { CreateBootstrap(); } //[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] // 如果当前是目标场景,并且场景中还没有 Bootstrap,则动态创建一个。 private static void CreateBootstrap() { if (SceneManager.GetActiveScene().name != SceneName) { return; } if (FindFirstObjectByType<TestDotsBulletHellDemoBootstrap>() != null) { return; } var bootstrap = new GameObject(BootstrapName); bootstrap.AddComponent<TestDotsBulletHellDemoBootstrap>(); } // 在初始化完成前,每帧都尝试一次初始化。 private void Update() { if (!_initialized) { TryInitialize(); } } // 显示简单的调试界面,帮助观察输入说明和当前子弹数量。 private void OnGUI() { if (!_initialized) { return; } var world = World.DefaultGameObjectInjectionWorld; if (world == null || !world.IsCreated || !_hasBulletQuery) { return; } const int width = 420; const int height = 118; GUI.color = new Color(0f, 0f, 0f, 0.72f); GUI.Box(new Rect(16f, 16f, width, height), GUIContent.none); GUI.color = Color.white; var style = new GUIStyle(GUI.skin.label) { fontSize = 18, richText = true }; int bulletCount = _bulletQuery.CalculateEntityCount(); GUI.Label(new Rect(28f, 24f, width - 24f, 32f), "<b>DOTS Bullet Storm Demo</b>", style); GUI.Label(new Rect(28f, 54f, width - 24f, 24f), "Left Mouse: summon a sword rain volley", GUI.skin.label); GUI.Label(new Rect(28f, 74f, width - 24f, 24f), "Swords fall from the sky and vanish on collider hit", GUI.skin.label); GUI.Label(new Rect(28f, 94f, width - 24f, 24f), $"Active DOTS bullets: {bulletCount:N0}", GUI.skin.label); } // 首次初始化入口: // 1. 确认当前 World 已可用 // 2. 创建演示场景的可视化物体 // 3. 创建 DOTS 子弹预制体和配置实体 // 4. 建立用于统计子弹数量的查询 private void TryInitialize() { if (SceneManager.GetActiveScene().name != SceneName) { Destroy(gameObject); return; } var world = World.DefaultGameObjectInjectionWorld; if (world == null || !world.IsCreated) { return; } var entityManager = world.EntityManager; EnsureScenePresentation(); EnsureShadowHitNotifier(); if (!HasDemoConfig(entityManager)) { CreateDemoConfig(entityManager); } _bulletQuery = entityManager.CreateEntityQuery(ComponentType.ReadOnly<TestDotsBullet>()); _hasBulletQuery = true; _initialized = true; } // 释放查询对象,避免资源泄露。 private void OnDestroy() { if (_hasBulletQuery) { var world = World.DefaultGameObjectInjectionWorld; if (world != null && world.IsCreated) { _bulletQuery.Dispose(); } _hasBulletQuery = false; } } // 判断配置单例是否已经存在,避免重复创建配置实体。 private bool HasDemoConfig(EntityManager entityManager) { var query = entityManager.CreateEntityQuery(ComponentType.ReadOnly<TestDotsDemoConfig>()); bool hasConfig = !query.IsEmptyIgnoreFilter; query.Dispose(); return hasConfig; } private void EnsureShadowHitNotifier() { if (GetComponent<TestDotsShadowHitNotifier>() == null) { gameObject.AddComponent<TestDotsShadowHitNotifier>(); } } // 创建整个 DOTS 演示所需的两个核心实体: // 1. 子弹预制体实体 // 2. 保存全局参数的配置实体 private void CreateDemoConfig(EntityManager entityManager) { _bulletMesh = BuildBulletMesh(); _bulletMaterial = BuildBulletMaterial(); var renderMeshArray = new RenderMeshArray( new[] { _bulletMaterial }, new[] { _bulletMesh }); var bulletPrefab = entityManager.CreateEntity(); RenderMeshUtility.AddComponents( bulletPrefab, entityManager, new RenderMeshDescription( ShadowCastingMode.Off, receiveShadows: false, motionVectorGenerationMode: MotionVectorGenerationMode.Camera, layer: 0), renderMeshArray, MaterialMeshInfo.FromRenderMeshArrayIndices(0, 0)); entityManager.AddComponent<Prefab>(bulletPrefab); entityManager.AddComponentData( bulletPrefab, LocalTransform.FromPositionRotationScale( float3.zero, quaternion.identity, 0.18f)); entityManager.AddComponentData( bulletPrefab, new TestDotsBullet { PreviousPosition = float3.zero, Velocity = float3.zero, Age = 0f, Lifetime = 0f }); entityManager.AddComponentData( bulletPrefab, new TestDotsBulletSpawnInit { Index = 0, Count = 0 }); entityManager.AddComponentData( bulletPrefab, new TestDotsBulletHit { Value = 0 }); var configEntity = entityManager.CreateEntity(); entityManager.AddComponentData( configEntity, new TestDotsDemoConfig { BulletPrefab = bulletPrefab, SpawnCount = 10000, BulletSpeed = 36f, BulletLifetime = 7f, BulletScale = 0.18f, MaxDistance = 180f, GroundY = 0f, RainSpawnCenter = new float3(0f, 28f, 24f), RainSpawnHalfExtents = new float2(24f, 34f) }); } // 确保测试场景中的地面、发射器、背景墙和相机都已经准备好。 private void EnsureScenePresentation() { if (GameObject.Find(DemoRootName) != null) { ConfigureCamera(); return; } var root = new GameObject(DemoRootName); CreateFloor(root.transform); CreateLauncher(root.transform, "Left Launcher", new Vector3(-8f, 1.5f, -16f), new Color(0.25f, 0.85f, 1f)); CreateLauncher(root.transform, "Right Launcher", new Vector3(8f, 1.5f, -16f), new Color(1f, 0.45f, 0.25f)); CreateBackdrop(root.transform); ConfigureCamera(); } // 设置主相机位置和背景色,方便完整观察弹幕表现。 private void ConfigureCamera() { var camera = Camera.main; if (camera == null) { return; } camera.transform.position = new Vector3(0f, 18f, -34f); camera.transform.rotation = Quaternion.Euler(22f, 0f, 0f); camera.backgroundColor = new Color(0.05f, 0.07f, 0.12f); camera.clearFlags = CameraClearFlags.SolidColor; } // 创建地面。 private static void CreateFloor(Transform parent) { var floor = GameObject.CreatePrimitive(PrimitiveType.Cube); floor.name = "Arena Floor"; floor.transform.SetParent(parent); floor.transform.position = new Vector3(0f, -0.5f, 30f); floor.transform.localScale = new Vector3(80f, 1f, 120f); floor.GetComponent<Renderer>().sharedMaterial = BuildSurfaceMaterial(new Color(0.09f, 0.11f, 0.14f)); EnsureShadowProxy(floor); } // 创建背景墙,便于观察子弹是否飞远。 private static void CreateBackdrop(Transform parent) { var wall = GameObject.CreatePrimitive(PrimitiveType.Cube); wall.name = "Impact Wall"; wall.transform.SetParent(parent); wall.transform.position = new Vector3(0f, 8f, 66f); wall.transform.localScale = new Vector3(70f, 16f, 1f); wall.GetComponent<Renderer>().sharedMaterial = BuildSurfaceMaterial(new Color(0.16f, 0.18f, 0.24f)); EnsureShadowProxy(wall); } // 创建一个简化的发射器模型,由底座和炮管组成。 private static void CreateLauncher(Transform parent, string launcherName, Vector3 position, Color accent) { var baseMesh = GameObject.CreatePrimitive(PrimitiveType.Cylinder); baseMesh.name = launcherName; baseMesh.transform.SetParent(parent); baseMesh.transform.position = position; baseMesh.transform.localScale = new Vector3(1.6f, 1.5f, 1.6f); baseMesh.GetComponent<Renderer>().sharedMaterial = BuildSurfaceMaterial(accent * 0.7f); EnsureShadowProxy(baseMesh); var barrel = GameObject.CreatePrimitive(PrimitiveType.Cube); barrel.name = $"{launcherName} Barrel"; barrel.transform.SetParent(baseMesh.transform); barrel.transform.localPosition = new Vector3(0f, 0.6f, 2.5f); barrel.transform.localScale = new Vector3(0.7f, 0.45f, 4.8f); barrel.GetComponent<Renderer>().sharedMaterial = BuildSurfaceMaterial(accent); EnsureShadowProxy(barrel); } private static void EnsureShadowProxy(GameObject target) { if (target.GetComponent<BoxCollider>() != null && target.GetComponent<TestDotsShadowColliderProxy>() == null) { target.AddComponent<TestDotsShadowColliderProxy>(); } } // 创建场景物件所用的基础材质。 private static Material BuildSurfaceMaterial(Color color) { var shader = Shader.Find("Universal Render Pipeline/Lit"); if (shader == null) { shader = Shader.Find("Standard"); } var material = new Material(shader) { color = color, hideFlags = HideFlags.DontSave }; return material; } // 直接复用 Unity 立方体的 Mesh 作为子弹外形。 private static Mesh BuildBulletMesh() { if (TryLoadBulletVisualFromPrefab(out var swordMesh, out _)) { return swordMesh; } var template = GameObject.CreatePrimitive(PrimitiveType.Cube); var mesh = template.GetComponent<MeshFilter>().sharedMesh; template.hideFlags = HideFlags.HideAndDontSave; Destroy(template); return mesh; } // 创建子弹使用的材质。 private static Material BuildBulletMaterial() { if (TryLoadBulletVisualFromPrefab(out _, out var swordMaterial)) { return swordMaterial; } var shader = Shader.Find("Universal Render Pipeline/Unlit"); if (shader == null) { shader = Shader.Find("Universal Render Pipeline/Lit"); } if (shader == null) { shader = Shader.Find("Standard"); } var material = new Material(shader) { color = new Color(1f, 0.86f, 0.3f), hideFlags = HideFlags.DontSave }; return material; } private static bool TryLoadBulletVisualFromPrefab(out Mesh mesh, out Material material) { mesh = null; material = null; var prefab = Resources.Load<GameObject>(BulletVisualResourcePath); if (prefab == null) { return false; } var skinnedRenderer = prefab.GetComponentInChildren<SkinnedMeshRenderer>(true); if (skinnedRenderer != null && skinnedRenderer.sharedMesh != null && skinnedRenderer.sharedMaterial != null) { mesh = skinnedRenderer.sharedMesh; material = skinnedRenderer.sharedMaterial; return true; } var meshRenderer = prefab.GetComponentInChildren<MeshRenderer>(true); var meshFilter = prefab.GetComponentInChildren<MeshFilter>(true); if (meshRenderer != null && meshFilter != null && meshFilter.sharedMesh != null && meshRenderer.sharedMaterial != null) { mesh = meshFilter.sharedMesh; material = meshRenderer.sharedMaterial; return true; } return false; } } [UpdateInGroup(typeof(InitializationSystemGroup))] // 负责响应输入并批量生成子弹的系统。 public partial class TestDotsBulletSpawnSystem : SystemBase { // 没有配置单例时不运行。 protected override void OnCreate() { RequireForUpdate<TestDotsDemoConfig>(); } // 检查左右键输入,并根据输入模式一次性生成大量子弹实体。 protected override void OnUpdate() { if (!Input.GetMouseButtonDown(0)) { return; } var config = SystemAPI.GetSingleton<TestDotsDemoConfig>(); var spawnedBullets = EntityManager.Instantiate(config.BulletPrefab, config.SpawnCount, Allocator.TempJob); var initSpawnDataJob = new InitSpawnDataJob { Entities = spawnedBullets, TotalCount = spawnedBullets.Length, SpawnInitLookup = GetComponentLookup<TestDotsBulletSpawnInit>() }; Dependency = initSpawnDataJob.Schedule(spawnedBullets.Length, 64, Dependency); Dependency = spawnedBullets.Dispose(Dependency); } [BurstCompile] private struct InitSpawnDataJob : IJobParallelFor { [ReadOnly] public NativeArray<Entity> Entities; public int TotalCount; [NativeDisableParallelForRestriction] public ComponentLookup<TestDotsBulletSpawnInit> SpawnInitLookup; public void Execute(int index) { SpawnInitLookup[Entities[index]] = new TestDotsBulletSpawnInit { Index = index, Count = TotalCount }; } } // 将大量子弹均匀分布在一个扇形范围内,并加入一点上下波动。 internal static float3 ComputeRainSpawnOrigin(int index, int total, float3 center, float2 halfExtents) { float normalized = total <= 1 ? 0.5f : index / (float)(total - 1); float x = math.lerp(-halfExtents.x, halfExtents.x, math.frac(normalized * 61.8034f)); float z = math.lerp(-halfExtents.y, halfExtents.y, math.frac(normalized * 37.719f)); float yWave = math.sin(index * 0.173f) * 2.5f; return center + new float3(x, yWave, z); } } [BurstCompile] [UpdateInGroup(typeof(InitializationSystemGroup))] [UpdateAfter(typeof(TestDotsBulletSpawnSystem))] public partial struct TestDotsBulletInitSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate<TestDotsDemoConfig>(); state.RequireForUpdate<TestDotsBulletSpawnInit>(); } [BurstCompile] public void OnUpdate(ref SystemState state) { var config = SystemAPI.GetSingleton<TestDotsDemoConfig>(); var pendingQuery = SystemAPI.QueryBuilder().WithAll<TestDotsBulletSpawnInit>().Build(); int pendingCount = pendingQuery.CalculateEntityCount(); if (pendingCount <= 0) { return; } var initJob = new InitializeBulletsJob { BulletSpeed = config.BulletSpeed, BulletLifetime = config.BulletLifetime, BulletScale = config.BulletScale, RainSpawnCenter = config.RainSpawnCenter, RainSpawnHalfExtents = config.RainSpawnHalfExtents, DownRotation = quaternion.RotateX(math.PI * 0.5f) }; state.Dependency = initJob.ScheduleParallel(state.Dependency); } [BurstCompile] public partial struct InitializeBulletsJob : IJobEntity { public float BulletSpeed; public float BulletLifetime; public float BulletScale; public float3 RainSpawnCenter; public float2 RainSpawnHalfExtents; public quaternion DownRotation; private void Execute(ref LocalTransform transform, ref TestDotsBullet bullet, ref TestDotsBulletSpawnInit spawnInit, EnabledRefRW<TestDotsBulletSpawnInit> pendingInit) { float3 spawnOrigin = TestDotsBulletSpawnSystem.ComputeRainSpawnOrigin(spawnInit.Index, spawnInit.Count, RainSpawnCenter, RainSpawnHalfExtents); transform = LocalTransform.FromPositionRotationScale(spawnOrigin, DownRotation, BulletScale); bullet = new TestDotsBullet { PreviousPosition = spawnOrigin, Velocity = new float3(0f, -BulletSpeed, 0f), Age = 0f, Lifetime = BulletLifetime }; spawnInit.Count = 0; pendingInit.ValueRW = false; } } } [BurstCompile] [UpdateInGroup(typeof(SimulationSystemGroup))] // 负责推进所有子弹的运动。 public partial struct TestDotsBulletMoveSystem : ISystem { [BurstCompile] // 没有配置单例时不运行。 public void OnCreate(ref SystemState state) { state.RequireForUpdate<TestDotsDemoConfig>(); } [BurstCompile] // 调度并行 Job,对所有子弹执行位置更新。 public void OnUpdate(ref SystemState state) { var moveJob = new MoveBulletsJob { DeltaTime = SystemAPI.Time.DeltaTime }; state.Dependency = moveJob.ScheduleParallel(state.Dependency); } [BurstCompile] // 子弹移动 Job:更新位置并累加年龄。 public partial struct MoveBulletsJob : IJobEntity { public float DeltaTime; private void Execute(ref LocalTransform transform, ref TestDotsBullet bullet) { bullet.PreviousPosition = transform.Position; transform.Position += bullet.Velocity * DeltaTime; bullet.Age += DeltaTime; } } } [BurstCompile] [UpdateInGroup(typeof(SimulationSystemGroup))] [UpdateAfter(typeof(TestDotsBulletMoveSystem))] [UpdateBefore(typeof(TestDotsBulletCleanupSystem))] public partial struct TestDotsBulletShadowCollisionSystem : ISystem { [BurstCompile] public void OnCreate(ref SystemState state) { state.RequireForUpdate<TestDotsDemoConfig>(); state.RequireForUpdate<TestDotsShadowBoxCollider>(); state.RequireForUpdate<UnityPhysics.PhysicsWorldSingleton>(); } [BurstCompile] public void OnUpdate(ref SystemState state) { var physicsWorldSingleton = SystemAPI.GetSingleton<UnityPhysics.PhysicsWorldSingleton>(); var ecbSingleton = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>(); var collisionJob = new DetectShadowColliderHitsJob { PhysicsWorld = physicsWorldSingleton, ShadowColliderLookup = state.GetComponentLookup<TestDotsShadowBoxCollider>(true), Ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged).AsParallelWriter() }; state.Dependency = collisionJob.ScheduleParallel(state.Dependency); } [BurstCompile] public partial struct DetectShadowColliderHitsJob : IJobEntity { [ReadOnly] public UnityPhysics.PhysicsWorldSingleton PhysicsWorld; [ReadOnly] public ComponentLookup<TestDotsShadowBoxCollider> ShadowColliderLookup; public EntityCommandBuffer.ParallelWriter Ecb; private void Execute([ChunkIndexInQuery] int chunkIndex, Entity entity, ref LocalTransform transform, in TestDotsBullet bullet, ref TestDotsBulletHit hit) { if (hit.Value != 0) { return; } var rayInput = new UnityPhysics.RaycastInput { Start = bullet.PreviousPosition, End = transform.Position, Filter = UnityPhysics.CollisionFilter.Default }; if (!PhysicsWorld.CastRay(rayInput, out UnityPhysics.RaycastHit rayHit)) { return; } Entity hitEntity = rayHit.Entity; if (!ShadowColliderLookup.HasComponent(hitEntity)) { return; } var shadowCollider = ShadowColliderLookup[hitEntity]; transform.Position = rayHit.Position; hit.Value = 1; var hitEventEntity = Ecb.CreateEntity(chunkIndex); Ecb.AddComponent(chunkIndex, hitEventEntity, new TestDotsShadowHitEvent { ColliderInstanceId = shadowCollider.ColliderInstanceId, HitPoint = rayHit.Position }); } } } [BurstCompile] [UpdateInGroup(typeof(SimulationSystemGroup))] [UpdateAfter(typeof(TestDotsBulletMoveSystem))] // 负责回收寿命结束或飞出范围的子弹。 public partial struct TestDotsBulletCleanupSystem : ISystem { [BurstCompile] // 没有配置单例时不运行。 public void OnCreate(ref SystemState state) { state.RequireForUpdate<TestDotsDemoConfig>(); } [BurstCompile] // 在子弹移动之后执行清理逻辑。 public void OnUpdate(ref SystemState state) { var config = SystemAPI.GetSingleton<TestDotsDemoConfig>(); var ecbSingleton = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>(); var cleanupJob = new CleanupBulletsJob { MaxDistanceSq = config.MaxDistance * config.MaxDistance, Ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged).AsParallelWriter() }; state.Dependency = cleanupJob.ScheduleParallel(state.Dependency); } [BurstCompile] // 通过 ECB 延迟记录销毁命令,安全地并行清理子弹。 public partial struct CleanupBulletsJob : IJobEntity { public float MaxDistanceSq; public EntityCommandBuffer.ParallelWriter Ecb; private void Execute([ChunkIndexInQuery] int chunkIndex, Entity entity, in LocalTransform transform, in TestDotsBullet bullet, in TestDotsBulletHit hit) { if (hit.Value != 0 || bullet.Age >= bullet.Lifetime || math.lengthsq(transform.Position) >= MaxDistanceSq) { Ecb.DestroyEntity(chunkIndex, entity); } } } }
using System.Collections.Generic; using Unity.Collections; using Unity.Entities; using Unity.Mathematics; using Unity.Transforms; using UnityEngine; using UnityPhysics = Unity.Physics; [DisallowMultipleComponent] [RequireComponent(typeof(UnityEngine.BoxCollider))] public class TestDotsShadowColliderProxy : MonoBehaviour { private static readonly Dictionary<int, TestDotsShadowColliderProxy> ActiveProxies = new(); private UnityEngine.BoxCollider _boxCollider; private World _cachedWorld; private EntityManager _entityManager; private Entity _shadowEntity; private bool _registered; private bool _loggedRegistration; private BlobAssetReference<UnityPhysics.Collider> _colliderBlob; private float3 _lastScaledSize; public static bool TryNotifyHit(int instanceId, Vector3 hitPoint) { if (!ActiveProxies.TryGetValue(instanceId, out var proxy) || proxy == null) { return false; } proxy.OnShadowHit(hitPoint); return true; } private void Awake() { _boxCollider = GetComponent<BoxCollider>(); } private void OnEnable() { TryRegisterShadowEntity(); } private void LateUpdate() { if (!_registered) { TryRegisterShadowEntity(); return; } SyncShadowEntity(); } private void OnDisable() { UnregisterShadowEntity(); } private void OnDestroy() { UnregisterShadowEntity(); } private void TryRegisterShadowEntity() { if (_registered || _boxCollider == null) { return; } var world = World.DefaultGameObjectInjectionWorld; if (world == null || !world.IsCreated) { return; } _cachedWorld = world; _entityManager = world.EntityManager; _shadowEntity = _entityManager.CreateEntity(typeof(TestDotsShadowBoxCollider), typeof(UnityPhysics.PhysicsCollider), typeof(LocalTransform), typeof(UnityPhysics.PhysicsWorldIndex)); _entityManager.SetSharedComponent(_shadowEntity, new UnityPhysics.PhysicsWorldIndex()); _registered = true; ActiveProxies[GetInstanceID()] = this; if (!_loggedRegistration) { Debug.Log($"Registered DOTS shadow collider for {name}", this); _loggedRegistration = true; } SyncShadowEntity(); } private void SyncShadowEntity() { if (!_registered || !_entityManager.Exists(_shadowEntity)) { _registered = false; return; } Vector3 scaledSize = Vector3.Scale(_boxCollider.size, Abs(transform.lossyScale)); float3 scaledSize3 = scaledSize; if (!_colliderBlob.IsCreated || !scaledSize3.Equals(_lastScaledSize)) { if (_colliderBlob.IsCreated) { _colliderBlob.Dispose(); } _colliderBlob = UnityPhysics.BoxCollider.Create(new UnityPhysics.BoxGeometry { Center = float3.zero, Orientation = quaternion.identity, Size = scaledSize3, BevelRadius = 0f }); _entityManager.SetComponentData(_shadowEntity, new UnityPhysics.PhysicsCollider { Value = _colliderBlob }); _lastScaledSize = scaledSize3; } float3 worldCenter = transform.TransformPoint(_boxCollider.center); _entityManager.SetComponentData( _shadowEntity, new TestDotsShadowBoxCollider { ColliderInstanceId = GetInstanceID(), Center = worldCenter, HalfExtents = scaledSize3 * 0.5f, Rotation = transform.rotation }); _entityManager.SetComponentData( _shadowEntity, LocalTransform.FromPositionRotationScale(worldCenter, transform.rotation, 1f)); } private void UnregisterShadowEntity() { ActiveProxies.Remove(GetInstanceID()); if (_registered && _cachedWorld != null && _cachedWorld.IsCreated && _entityManager.Exists(_shadowEntity)) { _entityManager.DestroyEntity(_shadowEntity); } if (_colliderBlob.IsCreated) { _colliderBlob.Dispose(); } _registered = false; } private void OnShadowHit(Vector3 hitPoint) { //Debug.Log($"Sword bullet hit collider: {name} at {hitPoint}", this); //Debug.Log($"Sword bullet hit collider", this); } private static Vector3 Abs(Vector3 value) { return new Vector3(Mathf.Abs(value.x), Mathf.Abs(value.y), Mathf.Abs(value.z)); } } [DisallowMultipleComponent] public class TestDotsShadowHitNotifier : MonoBehaviour { private EntityManager _entityManager; private EntityQuery _hitEventQuery; private World _cachedWorld; private bool _hasQuery; private bool _loggedShadowColliderCount; private void LateUpdate() { var world = World.DefaultGameObjectInjectionWorld; if (world == null || !world.IsCreated) { return; } if (_cachedWorld != world || !_hasQuery) { DisposeQueryIfNeeded(); _cachedWorld = world; _entityManager = world.EntityManager; _hitEventQuery = _entityManager.CreateEntityQuery(ComponentType.ReadOnly<TestDotsShadowHitEvent>()); _hasQuery = true; } if (!_loggedShadowColliderCount) { var shadowColliderQuery = _entityManager.CreateEntityQuery(ComponentType.ReadOnly<TestDotsShadowBoxCollider>()); int shadowColliderCount = shadowColliderQuery.CalculateEntityCount(); shadowColliderQuery.Dispose(); Debug.Log($"Active DOTS shadow colliders: {shadowColliderCount}", this); _loggedShadowColliderCount = true; } if (_hitEventQuery.IsEmptyIgnoreFilter) { return; } using var hitEvents = _hitEventQuery.ToComponentDataArray<TestDotsShadowHitEvent>(Allocator.Temp); using var eventEntities = _hitEventQuery.ToEntityArray(Allocator.Temp); for (int index = 0; index < hitEvents.Length; index++) { TestDotsShadowColliderProxy.TryNotifyHit(hitEvents[index].ColliderInstanceId, hitEvents[index].HitPoint); } _entityManager.DestroyEntity(eventEntities); } private void OnDestroy() { DisposeQueryIfNeeded(); } private void DisposeQueryIfNeeded() { if (!_hasQuery) { return; } if (_cachedWorld != null && _cachedWorld.IsCreated) { _hitEventQuery.Dispose(); } _hasQuery = false; } }

浙公网安备 33010602011771号