Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Tutorial?
#20
Here's an example on how to filter out contacts and determine enter/exit/stay events fast, regardless of how many colliders are there in the scene. It's a component that you add to the solver, and it calls enter/exit/stay Unity events. The actual contact passed to each event is guaranteed to be between unique <actor, collider> pairs, but the particular  particle in the contact passed for the actor is undefined (in case there's more than one contact between the actor and the collider, it could pass a contact for any of the actor particles).

Not terribly fast (not multithreaded, non vectorized, vanilla C# only) but reasonably well optimized and useable right away:

Code:
using UnityEngine;
using UnityEngine.Events;
using System;
using System.Collections.Generic;

namespace Obi
{
    [RequireComponent(typeof(ObiSolver))]
    public class ObiContactEventDispatcher : MonoBehaviour
    {
        private ObiSolver solver;
        private Oni.Contact[] prevData;
        private int prevCount;
        private ContactComparer comparer;

        private class ContactComparer : IComparer<Oni.Contact>
        {
            ObiSolver solver;

            public ContactComparer(ObiSolver solver)
            {
                this.solver = solver;
            }

            public int Compare(Oni.Contact x, Oni.Contact y)
            {
                return CompareByRef(ref x, ref y, solver);
            }
        }

        private static int CompareByRef(ref Oni.Contact a, ref Oni.Contact b, ObiSolver solver)
        {
            if (a.other == b.other)
            {
                int hashA = solver.particleToActor[a.particle].actor.GetInstanceID();
                int hashB = solver.particleToActor[b.particle].actor.GetInstanceID();
                return hashA.CompareTo(hashB);
            }
            return a.other.CompareTo(b.other);
        }

        [System.Serializable]
        public class ContactCallback : UnityEvent<ObiSolver, Oni.Contact> { }

        public float distanceThreshold = 0.01f;
        public ContactCallback onContactEnter = new ContactCallback();
        public ContactCallback onContactStay = new ContactCallback();
        public ContactCallback onContactExit = new ContactCallback();

        void Awake()
        {
            solver = GetComponent<ObiSolver>();
            comparer = new ContactComparer(solver);
            prevData = new Oni.Contact[0];
        }

        void OnEnable()
        {
            solver.OnCollision += Solver_OnCollision;
        }

        void OnDisable()
        {
            solver.OnCollision -= Solver_OnCollision;
        }

        private int FilterOutDistantContacts(Oni.Contact[] data, int count)
        {
            int filteredCount = count;

            // simply iterate trough all contacts,
            // moving the ones above the threshold to the end of the array:
            for (int i = count -1 ; i >= 0; --i)
                if (data[i].distance > distanceThreshold)
                    ObiUtils.Swap(ref data[i], ref data[--filteredCount]);

            return filteredCount;
        }

        private int RemoveDuplicates(Oni.Contact[] data, int count)
        {
            if (count == 0)
                return 0;

            // assuming the array is sorted, iterate trough the array
            // replacing duplicates by the first contact that's different:
            int i = 0, r = 0;
            while (++i != count)
                if (CompareByRef(ref data[i], ref data[r], solver) != 0 && ++r != i)
                    data[r] = data[i];

            return ++r;
        }

        private void InvokeCallbacks(Oni.Contact[] data, int count)
        {
            int a = 0, b = 0;
            int lengthA = count, lengthB = prevCount;

            // while we haven't reached the end of either array:
            while (a < lengthA && b < lengthB)
            {
                // compare both contacts:
                int compare = CompareByRef(ref data[a], ref prevData[b], solver);

                // call the appropiate event depending on the comparison result:
                if (compare < 0)
                    onContactEnter.Invoke(solver, data[a++]);
                else if (compare > 0)
                    onContactExit.Invoke(solver, prevData[b++]);
                else
                {
                    onContactStay.Invoke(solver, data[a++]); b++;
                }
            }

            // finish iterating trough both lists:
            while (a < lengthA)
                onContactEnter.Invoke(solver, data[a++]);

            while (b < lengthB)
                onContactExit.Invoke(solver, prevData[b++]);
        }

        void Solver_OnCollision(object sender, ObiSolver.ObiCollisionEventArgs args)
        {
            // here we access the internal backing array (Data) directly,
            // instead of using the accessor property. This slightly improves performance.
            // note: the backing array length is the lists' capacity, so we
            // need to use args.contacts.Count to get the actual number of contacts.

            // skip all contacts above the distance threshold by moving them to the end of the array:
            int filteredCount = FilterOutDistantContacts(args.contacts.Data, args.contacts.Count);

            // sort the remaining contacts by collider, then by actor:
            Array.Sort(args.contacts.Data, 0, filteredCount, comparer);

            // remove duplicates:
            filteredCount = RemoveDuplicates(args.contacts.Data, filteredCount);

            // zip trough the current and previous contact lists once, invoking events when appropiate.
            InvokeCallbacks(args.contacts.Data, filteredCount);

            // store current contact list/count for next frame.
            // could get better performance by double buffering instead of copying:

            if (filteredCount > prevData.Length)
                Array.Resize(ref prevData, filteredCount);
            Array.Copy(args.contacts.Data, prevData, filteredCount);

            prevCount = filteredCount;
        }

    }
}

It works like this:
  • First, it moves all contacts above a distance threshold to the end of the contacts array. This ensures only contacts within the threshold are actually considered.
  • Sorts the remaining contacts
  • Removes duplicate contacts (between the same <actor, collider> pair). Since contacts are sorted, this can be done in linear time.
  • Uses a simple zipper-like algorithm to iterate trough both the current contact list and the last frame's contact list. Again, since they're both sorted, comparing them in linear time lets us know if they exist only in the new list (enter event), only in the previous list (exit event) or in both (stay event).
  • Stores the current contact list as the previous list, for the next frame.

"n" being the amount of contacts this frame and "m" the amount of contacts in the previous frame, the algorithmic worst case cost of the whole thing is O(n*log(n) + m). So quite good compared to the naive quadratic approach. You could further optimize it by using parallel sorting, as sorting is the bottleneck.

Once you have this component, you can just subscribe to the events and do whatever you need. For instance, subscribing this sample component:

Code:
using UnityEngine;
using Obi;

public class ContactBlinker : MonoBehaviour
{
 
    public void OnEnter(ObiSolver solver, Oni.Contact c)
    {
        var col = ObiColliderWorld.GetInstance().colliderHandles[c.other].owner;

        var blinker = col.GetComponent<Blinker>();
        blinker.highlightColor = Color.green;
        blinker.Blink();
    }

    public void OnExit(ObiSolver solver, Oni.Contact c)
    {
        var col = ObiColliderWorld.GetInstance().colliderHandles[c.other].owner;

        var blinker = col.GetComponent<Blinker>();
        blinker.highlightColor = Color.red;
        blinker.Blink();
    }

}

Makes colliders blink green when the actor enters, and blink red when it exits. I'm not providing code for the Blinker component, as it simply changes the material's color when you call Blink(). Here's the result:

Reply


Messages In This Thread
Tutorial? - by Havie - 13-09-2020, 10:24 PM
RE: Tutorial? - by josemendez - 14-09-2020, 08:06 AM
RE: Tutorial? - by Havie - 16-09-2020, 09:00 PM
RE: Tutorial? - by josemendez - 16-09-2020, 10:18 PM
RE: Tutorial? - by Havie - 17-09-2020, 08:10 PM
RE: Tutorial? - by josemendez - 18-09-2020, 07:01 AM
RE: Tutorial? - by Havie - 18-09-2020, 12:14 AM
RE: Tutorial? - by josemendez - 18-09-2020, 07:10 AM
RE: Tutorial? - by Havie - 18-09-2020, 09:12 PM
RE: Tutorial? - by josemendez - 18-09-2020, 09:44 PM
RE: Tutorial? - by Havie - 19-09-2020, 04:24 PM
RE: Tutorial? - by josemendez - 19-09-2020, 05:32 PM
RE: Tutorial? - by Havie - 19-09-2020, 10:41 PM
RE: Tutorial? - by josemendez - 20-09-2020, 12:57 AM
RE: Tutorial? - by Havie - 20-09-2020, 01:21 PM
RE: Tutorial? - by Havie - 20-09-2020, 04:18 PM
RE: Tutorial? - by josemendez - 20-09-2020, 07:20 PM
RE: Tutorial? - by Havie - 21-09-2020, 07:02 PM
RE: Tutorial? - by josemendez - 21-09-2020, 08:03 PM
RE: Tutorial? - by josemendez - 22-09-2020, 02:21 PM