QueryableMonoBehaviour : SQL-like syntax for Unity behaviours
Ever wished you could query MonoBehaviours like they were a database to easily filter and sort through?
Unity, fetch me a list of all goblin monsters in this hero's attack range ordered by lowest health.
Using this ListQuery class from an earlier post, we can build a base MonoBehaviour class that enables exactly that.
using System.Collections.Generic; using UnityEngine; public abstract class QueryableMonoBehaviour<T> : MonoBehaviour where T : QueryableMonoBehaviour<T> { private static List<T> _instances = new List<T>(); // bookkeeping: protected virtual void Awake() { _instances.Add((T)this); } protected virtual void OnEnable() { if (_instances.Contains((T)this) == false) _instances.Add((T)this); } protected virtual void OnDisable() { _instances.Remove((T)this); } protected virtual void OnDestroy() { _instances.Remove((T)this); } // query begin: public static IListQueryAfterSelect<T> Select() { return ListQuery<T>.Create(_instances).Select(); } }
The class itself is simple. It maintains an internal list of objects as they're created/enabled and destroyed/disabled. The magic part is the static Select() function at the bottom that begins our ListQuery. Again, you need the ListQuery class from an earlier post to use this. Inherit your own class from it class Target : QueryableMonoBehaviour<Target> and you're good to go.
If for no other reason, use it to replace all of your calls to GameObject.FindObjectsOfType<T>.
Yikes. Those numbers (especially garbage) are inflated because I had 2000 objects in my list, but you get the idea. Both functions are a single line of code too:
private void GameObjectFindTargets() { Target[] targets = GameObject.FindObjectsOfType<Target>(); } private void QueryFindTargets() { List<Target> targets = Target.Select().All(); }
But a QueryableMonoBehaviour allows you to do so much more. We'll start simple. Let's find all monsters that are goblins:
List<Monster> goblins = Monster.Select() .Where(monster => monster.type == Monster.Type.Goblin) .All();
If you're confused about the => bit, it's called the lambda operator and it creates lambda expressions. You'll want to read up on them before continuing if you're unfamiliar.
We can of course apply more than one conditional to our where clause. The player just used their super cool mineral deposit scanner, and now you want to highlight all the resources in the area that are harvestable:
List<Resource> resources = Resource.Select() .Where(resource => resource.IsHarvestable() && Vector3.Distance(resource.transform.position, player.transform.position) <= scannerRange) .All(); // highlight them
The OrderBy clause allows us to add sorting functionality to the list of results. Here we prepare the UI before a multiplayer match starts by collecting our teammates and ordering them by name:
List<Hero> heroes = Hero.Select() .Where(hero => hero.team == myTeam) .OrderBy((hero1, hero2) => hero1.playerName.CompareTo(hero2.playerName)) .All();
OrderBy takes a comparison delegate rather than a lambda expression. All basic types include a CompareTo method, but you can also write your own comparison if you need to order by custom objects or multiple fields.
Here's a cheeky way to abuse OrderBy to find specific objects, like the monster AI finding the weakest target in range:
Hero weakestTarget = Hero.Select() .Where(hero => Vector3.Distance(hero.transform.position, this.transform.position) <= attackRange) .OrderBy((hero1, hero2) => hero1.health.CompareTo(hero2.health)) .One();
The catch is that OrderBy sorts the entire collection, which is less than ideal if you're just trying to pick a single object, but for small datasets like a list of heroes it should be fine.
Every query will generate ~32 bytes of garbage regardless of what it finds, so while I wouldn't recommend you use it every single frame, it's not a big deal if you do.
Hope you like it.














