Search Unity

Showcase Things - indexing MonoBehaviours by type for easy searches

Discussion in 'Scripting' started by halley, Feb 25, 2023.

  1. halley

    halley

    Joined:
    Aug 26, 2013
    Posts:
    2,440
    When I first started Unity, years ago, I kinda assumed that since there was a FindObjectsByType function, that Unity was surely indexing things behind the scenes. Not a chance. (I also thought that Unity was surely indexing things by layer, and by tag, but those apparently aren't true, either.)

    However, it's so useful to have some of your classes found by their type, that I immediately added this class to my toolbox. I don't use this abstract base class for everything in my game, but I do use it a LOT instead of tags. Want to find all Vehicles? Want to find the closest Interactive lever or doorknob or fruit in front of the player? Want to iterate through every Enemy? Want to apply damage to all Damageables in a given radius? These are much easier when you index your types.

    Code (CSharp):
    1. // Thing.cs
    2. // Automatically indexed MonoBehaviour for searching by type.
    3. //
    4. using System;
    5. using System.Collections;
    6. using System.Collections.Generic;
    7. using UnityEngine;
    8.  
    9. namespace Things
    10. {
    11.     // A Thing is a MonoBehaviour which can be found or iterated by type.
    12.     //
    13.     // Every Thing is registered on Awake and unregistered on Destroy.
    14.     // Finding all instances of a specific class of Thing in an area
    15.     // is quicker than finding them using FindObject calls.
    16.     //
    17.     public abstract class Thing: MonoBehaviour
    18.     {
    19.         public virtual void Awake()
    20.         {
    21.             Remember(this.GetType());
    22.         }
    23.  
    24.         public virtual void OnDestroy()
    25.         {
    26.             Forget(this.GetType());
    27.         }
    28.  
    29.         // In-Memory registry of all Things by type.
    30.         // If Cow : Mammal : Animal : Thing, then we register
    31.         // a Cow instance under each of Cow, Mammal and Animal.
    32.         //
    33.         private static Dictionary<Type,HashSet<Thing>> allThings;
    34.  
    35.         private void Remember(Type type)
    36.         {
    37.             if (type == null || type == typeof(Thing))
    38.                 return;
    39.             if (allThings == null)
    40.                 allThings = new Dictionary<Type,HashSet<Thing>>();
    41.             if (!allThings.ContainsKey(type))
    42.                 allThings[type] = new HashSet<Thing>();
    43.             allThings[type].Add(this);
    44.             Remember(type.BaseType);
    45.         }
    46.  
    47.         private void Forget(Type type)
    48.         {
    49.             if (type == null || type == typeof(Thing))
    50.                 return;
    51.             if (allThings == null)
    52.                 return;
    53.             if (!allThings.ContainsKey(type))
    54.                 return;
    55.             allThings[type].Remove(this);
    56.             Forget(type.BaseType);
    57.         }
    58.  
    59.         // Find things by Type only.
    60.         //
    61.         public static IEnumerable<T> FindThings<T>()
    62.             where T: Thing
    63.         {
    64.             if (allThings == null)
    65.                 yield break;
    66.             Type type = typeof(T);
    67.             if (!allThings.ContainsKey(type))
    68.                 yield break;
    69.             foreach (Thing thing in allThings[type])
    70.                 yield return thing as T;
    71.         }
    72.  
    73.         // Find things by Type, Proximity, Field Angle, and/or Exclusion.
    74.         //
    75.         // source: object positions measured relative to this transform
    76.         // range: objects outside this distance ignored
    77.         // fov: objects outside this wedge from source.forward ignored
    78.         // exclude: objects inside this transform hierarchy are ignored
    79.         //
    80.         public static T FindNearestThing<T>(Transform source,
    81.             float range=float.PositiveInfinity,
    82.             float fov=360f,
    83.             Transform exclude=null)
    84.             where T: Thing
    85.         {
    86.             if (source == null)
    87.                 return null;
    88.  
    89.             Thing best = null;
    90.             float closestSq = float.PositiveInfinity;
    91.             Vector3 position = source.position;
    92.             float rangeSq = range*range;
    93.             float halfFov = 0.5f*fov;
    94.  
    95.             foreach (Thing thing in allThings[type])
    96.             {
    97.                 if (thing == null)
    98.                     continue;
    99.                 if (exclude != null && thing.transform.IsChildOf(exclude))
    100.                     continue;
    101.                 Vector3 direction = thing.transform.position - position;
    102.                 float angle = Vector3.Angle(direction, source.forward);
    103.                 if (angle > halfFov)
    104.                     continue;
    105.                 float dSq =
    106.                     (thing.transform.position - position).sqrMagnitude;
    107.                 if (dSq >= closestSq)
    108.                     continue;
    109.                 if (dSq > rangeSq)
    110.                     continue;
    111.                 best = thing;
    112.                 closestSq = dSq;
    113.             }
    114.  
    115.             return best as T;
    116.         }
    117.  
    118.     }
    119. }
     
    Bunny83 and mopthrow like this.
  2. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,999
    Wouldn't that class make more sense as a generic class? That way you can have an independed static Hashset and you don't need to deal with reflection System.Type stuff.
     
  3. halley

    halley

    Joined:
    Aug 26, 2013
    Posts:
    2,440
    If it's a class, then it can only index things that are not MonoBehaviours (C# has no multiple inheritance), and thus have no transform position of their own. It would be like finding the position of every List.

    Edit: Oh, I think I see what you mean about generic class. You mean Thing<T>, still extending MonoBehaviour. That can work, it just makes it slightly more complicated to find base types, and the declaration of types. I really use the base typing a lot. FindThings<Vehicle> is better than FindThings<Motorcycle> + <Car> + <OfficeChair> + etc., so the generic would still need to communicate lookups up the type base chain. Tomato, Tomato, I guess.

    I don't really see using System.Type as reflection. Types make the world go 'round. Detecting and working with methods and fields is where you start to need the System.Reflection namespace, which we're not doing here. But even that has its uses... look at all the stuff Unity does with reflection all day long. It can't even call Start() without using reflection (as it needs to detect if it's a coroutine or not).
     
    Last edited: Feb 25, 2023