Unity
Xapi.Core, Xapi.Client and Xapi.Client.Outbox use their netstandard2.0 builds in Unity. WebGL players
also need UnityWebRequestHandler from the Unity package in src/Xapi.Unity. This page says what has been
tested, how to install the packages, and what to watch for.
What is tested
The tests in tests/Unity read,
validate and write statements, generate UUIDv7 ids, send statements to an LRS and read them back, sign a
statement with RSA and send it as multipart/mixed, check the 401 for wrong credentials, and queue statements in
a FileOutboxStore while the LRS cannot be reached, then deliver them after the store is reopened. The client
and outbox tests run with HttpClientHandler and with UnityWebRequestHandler.
| Where | Scripting backend | Result |
|---|---|---|
| Editor (play mode), macOS | Mono | Passed |
| macOS player, Apple silicon | IL2CPP, Managed Stripping Level High | Passed |
| Editor (play mode), Linux (CI) | Mono | Passed |
| Linux player, x86-64 (CI) | IL2CPP | Passed |
| WebGL, headless Chromium (local) | IL2CPP | Passed with UnityWebRequestHandler |
| Android, iOS | IL2CPP | Not tested yet |
Tested with Unity 6.3 LTS (6000.3.26f1), API Compatibility Level .NET Standard 2.1 (the default),
NuGetForUnity 4.5.0 and the packages at 0.1.0-preview.1 from nuget.org. The WebGL row used packages packed from
the repository, because the FileOutboxStore of 0.1.0-preview.1 never finishes opening an existing store in
WebGL. CI runs the Linux rows on every change
to the packages, with packages packed from the commit. WebGL runs locally for now
(CONTRIBUTING.md).
The packages do not use reflection, so they need no link.xml: the player tests pass with the highest stripping
level.
Install
Unity does not read NuGet packages by itself.
NuGetForUnity installs them and their dependencies into
Assets/Packages. Add it to Packages/manifest.json:
{
"dependencies": {
"com.github-glitchenzo.nugetforunity": "https://github.com/GlitchEnzo/NuGetForUnity.git?path=/src/NuGetForUnity#v4.5.0"
}
}
Then open NuGet > Manage NuGet Packages, turn on Show Prerelease, and install Xapi.Client (or
Xapi.Client.Outbox, which brings Xapi.Client and Xapi.Core). NuGetForUnity also installs
System.Text.Json 10.0.0, System.Text.Encodings.Web, System.IO.Pipelines, Microsoft.Bcl.AsyncInterfaces,
Microsoft.Bcl.TimeProvider, System.Diagnostics.DiagnosticSource and System.Runtime.CompilerServices.Unsafe. It
lists System.Memory, System.Buffers and System.Threading.Tasks.Extensions too, but adds no DLLs for them,
because Unity's .NET Standard 2.1 profile has those types.
If another package in the project brings an older System.Text.Json, NuGetForUnity keeps the newer one;
Xapi.Core needs 10.0.0 or later.
Sending statements
The code is the same as on .NET (see getting-started.md and outbox.md). A sketch of a component that queues statements and delivers them in the background:
public class XapiTracker : MonoBehaviour
{
static readonly HttpClient Http = new HttpClient();
FileOutboxStore store;
StatementOutbox outbox;
readonly CancellationTokenSource shutdown = new CancellationTokenSource();
async void Start()
{
var client = new XapiClient(Http, new XapiClientOptions
{
Endpoint = new Uri("https://lrs.example.com/xapi/"),
Credentials = XapiCredentials.Basic(key, secret),
});
store = await FileOutboxStore.OpenAsync(Path.Combine(Application.persistentDataPath, "xapi-outbox"));
outbox = new StatementOutbox(client, store);
_ = outbox.RunAsync(shutdown.Token);
}
public async void Completed(string levelId)
{
await outbox.EnqueueAsync(new Statement(
Agent.FromMbox(playerEmail),
Verbs.Completed,
new Activity(new Iri("https://example.com/games/" + levelId))));
}
async void OnApplicationPause(bool paused)
{
if (paused && outbox != null)
{
await outbox.FlushAsync();
}
}
void OnApplicationQuit()
{
shutdown.Cancel();
outbox?.Dispose();
store?.Dispose();
}
}
- Use one
HttpClientfor the life of the app, as on .NET. - Keep the store under
Application.persistentDataPath, which survives app updates. - The library awaits with
ConfigureAwait(false), so its work does not run on the main thread, but anawaitin aMonoBehaviourmethod still resumes there. The outbox'sDelivered,DeadLetteredandPausedevents are raised on the thread that runs the flush, often a thread-pool thread; do not touch Unity objects from them. - Do not block on a task with
.Resultor.Wait()on the main thread.
WebGL
Browsers do not give WebAssembly sockets, so HttpClient with its default handler sends nothing in a WebGL
player: the request never starts and the task never completes. UnityWebRequestHandler sends the requests with
UnityWebRequest, which hands them to the browser. It ships from version 0.2.0 on. Install it from this
repository with the Package Manager (Add package from git URL), or in Packages/manifest.json:
{
"dependencies": {
"io.github.nurullahturkoglu.xapi.unity": "https://github.com/nurullahturkoglu/xapi-dotnet.git?path=/src/Xapi.Unity#v0.2.0"
}
}
Then give the client an HttpClient that uses it. Create the handler on the main thread, for example in
Awake:
http = new HttpClient(new UnityWebRequestHandler());
It works on the other platforms too, so one HttpClient can serve every build. Requests sent from another
thread are posted to the main thread, which therefore must not block waiting for them.
The LRS has to allow the page's origin with CORS, because the browser checks it. The xapi-lrs image does that
with LrsHost__AllowedOrigins (lrs.md).
A WebGL player has one thread and no thread pool. In the tests, Task.Delay and Task.Run never completed
there. That changes how the outbox is used:
- Do not start
RunAsync, which waits with timers. CallFlushAsyncfrom a coroutine instead, for example every few seconds and after enqueueing. - The tests reopen a
FileOutboxStoreunderApplication.temporaryCachePathwithin one page. Whether a queue underApplication.persistentDataPathsurvives a page reload has not been tested.