Unity SDK
com.openplay.sdk is the C# client that connects to Open Play from Unity: guest and email/password sessions, restoring the session across restarts, plugin commands and queries, and the realtime socket. This page honestly describes installation, usage, and what has been verified.
Warning
The package is verified only with Unity 6000.6.7f1; other editor versions and most platforms are not verified (Verified platforms). This document is for package version 0.1.0.
API reference: the generated reference for the C# types is at https://open-play.b4klabs.dev/docs/sdk/unity/. It is in Turkish for now, because it mirrors the Turkish source comments.
Installation
The package comes from the GitHub Packages npm registry at https://npm.pkg.github.com/@ferdiunal. The package name is unscoped (com.openplay.sdk); the scope is in the registry address.
-
Credentials. GitHub asks for a token even for public packages. Create
~/.upmconfig.tomlin your user folder. The token is a classic token withread:packagespermission (a fine-grained token was not tried) and is a secret: do not put it in the repository, do not write it to logs.[npmAuth."https://npm.pkg.github.com/@ferdiunal"] token = "<token with read:packages permission>" alwaysAuth = true -
Manifest. Add the scoped registry and the dependency to your game project’s
Packages/manifest.jsonfile:{ "dependencies": { "com.openplay.sdk": "0.1.0" }, "scopedRegistries": [ { "name": "Open Play", "url": "https://npm.pkg.github.com/@ferdiunal", "scopes": ["com.openplay"] } ] } -
Open Unity. Two known behaviors: the package does not appear on Package Manager’s search screen (GitHub’s search endpoint returns
405; you write the package intomanifest.jsonby hand), and the log shows the warningPackage 'com.openplay.sdk' has been installed without a signature(harmless).
The com.unity.nuget.newtonsoft-json 3.2.1 and com.unity.modules.unitywebrequest dependencies are resolved automatically. As a fallback, a local path ("com.openplay.sdk": "file:<path>/sdks/unity") can be used; this is only for those who have access to the source repository.
The package includes a Basic Login sample (Package Manager › Open Play SDK › Samples).
Basic usage
OpenPlayClient.Create(OpenPlayUnity.Options(baseUrl, environmentId)) sets up the client; Options chooses the main-thread pump, the PlayerPrefs store, and the transport suitable for the platform. The component in the Quickstart restores the stored session and opens a guest if there is none.
OpenPlayOptions settings:
| Setting | Meaning |
|---|---|
BaseUrl | The API root. Absolute https (loopback http in development); cannot contain a query, fragment, or user info |
EnvironmentId | The environment ID (x-crx-env); cannot be empty |
AllowInsecureTransport | Development only: plain http/ws on loopback addresses (localhost, 127.0.0.1, ::1). Default false |
NativeOrigin | The fixed Origin sent on ticket and WebSocket requests in a native build. If empty, derived from BaseUrl. Must be in the environment’s domains or in the global allowed list |
PageOrigin | WebGL only: the page’s address; must be the same origin as BaseUrl (OpenPlayUnity.Options fills it in) |
SecretStore | The persistent store for the guest secret and the refresh token (see below) |
TimeoutMs | Total request time: 1–120,000, default 15,000 |
MaxResponseBytes | Largest response: 1 byte–16 MiB, default 2 MiB |
UserAgent | If empty, it is not sent at all |
Realtime | Realtime settings (AutoReconnect, ReconnectBaseMs, MaxReconnectAttempts, HeartbeatMs, AuthTimeoutMs, MaxPendingInputs, OnIdleClose) |
A wrong setting gives OpenPlayException with ErrorKind.Configuration and a coded reason (for example base_url_scheme_invalid, insecure_transport_not_loopback, environment_id_required).
Session
client.Session:
| Method | What does it do? |
|---|---|
RestoreAsync() | Restores the stored session; does not create a guest if there is none |
SignInGuestAsync() | Guest sign-in (Guest) |
SignUpEmailAsync, SignInEmailAsync, UpgradeGuestAsync | Email sign-up/sign-in/upgrade (Email) |
ChangePasswordAsync, RequestPasswordResetAsync, ResetPasswordAsync | Password operations |
RenameAsync(displayName) | Changing the display name (2–24 characters, 10 per hour) |
RefreshAsync() · LogoutAsync() | Refresh and sign-out |
StateChanged event · State | SessionState: Status, PlayerId, SessionId, IsGuest, DisplayName, Email |
SessionStatus: SignedOut, Authenticating, Authenticated, Refreshing, ReauthRequired, SigningOut, LogoutUncertain, Banned (terminal; you leave it only with LogoutAsync), Disposed. Passwords and tokens are carried with SecretText; they do not enter ToString() or logs.
For email and password examples, see the Email and password page.
Secret storage
The guest secret and the refresh token are stored with ISecretStore. The default PlayerPrefs store is plain text and is not a secure store (no iOS Keychain, no Android Keystore). In production, provide your own ISecretStore implementation that writes to the platform’s secure store:
using System.Threading;
using System.Threading.Tasks;
using OpenPlay.Support;
/// <summary>Platformunuzun güvenli deposunu (iOS Keychain, Android Keystore, masaüstü kimlik deposu) temsil eden kendi arayüzünüz.</summary>
public interface IPlatformKeystore
{
string Read(string key);
void Write(string key, string value);
void Delete(string key);
}
/// <summary>
/// ISecretStore sözleşmesi: misafir bootstrap sırrı ve yenileme belirteci burada saklanır.
/// Varsayılan PlayerPrefs deposu düz metindir ve güvenli depo DEĞİLDİR; üretimde bunun gibi kendi deponuzu verin:
/// options.SecretStore = new KeystoreSecretStore(myKeystore);
/// </summary>
public sealed class KeystoreSecretStore : ISecretStore
{
private readonly IPlatformKeystore _keystore;
public KeystoreSecretStore(IPlatformKeystore keystore) { _keystore = keystore; }
public Task<string> GetAsync(string key, CancellationToken cancellationToken) => Task.FromResult(_keystore.Read(key));
public Task SetAsync(string key, string value, CancellationToken cancellationToken)
{
_keystore.Write(key, value);
return Task.CompletedTask;
}
public Task ClearAsync(string key, CancellationToken cancellationToken)
{
_keystore.Delete(key);
return Task.CompletedTask;
}
}
Contract: the value does not enter logs, exception messages, or ToString(); GetAsync returns null for a missing key; every operation can fail or return late (the SDK gives up after 3 s).
Plugin commands and queries
client.Plugin(binding, pin) gives a plugin client (PrepareCommand, QueryAsync, PublicQueryAsync, VerifyServerSchemaAsync). When a command is prepared, a single Idempotency-Key is generated and the body is frozen; on an uncertain outcome (ErrorKind.UnknownOutcome) it is queried with LookupAsync using the same key, and if it was lost the exact same command is resent with ResumeAsync. No blind new command is generated.
Note
In the published package, only the
package-rideplugin has a generated typed binding (OpenPlay.PackageRide). For any other plugin there is no generated Unity binding today; you work with a rawPluginBinding. The binding is pinned to the schema digest on the server (schemaSha256): on a mismatch you getUpgradeRequired.
using System.Threading.Tasks;
using OpenPlay;
using OpenPlay.PackageRide;
using OpenPlay.Plugins;
/// <summary>
/// Eklenti komutu: yayımlı pakette yalnız package-ride için üretilmiş tipli bağlama vardır.
/// Komut hazırlanırken tek Idempotency-Key üretilir; belirsiz sonuçta AYNI anahtarla sorgulanır.
/// </summary>
public static class PluginCall
{
public static async Task ClaimLoginBonus(OpenPlayClient client, long expectedVersion)
{
var rides = PackageRideClient.Create(client); // bağlama sürümü ve şema özeti sunucuyla eşleşmezse UpgradeRequired
var command = rides.Commands.ClaimLoginBonus.Prepare(new LoginBonusInput { ExpectedVersion = expectedVersion });
try
{
var receipt = await command.SendAsync();
LoginBonusOutput output = rides.Commands.ClaimLoginBonus.ParseOutput(receipt); // tipli çıktı
UnityEngine.Debug.Log("giriş bonusu işlendi, HTTP durumu: " + receipt.Status + ", çıktı: " + output.GetType().Name);
}
catch (OpenPlayException ex) when (ex.Kind == ErrorKind.UnknownOutcome)
{
// Yanıt kayboldu; komut uygulanmış olabilir. Yeni komut üretmeyin, aynı anahtarla sorgulayın.
var lookup = await command.LookupAsync();
if (lookup.Status == CommandLookupStatus.NotFound) await command.ResumeAsync(); // tam aynı anahtar ve gövde
}
}
}
Realtime
client.Realtime.ConnectAsync() gets the ticket, opens the socket, and authenticates; on a drop it reconnects with increasing delay. In a native build the ticket is obtained with Authorization: Bearer + x-crx-env + the fixed Origin; no cookie is used. To open a subscription, use RealtimeConnection.JoinAsync(module, JoinTarget.PlayerSelf() / Room / Scene / InterestSet). Protocol details: Realtime.
using System;
using System.Threading.Tasks;
using OpenPlay;
using OpenPlay.Realtime;
using OpenPlay.Unity;
/// <summary>
/// Realtime soketi: bilet alma, bağlanma ve yeniden bağlanma SDK'dadır.
/// Sunucu etkinlik göstermeyen soketi 4100 koduyla kapatır; OnIdleClose verirseniz otomatik yeniden bağlanma olmaz.
/// </summary>
public static class RealtimeConnect
{
public static OpenPlayClient Create(Uri baseUrl, Guid environmentId, Action showContinue)
{
var options = OpenPlayUnity.Options(baseUrl, environmentId);
options.Realtime = new RealtimeOptions { OnIdleClose = _ => showContinue() }; // "Devam et" düğmesini gösterin
return OpenPlayClient.Create(options);
}
/// <summary>Oturum açıldıktan sonra çağrılır; "Devam et" onayında da aynı çağrı yeni bilet alıp sürdürür.</summary>
public static Task<RealtimeConnection> Connect(OpenPlayClient client) => client.Realtime.ConnectAsync();
}
WebGL
In the WebGL player the SDK switches to browser mode (OpenPlayUnity.Options chooses this itself): HTTP goes through UnityWebRequest, realtime goes through the browser’s WebSocket (a small jslib bridge), and the browser manages the session cookies. The SDK does not set a Cookie, Origin, Authorization, or User-Agent header; every mutation carries x-csrf-token.
- Same origin only (SDK limitation). Today the Unity WebGL SDK requires the game page and the Open Play API to be served from the same root; the server now grants CORS to origins in the environment domain list, but the Unity SDK does not use it yet (Architecture and hosting). A different origin gives
OpenPlayExceptionwithConfigurationand the codewebgl_cross_origin_unsupported; it does not fail silently. - Hidden tab. The heartbeat continues with the browser timer, and when the tab goes to the background the connection stays open. Messages and HTTP completions that arrive while hidden wait for the Unity frame and are processed when the tab becomes visible. With
Application.runInBackgroundoff, the game loop stops in a hidden tab; if your game must progress while hidden, setApplication.runInBackground = true. - Multiple tabs. Tabs on the same origin share a single cookie jar; signing in in the second tab changes the first tab’s session cookie.
Verified platforms
Level: R = measured in Unity, B = documentation or dotnet indicator only, — = none.
| Target | Level | Note |
|---|---|---|
| Unity 6000.6.7f1 Editor (Linux) | R | Mono and IL2CPP measured |
| Windows | B | not verified |
| macOS | B | not verified |
| Android | B | not verified (no phone measurement was made) |
| iOS | — | not verified |
| WebGL (same origin) | R | The BasicLogin WebGL player against a real API: guest and email sign-in, a plugin query, realtime Ready, join, and input acknowledgment; the heartbeat continued for 35 s in a hidden tab. Chrome only; Firefox and Safari are not verified |
The unity: 2022.3 field in package.json is only a lower bound; Unity 2022.3 is not verified.
Errors
All errors arrive as OpenPlayException: Kind (ErrorKind), Status, Attempts, Code, Operation, DeliveryState, Reference, Reason. The ErrorKind values: Http, Network, Timeout, Cancelled, Contract, Configuration, Pagination, Codegen, Auth, UnknownOutcome, Storage, Realtime, Backpressure, UpgradeRequired, CapabilityUnavailable, Disposed, ScopeChanged. Their meanings are the same as those in the TypeScript SDK (TypeScript SDK › Errors).
Out of scope and known limits
- Not in the Unity SDK: OAuth (Google), catalog, store, leaderboard, notifications, and administration operations with a server key. Use raw REST for these; do not put a server key into the game client.
- There is no response compression (
permessage-deflate). - The access JWT is not exposed to the application (Server-side verification).
- Android, iOS, and macOS player runs were not made; WebGL works only on the same origin.
- The
PlayerPrefsstore is not secure; in production provide your own store.