Skip to content

feat(unity): bind scene actors to profiles#14

Merged
JOY (JOY) merged 3 commits into
devfrom
feat/actor-profile-binding
May 17, 2026
Merged

feat(unity): bind scene actors to profiles#14
JOY (JOY) merged 3 commits into
devfrom
feat/actor-profile-binding

Conversation

@JOY
Copy link
Copy Markdown
Contributor

Summary

  • Add ActorProfileBinder so a Unity scene NPC-like body can load or create its own Nakama actor profile.
  • Seed actor-local memory through secondspawn_actor_memory_add without mixing it into the player profile.
  • Expose CurrentProfile, IsReady, and ProfileLoaded for prototype NPC/interaction code to consume later.

Why

SECOND SPAWN needs many NPC-like bodies and player-inhabited bodies where each actor can own separate stats, traits, soul, memory, runtime, and activity state. This component gives scene objects a small bridge to the actor profile registry from PR #12.

Verification

pm install in �ackend/nakama for this fresh worktree so the pinned local TypeScript version is used.

pm run build in �ackend/nakama passed.

pm test in �ackend/nakama passed.

  • go test -count=1 ./... in �ackend/gateway passed.
  • go vet ./... in �ackend/gateway passed.
  • Markdownlint passed with the repo CI scope.
  • Changed-file em-dash/en-dash scan passed.
  • git diff --check passed.

Unity Caveat

This PR adds C# only. Clean-worktree Unity batchmode compile is still blocked in this repo family by the existing Package Manager path undefined issue, so Editor/MCP compile verification is still required before a no-caveat merge claim.

Gemini (@gemini-code-assist) review
Codex (@codex) review

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@JOY
Copy link
Copy Markdown
Contributor Author

Local code-review skill pass

Target: PR #14 eat(unity): bind scene actors to profiles

Engine Specialist Findings: N/A - no engine configured

.Codex/docs/technical-preferences.md is not present in this worktree, so engine-specialist routing is skipped per the local code-review skill.

Testability: TESTABLE WITH UNITY CAVEAT

  • The component is a thin Unity bridge over already-tested Nakama actor-profile RPCs from PR feat(nakama): add actor profile registry #12.
  • Observable hooks are exposed through CurrentProfile, IsReady, IsLoading, ActorId, and ProfileLoaded.
  • Backend regression tests for actor profile creation, independent actor memory, zero-value preservation, normalized retrieval, and actor ID length limits remain in the base PR stack.
  • Unity compile/play-mode verification is still blocked in clean worktrees by the existing Package Manager path undefined issue, so Editor/MCP verification remains a caveat.

ADR Compliance: NO ADRS FOUND

No direct ADR reference was found in the implementation commit or file header. The implementation follows ADR 0010 direction by keeping actor state in Nakama and adding no separate game API gateway or client-authoritative state mutation.

Standards Compliance: PASS WITH SUGGESTIONS

  • Public class and public coroutine have documentation through the class-level summary and clear method names.
  • No update-loop or per-frame allocation path is added.
  • Configuration is serialized on the component rather than hardcoded in static state.
  • The LoadActorProfile coroutine is a little long for the local standard, but it is linear startup work and not a hot path. Split later if actor binding grows.

Architecture: CLEAN FOR PROTOTYPE

  • The component binds a scene object to one actor profile without mixing actor memory into the player profile.
  • It depends on SecondSpawnGatewayClient, matching the current Unity prototype client pattern.
  • It does not grant items, currency, BodyTime, combat authority, or world effects.
  • It exposes an event for later NPC/interaction code instead of requiring polling.

SOLID: APPROVED WITH SUGGESTIONS

Single responsibility is acceptable: bind one scene actor to one profile and optional seed memory. Longer-term, memory seeding and visual/name application could split if this becomes a general NPC spawn framework.

Game-Specific Concerns

Positive Observations

  • Gives designers/agents a simple component to attach to many NPC/body GameObjects.
  • Keeps actor-local memory separated from player consciousness/profile memory.
  • Uses idempotent memory seeding through the backend upsert behavior, so repeated scene starts should not spam duplicate memories.

Required Changes

None from local code-review.

Suggestions

  • Later: wire ProfileLoaded into visual prefab selection and prototype NPC brain identity.
  • Later: add a small inspector/editor helper to generate stable actor IDs from scene paths.
  • Later: add Unity Play Mode tests once the package/compile environment is stable.

Verdict: APPROVED WITH SUGGESTIONS

Codex (@codex) review was requested in the PR body, but prior PRs indicate the Codex Cloud connector may still be at usage limit. Gemini review is requested as the independent external reviewer.

@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the ActorProfileBinder class, a MonoBehaviour designed to link scene-based NPCs with Nakama actor profiles, handling profile retrieval, memory seeding, and basic data application. The review feedback highlights several areas for improvement: ensuring the LoadActorProfile coroutine is private and robust against synchronous completion to maintain accurate loading states, defaulting _actorId to an empty string to allow fallback logic to function correctly, and reconsidering the runtime renaming of GameObjects to prevent breaking external script references.

Comment on lines +60 to +61
public IEnumerator LoadActorProfile()
{
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The LoadActorProfile method is public, which allows it to be started externally, bypassing the _loadRoutine tracking in Refresh(). Additionally, if the coroutine finishes synchronously (e.g., if _gateway is null), Refresh() will assign the Coroutine handle to _loadRoutine after the method has finished, causing IsLoading to stay true indefinitely. It is recommended to make this method private and add a yield return null; at the start to ensure the handle is assigned before any potential early exit.

        private IEnumerator LoadActorProfile()
        {
            yield return null;

public sealed class ActorProfileBinder : MonoBehaviour
{
[SerializeField] private bool _loadOnStart = true;
[SerializeField] private string _actorId = "npc-guide";
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The default value "npc-guide" for _actorId prevents the fallback logic in ResolveActorId() from using gameObject.name unless the field is manually cleared in the Inspector. This can lead to ID collisions if multiple NPCs are placed in a scene without unique configuration. Consider defaulting this to an empty string to allow the fallback to work by default.

        [SerializeField] private string _actorId = "";

if (!string.IsNullOrWhiteSpace(profile.display_name))
{
_displayName = profile.display_name.Trim();
gameObject.name = _displayName;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Renaming the GameObject at runtime to match the profile's display name can have unintended side effects, such as breaking external scripts that rely on GameObject.Find or making the hierarchy difficult to navigate during play mode. Consider if this renaming is strictly necessary or if the display name should be managed by a dedicated UI component instead.

@JOY
Copy link
Copy Markdown
Contributor Author

Delta after Gemini review on ActorProfileBinder:

  • Made the load coroutine private and kept Refresh() as the public entrypoint.
  • Added an explicit _isLoading flag so loading state stays correct even if the coroutine exits before or around the first yielded request.
  • Changed _actorId default to an empty string so fallback to the scene GameObject name actually works.
  • Made runtime GameObject renaming opt-in through _applyDisplayNameToGameObject; by default the profile display name is stored on the component without mutating hierarchy names other scripts may depend on.

Verification on latest head 47370b3:

  • npm run build in backend/nakama passed.
  • npm test in backend/nakama passed.
  • go test -count=1 ./... in backend/gateway passed.
  • go vet ./... in backend/gateway passed.
  • Markdownlint passed with the repo CI scope.
  • Changed-file em-dash/en-dash scan passed.
  • git diff --check passed.

Unity caveat remains: clean-worktree Unity batchmode compile is still blocked by the known Package Manager issue, so Editor/MCP compile verification remains required before a no-caveat merge claim.

Gemini (@gemini-code-assist) please delta re-review latest commit 47370b3.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the ActorProfileBinder component, which synchronizes Unity NPC objects with Nakama actor profiles and handles initial memory seeding. The review feedback identifies opportunities to improve state reliability by explicitly resetting the loading flag when a coroutine is stopped, and recommends normalizing string identifiers like actor types and memory kinds to lowercase to ensure compatibility with case-sensitive backend logic.

Comment on lines +54 to +57
if (_loadRoutine != null)
{
StopCoroutine(_loadRoutine);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

When StopCoroutine is called, the LoadActorProfileRoutine is immediately terminated, and the _isLoading = false; line (line 66) is skipped. If Refresh is called while a load is in progress, _isLoading remains true until the new routine finishes. However, if the routine is stopped and not restarted (e.g., if the object is disabled), the state will be stuck. It's safer to reset the loading state explicitly when stopping the routine.

            if (_loadRoutine != null)
            {
                StopCoroutine(_loadRoutine);
                _loadRoutine = null;
                _isLoading = false;
            }

yield return _gateway.AddNakamaActorMemory(new ActorMemoryAddRequestDto
{
actor_id = profile.actor_id,
kind = string.IsNullOrWhiteSpace(_seedMemoryKind) ? "system" : _seedMemoryKind.Trim(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The backend normalizeMemoryKind function (in backend/gateway/internal/character/store.go) uses a case-sensitive switch statement for validation. If a value like "Combat" or "System" is entered in the Unity inspector, it will fail to match the backend constants and default to "system". Normalizing to lowercase ensures the intended category is correctly applied.

                    kind = string.IsNullOrWhiteSpace(_seedMemoryKind) ? "system" : _seedMemoryKind.Trim().ToLowerInvariant(),

Comment on lines +126 to +129
actor_type = string.IsNullOrWhiteSpace(_actorType) ? "npc" : _actorType.Trim(),
display_name = string.IsNullOrWhiteSpace(_displayName) ? gameObject.name : _displayName.Trim(),
archetype_id = string.IsNullOrWhiteSpace(_archetypeId) ? "prototype-npc" : _archetypeId.Trim(),
visual_prefab_key = string.IsNullOrWhiteSpace(_visualPrefabKey) ? "prototype-npc" : _visualPrefabKey.Trim()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Identifiers like actor_type, archetype_id, and visual_prefab_key are typically treated as case-sensitive keys on the backend. To prevent issues caused by accidental capitalization in the Unity inspector, it's recommended to normalize these strings to lowercase before sending the request.

                actor_type = string.IsNullOrWhiteSpace(_actorType) ? "npc" : _actorType.Trim().ToLowerInvariant(),
                display_name = string.IsNullOrWhiteSpace(_displayName) ? gameObject.name : _displayName.Trim(),
                archetype_id = string.IsNullOrWhiteSpace(_archetypeId) ? "prototype-npc" : _archetypeId.Trim().ToLowerInvariant(),
                visual_prefab_key = string.IsNullOrWhiteSpace(_visualPrefabKey) ? "prototype-npc" : _visualPrefabKey.Trim().ToLowerInvariant()

@JOY JOY (JOY) force-pushed the feat/actor-profile-registry branch from bdf5940 to ca4087e Compare May 16, 2026 23:57
@JOY JOY (JOY) force-pushed the feat/actor-profile-binding branch from 47370b3 to 755e672 Compare May 17, 2026 00:01
@JOY JOY (JOY) changed the base branch from feat/actor-profile-registry to dev May 17, 2026 00:01
@JOY
Copy link
Copy Markdown
Contributor Author

Delta after Gemini review and stack rebase:

  • Rebased PR feat(unity): bind scene actors to profiles #14 onto current dev after actor profile registry merged, so this PR only carries the Unity actor binding component.
  • Accepted the coroutine loading-state finding. Refresh() now clears _loadRoutine and _isLoading immediately when stopping an in-flight load coroutine.
  • Accepted the case-normalization finding. Actor type and seed memory kind are normalized with ToLowerInvariant() before they are sent to Nakama.

Verification on latest head 755e672:

  • npm run build in backend/nakama passed.
  • npm test in backend/nakama passed.
  • go test -count=1 ./... in backend/gateway passed.
  • go vet ./... in backend/gateway passed.
  • Markdownlint passed with the repo CI scope.
  • git diff --check passed.
  • Changed-file em-dash/en-dash scan passed.

Unity caveat remains: clean-worktree Unity batchmode compile is still blocked by the known Package Manager issue, so Editor/MCP compile verification remains required before a no-caveat merge claim.

Gemini (@gemini-code-assist) please delta re-review latest commit 755e672.

@gemini-code-assist
Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

@JOY
Copy link
Copy Markdown
Contributor Author

Local code-review delta pass

Target: PR #14 latest head 755e672 after rebase onto dev.

Engine Specialist Findings: N/A - no engine configured

.Codex/docs/technical-preferences.md is not present, so engine-specialist routing is skipped per the local code-review skill.

Testability: TESTABLE WITH UNITY CAVEAT

  • The component is a thin Unity bridge over actor-profile RPCs already covered in PR feat(nakama): add actor profile registry #12.
  • Latest delta covers Gemini findings: stopped coroutine loading state and identifier case normalization.
  • Unity batchmode compile remains blocked by the known Package Manager issue, so Editor/MCP compile verification remains the caveat.

ADR Compliance: COMPLIANT

The component keeps actor state in Nakama and does not grant gameplay authority, provider keys, inventory, BodyTime, or combat effects.

Standards Compliance: PASS

  • No per-frame work added.
  • Loading state is reset when replacing a coroutine.
  • Actor type and memory kind are normalized before backend calls.
  • Runtime GameObject renaming remains opt-in.

Architecture: CLEAN

The binder maps one scene actor object to one Nakama actor profile and exposes ProfileLoaded for future visual or brain wiring without polling.

Required Changes

None.

Suggestions

  • Add Play Mode coverage once Unity package resolution is stable.
  • Later wire profile-loaded data into visual prefab selection and NPC brain identity.

Verdict: APPROVED WITH CAVEAT

Gemini delta review is currently blocked by daily quota, but this local pass verifies the latest Gemini findings were addressed and CI is green.

@JOY JOY (JOY) merged commit 2ddec0b into dev May 17, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant