Simple Query System

While making games using multiple scene development, I always find the need to get some value or object that is provided by a system found in another scene. Like the signal system, I want a similar system where I could query values without knowing the type of the provider.

When I posted my signal system on Reddit, someone pointed me to this blog post which describes a similar system that solves the same problem. I have to admit, I think it’s the better way. What I liked about it is that the events are type safe even to its parameters. Refactoring this system would indeed be a lot better.

I thought about this and I realized that I could make a query system with the same goals in mind – type safety. I did make one and I think I’ll be using this to our game from now on. If you’re going to use this code, I have to warn you that it’s not that battle tested compared to an older query system that I’ve been using. So use it with caution.

Usage

I’ll start with how it would be used. There are three entities that interact with the system. These are the query request, query requester and the query provider. The requester and provider can be in the same scene or in different scenes. The query request is nothing more than a class that holds the parameters of the request. This is a sample request:

public class TestRequest : QueryRequest {

    private readonly int intParam;
    private readonly string stringParam;

    public TestRequest(int intParam, string stringParam) {
        this.intParam = intParam;
        this.stringParam = stringParam;
    }

    public int IntParam {
        get {
            return intParam;
        }
    }

    public string StringParam {
        get {
            return stringParam;
        }
    }

}

TestRequest here is an immutable class with two parameters. An actual game query request class may have more parameters.

Registration of the provider looks like this:

public class QueryManagerTestProvider : MonoBehaviour {

    private void Awake() {
        QueryManager.RegisterProvider<TestRequest, GameObject>(TestProvider);
    }

    private GameObject TestProvider(TestRequest request) {
        // Log the parameters just to show that they are passed
        Debug.Log("intParam: " + request.IntParam);
        Debug.Log("stringParam: " + request.StringParam);

        return this.gameObject;
    }

}

Providers are simply delegates so that they can be written easily. A single MonoBehaviour or class may register more than one providers.

A sample query request will look like this:

public class QueryManagerTestRequester : MonoBehaviour {

    private void Start() {
        GameObject result = QueryManager.Query<TestRequest, GameObject>(new TestRequest(77, "Hello Query Manager"));
        Debug.Log("result: " + result.gameObject.name);
    }

}

Putting them together will output this result:

QueryManagerResult

In my test, QueryManagerTestProvider and QueryManagerTestRequester are placed in different scenes so that I could verify that it works with such setup. Essentially, what I have done here is I was able to retrieve a GameObject that is from a different scene.

Framework Code

Let’s start with the base class QueryRequest:

public abstract class QueryRequest {
}

Yeah, that is it. It’s just used as a common type for all request classes. You can see it’s usage later. Why is it not an interface? The intent is for the user to make separate request classes that are lightweight. This prevents having big classes or MonoBehaviour classes that can also act as a query request.

Next is the QueryManagerImplementation class which will be the class that is used internally by the static class QueryManager. It looks like this:

class QueryManagerImplementation {

    private delegate object QueryProvider(QueryRequest request); // The internal delegate that we manage

    private Dictionary<Type, QueryProvider> providerMap = new Dictionary<Type, QueryProvider>();

    public QueryManagerImplementation() {
    }

    public void RegisterProvider<R, V>(QueryManager.QueryProvider<R, V> provider) where R : QueryRequest {
        Type type = typeof(R);
        Assertion.Assert(!this.providerMap.ContainsKey(type)); // Should not contain the provider for a certain request yet

        // Make the internal delegate which invokes the generic delegate
        QueryProvider internalProvider = delegate (QueryRequest request) {
            return provider((R)request);
        };
        this.providerMap[type] = internalProvider;
    }

    public bool HasProvider<R>() where R : QueryRequest {
        return this.providerMap.ContainsKey(typeof(R));
    }

    public V Query<R, V>(R request) where R : QueryRequest {
        Type type = typeof(R);

        // Invoke the provider
        // This will throw an error if a provider does not exist
        return (V)this.providerMap[type](request);
    }

}

Providers are simply maintained in a Dictionary where the key is the type of the request. The methods are self explanatory. The generic identifier R refers to the requester type and V refers to the type of the result value. Notice how QueryRequest is used here as a qualifier for R which limits what class types can be passed.

Finally, the static QueryManager class looks like this:

public static class QueryManager {

    public delegate V QueryProvider<R, V>(R request) where R : QueryRequest;

    private static readonly QueryManagerImplementation INTERNAL_MANAGER = new QueryManagerImplementation();

    public static void RegisterProvider<R, V>(QueryProvider<R, V> provider) where R : QueryRequest {
        INTERNAL_MANAGER.RegisterProvider(provider);
    }

    public static bool HasProvider<R>() where R : QueryRequest {
        return INTERNAL_MANAGER.HasProvider<R>();
    }

    public static V Query<R, V>(R request) where R : QueryRequest {
        return INTERNAL_MANAGER.Query<R, V>(request);
    }

}

Caveats

Like Signals, calling query requests are slower compared to just calling the provider method directly. Careful not to use it inside Update() or in parts where it is invoked repeatedly like in loops. Cache the results if you can.

Another disadvantage of this system is garbage. Every time you want to query for a value, you may instantiate a request class especially if it’s immutable. Although this can be mitigated by making a request class mutable and maintain only one instance of it then reuse that instance to make queries. Another way is to use the Factory design pattern for request instances.

This is it for now. See you next time.

Advertisements

2 thoughts on “Simple Query System

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s