nccgs 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/agents/nccgs-accessibility-specialist.md +26 -0
- package/.claude/agents/nccgs-adversarial-reviewer.md +26 -0
- package/.claude/agents/nccgs-ai-programmer.md +26 -0
- package/.claude/agents/nccgs-analytics-engineer.md +26 -0
- package/.claude/agents/nccgs-art-direction-lead.md +26 -0
- package/.claude/agents/nccgs-audio-direction-lead.md +26 -0
- package/.claude/agents/nccgs-creative-director.md +26 -0
- package/.claude/agents/nccgs-documentation-manager.md +26 -0
- package/.claude/agents/nccgs-economy-designer.md +26 -0
- package/.claude/agents/nccgs-engine-programmer.md +26 -0
- package/.claude/agents/nccgs-game-design-lead.md +26 -0
- package/.claude/agents/nccgs-game-designer.md +26 -0
- package/.claude/agents/nccgs-gameplay-programmer.md +26 -0
- package/.claude/agents/nccgs-level-designer.md +26 -0
- package/.claude/agents/nccgs-live-ops-designer.md +26 -0
- package/.claude/agents/nccgs-localization-lead.md +26 -0
- package/.claude/agents/nccgs-narrative-lead.md +26 -0
- package/.claude/agents/nccgs-network-programmer.md +26 -0
- package/.claude/agents/nccgs-performance-analyst.md +26 -0
- package/.claude/agents/nccgs-producer.md +26 -0
- package/.claude/agents/nccgs-production-coordinator.md +23 -0
- package/.claude/agents/nccgs-programming-lead.md +26 -0
- package/.claude/agents/nccgs-prototyper.md +26 -0
- package/.claude/agents/nccgs-qa-engineer.md +26 -0
- package/.claude/agents/nccgs-qa-lead.md +26 -0
- package/.claude/agents/nccgs-release-engineer.md +26 -0
- package/.claude/agents/nccgs-release-lead.md +26 -0
- package/.claude/agents/nccgs-security-engineer.md +26 -0
- package/.claude/agents/nccgs-sound-designer.md +26 -0
- package/.claude/agents/nccgs-systems-designer.md +26 -0
- package/.claude/agents/nccgs-technical-architect.md +26 -0
- package/.claude/agents/nccgs-technical-artist.md +26 -0
- package/.claude/agents/nccgs-technical-director.md +26 -0
- package/.claude/agents/nccgs-tools-programmer.md +26 -0
- package/.claude/agents/nccgs-ui-programmer.md +26 -0
- package/.claude/agents/nccgs-unity-build-specialist.md +26 -0
- package/.claude/agents/nccgs-unity-content-specialist.md +26 -0
- package/.claude/agents/nccgs-unity-implementer.md +26 -0
- package/.claude/agents/nccgs-unity-rendering-specialist.md +26 -0
- package/.claude/agents/nccgs-unity-systems-specialist.md +26 -0
- package/.claude/agents/nccgs-unity-ui-specialist.md +26 -0
- package/.claude/agents/nccgs-ux-designer.md +26 -0
- package/.claude/agents/nccgs-verification-engineer.md +26 -0
- package/.claude/agents/nccgs-world-builder.md +26 -0
- package/.claude/agents/nccgs-writer.md +26 -0
- package/.claude/nccgs/THIRD_PARTY_NOTICES.md +13 -0
- package/.claude/nccgs/VERSION +1 -0
- package/.claude/nccgs/constitution.md +131 -0
- package/.claude/nccgs/hooks/agent-audit.mjs +10 -0
- package/.claude/nccgs/hooks/common.mjs +41 -0
- package/.claude/nccgs/hooks/post-compact.mjs +1 -0
- package/.claude/nccgs/hooks/pre-compact.mjs +8 -0
- package/.claude/nccgs/hooks/protect-git.mjs +22 -0
- package/.claude/nccgs/hooks/protect-write.mjs +17 -0
- package/.claude/nccgs/hooks/session-start.mjs +15 -0
- package/.claude/nccgs/hooks/session-stop.mjs +7 -0
- package/.claude/nccgs/protocols/agent-contract.md +34 -0
- package/.claude/nccgs/protocols/context-packets.md +13 -0
- package/.claude/nccgs/protocols/evidence.md +15 -0
- package/.claude/nccgs/protocols/model-routing.md +22 -0
- package/.claude/nccgs/protocols/orchestration.md +26 -0
- package/.claude/nccgs/protocols/unity-boundary.md +11 -0
- package/.claude/nccgs/settings.fragment.json +77 -0
- package/.claude/nccgs/studio.json +347 -0
- package/.claude/nccgs/tools/configure-models.mjs +39 -0
- package/.claude/nccgs/unity-skills-manifest.json +727 -0
- package/.claude/nccgs/workflow-catalog.json +317 -0
- package/.claude/rules/nccgs-canonical-docs.md +19 -0
- package/.claude/rules/nccgs-editor-tools.md +12 -0
- package/.claude/rules/nccgs-localization.md +12 -0
- package/.claude/rules/nccgs-networking.md +13 -0
- package/.claude/rules/nccgs-performance.md +14 -0
- package/.claude/rules/nccgs-rendering.md +16 -0
- package/.claude/rules/nccgs-security.md +13 -0
- package/.claude/rules/nccgs-tests.md +18 -0
- package/.claude/rules/nccgs-ui.md +14 -0
- package/.claude/rules/nccgs-unity-assets.md +21 -0
- package/.claude/rules/nccgs-unity-code.md +22 -0
- package/.claude/skills/accessibility-review/SKILL.md +14 -0
- package/.claude/skills/architecture-decision/SKILL.md +14 -0
- package/.claude/skills/asset-audit/SKILL.md +14 -0
- package/.claude/skills/audit/SKILL.md +16 -0
- package/.claude/skills/audit/references/dimensions.md +46 -0
- package/.claude/skills/balance-review/SKILL.md +14 -0
- package/.claude/skills/bug-triage/SKILL.md +14 -0
- package/.claude/skills/build-live-game/SKILL.md +317 -0
- package/.claude/skills/build-live-game/references/achievements.md +779 -0
- package/.claude/skills/build-live-game/references/apis.md +280 -0
- package/.claude/skills/build-live-game/references/authentication.md +437 -0
- package/.claude/skills/build-live-game/references/battlepass.md +860 -0
- package/.claude/skills/build-live-game/references/cloud-code.md +563 -0
- package/.claude/skills/build-live-game/references/cloud-save.md +474 -0
- package/.claude/skills/build-live-game/references/deployment.md +216 -0
- package/.claude/skills/build-live-game/references/player-account.md +813 -0
- package/.claude/skills/build-live-game/references/remote-config.md +96 -0
- package/.claude/skills/build-live-game/references/tooling.md +431 -0
- package/.claude/skills/closure/SKILL.md +14 -0
- package/.claude/skills/code-review/SKILL.md +14 -0
- package/.claude/skills/compatibility-review/SKILL.md +14 -0
- package/.claude/skills/context-pack/SKILL.md +14 -0
- package/.claude/skills/dependency-review/SKILL.md +14 -0
- package/.claude/skills/design/SKILL.md +20 -0
- package/.claude/skills/design-review/SKILL.md +14 -0
- package/.claude/skills/evidence-review/SKILL.md +14 -0
- package/.claude/skills/hotfix/SKILL.md +14 -0
- package/.claude/skills/implement-in-app-purchases/README.md +233 -0
- package/.claude/skills/implement-in-app-purchases/SKILL.md +158 -0
- package/.claude/skills/implement-in-app-purchases/references/api-notes.md +562 -0
- package/.claude/skills/implement-in-app-purchases/references/codeless-catalog.md +331 -0
- package/.claude/skills/implement-in-app-purchases/references/convert-adapty.md +268 -0
- package/.claude/skills/implement-in-app-purchases/references/convert-essentialkit.md +239 -0
- package/.claude/skills/implement-in-app-purchases/references/convert-revenuecat.md +275 -0
- package/.claude/skills/implement-in-app-purchases/references/convert-unipay.md +145 -0
- package/.claude/skills/implement-in-app-purchases/references/migration-v4-to-v5.md +204 -0
- package/.claude/skills/implement-in-app-purchases/references/path-add-iap-to-new-project.md +198 -0
- package/.claude/skills/implement-in-app-purchases/references/path-convert-native-google-billing.md +356 -0
- package/.claude/skills/implement-in-app-purchases/references/path-convert-native-storekit.md +446 -0
- package/.claude/skills/implement-in-app-purchases/references/path-implement-iap-d2c.md +680 -0
- package/.claude/skills/implement-in-app-purchases/references/platform-notes.md +204 -0
- package/.claude/skills/implement-in-app-purchases/references/pre-check.md +205 -0
- package/.claude/skills/incident-recovery/SKILL.md +14 -0
- package/.claude/skills/initialize-ai-navigation/SKILL.md +95 -0
- package/.claude/skills/initialize-ai-navigation/references/navigation-system.md +794 -0
- package/.claude/skills/levelplay-unity-integration/CHANGELOG.md +57 -0
- package/.claude/skills/levelplay-unity-integration/README.md +74 -0
- package/.claude/skills/levelplay-unity-integration/SKILL.md +1126 -0
- package/.claude/skills/levelplay-unity-integration/references/banner-api.md +920 -0
- package/.claude/skills/levelplay-unity-integration/references/best-practices.md +536 -0
- package/.claude/skills/levelplay-unity-integration/references/ilrd-api.md +337 -0
- package/.claude/skills/levelplay-unity-integration/references/initialization-api.md +630 -0
- package/.claude/skills/levelplay-unity-integration/references/interstitial-api.md +899 -0
- package/.claude/skills/levelplay-unity-integration/references/ios-setup.md +491 -0
- package/.claude/skills/levelplay-unity-integration/references/migration-sdk-9.md +666 -0
- package/.claude/skills/levelplay-unity-integration/references/privacy-settings.md +608 -0
- package/.claude/skills/levelplay-unity-integration/references/rewarded-api.md +902 -0
- package/.claude/skills/localization/SKILL.md +135 -0
- package/.claude/skills/localization/references/api-notes.md +76 -0
- package/.claude/skills/localization/resources/L10nBatchProcessor.cs +69 -0
- package/.claude/skills/localization/resources/LocalizedFontAsset.cs +18 -0
- package/.claude/skills/localize-game/SKILL.md +14 -0
- package/.claude/skills/migrate-project/SKILL.md +21 -0
- package/.claude/skills/migrate-project/references/procedure.md +63 -0
- package/.claude/skills/milestone-review/SKILL.md +14 -0
- package/.claude/skills/new-unity-project/SKILL.md +179 -0
- package/.claude/skills/optimize-audio/SKILL.md +199 -0
- package/.claude/skills/optimize-audio/resources/audio-import-api.md +146 -0
- package/.claude/skills/optimize-audio/resources/platform-settings.md +48 -0
- package/.claude/skills/optimize-text-mesh-pro/SKILL.md +182 -0
- package/.claude/skills/optimize-web/SKILL.md +393 -0
- package/.claude/skills/optimize-web/resources/WebOptimizer.cs +21 -0
- package/.claude/skills/optimize-web/resources/toktx-examples.sh +11 -0
- package/.claude/skills/performance-audit/SKILL.md +14 -0
- package/.claude/skills/physics-3d-collision/SKILL.md +442 -0
- package/.claude/skills/physics-3d-collision/references/troubleshooting.md +41 -0
- package/.claude/skills/physics-3d-collision/resources/CollisionDebugger.cs +33 -0
- package/.claude/skills/plan-feature/SKILL.md +14 -0
- package/.claude/skills/playtest/SKILL.md +14 -0
- package/.claude/skills/project-stage/SKILL.md +14 -0
- package/.claude/skills/prototype-feature/SKILL.md +14 -0
- package/.claude/skills/qa-plan/SKILL.md +14 -0
- package/.claude/skills/release/SKILL.md +18 -0
- package/.claude/skills/release-readiness/SKILL.md +14 -0
- package/.claude/skills/retrospective/SKILL.md +14 -0
- package/.claude/skills/review/SKILL.md +16 -0
- package/.claude/skills/security-audit/SKILL.md +14 -0
- package/.claude/skills/setup-multiplayer-services/SKILL.md +39 -0
- package/.claude/skills/setup-multiplayer-services/references/dgs-entrypoint.md +79 -0
- package/.claude/skills/setup-multiplayer-services/references/entrypoints.md +213 -0
- package/.claude/skills/setup-multiplayer-services/references/examples.md +33 -0
- package/.claude/skills/setup-multiplayer-services/references/implementation-fit.md +30 -0
- package/.claude/skills/setup-multiplayer-services/references/underlying-services.md +11 -0
- package/.claude/skills/setup-multiplayer-services/references/workflows-prerequisites.md +17 -0
- package/.claude/skills/setup-vivox-voice-chat/SKILL.md +118 -0
- package/.claude/skills/setup-vivox-voice-chat/evals/.env.example +6 -0
- package/.claude/skills/setup-vivox-voice-chat/evals/README.md +101 -0
- package/.claude/skills/setup-vivox-voice-chat/evals/promptfooconfig.yaml +32 -0
- package/.claude/skills/setup-vivox-voice-chat/evals/tests/init-and-login.yaml +76 -0
- package/.claude/skills/setup-vivox-voice-chat/evals/tests/text-chat.yaml +65 -0
- package/.claude/skills/setup-vivox-voice-chat/evals/tests/voice-channels.yaml +74 -0
- package/.claude/skills/setup-vivox-voice-chat/references/events-and-participants.md +79 -0
- package/.claude/skills/setup-vivox-voice-chat/references/init-and-login.md +92 -0
- package/.claude/skills/setup-vivox-voice-chat/references/text-chat.md +93 -0
- package/.claude/skills/setup-vivox-voice-chat/references/troubleshooting.md +47 -0
- package/.claude/skills/setup-vivox-voice-chat/references/voice-channels.md +90 -0
- package/.claude/skills/shader-graph-create-custom-node/SKILL.md +25 -0
- package/.claude/skills/shader-graph-create-custom-node/resources/all_hints.hlsl +182 -0
- package/.claude/skills/sprint-plan/SKILL.md +14 -0
- package/.claude/skills/sprite-editor/SKILL.md +66 -0
- package/.claude/skills/sprite-editor/references/api_reference.md +151 -0
- package/.claude/skills/sprite-editor/references/background.md +112 -0
- package/.claude/skills/sprite-editor/references/templates.md +72 -0
- package/.claude/skills/sprite-editor/scripts/AutomaticSliceTexture.cs +40 -0
- package/.claude/skills/sprite-editor/scripts/GenerateNewSpriteRects.cs +200 -0
- package/.claude/skills/sprite-editor/scripts/GetTextureSourceImageSize.cs +32 -0
- package/.claude/skills/sprite-editor/scripts/GetTextureToSlice.cs +55 -0
- package/.claude/skills/sprite-editor/scripts/GridSliceTexture.cs +40 -0
- package/.claude/skills/sprite-editor/scripts/IsometricSliceTexture.cs +141 -0
- package/.claude/skills/sprite-editor/scripts/README.md +134 -0
- package/.claude/skills/sprite-editor/scripts/SetPivotExample.cs +58 -0
- package/.claude/skills/sprite-editor/scripts/SpriteToPng.cs +88 -0
- package/.claude/skills/status/SKILL.md +16 -0
- package/.claude/skills/story-readiness/SKILL.md +14 -0
- package/.claude/skills/test/SKILL.md +16 -0
- package/.claude/skills/ui/SKILL.md +142 -0
- package/.claude/skills/ui-imgui/SKILL.md +186 -0
- package/.claude/skills/ui-imgui/references/gui-elements.md +156 -0
- package/.claude/skills/ui-imgui/references/templates.md +141 -0
- package/.claude/skills/ui-review/SKILL.md +14 -0
- package/.claude/skills/ui-ugui/SKILL.md +282 -0
- package/.claude/skills/ui-ugui/references/scrollview-setup.md +29 -0
- package/.claude/skills/ui-uitk/SKILL.md +235 -0
- package/.claude/skills/ui-uitk/references/common-issues.md +74 -0
- package/.claude/skills/ui-uitk/references/custom-elements.md +241 -0
- package/.claude/skills/ui-uitk/references/painter2d.md +282 -0
- package/.claude/skills/ui-uitk/references/pointermanipulator-guide.md +94 -0
- package/.claude/skills/ui-uitk/references/svg-icons.md +136 -0
- package/.claude/skills/ui-uitk/references/ui-runtime-binding.md +234 -0
- package/.claude/skills/ui-uitk/references/uss-guide.md +138 -0
- package/.claude/skills/unity-cli/CHANGELOG.md +233 -0
- package/.claude/skills/unity-cli/SECURITY.md +22 -0
- package/.claude/skills/unity-cli/SKILL.md +414 -0
- package/.claude/skills/unity-cli/references/auth-license-cloud.md +146 -0
- package/.claude/skills/unity-cli/references/build-run-test.md +349 -0
- package/.claude/skills/unity-cli/references/collaboration.md +472 -0
- package/.claude/skills/unity-cli/references/config-hub.md +103 -0
- package/.claude/skills/unity-cli/references/diagnostics-maintenance.md +326 -0
- package/.claude/skills/unity-cli/references/editors-install.md +327 -0
- package/.claude/skills/unity-cli/references/integration-advanced.md +472 -0
- package/.claude/skills/unity-cli/references/projects-templates.md +574 -0
- package/.claude/skills/unity-package-management/SKILL.md +304 -0
- package/.claude/skills/unity-package-management/references/select-packages.md +108 -0
- package/.claude/skills/urp-postprocessing/SKILL.md +188 -0
- package/.claude/skills/urp-postprocessing/references/code-templates.md +119 -0
- package/.claude/skills/urp-postprocessing/references/effect-reference.md +86 -0
- package/.claude/skills/validate-urp-render-graph-renderer-feature/SKILL.md +269 -0
- package/.claude/skills/work/SKILL.md +36 -0
- package/.claude/skills/work/references/classification.md +41 -0
- package/.claude/skills/work/references/closure.md +50 -0
- package/.claude/skills/work/references/feature-contracts.md +32 -0
- package/.claude/skills/work/references/verification.md +26 -0
- package/CLAUDE.md +6 -0
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/THIRD_PARTY_NOTICES.md +23 -0
- package/UPGRADING.md +32 -0
- package/VERSION +1 -0
- package/docs/ARCHITECTURE.md +73 -0
- package/docs/HUONG-DAN-MIGRATE-VA-SU-DUNG.md +324 -0
- package/docs/MIGRATION-MATRIX.md +23 -0
- package/docs/PROJECT-POLICY.md +67 -0
- package/docs/WORKFLOWS.md +61 -0
- package/package-assets/setup-vivox-voice-chat-evals.gitignore +7 -0
- package/package.json +41 -0
- package/scaffold/.nccgs/bugs/.gitkeep +1 -0
- package/scaffold/.nccgs/closures/.gitkeep +1 -0
- package/scaffold/.nccgs/context/.gitkeep +1 -0
- package/scaffold/.nccgs/decisions/.gitkeep +1 -0
- package/scaffold/.nccgs/evidence/.gitkeep +1 -0
- package/scaffold/.nccgs/features/.gitkeep +1 -0
- package/scaffold/.nccgs/migrations/.gitkeep +1 -0
- package/scaffold/.nccgs/playtests/.gitkeep +1 -0
- package/scaffold/.nccgs/project.yaml +103 -0
- package/scaffold/.nccgs/requirements.yaml +10 -0
- package/scaffold/.nccgs/reviews/.gitkeep +1 -0
- package/scaffold/.nccgs/state.md +40 -0
- package/scaffold/.nccgs/templates/agent-handoff.md +25 -0
- package/scaffold/.nccgs/templates/architecture-decision.md +27 -0
- package/scaffold/.nccgs/templates/closure-record.md +51 -0
- package/scaffold/.nccgs/templates/context-packet.yaml +17 -0
- package/scaffold/.nccgs/templates/evidence-record.md +23 -0
- package/scaffold/.nccgs/templates/feature-contract.md +35 -0
- package/scaffold/.nccgs/templates/migration-plan.md +40 -0
- package/scaffold/.nccgs/templates/waiver.md +11 -0
- package/scripts/cli.mjs +56 -0
- package/scripts/install.mjs +267 -0
- package/scripts/sync-unity-skills.mjs +126 -0
- package/scripts/validate.mjs +205 -0
- package/tests/framework.test.mjs +121 -0
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
# Player Account Reference
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
|
|
5
|
+
- [Architecture Overview](#architecture-overview)
|
|
6
|
+
- [Access Classes](#access-classes)
|
|
7
|
+
- [Assembly References](#assembly-references)
|
|
8
|
+
- [Package Setup](#package-setup)
|
|
9
|
+
- [Sign-In: Anonymous](#sign-in-anonymous)
|
|
10
|
+
- [Sign-In: Unity (Browser)](#sign-in-unity-browser)
|
|
11
|
+
- [PlayerAccountService API](#playeraccountservice-api)
|
|
12
|
+
- [Sign-In: Username/Password](#sign-in-usernamepassword)
|
|
13
|
+
- [Username/Password Constraints](#usernamepassword-constraints)
|
|
14
|
+
- [Player Identity](#player-identity)
|
|
15
|
+
- [Cloud Save Operations](#cloud-save-operations)
|
|
16
|
+
- [Namespace Aliases](#namespace-aliases)
|
|
17
|
+
- [Save Default Data](#save-default-data-private-to-player)
|
|
18
|
+
- [Save Public Data](#save-public-data-visible-to-other-players)
|
|
19
|
+
- [Load Default Data](#load-default-data)
|
|
20
|
+
- [Load Public Data](#load-public-data-own)
|
|
21
|
+
- [Load Public Data (Other Player)](#load-public-data-another-player)
|
|
22
|
+
- [Load All Keys](#load-all-keys)
|
|
23
|
+
- [Server-Authoritative Writes via Cloud Code (Optional)](#server-authoritative-writes-via-cloud-code-optional)
|
|
24
|
+
- [Reading Protected Data](#reading-protected-data-client-side)
|
|
25
|
+
- [Writing Protected/Custom Data](#writing-protectedcustom-data-server-side)
|
|
26
|
+
- [Full Client Implementation](#full-client-implementation)
|
|
27
|
+
- [PlayerAccountTester](#playeraccounttester)
|
|
28
|
+
- [Assembly Definition](#assembly-definition)
|
|
29
|
+
- [Error Handling](#error-handling)
|
|
30
|
+
- [AuthenticationException](#authenticationexception)
|
|
31
|
+
- [PlayerAccountsException](#playeraccountsexception)
|
|
32
|
+
- [CloudSaveException](#cloudsaveexception)
|
|
33
|
+
- [Validation](#validation)
|
|
34
|
+
- [Asset Store Building Block](#asset-store-building-block)
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Architecture Overview
|
|
39
|
+
|
|
40
|
+
| Concern | Service | API |
|
|
41
|
+
|---|---|---|
|
|
42
|
+
| Sign in (anonymous) | Authentication | `SignInAnonymouslyAsync()` |
|
|
43
|
+
| Sign in (Unity browser) | PlayerAccounts then Authentication | `StartSignInAsync()` then `SignInWithUnityAsync(AccessToken)` |
|
|
44
|
+
| Sign in (username/password) | Authentication | `SignUpWithUsernamePasswordAsync` / `SignInWithUsernamePasswordAsync` |
|
|
45
|
+
| Player identity | Authentication | `PlayerId`, `PlayerName`, `UpdatePlayerNameAsync` |
|
|
46
|
+
| Per-player data | Cloud Save | `CloudSaveService.Instance.Data.Player` |
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
UnityServices.InitializeAsync()
|
|
50
|
+
|
|
|
51
|
+
v
|
|
52
|
+
AuthenticationService.Instance
|
|
53
|
+
|
|
|
54
|
+
+--------+-----------+
|
|
55
|
+
| | |
|
|
56
|
+
v v v
|
|
57
|
+
Anon Unity/PA Password
|
|
58
|
+
| (browser) |
|
|
59
|
+
| | |
|
|
60
|
+
| PlayerAccountService.Instance
|
|
61
|
+
| .StartSignInAsync()
|
|
62
|
+
| |
|
|
63
|
+
| SignedIn event fires
|
|
64
|
+
| |
|
|
65
|
+
| SignInWithUnityAsync(AccessToken)
|
|
66
|
+
| | |
|
|
67
|
+
+--------+-----------+
|
|
68
|
+
|
|
|
69
|
+
v
|
|
70
|
+
PlayerId / PlayerName
|
|
71
|
+
|
|
|
72
|
+
v
|
|
73
|
+
CloudSaveService.Instance.Data.Player
|
|
74
|
+
| | |
|
|
75
|
+
v v v
|
|
76
|
+
Default Public Protected
|
|
77
|
+
(owner (anyone (owner read,
|
|
78
|
+
read+write) read, server-only
|
|
79
|
+
owner write)
|
|
80
|
+
write)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Access Classes
|
|
86
|
+
|
|
87
|
+
Cloud Save player data supports three access classes. The access class used when saving determines which access class must be used when loading.
|
|
88
|
+
|
|
89
|
+
| Access Class | Read | Write | Use Case |
|
|
90
|
+
|---|---|---|---|
|
|
91
|
+
| Default | Owner only | Owner only | Private settings, preferences, game progress |
|
|
92
|
+
| Public | Any player | Owner only | Display names, public profiles, shared stats |
|
|
93
|
+
| Protected | Owner only | Server only (Cloud Code) | Anti-cheat data, server-awarded state |
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Assembly References
|
|
98
|
+
|
|
99
|
+
Your `.asmdef` must reference all four assemblies. `Unity.Services.Authentication.PlayerAccounts` is a **separate assembly** from `Unity.Services.Authentication`, even though both ship inside the `com.unity.services.authentication` package.
|
|
100
|
+
|
|
101
|
+
| Assembly Name | Package | Purpose |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| `Unity.Services.Core` | `com.unity.services.core` | `UnityServices.InitializeAsync()` |
|
|
104
|
+
| `Unity.Services.Authentication` | `com.unity.services.authentication` | `AuthenticationService.Instance`, sign-in methods, player identity |
|
|
105
|
+
| `Unity.Services.Authentication.PlayerAccounts` | `com.unity.services.authentication` (separate assembly in same package) | `PlayerAccountService.Instance`, browser-based Unity sign-in |
|
|
106
|
+
| `Unity.Services.CloudSave` | `com.unity.services.cloudsave` | `CloudSaveService.Instance.Data.Player`, save/load operations |
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Package Setup
|
|
111
|
+
|
|
112
|
+
Ensure the following packages are listed in `Packages/manifest.json`:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"dependencies": {
|
|
117
|
+
"com.unity.services.authentication": "3.6.1",
|
|
118
|
+
"com.unity.services.cloudsave": "3.4.0",
|
|
119
|
+
"com.unity.services.core": "1.16.0"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**Optional** — add `com.unity.services.cloudcode` if using Protected player data or Custom (shared) data:
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
"com.unity.services.cloudcode": "2.10.3"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Sign-In: Anonymous
|
|
133
|
+
|
|
134
|
+
```csharp
|
|
135
|
+
using Unity.Services.Core;
|
|
136
|
+
using Unity.Services.Authentication;
|
|
137
|
+
using UnityEngine;
|
|
138
|
+
|
|
139
|
+
async Task SignInAnonymouslyAsync()
|
|
140
|
+
{
|
|
141
|
+
await UnityServices.InitializeAsync();
|
|
142
|
+
|
|
143
|
+
if (!AuthenticationService.Instance.IsSignedIn)
|
|
144
|
+
{
|
|
145
|
+
await AuthenticationService.Instance.SignInAnonymouslyAsync();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
Debug.Log($"Signed in as {AuthenticationService.Instance.PlayerId}");
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`SignInAnonymouslyAsync` handles both new and returning players. If a session token is cached from a previous session, it signs in silently with the cached token. If no token exists, it creates a new anonymous account. Always check `IsSignedIn` before calling to avoid double sign-in errors.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Sign-In: Unity (Browser)
|
|
157
|
+
|
|
158
|
+
Uses `PlayerAccountService` to open a system browser for Unity account sign-in. After the user signs in via the browser, the `SignedIn` event fires and provides an `AccessToken` that is passed to `AuthenticationService.Instance.SignInWithUnityAsync`.
|
|
159
|
+
|
|
160
|
+
**Important:** The `Task` returned by `StartSignInAsync` completes when the browser **opens**, NOT when sign-in finishes. You must subscribe to the `SignedIn` event to know when authentication is complete.
|
|
161
|
+
|
|
162
|
+
```csharp
|
|
163
|
+
using Unity.Services.Core;
|
|
164
|
+
using Unity.Services.Authentication;
|
|
165
|
+
using Unity.Services.Authentication.PlayerAccounts;
|
|
166
|
+
using UnityEngine;
|
|
167
|
+
|
|
168
|
+
void Start()
|
|
169
|
+
{
|
|
170
|
+
// Wire the event BEFORE calling StartSignInAsync
|
|
171
|
+
PlayerAccountService.Instance.SignedIn += OnPlayerAccountSignedIn;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async Task StartSignInWithUnityAsync()
|
|
175
|
+
{
|
|
176
|
+
await UnityServices.InitializeAsync();
|
|
177
|
+
|
|
178
|
+
if (PlayerAccountService.Instance.IsSignedIn)
|
|
179
|
+
{
|
|
180
|
+
// Already signed in to Player Accounts -- go straight to Authentication
|
|
181
|
+
OnPlayerAccountSignedIn();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Opens the system browser. Task completes when browser opens, NOT when sign-in finishes.
|
|
186
|
+
await PlayerAccountService.Instance.StartSignInAsync();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async void OnPlayerAccountSignedIn()
|
|
190
|
+
{
|
|
191
|
+
try
|
|
192
|
+
{
|
|
193
|
+
await AuthenticationService.Instance.SignInWithUnityAsync(
|
|
194
|
+
PlayerAccountService.Instance.AccessToken);
|
|
195
|
+
Debug.Log($"Signed in as {AuthenticationService.Instance.PlayerId}");
|
|
196
|
+
}
|
|
197
|
+
catch (AuthenticationException ex)
|
|
198
|
+
{
|
|
199
|
+
Debug.LogError($"Unity sign-in failed: {ex.Message}");
|
|
200
|
+
}
|
|
201
|
+
catch (RequestFailedException ex)
|
|
202
|
+
{
|
|
203
|
+
Debug.LogError($"Request failed: {ex.Message}");
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Namespace: `Unity.Services.Authentication.PlayerAccounts`. Access via `PlayerAccountService.Instance` (`IPlayerAccountService`).
|
|
209
|
+
|
|
210
|
+
If using any authentication other than anonymous, it requires the Unity identity provider to be enabled in **Project Settings > Services > Authentication > Identity Providers**.
|
|
211
|
+
|
|
212
|
+
### PlayerAccountService API
|
|
213
|
+
|
|
214
|
+
| Member | Type | Description |
|
|
215
|
+
|---|---|---|
|
|
216
|
+
| `StartSignInAsync(bool isSigningUp = false)` | `Task` | Opens browser for sign-in (or sign-up if `true`). Task completes when browser opens. |
|
|
217
|
+
| `SignOut()` | `void` | Signs out of Player Accounts and revokes the access token. Synchronous. |
|
|
218
|
+
| `RefreshTokenAsync()` | `Task` | Refreshes the current access token using the refresh token. |
|
|
219
|
+
| `AccessToken` | `string` | Access token obtained during sign-in. Pass this to `SignInWithUnityAsync`. |
|
|
220
|
+
| `IdToken` | `string` | ID token obtained during sign-in. |
|
|
221
|
+
| `IdTokenClaims` | `IdToken` | Parsed claims from the ID token (email, subject, etc.). |
|
|
222
|
+
| `IsSignedIn` | `bool` | Whether the player is signed in to Player Accounts. |
|
|
223
|
+
| `AccountPortalUrl` | `string` | URL to the Unity Player Account portal. |
|
|
224
|
+
| `SignedIn` | `event Action` | Fires when browser sign-in completes successfully. |
|
|
225
|
+
| `SignedOut` | `event Action` | Fires when the player signs out. |
|
|
226
|
+
| `SignInFailed` | `event Action<RequestFailedException>` | Fires when sign-in fails. |
|
|
227
|
+
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Sign-In: Username/Password
|
|
231
|
+
|
|
232
|
+
Two separate methods: `SignUpWithUsernamePasswordAsync` for first-time registration, `SignInWithUsernamePasswordAsync` for subsequent sign-ins.
|
|
233
|
+
|
|
234
|
+
```csharp
|
|
235
|
+
using Unity.Services.Core;
|
|
236
|
+
using Unity.Services.Authentication;
|
|
237
|
+
using UnityEngine;
|
|
238
|
+
|
|
239
|
+
async Task SignUpWithPasswordAsync(string username, string password)
|
|
240
|
+
{
|
|
241
|
+
await UnityServices.InitializeAsync();
|
|
242
|
+
|
|
243
|
+
try
|
|
244
|
+
{
|
|
245
|
+
await AuthenticationService.Instance.SignUpWithUsernamePasswordAsync(username, password);
|
|
246
|
+
Debug.Log($"Signed up and signed in as {AuthenticationService.Instance.PlayerId}");
|
|
247
|
+
}
|
|
248
|
+
catch (AuthenticationException ex) when (ex.ErrorCode == AuthenticationErrorCodes.AccountAlreadyLinked)
|
|
249
|
+
{
|
|
250
|
+
// Error code 10003: username already exists. Sign in instead.
|
|
251
|
+
Debug.LogWarning("Username already exists, signing in instead.");
|
|
252
|
+
await AuthenticationService.Instance.SignInWithUsernamePasswordAsync(username, password);
|
|
253
|
+
}
|
|
254
|
+
catch (AuthenticationException ex)
|
|
255
|
+
{
|
|
256
|
+
Debug.LogError($"Sign-up failed: {ex.Message} (code: {ex.ErrorCode})");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async Task SignInWithPasswordAsync(string username, string password)
|
|
261
|
+
{
|
|
262
|
+
await UnityServices.InitializeAsync();
|
|
263
|
+
|
|
264
|
+
try
|
|
265
|
+
{
|
|
266
|
+
await AuthenticationService.Instance.SignInWithUsernamePasswordAsync(username, password);
|
|
267
|
+
Debug.Log($"Signed in as {AuthenticationService.Instance.PlayerId}");
|
|
268
|
+
}
|
|
269
|
+
catch (AuthenticationException ex)
|
|
270
|
+
{
|
|
271
|
+
Debug.LogError($"Sign-in failed: {ex.Message} (code: {ex.ErrorCode})");
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Requires the username/password identity provider in **Project Settings > Services > Authentication > Identity Providers**.
|
|
277
|
+
|
|
278
|
+
### Username/Password Constraints
|
|
279
|
+
|
|
280
|
+
| Field | Constraint |
|
|
281
|
+
|---|---|
|
|
282
|
+
| Username length | 3-20 characters |
|
|
283
|
+
| Username characters | `A-Z`, `a-z`, `0-9`, `.`, `-`, `@`, `_` |
|
|
284
|
+
| Username case | Case insensitive (stored lowercase) |
|
|
285
|
+
| Password length | 8-30 characters |
|
|
286
|
+
| Password requirements | At least 1 lowercase + 1 uppercase + 1 number + 1 symbol |
|
|
287
|
+
|
|
288
|
+
---
|
|
289
|
+
|
|
290
|
+
## Player Identity
|
|
291
|
+
|
|
292
|
+
After any successful sign-in, the following properties and methods are available on `AuthenticationService.Instance`:
|
|
293
|
+
|
|
294
|
+
| Member | Type | Description |
|
|
295
|
+
|---|---|---|
|
|
296
|
+
| `PlayerId` | `string` | Unique player identifier, available immediately after sign-in |
|
|
297
|
+
| `PlayerName` | `string` | Player display name (may be null until fetched) |
|
|
298
|
+
| `IsSignedIn` | `bool` | True if a token exists in memory |
|
|
299
|
+
| `IsAnonymous` | `bool` | True if signed in anonymously |
|
|
300
|
+
| `SignedIn` | `event Action` | Fires on successful sign-in |
|
|
301
|
+
| `SignedOut` | `event Action` | Fires on sign-out |
|
|
302
|
+
| `Expired` | `event Action` | Fires when access token expires; SDK auto-attempts refresh |
|
|
303
|
+
|
|
304
|
+
```csharp
|
|
305
|
+
// Get player name. autoGenerate: true creates a random name if none exists.
|
|
306
|
+
string name = await AuthenticationService.Instance.GetPlayerNameAsync(autoGenerate: true);
|
|
307
|
+
|
|
308
|
+
// Update player name.
|
|
309
|
+
string updatedName = await AuthenticationService.Instance.UpdatePlayerNameAsync("NewDisplayName");
|
|
310
|
+
|
|
311
|
+
// Sign out. Synchronous (void), NOT async.
|
|
312
|
+
AuthenticationService.Instance.SignOut();
|
|
313
|
+
// Optionally clear cached credentials:
|
|
314
|
+
// AuthenticationService.Instance.SignOut(clearCredentials: true);
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
**Warning:** `SignOut` is synchronous (`void`). There is no `SignOutAsync` method.
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Cloud Save Operations
|
|
322
|
+
|
|
323
|
+
### Namespace Aliases
|
|
324
|
+
|
|
325
|
+
Use namespace aliases to avoid ambiguity between root-level and `Models.Data.Player` option classes:
|
|
326
|
+
|
|
327
|
+
```csharp
|
|
328
|
+
using Unity.Services.CloudSave;
|
|
329
|
+
using Unity.Services.CloudSave.Models;
|
|
330
|
+
using Unity.Services.CloudSave.Models.Data.Player;
|
|
331
|
+
using PlayerLoadOptions = Unity.Services.CloudSave.Models.Data.Player.LoadOptions;
|
|
332
|
+
using PlayerLoadAllOptions = Unity.Services.CloudSave.Models.Data.Player.LoadAllOptions;
|
|
333
|
+
using PlayerSaveOptions = Unity.Services.CloudSave.Models.Data.Player.SaveOptions;
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
> **Namespace collision:** `SaveOptions` and `LoadOptions` exist in both `Unity.Services.CloudSave`
|
|
337
|
+
> and `Unity.Services.CloudSave.Models.Data.Player`. Always use the `using` aliases to avoid
|
|
338
|
+
> ambiguity errors.
|
|
339
|
+
|
|
340
|
+
### Save Default Data (Private to Player)
|
|
341
|
+
|
|
342
|
+
```csharp
|
|
343
|
+
var data = new Dictionary<string, object>
|
|
344
|
+
{
|
|
345
|
+
{ "settings_volume", 0.8f },
|
|
346
|
+
{ "settings_difficulty", "hard" },
|
|
347
|
+
{ "last_login", DateTime.UtcNow.ToString("o") }
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
await CloudSaveService.Instance.Data.Player.SaveAsync(data);
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Save Public Data (Visible to Other Players)
|
|
354
|
+
|
|
355
|
+
```csharp
|
|
356
|
+
var publicData = new Dictionary<string, object>
|
|
357
|
+
{
|
|
358
|
+
{ "display_name", "Hero123" },
|
|
359
|
+
{ "avatar_id", 42 }
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
var publicSaveOptions = new PlayerSaveOptions(new PublicWriteAccessClassOptions());
|
|
363
|
+
await CloudSaveService.Instance.Data.Player.SaveAsync(publicData, publicSaveOptions);
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Load Default Data
|
|
367
|
+
|
|
368
|
+
```csharp
|
|
369
|
+
var keys = new HashSet<string> { "settings_volume", "settings_difficulty" };
|
|
370
|
+
var result = await CloudSaveService.Instance.Data.Player.LoadAsync(keys);
|
|
371
|
+
|
|
372
|
+
if (result.TryGetValue("settings_volume", out var volumeItem))
|
|
373
|
+
{
|
|
374
|
+
float volume = volumeItem.Value.GetAs<float>();
|
|
375
|
+
}
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
For string values saved via `Dictionary<string, object>`, `GetAsString()` returns the plain string. For complex objects, `GetAsString()` returns the JSON; use `item.Value.GetAs<T>()` to deserialize.
|
|
379
|
+
|
|
380
|
+
### Load Public Data (Own)
|
|
381
|
+
|
|
382
|
+
```csharp
|
|
383
|
+
var publicLoadOptions = new PlayerLoadOptions(new PublicReadAccessClassOptions());
|
|
384
|
+
var publicResult = await CloudSaveService.Instance.Data.Player.LoadAsync(
|
|
385
|
+
new HashSet<string> { "display_name", "avatar_id" }, publicLoadOptions);
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Load Public Data (Another Player)
|
|
389
|
+
|
|
390
|
+
```csharp
|
|
391
|
+
var otherPlayerOptions = new PlayerLoadOptions(new PublicReadAccessClassOptions(otherPlayerId));
|
|
392
|
+
var otherResult = await CloudSaveService.Instance.Data.Player.LoadAsync(
|
|
393
|
+
new HashSet<string> { "display_name", "avatar_id" }, otherPlayerOptions);
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### Load All Keys
|
|
397
|
+
|
|
398
|
+
```csharp
|
|
399
|
+
using Unity.Services.CloudSave.Models.Data.Player;
|
|
400
|
+
using PlayerLoadAllOptions = Unity.Services.CloudSave.Models.Data.Player.LoadAllOptions;
|
|
401
|
+
|
|
402
|
+
var allItems = await CloudSaveService.Instance.Data.Player.LoadAllAsync(
|
|
403
|
+
new PlayerLoadAllOptions(new DefaultReadAccessClassOptions()));
|
|
404
|
+
|
|
405
|
+
foreach (var kv in allItems)
|
|
406
|
+
Debug.Log($"{kv.Key}: {kv.Value.Value.GetAsString()}");
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
**Access class matching rule:** Data saved with `PublicWriteAccessClassOptions` must be loaded with `PublicReadAccessClassOptions`. Data saved with no options (Default) must be loaded with `DefaultReadAccessClassOptions` or no options. Mismatched access classes return empty results without error.
|
|
410
|
+
|
|
411
|
+
---
|
|
412
|
+
|
|
413
|
+
## Server-Authoritative Writes via Cloud Code (Optional)
|
|
414
|
+
|
|
415
|
+
The Default and Public access classes above are client-writable — suitable for preferences, display names, and non-sensitive data. Two scenarios require routing writes through a Cloud Code module instead:
|
|
416
|
+
|
|
417
|
+
- **Non-player shared data** (guild info, level configs, global state) — no single player owns this data, so it must be written via Cloud Code to the **Custom** bucket using `IGameApiClient`.
|
|
418
|
+
- **Tamper-proof player data** — if the game design requires anti-cheat protection for specific player state (e.g. XP, currency, unlocked items), write to the **Protected** bucket via Cloud Code. This is a game-design decision, not a requirement.
|
|
419
|
+
|
|
420
|
+
### Reading Protected Data (Client-Side)
|
|
421
|
+
|
|
422
|
+
```csharp
|
|
423
|
+
var result = await CloudSaveService.Instance.Data.Player.LoadAsync(
|
|
424
|
+
new HashSet<string> { "player_level", "currency" },
|
|
425
|
+
new PlayerLoadOptions(new ProtectedReadAccessClassOptions()));
|
|
426
|
+
|
|
427
|
+
if (result.TryGetValue("player_level", out var item))
|
|
428
|
+
{
|
|
429
|
+
int level = item.Value.GetAs<int>();
|
|
430
|
+
}
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
> **Common mistake:** Using default `LoadOptions` instead of `ProtectedReadAccessClassOptions`
|
|
434
|
+
> returns no data, because the keys were written to the Protected bucket by Cloud Code.
|
|
435
|
+
|
|
436
|
+
### Writing Protected/Custom Data (Server-Side)
|
|
437
|
+
|
|
438
|
+
Writes to Protected and Custom buckets must go through a Cloud Code module. The module uses `IGameApiClient` with the service token to call Cloud Save server-side APIs.
|
|
439
|
+
|
|
440
|
+
To create the module, follow [cloud-code.md — Module Creation](cloud-code.md#module-creation). All scaffolding files are required:
|
|
441
|
+
|
|
442
|
+
- [ ] `.sln` (generate fresh GUIDs)
|
|
443
|
+
- [ ] `.csproj` (net9.0, CloudCode.Apis + CloudCode.Core)
|
|
444
|
+
- [ ] `ModuleSetup.cs` (registers `GameApiClient`)
|
|
445
|
+
- [ ] `Properties/PublishProfiles/FolderProfile.pubxml` — **without this file, deployment fails with "Failed to retrieve main project"**
|
|
446
|
+
- [ ] `Assets/CloudCode/<ModuleName>.ccmr` (points to the `.sln`)
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
## Full Client Implementation
|
|
451
|
+
|
|
452
|
+
A complete `MonoBehaviour` covering all three sign-in methods, player identity, and Cloud Save operations.
|
|
453
|
+
|
|
454
|
+
```csharp
|
|
455
|
+
using System;
|
|
456
|
+
using System.Collections.Generic;
|
|
457
|
+
using System.Threading.Tasks;
|
|
458
|
+
using Unity.Services.Authentication;
|
|
459
|
+
using Unity.Services.Authentication.PlayerAccounts;
|
|
460
|
+
using Unity.Services.CloudSave;
|
|
461
|
+
using Unity.Services.CloudSave.Models.Data.Player;
|
|
462
|
+
using Unity.Services.Core;
|
|
463
|
+
using UnityEngine;
|
|
464
|
+
using PlayerLoadOptions = Unity.Services.CloudSave.Models.Data.Player.LoadOptions;
|
|
465
|
+
using PlayerLoadAllOptions = Unity.Services.CloudSave.Models.Data.Player.LoadAllOptions;
|
|
466
|
+
using PlayerSaveOptions = Unity.Services.CloudSave.Models.Data.Player.SaveOptions;
|
|
467
|
+
|
|
468
|
+
public class PlayerAccountManager : MonoBehaviour
|
|
469
|
+
{
|
|
470
|
+
public string PlayerId => AuthenticationService.Instance.PlayerId;
|
|
471
|
+
public string PlayerName => AuthenticationService.Instance.PlayerName;
|
|
472
|
+
public bool IsSignedIn => AuthenticationService.Instance.IsSignedIn;
|
|
473
|
+
|
|
474
|
+
public event Action SignedIn;
|
|
475
|
+
|
|
476
|
+
async void Start()
|
|
477
|
+
{
|
|
478
|
+
await UnityServices.InitializeAsync();
|
|
479
|
+
|
|
480
|
+
AuthenticationService.Instance.SignedIn += () =>
|
|
481
|
+
{
|
|
482
|
+
Debug.Log($"Signed in: {AuthenticationService.Instance.PlayerId}");
|
|
483
|
+
SignedIn?.Invoke();
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
// Wire PlayerAccountService event BEFORE any StartSignInAsync call
|
|
487
|
+
PlayerAccountService.Instance.SignedIn += OnPlayerAccountSignedIn;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// --- Sign-In Methods ---
|
|
491
|
+
|
|
492
|
+
public async Task SignInAnonymouslyAsync()
|
|
493
|
+
{
|
|
494
|
+
if (AuthenticationService.Instance.IsSignedIn) return;
|
|
495
|
+
await AuthenticationService.Instance.SignInAnonymouslyAsync();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/// <summary>
|
|
499
|
+
/// Starts the browser-based Unity sign-in flow.
|
|
500
|
+
/// The returned Task completes when the browser opens, NOT when sign-in finishes.
|
|
501
|
+
/// Subscribe to SignedIn to know when authentication is complete.
|
|
502
|
+
/// </summary>
|
|
503
|
+
public async Task StartSignInWithUnity()
|
|
504
|
+
{
|
|
505
|
+
if (PlayerAccountService.Instance.IsSignedIn)
|
|
506
|
+
{
|
|
507
|
+
OnPlayerAccountSignedIn();
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
await PlayerAccountService.Instance.StartSignInAsync();
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async void OnPlayerAccountSignedIn()
|
|
515
|
+
{
|
|
516
|
+
try
|
|
517
|
+
{
|
|
518
|
+
await AuthenticationService.Instance.SignInWithUnityAsync(
|
|
519
|
+
PlayerAccountService.Instance.AccessToken);
|
|
520
|
+
}
|
|
521
|
+
catch (AuthenticationException ex)
|
|
522
|
+
{
|
|
523
|
+
Debug.LogError($"Unity sign-in failed: {ex.Message}");
|
|
524
|
+
}
|
|
525
|
+
catch (RequestFailedException ex)
|
|
526
|
+
{
|
|
527
|
+
Debug.LogError($"Request failed: {ex.Message}");
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
public async Task SignUpWithPasswordAsync(string username, string password)
|
|
532
|
+
{
|
|
533
|
+
try
|
|
534
|
+
{
|
|
535
|
+
await AuthenticationService.Instance.SignUpWithUsernamePasswordAsync(username, password);
|
|
536
|
+
}
|
|
537
|
+
catch (AuthenticationException ex) when (ex.ErrorCode == AuthenticationErrorCodes.AccountAlreadyLinked)
|
|
538
|
+
{
|
|
539
|
+
// Error code 10003: username already exists
|
|
540
|
+
Debug.LogWarning("Username already exists. Use SignInWithPasswordAsync instead.");
|
|
541
|
+
throw;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
public async Task SignInWithPasswordAsync(string username, string password)
|
|
546
|
+
{
|
|
547
|
+
await AuthenticationService.Instance.SignInWithUsernamePasswordAsync(username, password);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
public void SignOut()
|
|
551
|
+
{
|
|
552
|
+
AuthenticationService.Instance.SignOut();
|
|
553
|
+
// Optionally also sign out of Player Accounts:
|
|
554
|
+
// PlayerAccountService.Instance.SignOut();
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// --- Player Identity ---
|
|
558
|
+
|
|
559
|
+
public async Task<string> GetPlayerNameAsync()
|
|
560
|
+
{
|
|
561
|
+
return await AuthenticationService.Instance.GetPlayerNameAsync(autoGenerate: true);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
public async Task<string> UpdatePlayerNameAsync(string newName)
|
|
565
|
+
{
|
|
566
|
+
return await AuthenticationService.Instance.UpdatePlayerNameAsync(newName);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// --- Cloud Save: Default (Private) ---
|
|
570
|
+
|
|
571
|
+
public async Task SaveDefaultAsync(string key, object value)
|
|
572
|
+
{
|
|
573
|
+
await CloudSaveService.Instance.Data.Player.SaveAsync(
|
|
574
|
+
new Dictionary<string, object> { { key, value } });
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
public async Task<string> LoadDefaultAsync(string key)
|
|
578
|
+
{
|
|
579
|
+
var result = await CloudSaveService.Instance.Data.Player.LoadAsync(
|
|
580
|
+
new HashSet<string> { key });
|
|
581
|
+
return result.TryGetValue(key, out var item) ? item.Value.GetAsString() : null;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// --- Cloud Save: Public ---
|
|
585
|
+
|
|
586
|
+
public async Task SavePublicAsync(string key, object value)
|
|
587
|
+
{
|
|
588
|
+
var options = new PlayerSaveOptions(new PublicWriteAccessClassOptions());
|
|
589
|
+
await CloudSaveService.Instance.Data.Player.SaveAsync(
|
|
590
|
+
new Dictionary<string, object> { { key, value } }, options);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
public async Task<string> LoadPublicAsync(string key)
|
|
594
|
+
{
|
|
595
|
+
var options = new PlayerLoadOptions(new PublicReadAccessClassOptions());
|
|
596
|
+
var result = await CloudSaveService.Instance.Data.Player.LoadAsync(
|
|
597
|
+
new HashSet<string> { key }, options);
|
|
598
|
+
return result.TryGetValue(key, out var item) ? item.Value.GetAsString() : null;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// --- Cloud Save: All Keys ---
|
|
602
|
+
|
|
603
|
+
public async Task<List<string>> LoadAllDefaultKeysAsync()
|
|
604
|
+
{
|
|
605
|
+
var result = await CloudSaveService.Instance.Data.Player.LoadAllAsync(
|
|
606
|
+
new PlayerLoadAllOptions(new DefaultReadAccessClassOptions()));
|
|
607
|
+
return new List<string>(result.Keys);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
614
|
+
## PlayerAccountTester
|
|
615
|
+
|
|
616
|
+
Attach alongside `PlayerAccountManager` on the same GameObject. Signs in anonymously, logs identity, then exercises a Cloud Save round-trip and lists all Default keys.
|
|
617
|
+
|
|
618
|
+
```csharp
|
|
619
|
+
using System.Collections.Generic;
|
|
620
|
+
using System.Text;
|
|
621
|
+
using System.Threading.Tasks;
|
|
622
|
+
using UnityEngine;
|
|
623
|
+
|
|
624
|
+
/// <summary>
|
|
625
|
+
/// Attach alongside PlayerAccountManager to run a quick in-Editor smoke test.
|
|
626
|
+
/// On start: signs in anonymously and logs player identity.
|
|
627
|
+
/// After sign-in: saves a probe value to the Default bucket, loads it back,
|
|
628
|
+
/// and logs all Default keys visible for this player.
|
|
629
|
+
/// </summary>
|
|
630
|
+
public class PlayerAccountTester : MonoBehaviour
|
|
631
|
+
{
|
|
632
|
+
[SerializeField] PlayerAccountManager m_Manager;
|
|
633
|
+
|
|
634
|
+
async void Start()
|
|
635
|
+
{
|
|
636
|
+
if (m_Manager == null)
|
|
637
|
+
m_Manager = GetComponent<PlayerAccountManager>();
|
|
638
|
+
|
|
639
|
+
m_Manager.SignedIn += OnSignedIn;
|
|
640
|
+
await m_Manager.SignInAnonymouslyAsync();
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
async void OnSignedIn()
|
|
644
|
+
{
|
|
645
|
+
LogIdentity("Signed in");
|
|
646
|
+
|
|
647
|
+
await Task.Delay(2000);
|
|
648
|
+
|
|
649
|
+
// Fetch the server-side player name — may be null after anonymous sign-in.
|
|
650
|
+
await m_Manager.GetPlayerNameAsync();
|
|
651
|
+
LogIdentity("After name fetch");
|
|
652
|
+
|
|
653
|
+
// Round-trip a probe value through the Default Cloud Save bucket.
|
|
654
|
+
const string key = "tester_probe";
|
|
655
|
+
const string expected = "ok";
|
|
656
|
+
await m_Manager.SaveDefaultAsync(key, expected);
|
|
657
|
+
var loaded = await m_Manager.LoadDefaultAsync(key);
|
|
658
|
+
|
|
659
|
+
var match = loaded == expected ? "OK" : $"MISMATCH (got '{loaded}')";
|
|
660
|
+
Debug.Log($"[PlayerAccountTester] Save→Load round-trip: {match}");
|
|
661
|
+
|
|
662
|
+
// List all keys in the Default bucket for this player.
|
|
663
|
+
var keys = await m_Manager.LoadAllDefaultKeysAsync();
|
|
664
|
+
LogKeys(keys);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
void LogIdentity(string label)
|
|
668
|
+
{
|
|
669
|
+
var sb = new StringBuilder();
|
|
670
|
+
sb.AppendLine($"[PlayerAccountTester] {label}:");
|
|
671
|
+
sb.AppendLine($" PlayerId : {m_Manager.PlayerId ?? "<null>"}");
|
|
672
|
+
sb.AppendLine($" PlayerName : {(string.IsNullOrEmpty(m_Manager.PlayerName) ? "<none>" : m_Manager.PlayerName)}");
|
|
673
|
+
sb.AppendLine($" IsSignedIn : {m_Manager.IsSignedIn}");
|
|
674
|
+
Debug.Log(sb.ToString());
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
void LogKeys(List<string> keys)
|
|
678
|
+
{
|
|
679
|
+
var sb = new StringBuilder();
|
|
680
|
+
sb.AppendLine($"[PlayerAccountTester] Default bucket keys ({keys.Count}):");
|
|
681
|
+
foreach (var k in keys)
|
|
682
|
+
sb.AppendLine($" {k}");
|
|
683
|
+
Debug.Log(sb.ToString());
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
---
|
|
689
|
+
|
|
690
|
+
## Assembly Definition
|
|
691
|
+
|
|
692
|
+
Your `.asmdef` must reference both `Unity.Services.Authentication` and `Unity.Services.Authentication.PlayerAccounts`. These are **separate assemblies** within the same package (`com.unity.services.authentication`). Missing the PlayerAccounts reference causes `PlayerAccountService` to be unresolvable. `Unity.Services.CloudCode` is only needed if using Protected or Custom data writes.
|
|
693
|
+
|
|
694
|
+
```json
|
|
695
|
+
{
|
|
696
|
+
"name": "MyGame.PlayerAccount",
|
|
697
|
+
"rootNamespace": "",
|
|
698
|
+
"references": [
|
|
699
|
+
"Unity.Services.Core",
|
|
700
|
+
"Unity.Services.Authentication",
|
|
701
|
+
"Unity.Services.Authentication.PlayerAccounts",
|
|
702
|
+
"Unity.Services.CloudSave",
|
|
703
|
+
"Unity.Services.CloudCode"
|
|
704
|
+
],
|
|
705
|
+
"includePlatforms": [],
|
|
706
|
+
"excludePlatforms": [],
|
|
707
|
+
"allowUnsafeCode": false,
|
|
708
|
+
"overrideReferences": false,
|
|
709
|
+
"precompiledReferences": [],
|
|
710
|
+
"autoReferenced": true,
|
|
711
|
+
"defineConstraints": [],
|
|
712
|
+
"versionDefines": [],
|
|
713
|
+
"noEngineReferences": false
|
|
714
|
+
}
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
---
|
|
718
|
+
|
|
719
|
+
## Error Handling
|
|
720
|
+
|
|
721
|
+
### AuthenticationException
|
|
722
|
+
|
|
723
|
+
Thrown by all `AuthenticationService` methods. Extends `RequestFailedException`.
|
|
724
|
+
|
|
725
|
+
```csharp
|
|
726
|
+
try
|
|
727
|
+
{
|
|
728
|
+
await AuthenticationService.Instance.SignInAnonymouslyAsync();
|
|
729
|
+
}
|
|
730
|
+
catch (AuthenticationException ex)
|
|
731
|
+
{
|
|
732
|
+
// ex.ErrorCode contains a numeric error code from AuthenticationErrorCodes
|
|
733
|
+
Debug.LogError($"Auth failed: {ex.Message} (code: {ex.ErrorCode})");
|
|
734
|
+
}
|
|
735
|
+
catch (RequestFailedException ex)
|
|
736
|
+
{
|
|
737
|
+
// Network or other service errors
|
|
738
|
+
Debug.LogError($"Request failed: {ex.Message}");
|
|
739
|
+
}
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
Key error codes:
|
|
743
|
+
|
|
744
|
+
| Constant | Value | Meaning |
|
|
745
|
+
|---|---|---|
|
|
746
|
+
| `AccountAlreadyLinked` | 10003 | Username already exists (on sign-up) or external ID already linked |
|
|
747
|
+
| `InvalidParameters` | 10002 | Bad input parameters |
|
|
748
|
+
| `ClientInvalidUserState` | 10000 | Operation not valid in current state |
|
|
749
|
+
|
|
750
|
+
### PlayerAccountsException
|
|
751
|
+
|
|
752
|
+
Thrown by `PlayerAccountService` methods. Also extends `RequestFailedException`.
|
|
753
|
+
|
|
754
|
+
```csharp
|
|
755
|
+
try
|
|
756
|
+
{
|
|
757
|
+
await PlayerAccountService.Instance.StartSignInAsync();
|
|
758
|
+
}
|
|
759
|
+
catch (PlayerAccountsException ex)
|
|
760
|
+
{
|
|
761
|
+
Debug.LogError($"Player Accounts error: {ex.Message} (code: {ex.ErrorCode})");
|
|
762
|
+
}
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
Key error codes (from `PlayerAccountsErrorCodes`):
|
|
766
|
+
|
|
767
|
+
| Constant | Value | Meaning |
|
|
768
|
+
|---|---|---|
|
|
769
|
+
| `InvalidState` | 10101 | Player is already signed in |
|
|
770
|
+
| `MissingClientId` | 10102 | Client ID not configured in Unity Dashboard |
|
|
771
|
+
|
|
772
|
+
### CloudSaveException
|
|
773
|
+
|
|
774
|
+
Thrown by Cloud Save operations.
|
|
775
|
+
|
|
776
|
+
```csharp
|
|
777
|
+
try
|
|
778
|
+
{
|
|
779
|
+
await CloudSaveService.Instance.Data.Player.SaveAsync(data);
|
|
780
|
+
}
|
|
781
|
+
catch (CloudSaveValidationException ex)
|
|
782
|
+
{
|
|
783
|
+
foreach (var detail in ex.Details)
|
|
784
|
+
Debug.LogError($"Validation: {detail.Field} {string.Join(", ", detail.Messages)}");
|
|
785
|
+
}
|
|
786
|
+
catch (CloudSaveRateLimitedException ex)
|
|
787
|
+
{
|
|
788
|
+
Debug.LogError($"Rate limited. Retry after {ex.RetryAfter}s");
|
|
789
|
+
}
|
|
790
|
+
catch (CloudSaveException ex)
|
|
791
|
+
{
|
|
792
|
+
Debug.LogError($"Cloud Save error: {ex.Message} (reason: {ex.Reason})");
|
|
793
|
+
}
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
---
|
|
797
|
+
|
|
798
|
+
## Validation
|
|
799
|
+
|
|
800
|
+
After implementing a player account feature, verify:
|
|
801
|
+
|
|
802
|
+
1. **Compile check:** The project compiles without errors. All namespaces (`Unity.Services.Core`, `Unity.Services.Authentication`, `Unity.Services.Authentication.PlayerAccounts`, `Unity.Services.CloudSave`) resolve correctly.
|
|
803
|
+
2. **Initialization order:** `UnityServices.InitializeAsync()` is called before any sign-in method. Service singletons (`AuthenticationService.Instance`, `PlayerAccountService.Instance`, `CloudSaveService.Instance`) are accessed only after initialization.
|
|
804
|
+
3. **Assembly references:** The `.asmdef` references **both** `Unity.Services.Authentication` and `Unity.Services.Authentication.PlayerAccounts`. These are separate assemblies in the same package; omitting the PlayerAccounts reference makes `PlayerAccountService` unresolvable.
|
|
805
|
+
4. **SignedIn event wiring:** `PlayerAccountService.Instance.SignedIn` is subscribed **before** calling `StartSignInAsync()`. The `StartSignInAsync` Task completes when the browser opens, not when sign-in finishes.
|
|
806
|
+
5. **Access class matching:** Data saved with `PublicWriteAccessClassOptions` is loaded with `PublicReadAccessClassOptions`. Data saved with no options (Default) is loaded with `DefaultReadAccessClassOptions` or no options. Mismatched access classes return empty results without error.
|
|
807
|
+
6. **SignOut is synchronous:** `AuthenticationService.Instance.SignOut()` and `PlayerAccountService.Instance.SignOut()` are both `void` methods. Do not `await` them.
|
|
808
|
+
|
|
809
|
+
---
|
|
810
|
+
|
|
811
|
+
## Asset Store Building Block
|
|
812
|
+
|
|
813
|
+
- **Player Account Building Block:** Complete reference implementation with sign-in UI (anonymous, Unity browser, username/password), Cloud Save data management, and a Cloud Code module with generated client bindings. [Asset Store](https://assetstore.unity.com/packages/essentials/tutorial-projects/unity-building-block-player-account-341928)
|