Docs

Unity quickstart

The Unity SDK is two UPM packages for Unity 6000.2 and later: the same engine the .NET heads use, and a Unity head with a UnityWebRequest transport, a file store under Application.persistentDataPath, the native review prompts and a UI Toolkit panel.

1. Add the packages

The two packages ship as tarballs. Download both (version 0.3.4):

Commit both .tgz files to your project (for example under Packages/vendor/) and reference them by file: in Packages/manifest.json. The paths are relative to the Packages folder.

{
  "dependencies": {
    "com.mubold.wordsareflowing.core": "file:vendor/com.mubold.wordsareflowing.core-0.3.4.tgz",
    "com.mubold.wordsareflowing": "file:vendor/com.mubold.wordsareflowing-0.3.4.tgz"
  }
}

2. Create the loop and the panel

WordsAreFlowingUnity.Create builds an IFeedbackLoop from a WordsAreFlowingUnityOptions (the ApiKey is required). There is no container: you keep the loop yourself. Call it from Start, not Awake or a constructor — it creates a hidden GameObject.

using UnityEngine;
using UnityEngine.UIElements;
using WordsAreFlowing.Sdk.Core.Engine;
using WordsAreFlowing.Unity;
using WordsAreFlowing.Unity.Platform;
using WordsAreFlowing.Unity.Ui;

public class Feedback : MonoBehaviour
{
    [SerializeField] UIDocument document;
    IFeedbackLoop loop;
    FeedbackAskPanel panel;

    async void Start()
    {
        loop = WordsAreFlowingUnity.Create(new WordsAreFlowingUnityOptions
        {
            ApiKey = "waf_live_…",
            AppVersion = Application.version
        });
        panel = new FeedbackAskPanel(loop, CoroutineRunner.Create());
        document.rootVisualElement.Add(panel.Root);
        await loop.InitializeAsync();
    }

    public async void OnLevelCompleted()
    {
        await loop.TrackAsync("level_completed");
    }

    void OnDestroy() => panel?.Dispose();
}

Create fills in Platform from Application.platform and reports the SDK version itself. InitializeAsync loads the cached rules, then fetches fresh ones if the network allows. TrackAsync works as on the .NET page, and returns a decision the same way.

3. The private ask: FeedbackAskPanel

FeedbackAskPanel is a plain C# class, not a MonoBehaviour. Add its Root element wherever you like; it hides itself and subscribes to DecisionMade, opening when a decision asks for it. It shows the same copy as the Blazor component, in English or French from the current UI culture, or a FeedbackAskTexts you pass in. If a send fails, the player's text is kept and an error line is shown; nothing is retried for you.

For your own "send feedback" door, call Open with a trigger name. The panel never calls TrackAsync, so neither path can cause a store prompt. Closed is raised whenever it closes. Call Dispose when you are done with it, or its handler stays attached to the loop.

Native review prompts

iOS: the SDK calls Unity's UnityEngine.iOS.Device.RequestStoreReview. Apple shows the sheet at most three times in 365 days, always in development builds, and never from TestFlight.

Android: the prompt needs Google's Play In-App Review plugin for Unity (com.google.play.review). It is optional: the SDK's Android review assembly compiles only when that plugin is installed. Without it the SDK cannot show a prompt on Android: a store-prompt decision there ends in None with the reason prompter_declined. In the editor and on desktop players the reason is prompter_unavailable, as on the web.

To test the path on a build that is on no store track, pass your own flow as reviewFlowOverride to Create; DefaultFlowFor is what it uses otherwise.

Stay on the main thread

Neither the engine nor the Unity head uses ConfigureAwait(false), so when you call the loop from Unity's main thread, its continuations — counter writes, and your DecisionMade handler — come back to the main thread. Keep it that way in your own code: do not add ConfigureAwait(false) to an await whose continuation touches a Unity object.