11 KiB
Unified Character Catalog Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Make CharacterConfig.json the sole runtime source for both character selection and lobby prefab loading.
Architecture: Add deterministic figure-resolution helpers to CharacterConfigData, where a one-based figure is the configured item’s list position. LobbyWorldController loads that resource configuration, uses it to translate the persisted ID and incoming network figures, then loads the resolved prefabPath; it no longer owns an actor-path list.
Tech Stack: Unity 2022.3.62f3, C#, Unity Test Framework 1.1.33, NUnit.
Global Constraints
- Keep the existing wire protocol unchanged:
LobbyJoinMsg.Figureremains a one-basedint. - Preserve the generated alphabetical configuration order, so every client built from identical content derives identical figures.
- Do not modify character prefabs, the game server, or the user’s unrelated working-tree changes.
- A missing, empty, or invalid character config must log an explicit error and must not index a hard-coded fallback list.
Task 1: Character configuration figure resolver
Files:
- Create:
Client/Assets/Script/Tests/EditMode/XWorld.Link.EditModeTests.asmdef - Create:
Client/Assets/Script/Tests/EditMode/CharacterConfigDataTests.cs - Modify:
Client/Assets/Script/xmain/Character/CharacterConfigData.cs
Interfaces:
-
Consumes: the ordered
CharacterConfigData.characterslist generated intoAssets/Resources/Config/CharacterConfig.json. -
Produces:
int GetFigure(string id),int GetFigure(CharacterConfigItem item), andCharacterConfigItem GetByFigure(int figure)onCharacterConfigData. -
Step 1: Create the EditMode test assembly
Create Client/Assets/Script/Tests/EditMode/XWorld.Link.EditModeTests.asmdef:
{
"name": "XWorld.Link.EditModeTests",
"rootNamespace": "",
"references": ["XWorld.Link"],
"includePlatforms": ["Editor"],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false,
"optionalUnityReferences": ["TestAssemblies"]
}
- Step 2: Write the failing catalog-mapping tests
Create Client/Assets/Script/Tests/EditMode/CharacterConfigDataTests.cs:
using System.Collections.Generic;
using NUnit.Framework;
using XGame;
public sealed class CharacterConfigDataTests
{
private static CharacterConfigData CreateConfig()
{
return new CharacterConfigData
{
defaultCharacterId = "Assasin",
characters = new List<CharacterConfigItem>
{
new CharacterConfigItem { id = "Assasin", prefabPath = "Assets/Game/Art/Actor/Prefab/Assasin.prefab" },
new CharacterConfigItem { id = "bls", prefabPath = "Assets/Game/Art/Actor/Prefab/bls.prefab" },
new CharacterConfigItem { id = "boy", prefabPath = "Assets/Game/Art/Actor/Prefab/boy.prefab" },
new CharacterConfigItem { id = "captain", prefabPath = "Assets/Game/Art/Actor/Prefab/captain.prefab" },
new CharacterConfigItem { id = "cityboy_sk", prefabPath = "Assets/Game/Art/Actor/Prefab/cityboy_sk.prefab" },
}
};
}
[Test]
public void GetFigureReturnsGeneratedListIndexForCityboy()
{
Assert.That(CreateConfig().GetFigure("cityboy_sk"), Is.EqualTo(5));
}
[Test]
public void GetFigureUsesDefaultForAnUnknownId()
{
Assert.That(CreateConfig().GetFigure("missing"), Is.EqualTo(1));
}
[Test]
public void GetByFigureReturnsTheConfiguredPrefabPath()
{
CharacterConfigItem item = CreateConfig().GetByFigure(5);
Assert.That(item.id, Is.EqualTo("cityboy_sk"));
Assert.That(item.prefabPath, Is.EqualTo("Assets/Game/Art/Actor/Prefab/cityboy_sk.prefab"));
}
[Test]
public void GetByFigureUsesDefaultForAnInvalidIndex()
{
Assert.That(CreateConfig().GetByFigure(99).id, Is.EqualTo("Assasin"));
}
[Test]
public void EmptyConfigHasNoUsableFigure()
{
CharacterConfigData config = new CharacterConfigData();
Assert.That(config.GetFigure("cityboy_sk"), Is.EqualTo(0));
Assert.That(config.GetByFigure(1), Is.Null);
}
}
- Step 3: Run the test to verify RED
Run from the Unity editor Test Runner: Window > General > Test Runner > EditMode > Run All.
Expected: compilation fails because CharacterConfigData does not yet define GetFigure or GetByFigure.
- Step 4: Implement the minimal configuration helpers
In Client/Assets/Script/xmain/Character/CharacterConfigData.cs, make GetDefault and Find null-safe and add these members to CharacterConfigData:
public CharacterConfigItem GetDefault()
{
CharacterConfigItem item = Find(defaultCharacterId);
return item ?? (characters != null && characters.Count > 0 ? characters[0] : null);
}
public CharacterConfigItem Find(string id)
{
if (string.IsNullOrEmpty(id) || characters == null)
{
return null;
}
for (int i = 0; i < characters.Count; i++)
{
CharacterConfigItem item = characters[i];
if (item != null && string.Equals(item.id, id, StringComparison.OrdinalIgnoreCase))
{
return item;
}
}
return null;
}
public int GetFigure(string id)
{
return GetFigure(Find(id) ?? GetDefault());
}
public int GetFigure(CharacterConfigItem item)
{
if (item == null || characters == null)
{
return 0;
}
for (int i = 0; i < characters.Count; i++)
{
if (ReferenceEquals(characters[i], item))
{
return i + 1;
}
}
return 0;
}
public CharacterConfigItem GetByFigure(int figure)
{
int index = figure - 1;
if (characters != null && index >= 0 && index < characters.Count && characters[index] != null)
{
return characters[index];
}
return GetDefault();
}
- Step 5: Run the EditMode tests to verify GREEN
Run from the Unity editor Test Runner: Window > General > Test Runner > EditMode > Run All.
Expected: all five CharacterConfigDataTests pass.
- Step 6: Commit the independently tested resolver
git add -- Client/Assets/Script/xmain/Character/CharacterConfigData.cs Client/Assets/Script/Tests/EditMode/XWorld.Link.EditModeTests.asmdef Client/Assets/Script/Tests/EditMode/CharacterConfigDataTests.cs
git commit -m "feat: resolve character figures from config"
Task 2: Use the catalog to load lobby avatars
Files:
- Modify:
Client/Assets/Script/xmain/Client/LobbyWorldController.cs
Interfaces:
-
Consumes:
CharacterConfigData.GetFigure(string)andCharacterConfigData.GetByFigure(int)from Task 1, plusResources/Config/CharacterConfig.json. -
Produces: lobby avatar loading that resolves all selected and remote figures through configured
prefabPathvalues. -
Step 1: Replace the lobby’s hard-coded paths with resource-config resolution
In LobbyWorldController.cs:
- Delete
ActorPathsand add:
private const string CharacterConfigResourcePath = "Config/CharacterConfig";
private CharacterConfigData characterConfig;
- Add these instance methods:
private bool EnsureCharacterConfig()
{
if (characterConfig != null)
{
return characterConfig.GetDefault() != null;
}
TextAsset asset = Resources.Load<TextAsset>(CharacterConfigResourcePath);
if (asset == null)
{
Debug.LogError("[LobbyWorldController] missing character config: Resources/" + CharacterConfigResourcePath + ".json");
return false;
}
characterConfig = JsonUtility.FromJson<CharacterConfigData>(asset.text);
if (characterConfig == null || characterConfig.GetDefault() == null)
{
Debug.LogError("[LobbyWorldController] character config is empty or invalid.");
characterConfig = null;
return false;
}
return true;
}
private int ResolveFigure(int figure)
{
if (!EnsureCharacterConfig())
{
return 0;
}
return characterConfig.GetFigure(characterConfig.GetByFigure(figure));
}
private int GetSelectedFigure()
{
if (!EnsureCharacterConfig())
{
return 0;
}
return characterConfig.GetFigure(PlayerPrefs.GetString(SelectedCharacterKey, string.Empty));
}
private string GetPrefabPath(int figure)
{
if (!EnsureCharacterConfig())
{
return null;
}
CharacterConfigItem item = characterConfig.GetByFigure(figure);
return item == null ? null : item.prefabPath;
}
-
In
Initialize, callEnsureCharacterConfig()before creating an avatar; if it returnsfalse, return without joining the lobby. Replace the existingMathf.Clamp(..., ActorPaths.Length)assignment withselfFigure = figure <= 0 ? GetSelectedFigure() : ResolveFigure(figure);and return with an error if the result is0. -
In
RefreshSelectedFigure, replace the clamped figure assignment withint nextFigure = GetSelectedFigure();and return after logging if it is0. -
In
GetOrCreateAvatar, replace its clamped assignment withfigure = ResolveFigure(figure);; if that returns0, log[LobbyWorldController] cannot resolve figure because the character catalog is unavailable.and returnnull. Update each caller to return early when the result isnullbefore dereferencing the avatar. -
In
CoLoadActorPrefab, resolvestring prefabPath = GetPrefabPath(figure);; log and exit if it is null or empty; otherwise callXResLoader.coLoadRes(prefabPath, typeof(GameObject), ...). -
Delete the static
GetSelectedFigureimplementation at the end of the class.
- Step 2: Run EditMode tests and compile the client
Run Window > General > Test Runner > EditMode > Run All in Unity, then allow Unity to complete script compilation without Console errors.
Expected: all CharacterConfigDataTests pass and no compiler errors reference ActorPaths or a static call to GetSelectedFigure.
- Step 3: Perform the play-mode regression check
In the Unity editor:
- Run
Tools > Character > Generate Character Config. - Enter Play mode and open the character selector.
- Select
cityboy_sk. - Join or return to the lobby.
Expected: the selected lobby avatar is cityboy_sk, not Assasin; Console contains no missing-config or failed-prefab error. Repeat once with Assasin to confirm existing selection still loads.
- Step 4: Commit the lobby integration
git add -- Client/Assets/Script/xmain/Client/LobbyWorldController.cs Client/Assets/Script/Tests/EditMode/CharacterConfigDataTests.cs
git commit -m "fix: load lobby characters from config"
Self-review
- Spec coverage: Task 1 supplies deterministic ID/figure and figure/item mapping; Task 2 removes the duplicate lobby list, loads configured prefab paths, preserves the integer protocol, and verifies
cityboy_skin play mode. - Placeholder scan: no incomplete markers or unspecified error behavior remain.
- Type consistency: Task 1 defines all three
CharacterConfigDatamethods used by Task 2;ResolveFigureandGetSelectedFigurereturn anint, andGetPrefabPathreturns astring.