Use the results of RaycastCommand.Schedule on another Job. Accessing instances.

A little advanced hack to fix the impossibility of reading the results of RaycastHits returned by RaycastCommand.schedule in another job.
Cover Image

This is a small advanced tip for anybody using Jobs and Burst in Unity. If you are new to jobs, learn more about it and efficiency in my introductory tutorial on how to improve algorithm efficiency using Jobs, Burst and Separability.


If you are doing very intensive work that needs throwing a lot of raycasts, the use of RaycastCommand and RaycastCommand.Schedule is the fastest way to do it, unless you create your own physics engine, which I guess you don't want to. (Read the documentation about it).

Short version: RaycastCommand.Schedule is basically a job that sends all the raycasts and returns all the RaycastHit results. The results array contains the number of maximum allowed hits by the number of rays  (N x NumHitsAllowed).  Each RaycastHit will contain the data of the hit or "null" under the .collider property if the raycast didn't hit anything. In this case, you can skip the rest of the items of the part of the array that correspond to the ray and go to the next.

The next obvious step is to pass the results returned by this method in the form of a NativeArray<RaycastHit> to another job in order to gather the information you need from it. Not using jobs for these types of things is a bottleneck in your code.

Example:  In my case, working on the WaveMaker asset, I'm now intensively using Jobs, Burst and such, and in one of the processes I have to throw two rays per grid cell, one from the bottom of the water volume, and one from the surface looking downwards. That leads to thousands of rays if you want to achieve detailed waves. The results have to be analyzed by another process in order to gather the Collider ids and use those to find out which colliders were hit and where in order to gather more information.

The problem

Now... we have a serious problem. We can't get the collider from that array of RaycastHit's inside a job. Why? Because a job cannot use class instances, like Collider. They only use blittable types and only NativeXXX variables. The good news is , a workaround has been added in Version 2021.2 to be able to do this without any ugly hack, but you still need to do this to keep backward compatibility if you want users of versions previous to this one).

At least what you want to know is with WHO the ray hit, right?... if you want to get the UnityEngine.Collider instance from the RaycastHit.collider... well, you can't. I know, it sucks... So said Unity Support when I notified this. You can see it clearly in VisualStudio by opening the variable preview when running the job.

To be able to perform interactive debug on a multithreaded job you have to change the call to "job.Run" instead of "Schedule" after calling Complete() to whatever handle was used in the Schedule call. Also, commenting [BurstCompile] in the struct definition of the job so that code is not modified and lines executed correspond to whatever lines you see.

"UnityEngine.UnityException : FindObjectFromInstanceID can only be called from the main thread". This is what is called when you access .collider property, as well as others that return actual object instances in the scene.

The "hack"

So how do you access that valuable information inside the job? Here's the little trick you can use.

        internal struct RaycastHitPublic
            public Vector3 m_Point;
            public Vector3 m_Normal;
            public int m_FaceID;
            public float m_Distance;
            public Vector2 m_UV;
            public int m_ColliderID;

        public static int GetColliderID(RaycastHit hit)
                RaycastHitPublic h = *(RaycastHitPublic*)&hit;
                return h.m_ColliderID;

First you define a fake struct that mimics the internal contents of a RaycastHit until the point where the collider is stored. How do you know which struct to use? Maybe looking at the memory view of the variable in Visual Studio or looking at the Unity open C# source in GitHub. Sadly not everything is visible. This struct was given to me by the Support Team (thanks!).

If you call the static function GetColliderID, you will be able to convert the struct to your custom struct using pointers and gather the variable directly accessing the memory. It uses "unsafe" operations, which is dereferencing a variable. As you know, C# doesn't give you control on memory as much as C++, that variable could be cleaned or moved by the garbage collector, so it is an unsafe operation and you have to tell the compiler.

&hit returns the memory address of the variable hit.

(RaycastHitPublic*) casts that address to an address to a RaycastHitPublic. 

* before everything will return the memory block pointed by the previous pointer, now converted to RaycastHitPublic.

The collider is stored as an int, that is the instance ID. No hit returns 0. It's not the instance, as you can see, since the link between the id and the object instance itself is resolved when you access the property .collider. 

But you can't resolve it by hand using GetObjectByInstanceID or similar inside the job because you can't use the Collider class, so you should refactor your code in order to use the Ids instead of the instances. In my case, I have a list of Ids of detected colliders on the water, and I compare with it.

The results

See the speed of doing it in the main thread, calling handle.Complete() to finish all jobs, then gathering the info and then passing it to the next job.

Compared to what happens if you do it directly in the job.

An important requirement

Wait! You can't use "unsafe" code like this unless you have activated it in your assembly definition file (amdef) that contains your code  (same folder, or parent folder). You don't use assemblies?. In that case, you only have one, the main assembly (Assembly CSharp). There's a big discussion here. But in case you are planning to intensively use jobs that need unsafe code like this, create assets for projects and want to keep everything modular or just do unit testing, I seriously recommend doing it.

You click the asmdef file and activate the "unsafe" option, then apply the change. That will add the /unsafe option in the C# compiler for that particular assembly. In case you don't use assemblies, you can allow unsafe code on the global assembly by going to the project build settings. But I recommend the other approach better.

Hope it helped! Keep in the loop for more efficiency and advanced tips.