CEEJAYDocs

Unity

Link a player of your Unity game to their Discord account, and show the community's quests, shop, locker and rank in the game.

The Unity package starts a connect session, returns the link, the code and the URL of the QR image to your game, polls for the result, waits for the player's confirmation, keeps the player signed in and reads the member profile summary. When the player is connected it also provides the game with the community's quests, shop, locker, rank and daily reward. It draws nothing: your game shows the button.

It wraps the C# game core, which has no reference to Unity, with Unity's own network, browser, storage and timers. The package contains no logic that the core lacks, so what the core does is explained on Connect, Connect, inside a game and The game loop, inside a game.

Preview: not published yet

The package is not published yet, and its name, com.ceejay.connect, may change before the first release. It is built on the same code as the Ceejay.Connect package on NuGet, whose 173 unit tests run without Unity. The Unity integration is written against the Unity 2022.3 LTS API and compiles against mocked Unity APIs, but it is not yet tested in the Unity editor, on mobile, in WebGL or with IL2CPP. See What is being checked for what has been verified.

What you need

  • Unity 2022.3 LTS or newer, with the .NET Standard 2.1 API compatibility level. That is the default: Project Settings → Player → Other Settings → Api Compatibility Level.
  • Connect enabled for the community, and the public key of your game, ceejay_pk_…. The key ships inside the game and is not a secret. A secret key is rejected: it must never be inside a game. See Connect.

The package asks Unity for com.unity.nuget.newtonsoft-json, Unity's own build of Newtonsoft.Json, and the Package Manager installs it automatically.

Install

The package is not published, so install it from a folder or from Git.

From disk. Open Window → Package Manager, press +, choose Add package from disk, and pick sdk/unity/com.ceejay.connect/package.json in a checkout of the CEEJAY repository.

From a Git URL. Press +, choose Add package from git URL, and enter the URL of the folder:

The Git URL of the package
https://github.com/<owner>/<repository>.git?path=/sdk/unity/com.ceejay.connect#<branch or tag>

Use the URL of the CEEJAY repository you were given access to, and the branch or tag that holds the package. The repository is private: Unity uses the Git credentials of the machine.

Connect a player

CeejayUnity.Create is the whole set-up. It returns the core's client, with Unity's parts already chosen, and must be called on the main thread.

Connect a player in Unity
using System.Threading.Tasks;
using Ceejay;
using Ceejay.Connect;
using Ceejay.Connect.Unity;
using UnityEngine;

public class Discord : MonoBehaviour
{
    CeejayConnect connect;

    async void Start()
    {
        connect = CeejayUnity.Create("ceejay_pk_…");                           // the public key of your game
        await connect.RestoreAsync();                                          // when the game starts

        ConnectSession session = await connect.StartAsync(new StartParams { PlayerId = "p_19", PlayerName = "Nova77" });
        ShowWaiting(session.Code);                                             // always show the code too
        _ = connect.OpenLinkAsync(session, new UnityLinkOpener());             // opens the player's browser

        ConnectOutcome outcome = await connect.WaitForResultAsync(AskThePlayer);   // asks every 3 seconds
        if (outcome.IsConnected) Debug.Log(outcome.Connected.Member.DisplayName);  // name, level, coins, roles
        else Debug.Log(outcome.Ended.Status + " " + outcome.Ended.Reason);         // declined, expired, …
    }

    Task<bool> AskThePlayer(ConnectMember member, ConfirmDetails details) => ShowYesNoAsync("Connect to " + member.DisplayName + "?");
}

ShowWaiting and ShowYesNoAsync are your game's own screens. The player ID is your game's own ID for the player.

The connection is saved when both the Discord member and the game have confirmed. The confirmation prompt in WaitForResultAsync exists for a reason: a code shown on a stream can be approved by anyone who reads it, and this is where the player sees a name that is not theirs and declines. There is no default: the parameter is required.

A game written with coroutines can wait on a call without async:

Wait in a coroutine
var wait = connect.StartAsync(new StartParams { PlayerId = "p_19" }).AsYield();
yield return wait;
if (wait.Failed) { /* show the error */ }
var session = wait.Result;

What the package chooses for you

CeejayUnity.Create(publicKey, baseUrl, storage, getProof) returns the core's client with these parts. Leave baseUrl out for the shared CEEJAY service, and pass your dashboard URL for a dedicated instance.

The core asks forThe package gives itNotes
A transportUnityWebRequestTransportUnityWebRequest works on every platform. An HttpClient does not on WebGL
A storagePlayerPrefsConnectStorageNot a secure store: see Where the connection is stored
A link openerUnityLinkOpener, which you pass to OpenLinkAsyncApplication.OpenURL, for https links only
A timerTask.Delay, or UnityTimers.Delay on WebGLA web page has no threads
Pausing in the backgroundA hidden object that calls SetInForeground from OnApplicationPauseOn a phone the game stops polling for the result in the background, and polls immediately when it returns to the foreground

Everything else is the core: the retries, the renewal of the player session and the closed list of error codes. If your game signs players in with Unity Player Authentication or Firebase, pass getProof. CeejayUnity.UnityProof(playerId, accessToken) makes the proof for a Unity sign-in. On Steam, ConnectProof.Steam(ticket) makes the proof from the ticket of GetAuthTicketForWebApi("ceejay"), and the player ID is the SteamID64.

The Connect button sample

The package has a finished button to start from. It draws the four states for you, and asks the player to confirm with a Yes and No panel.

  1. Open Window → Package Manager, choose CEEJAY Connect, open Samples, and press Import beside Connect button. The script CeejayConnectButton.cs is added to Assets/Samples/CEEJAY Connect/<version>/Connect button/.
  2. Build the scene. The sample is a script and a description of the scene, not a saved scene: it takes about five minutes with Unity's default uGUI controls. SampleScene.md in the same folder has the steps.
The scene
Canvas                           (Screen Space - Overlay)
 ├─ ConnectButton                Button + the script CeejayConnectButton
 │   └─ Label                    Text ("Connect Discord")
 ├─ Code                         Text, shows "Code: K7M2QX" while waiting
 └─ ConfirmPanel                 Panel, inactive at the start
     ├─ Question                 Text ("Connect to Nova?")
     ├─ Yes                      Button
     └─ No                       Button
  1. Select ConnectButton and fill in the component: Public Key, Base Url (empty for the shared service), Player Id (type a test one such as p_19), Player Name, and the Button, Label, Code Label, Confirm Panel, Confirm Label, Yes Button and No Button from the scene. Add an EventSystem if the scene has none.
  2. Press Play. The button reads Connect Discord. Press it: the browser opens CEEJAY's page and the game shows the code. Approve in the browser, and within three seconds the Yes and No panel opens in the game with the Discord name. Press Yes, and the button reads Discord connected.

To reach the client from the rest of your game, read the Connect property of the script, which is the same CeejayConnect:

Reach the client from the sample
CeejayConnect connect = FindObjectOfType<CeejayConnectButton>().Connect;
ListResponse<PlayerQuest> quests = await connect.Hub.ListQuestsAsync();

The four states of the button

connect.State is one of four values. It is known once RestoreAsync() has run.

connect.StateWhat it meansWhat the button shows
NotConnectedThis device has no connection and no open sessionConnect Discord
WaitingA session is openThe code of connect.Session and a Cancel button
ConnectedThis device has the player's connectionConnected
NeedsAttentionConnected, and the player has something to doWhat connect.Attention says

connect.Attention is null in every other state:

connect.AttentionWhere to send the player
NotMemberThe Discord account never joined the server: connect.JoinUrl
LeftThe Discord account left the server: connect.JoinUrl
PendingNowhere. Discord's membership screening is still open
SharingThe game requests data beyond what the player consented to: connect.ManageUrl

Redraw the button from connect.OnChange(...) or the StateChanged event. A change is announced on the thread where it happened, which is the main thread when your code called the client from there. To be safe against any thread, set a flag in the listener and draw in Update(), as the sample does.

Quests, shop and rank

Once the player is connected and the community has switched the game loop on for your game, connect.Hub has the rest. The gates, the order of a first screen and the rules for spending are on The game loop, inside a game, and the eleven functions are listed on C# and .NET.

Use the game loop in Unity
ListResponse<PlayerQuest> quests = await connect.Hub.ListQuestsAsync();
ListResponse<PlayerShopItem> shop = await connect.Hub.ListShopAsync();
PlayerBalance balance = await connect.Hub.GetBalanceAsync();          // grey out Buy when balance.Spending.MaySpend is false
PlayerPurchase bought = await connect.Hub.BuyAsync(item.Id, item.Price);
await connect.Hub.CheckInAsync();                                      // the daily reward

BuyAsync generates an idempotency key for the purchase and sends every attempt with it, so a lost response never buys twice. A player who cannot spend yet is a CeejaySpendBlockedError: show "come back later", with HoldUntil or ResetsAt, and do not treat it as a failure.

Open the link in the player's own browser, never in a web view inside the game: Discord blocks sign-in there. On a console or a television, show session.Code, to be typed at your dashboard URL followed by /connect, and the image at session.QrUrl, which is an SVG. Open nothing.

The table says what the package is written to do on each platform. None of them has been run yet.

PlatformThe linkNotes
Windows, macOS, LinuxApplication.OpenURL opens the default browserNothing is paused: the game keeps running while the player is in the browser
iOS, AndroidApplication.OpenURL opens the system browserThe game goes to the background, so OnApplicationPause pauses the polling and the return triggers an immediate poll. Android blocks plain http by default: a test copy on a local address needs a network security config, and a real one uses https
WebGLApplication.OpenURL opens a tabCall StartAsync and OpenLinkAsync from the player's own click, or the browser blocks the tab. See WebGL
Consoles, televisionsNone. Show session.Code and the QR imageThe player types the code at your dashboard URL followed by /connect on a phone

WebGL

A web page has no threads, so the package waits on a coroutine there (UnityTimers). The core's request timeout is a timer too, so on WebGL a request that receives no response ends when UnityWebRequest.timeout expires. The package sets it from the call. Treat WebGL as the least tested path of all.

IL2CPP

The package ships a link.xml that keeps the core's classes from being stripped on IL2CPP. It preserves the whole assembly. It has not been seen working on an IL2CPP build yet, so build for IL2CPP early and play a connect through it.

Where the connection is stored

Two things must outlast the game being closed: the refresh token, and the open session with the session secret used to poll for the result. A phone often closes a game while its player is in the browser. Without the session the player would have to authorise with Discord again.

PlayerPrefsConnectStorage stores both in PlayerPrefs, as plain text. That is the simplest place that survives, and it is not private: on Windows it is the registry, and on a phone it is a file that the player can read on a rooted device. A refresh token is replaced every time it is used, so a copy is worth little, because a second use ends the connection. A game that wants more stores it in the iOS Keychain or the Android Keystore through a plugin of its own, with DelegateConnectStorage:

Keep the connection in a secure store
var storage = new DelegateConnectStorage(
    key => MySecureStore.Get(key),          // string, or null
    (key, value) => MySecureStore.Set(key, value),
    key => MySecureStore.Remove(key));
connect = CeejayUnity.Create("ceejay_pk_…", storage: storage);

MySecureStore is the plugin you choose. For a store with an async API, implement IConnectStorage instead. Use one client for one storage: two copies of the game that renew from the same storage at the same moment can sign the device out.

Errors

A refusal is a CeejayError with Status, Code, Message, RetryAfter, RequestId, Reason, Field and Feature. Test the Code, never the words. ErrorCodes lists them, and Errors says what each one means. No response at all is a CeejayConnectionError, with TimedOut. A mistake in how the game uses the client is an ordinary ArgumentException or InvalidOperationException.

If Connect is not enabled for this community, CEEJAY returns not_available, which is a CeejayNotAvailableError. Hide the button. No error ever shows a session secret, a refresh token, a proof or a player session.

What the package sends

The package only contacts your community's CEEJAY. It sends two headers, Authorization and Content-Type (Unity adds the usual User-Agent of its network stack), and these values:

  • the player ID your game gives it, and the player name, language and under-age flag if you pass them;
  • the device's sign-in: the refresh token and the player session that CEEJAY issued to this device, and, if you pass one, the Unity Player Authentication or Firebase token, or the connect pass, that proves the player.

It sends nothing else: no advertising ID, no device ID, no location, no contacts, no analytics and no crash reports. The Discord sign-in happens in the browser on CEEJAY's page, and the game never sees the player's Discord password or token. As with any request, CEEJAY's server sees the device's IP address.

For the store privacy forms, declare Identifiers → User ID on the App Store (collected, linked to the user, used for App Functionality, not used for tracking) and Personal info → User IDs on Google Play (collected, not shared, required for app functionality). Add the player's name if you pass PlayerName, and purchase history if you use the loop's shop. Your own declarations depend on everything else your game does. The same table is on Game kits.

A game that children play must say so in its set-up, which the community's team does, and send UnderAge = true for a player below the age your game may connect. CEEJAY then refuses to connect them.

What is being checked

The core is tested without Unity. Before the first release, the Unity layer is checked in the editor by connecting a test game to a test CEEJAY. These are the steps, and what each should do.

  1. Install the package from disk. Expected: no red errors in the Console. If there are any, the first line says which file and which line. The Unity layer is the eight files in Runtime/Unity.
  2. Import the sample and build the scene.
  3. Fill in the public key and a player ID, and press Play. Pressing Connect should open the browser. The confirmation prompt with the Discord name should appear in the game within three seconds of approving in the browser.
  4. Stop and press Play again: the button reads Discord connected straight away, because the connection was stored and renewed. Press the button while connected: the browser opens the page where the member manages their connections, including Disconnect.
  5. On a phone or in a build, put the game in the background while the browser is open, then come back: the state should move to Connected at once.

A step that fails prints a Console line that starts with CEEJAY Connect: and says why.

On this page

Community StudiosPrivacyTerms