/unity-tools

A code library for Unity3D that contains some basic building blocks that Unity lacks such as a REST client.

Primary LanguageC#MIT LicenseMIT

unity-tools

Build Status

Unity-tools is a toolkit for scripting in Unity3D which greatly streamlines asychronous tasks, particularly with HTTP requests.

Unity-tools is made up of:

  • UnityTask - Composable Asynchronous Tasks For Unity
  • UnityDispatcher - A Unity Thread Dispatcher
  • UnityAgent - A Unity REST API Client
  • litjson - A third party JSON parser used by the REST client

The project goals are:

  • Code that is high quality and ready for professional environments
  • Code that is thoroughly unit tested
  • Code that is decoupled from Unity's (as much as possible)
  • Code that is tested on all devices that Unity supports

UnityTask - Composable Asynchronous Tasks For Unity

In leue of the .NET 4.5 async methods and Task framework that are missing in Unity's version of Mono, I decided to implement a system to compose asynchronous tasks in a manageable way.

It is an implementation of the Promises/A+ specification which is used by the q framework.

When an asynchronous method is run, it returns immediately with a promise (in this case a UnityTask object) that the task will either succeed or fail in the future. This future object allows callbacks to be added to handle these events. Promises can also be combined into other promises allowing powerful compositions.

Creating an asynchronous method

public UnityTask<string> DownloadFile()
{
    return new UnityTask<string>( (task) =>
    {
        // This code executes on a new thread
        string result = SomeBlockingDownloadMethod();
        // Uncaught exceptions will get caught internally and reject the task automatically
        task.Resolve(result);
    });
}

The same thing as above with less sugar

public UnityTask<string> DownloadFile()
{
    // Create promise
    UnityTask<string> task = new UnityTask<string>();

    // Run something asynchronously
    new Thread( () => { 
        // Need to try catch here as the thread is not wrapped by the task like the above example.
        try
        {
            string result = SomeSlowBlockingDownloadMethod();
            task.resolve(result);
        }
        catch(Exception ex)
        {
            task.Reject(ex);
        }
    }).Start();

    return task;
}

Using the asynchronous method, all callbacks are optional

DownloadFile().Then(
    onFulfilled: result  => Debug.Log("Download data: " + result),
    onFailure: ex        => Debug.Log("Oh No an exception occurred."),
    onProgress: p        => Debug.Log("Progress changed: " + p),
    onEnd:               => Debug.Log("Clean up temporary files.") // Run regardless of outcome
);

Notifying progress

public UnityTask<string> CopyFiles(string sourceDir, string destDir)
{
    return new UnityTask( (task) =>
    {
        int count = 0;
        for (var sourceFile = Directory.EnumerateFiles(sourceDir))
        {
            File.Copy(sourceFile, destDir + File.GetFileName(sourceFile));
            // Trigger the onProgress callback
            task.Notify(count / (float) files.length);
            count++;
        }
        task.Resolve();
    }
}

Forcing synchronicity

try
{
    // The Result property blocks until the task ends
    string result = DownloadFile().Result;
    Debug.Log("Download data: " + result),
}
catch (Exception e)
{
     // The Result property also propagates exceptions from UnityTask.Reject()
     Debug.Log("Oh No an exception occurred.");
}

Debug.Log("Clean up temporary files.");

Composing tasks in parallel

var DownloadAllFiles = UnityTask.All(
    DownloadFile1(),
    DownloadFile2(),
    DownloadFile3()
).Then(
    onFulfilled: results    => Debug.Log("All files successfully downloaded."),
    onFailure: ex           => Debug.Log("Oh No an exception occurred.").
    onEnd:                  => Debug.Log("Clean up temporary files.")
);

Getting the results of parallel tasks

var DownloadAllFiles = UnityTask<string>.All(
    DownloadFile1(),
    DownloadFile2(),
    DownloadFile3()
).Then(
    onFulfilled: results    => {
        foreach (var result in results)
        {
            Debug.Log("Result: " + result);
        }
    },
    onFailure: ex           => Debug.Log("Oh No an exception occurred.").
    onEnd:                  => Debug.Log("Clean up temporary files.")
);

Composing tasks sequentially

// AllSequential takes in lambda functions that return the task.
// The functions are executed in order
var DownloadAllFiles = UnityTask.AllSequential(
    () => DownloadFile1(), 
    () => DownloadFile2(),
    () => DownloadFile3()
).Then(
    onFulfilled: o  => Debug.Log("All files successfully downloaded."),
    onFailure: ex   => Debug.Log("Oh No an exception occurred.").
    onEnd:          => Debug.Log("Clean up temporary files.")
);

Nested chaining

var DownloadAllFiles = UnityTask.All(
    DownloadFile1().Then(() => Debug.Log("File 1 is done!")),
    DownloadFile2().Then(() => Debug.Log("File 2 is done!")),
    DownloadFile3().Then(() => Debug.Log("File 3 is done!"))
).Then(
    onFulfilled: results    => Debug.Log("All files successfully downloaded."),
    onFailure: ex           => Debug.Log("Oh No an exception occurred.").
    onEnd:                  => Debug.Log("Clean up temporary files.")
);

Nested Alls

var DownloadAllFiles = UnityTask.All(
    DownloadFile1(),
    UnityTask.AllSequential(
        () => DownloadFile2(),
        () => DownloadFile3()
    ).Then(() => Debug.Log("Files 2 and 3 are done!")),
    DownloadFile4(),
    DownloadFile5()
).Then(
    onFulfilled: results    => Debug.Log("All files successfully downloaded."),
    onFailure: ex           => Debug.Log("Oh No an exception occurred.").
    onEnd:                  => Debug.Log("Clean up temporary files.")
);

UnityDispatcher - A Unity Thread Dispatcher

Unity is very fussy with the threads you call certain methods from. Method that call into the engine, and even properties such as Application.persistentDataPath need to be executed from the main Unity thread (i.e. from a MonoBehaviour.Update() ). A dispatcher executes code on the main thread so that the Unity API can be called safely.

You have to make sure that the UnityDispatcher MonoBehavour is attached to an active GameObject in the scene, otherwise the dispatcher will not execute actions.

Basic dispatching

void MethodCalledFromOtherThread()
{
    Debug.Log("I will execute first.");

    UnityDispatcher.Instance.Dispatch( () => 
    {
        // Executed on next update cycle
        Debug.Log("I will execute third.");
        Texture2D.EncodeToPNG("./test.png");
    });

    Debug.Log("I will execute second.");
}

Blocking dispatching

void MethodCalledFromOtherThread()
{
    Debug.Log("I will execute first.");

    // DispatchWait blocks the current thread until the action has executed.
    // This creates a continuation.
    UnityDispatcher.Instance.DispatchWait( () => 
    {
        // Executed on next update cycle
        Debug.Log("I will execute second.");
        Texture2D.EncodeToPNG("./test.png");
    });

    Debug.Log("I will execute third.");
}

Integration with tasks

var task = new UnityTask<string>( (task) =>
    {
        string result = SomeBlockingDownloadMethod();
        task.Resolve(result);
    }, 
    // You can pass an object that implements IDispatcher and callbacks will be automatically dispatched
    UnityDispatcher.Instance
); 

task.Then( 
    onSucess: () => Debug.Log("This will be called on the dispatch thread"))
);

UnityAgent - A Unity REST API Client

I really liked the style of the REST client that I use in Node.js called SuperAgent so I took heavy inspiration from it.

I have used UnityTasks to implement all the functionality so it is easy to compose API tasks.

I have found a good JSON library that works well with Unity and I am integrating it seamlessly with UnityAgent. For now you can only send and receive string data.

Simple Gets

new UnityAgent()
    .Get("www.google.com")
    .Begin()
    .Then(
        (response)  => Debug.Log(response.Body),
        (ex)        => Debug.Log("An error occurred")
    );

JSON Gets

new UnityAgent()
    .Get("http://httpbin.org/get")
    .Begin()
    .Then(
        (response)  => Debug.Log((string)response.JSON["some_key"]),
        (ex)        => Debug.Log("An error occurred")
    );

Simple Posts

new UnityAgent()
    .Post("http://httpbin.org/post")
    .Send("Hello")
    .Begin()
    .Then(
        onFailure: (ex) => Debug.Log("An error occurred")
    );

Composing Calls

var agent = new UnityAgent();
UnityTask<string>.All(
    agent.Get("www.url1.com").Begin(),
    agent.Get("www.url2.com").Begin(),
    agent.Get("www.url3.com").Begin()
).Then(
    onFulfilled: (responses)  => {
       // The All method returns an array of results
       foreach (var response in responses)
       {
            Debug.Log(response.Body);            
       } 
    },
    onFailure: (ex) => Debug.Log("An error occurred")
);

Build Environment

There are two seperate build environments that share the same code base. UnityProject is for testing the code with the Unity3D compiler and integrating it with Unity3D projects. XamarinProject can be opened in Xamarin Studio and will compile without Unity3D with the use of harness classes that pretend to be Unity but don't actually do anything (see FakeUnity.cs). As the project doesn't actually depend on Unity functionallity it is fine for testing.

I recommend using Xamarin for most of the development and only switching to Unity to make sure the code compiles there.

I am unit testing heavily, I haven't achieved 100% code coverage yet but I am working on it.

Contribute

If you have essential building blocks that help you develop in Unity that you wish to share, please do!

Todo List

  • [ ] Write more unit tests
  • [ ] Test on multiple devices
  • [ ] Think of more essential Unity tools!

Licence

MIT Licence