Pointing and Clicking with Unity ECS

How to select entities, or, more specifically, modify their component data, with Unity ECS.

Last updated on January 20, 2020. Created on December 19, 2019.

Video of changing prefab colors with Unity ECS.

Get the tutorial code from GitHub! 👀


Previously, I explained how to retrieve information about GameObjects by pointing and clicking on them. I also explained how to spawn entities with prefabs. So, is it possible to select such entities similarly to GameObjects?


As a prerequisite, ensure you have the latest versions of Entities, HybridRenderer, and Physics via Window -> Package Manager. If you can't see them, press the Advanced dropdown and select Show preview packages. Up-to-date versions of these libraries should be compatible, but that's not always the case, so you're free to clone my DOTS-ready demo repo like so:

git clone https://github.com/reeseschultz/ReeseUnityDemos.git

Add or remove the included code and scenes at your leisure.

The Goal

For demonstration, we want to change the color of the entities we select.

More technically, we must then reference a PhysicsWorld and EntityManager, one for raycasting entities, and the other for manipulating their data. We'll do this from a MonoBehaviour so we can attach the main camera to it. In LateUpdate, we'll check for mouse clicks. We'll project the clicked 2D coordinate from the camera to 3D space, casting a ray by a limited distance to check for a hit with an entity. How? By getting the hit's RigidBodyIndex, and using it as a key to access the corresponding entity in the NativeSlice that is PhysicsWorld.Bodies.

Finally, with the entity known, we'll get and modify its RenderMesh data with the EntityManager. Note that the RenderMesh is of ISharedComponentData, so we have to be careful only to modify it for one entity, and not all of them! That means we'll need a new copy of the RenderMesh for each selected entity.

The Code

Here it goes:

class PointAndClick : MonoBehaviour
    public static readonly RAYCAST_DISTANCE = 1000;
    public Camera Cam;

    PhysicsWorld physicsWorld => World.DefaultGameObjectInjectionWorld.GetExistingSystem<BuildPhysicsWorld>().PhysicsWorld;
    EntityManager entityManager => World.DefaultGameObjectInjectionWorld.EntityManager;

    void LateUpdate()
        if (!Input.GetMouseButtonDown(0) || Cam == null) return;

        var screenPointToRay = Cam.ScreenPointToRay(Input.mousePosition);
        var rayInput = new RaycastInput
            Start = screenPointToRay.origin,
            End = screenPointToRay.GetPoint(RAYCAST_DISTANCE),
            Filter = CollisionFilter.Default

        if (!physicsWorld.CastRay(rayInput, out RaycastHit hit)) return;

        var selectedEntity = physicsWorld.Bodies[hit.RigidBodyIndex].Entity;
        var renderMesh = entityManager.GetSharedComponentData<RenderMesh>(selectedEntity);
        var mat = new UnityEngine.Material(renderMesh.material);
        mat.SetColor("_Color", UnityEngine.Random.ColorHSV());
        renderMesh.material = mat;

        entityManager.SetSharedComponentData(selectedEntity, renderMesh);

To use this script, just add it to an empty GameObject in a scene; drag and drop a camera from the scene onto the script's Cam field.

Note that ScreenPointToRay returns a Ray, which performs the 2D to 3D projection, but PhysicsWorld requires the struct RayInput instead. Using the Ray we build said struct, taking the origin, an end (translating from the origin of the Ray by a distance RAYCAST_DISTANCE in the direction of the camera). Then we opt for the default CollisionFilter, which is basically a mask for controlling which entities can be hit by the raycast.

And again, mind that the RenderMesh derives from ISharedComponentData, which is why we must use SetSharedComponentData instead of SetComponentData. As opposed to IComponentData, ISharedComponentData is data that is referenced among all entities associated with it, as opposed to one-by-one, which can save a ton of memory.

But here we must slice and dice the shared data to actually be unique per-entity, hence why we must copy the Material from the existing RenderMesh, set a new Color on the copied Material, and effectively create new RenderMesh data by adding the new Material to it.

That's pretty much it. This is more complicated than pointing and clicking with GameObjects, but allows you to use the much more performant ECS paradigm with a feature expected in many games: selecting non-UI stuff in the game. You are so very welcome.

© Reese Schultz

My code is released under the MIT license.