The code on this page is extracted from a sample project that compiles against each SDK release. The guide cannot show code that does not build. Every snippet is regenerated from the release the install step pins, so the code matches the version you install.
Preconditions
- A Unity 2022.3 LTS or newer project that compiles cleanly.
- Write access to
Packages/manifest.json,Assets/, and the startup scene. - A PIO account, and a human who can reach its dashboard.
- Recommended: the PIO MCP server connected. It provides
check_integration, the live readout this guide’s verification loop uses. Without it, each verification falls back to the dashboard through your human.
Step 1: install the SDK
The SDK installs from the public PIO package registry. No key, login or.upmconfig.toml is needed.
Add the registry, the SDK and its one dependency to Packages/manifest.json:
Packages/manifest.json
scopedRegistries and dependencies blocks; every manifest edit in this guide is additive, and replacing the file would drop the project’s built-in modules. The SDK is pre-1.0, so pin the exact version shown, never a range. Installation & versioning carries the upgrade rules.
Check: the project compiles and using PIO; resolves. Without the Editor open, compile in batch mode, with UNITY_EDITOR set to the editor binary for the project’s Unity version:
error CS lines in the log mean the install compiles.
The log never confirms that the packages resolved. Check Packages/packages-lock.json: it must lock com.pioneeroptimisation.sdk at the pinned version from the PIO registry, and UniTask at the pinned revision. A failed or partial resolve can leave an older cached version compiling, and still exits 0. A registry 401, 403 or 404 in the log means the registry refused the request. Check the registry URL matches the snippet exactly. If it does, hand off to your human.
On macOS the editor binary lives at /Applications/Unity/Hub/Editor/<version>/Unity.app/Contents/MacOS/Unity; export it as UNITY_EDITOR once and every later step reuses it.
Step 2: choose a project and collect credentials
Three values connect the SDK to a project:ApiKey, AccountId, and ProjectId.
With the MCP server:
- Call
whoami. It names the account, your scopes, and the account’s projects. - Use the project your human names, or create one with
create_project. A project you create starts under a not-child-directed default and accepts events at once. A studio member must confirm the child-directed answer in the project’s settings within 14 days (theconfirm_bydate in tool responses), or the key is unpublished. - Call
get_api_keyfor the project. The response carries the API key and the account id. It needs an admin credential; aforbiddenerror names who can help. While the confirmation is outstanding the response carriesattestation_pending: trueand theconfirm_bydate; the key works throughout the grace.
Step 3: write the integration
CreateAssets/Scripts/PioBootstrap.cs containing exactly this file. It initialises once at startup, registers the action definitions with fallbacks, surfaces errors while integrating, requests a decision, and tracks events:
Samples~/BasicIntegration/Assets/Scripts/PioBootstrap.cs
using static PIO.PIO; line matters. The PIO class shares its name with the PIO namespace, so qualified calls like PIO.Init do not resolve from a script outside that namespace.
Each key you register must also exist as an action definition in the project. With the MCP server, call create_action_definition with the name, the candidate actions, and the default. The candidate actions are a game design decision, not something the guide or the tools can infer; take them from your task brief, or ask your human. This sample’s Tutorial definition offers Short, Medium and Long, with Short as the default, matching the code’s fallback. Register the event types the game will track the same way, with register_event_type. Without the MCP server, hand-off: your human creates both in the dashboard.
Wire it into the game: add a GameObject named PIO to the first loaded scene and attach PioBootstrap, filling the three credential fields in the Inspector.
Headless, or in a fresh project with no scene, create the scene with a throwaway Editor script instead of hand-writing scene YAML. The script only builds the scene; credentials never touch it. PioBootstrap itself reads PIO_API_KEY, PIO_ACCOUNT_ID, and PIO_PROJECT_ID from the environment when its Inspector fields are blank, so no key lands in a committed file:
Assets/Editor/PioSetup.cs
PioSettings asset (menu PIO/Setup Wizard), which the zero-argument PIO.Init() loads. The PIO/Verify Integration window expects that asset and reports a missing-settings failure even when a code-configured integration works; create the asset, or read past that one check.
The whole change, for a game with one decision point:
Step 4: run the game
Enter Play mode and pass through the decision point, so the SDK initialises, serves a decision, and sends events. Headless, drive the same path with a PlayMode test run under-runTests -testPlatform PlayMode. Two pieces of setup come first:
- Add
com.unity.test-framework(1.1.33) toPackages/manifest.json; step 1’s manifest does not include it. - Test assemblies cannot reference the default
Assembly-CSharp, so giveAssets/Scriptsits own assembly definition. Put the test assembly definition underAssets/Tests/PlayMode. Delete the temporaryAssets/Editor/PioSetup.csafter scene setup.
Assets/Scripts/PIO.Samples.asmdef:
Samples~/BasicIntegration/Assets/Scripts/PIO.Samples.asmdef
Assets/Tests/PlayMode/PIO.Samples.Tests.PlayMode.asmdef:
Samples~/BasicIntegration/Assets/Tests/PlayMode/PIO.Samples.Tests.PlayMode.asmdef
Samples~/BasicIntegration/Assets/Tests/PlayMode/PioIntegrationPlayModeTests.cs
PioBootstrap.Start() drive the whole loop: init, the decision, and both events, with one flush interval waited so the queue ships. Delete the test assembly after verification if the project does not keep tests.
Run it with a results file; for test runs the exit code alone is not a reliable pass signal:
playmode-results.xml.
Step 5: verify
With the MCP server, callcheck_integration after each run and read three things:
statusandstreaming:nevermeans nothing has arrived yet;integratedwithstreaming: truemeans events are flowing now.coverage: track the event types the project expects, listed incoverage.expected. Types underunregisteredare arriving without being registered; they still flow through, with a console warning. Register them withregister_event_type; without the MCP server, your human does it in the dashboard.next_step: the one thing to do now. Do it, run again, and check again.
check_integration can lag minutes behind real delivery. After a clean run, poll it patiently rather than reading a single never as failure.
Without the MCP server, hand-off: your human opens integration health and reads the same facts, or runs PIO/Verify Integration in the Editor.
You are done when check_integration reports status: "integrated" with your event types seen, or your human confirms the dashboard shows events arriving. Integration is one early stage of six; get_project_status names what remains after it, none of which needs code.
When something fails
- Init fails:
PIO.Statenames the phase, and theOnErrorline carries the code. Re-read the three credential values before touching code; most failures are a wrong or swapped id. - Events sent, console clean,
statusstillnever: normal for the first minutes after a fresh integration. Keep pollingcheck_integration; client-side silence is what success looks like. - Attestation overdue: a project unconfirmed past its
confirm_bydate has its key unpublished. Init then fails with a JSON parse error at the auth call, nothing naming attestation.check_integrationreportsattestation_overdue; confirm in project settings to restore events. - Decisions arrive but events never do: decisions and events travel to different services. Leave every
*UrlOverridefield blank; see Verify your integration. When overrides are set deliberately, for a PIO internal environment, check their values instead of blanking them. INGESTION_FAILED_PERMANENTLY, events dropped after retries: the SDK queues events on disk under Unity’s Company Name and Product Name, not under a PIO project. A machine that ran a different project under the same names retries that stale backlog, then drops it. Ifcheck_integrationshows this run’s events as seen, the drops were stale and nothing is wrong. To clear the backlog, quit the application, then deletepio_events.dbunderApplication.persistentDataPath. Leave the Product Name alone; changing it moves the whole persistent-data directory, saves included.- Any MCP tool error returns
error,message, andnext_step. Act onnext_steprather than retrying the same call.