发射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 才消失,最直觉的想法通常有两种:

  1. 在 Mono 世界里,每帧拿 DOTS 剑的位置做射线检测
  2. 在 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

流程是这样的:

  1. DOTS 命中系统检测到飞剑打中了某个影子碰撞体
  2. 创建一条 TestDotsShadowHitEvent
  3. 事件里带上:
    • 命中目标的 ColliderInstanceId
    • 命中点
  4. TestDotsShadowHitNotifier 读取这些事件
  5. 再通过 TestDotsShadowColliderProxy.TryNotifyHit(...)
    找回对应的 Mono 物体
  6. Mono 侧执行 OnShadowHit(...)

当前我们先做的是最简单的验证版:

    • 打印命中物体名称
    • 打印命中点

image

 

image

 

// 文件内逻辑关系总览:
// 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;
    }
}

 

 

 

posted @ 2026-03-22 13:58  三页菌  阅读(1)  评论(0)    收藏  举报