# Animation Test Scene Setup A standalone scene for testing character animations, skill VFX, weapon logic, and shape-change without any server connection. All triggers are issued via local commands. --- ## Related Source Files | File | Namespace / Class | Role | |---|---|---| | `Assets/PerfectWorld/Scripts/Move/CECPlayer.cs` | `BrewMonster.CECPlayer` | Abstract base — model load, action dispatch, weapon, shape, GFX state | | `Assets/Scripts/CECHostPlayer.cs` | `BrewMonster.CECHostPlayer` | Concrete host player — message routing, skill prep, combat messages | | `Assets/Scripts/CECHostPlayer.Skill.cs` | partial `CECHostPlayer` | Skill shortcut, `GetNormalSkill`, spam guard | | `Assets/Scripts/CECHostPlayer.Combat.cs` | partial `CECHostPlayer` | `PlayAttackEffect` callers, melee/skill result handlers | | `Assets/PerfectWorld/Scripts/NPC/CECModel.cs` | `BrewMonster.CECModel` | Wraps model `GameObject`; manages `SkeletonBuilder`, hooks, `NamedAnimancerComponent` | | `Assets/PerfectWorld/Scripts/Players/CECPlayerActionController.cs` | `CECPlayerActionController` | Routes Play/Queue calls to the active `CECPlayerActionPlayPolicy` | | `Assets/PerfectWorld/Scripts/Players/CECPlayerActionPlayPolicy.cs` | `CECPlayerActionPlayPolicy` | Default policy (no split-body); drives Animancer clips | | `Assets/PerfectWorld/Scripts/Managers/NPCManager.cs` | `NPCManager` | Async model loader (`GetModelPlayer`, `GetDummyModel`) | | `Assets/PerfectWorld/Scripts/Managers/CECAttacksMan.cs` | `CECAttacksMan` | Singleton: attack events, skill GFX preload, state-action config | | `Assets/PerfectWorld/Scripts/Managers/CECSkillGfxMan.cs` | `CECSkillGfxMan` / `CECSkillGfxEvent` | Skill GFX event state-machine (fly → hit → ground-hit) | | `Assets/PerfectWorld/Scripts/Managers/A3DSkillGfxMan.cs` | `A3DSkillGfxMan` | Low-level GFX composer manager | | `Assets/PerfectWorld/Scripts/Vfx/A3DSkillGfxComposerMan.cs` | `A3DSkillGfxComposerMan` | Per-skill GFX composer (fly/hit/ground-hit paths) | | `Assets/PerfectWorld/Scripts/Sound/SFXManager.cs` | `SFXManager` | Sound pool, `sound.txt` table, `PlaySkillSfxAtPointAsync` | --- ## 1. Model Loading ### Entry Point `CECPlayer.SetPlayerModel(byte profession, byte gender)` (async) ### Flow ``` SetPlayerModel(profession, gender) └─ InitializePlayerCECModel(profession, gender) ├─ NPCManager.Instance.GetModelPlayer(profession, gender) │ └─ Loads .ecm prefab from Addressables / StreamingAssets │ path: "models/players/形象//躯干/.ecm" ├─ Finds SkeletonBuilder on model (retries next frame if not ready) ├─ Finds CombineActHolder → reads CombinedActionSO ├─ CECModel.SetSkeletonBuilder / SetNamedAnimancerComponent / SetTransform ├─ CECModel.SetCombinedAction (action map) └─ CECModel.InitializeSkeletonBuilder() ← hooks now available └─ AttachWeapon() └─ RecreateActionController() └─ BuildActionList() ← maps skill IDs → PLAYER_ACTION_INFO_CONFIG └─ PlayAction(ACT_STAND / ACT_FIGHTSTAND) ``` ### Key Model Paths (NPCManager._playerModelPaths) | Index | Path | |---|---| | 0 | `models/players/形象/武侠男/躯干/武侠男.ecm` | | 1 | `models/players/形象/武侠女/躯干/武侠女.ecm` | | 2 | `models/players/形象/法师男/躯干/法师男.ecm` | | 3 | `models/players/形象/法师女/躯干/法师女.ecm` | | 4–5 | 巫师男/女 | | 6–9 | 妖族 (妖精, 妖兽男) | | 10–11 | 刺客男/女 | | 12–15 | 羽族 (羽芒, 羽灵) | | 16–19 | 灵族 (剑灵, 魅灵) | | 20+ | Profession transform models (白虎, 火狐狸, 影族变身…) | | 24+ | Skill transform dummy models (金钱蛙, 婚礼童, 树鸡, 龙…) | --- ## 2. Animation System ### Components on Model Prefab - `SkeletonBuilder` — builds skeleton and registers hook points - `NamedAnimancerComponent` — Animancer component; clips referenced by name - `CombineActHolder` → `CombinedActionSO` — maps action prefix+weapon suffix to clip names ### CECPlayerActionController Located: `Assets/PerfectWorld/Scripts/Players/CECPlayerActionController.cs` Channels: - `ACT_CHANNEL_UPPERBODY = 0` - `ACT_CHANNEL_LOWERBODY = 1` - `ACT_CHANNEL_WOUND = 2` Key methods: ```csharp // Immediate play PlayNonSkillActionWithName(int iAction, string szActName, bool bRestart, int nTransTime, bool bNoFx, CECAttackEvent, uint dwFlagMode) // Queue after current QueueNonSkillActionWithName(int iAction, string szActName, int nTransTime, bool bForceStop, bool bNoFx, bool bResetSpeed, bool bResetActFlag, CECAttackEvent, uint dwNewFlagMode) // Skill cast (charging / 吟唱) PlaySkillCastActionWithName(int idSkill, string szActName, bool bNoFX) // Skill attack (施放) PlaySkillAttackActionWithName(int idSkill, string szActName, bool bNoFX, CECAttackEvent, uint dwFlagMode) QueueSkillAttackActionWithName(int idSkill, string szActName, int nTransTime, bool bNoFX, ...) // Stop StopSkillCastAction() StopSkillAttackAction() StopChannelAction() ``` Flag mode constants (in `CECPlayer`): - `COMACT_FLAG_MODE_ONCE_IGNOREGFX = 2` - `COMACT_FLAG_MODE_ONCE_MULTIIGNOREGFX = 3` ### PLAYER_ACTION_TYPE Enum (key values) | Constant | Description | |---|---| | `ACT_STAND` | Idle stand | | `ACT_FIGHTSTAND` | Combat idle (maps to ACT_STAND when no fight anim) | | `ACT_RUN` / `ACT_WALK` | Movement | | `ACT_JUMP_START` / `ACT_JUMP_LOOP` / `ACT_JUMP_LAND` | Jump phases | | `ACT_ATTACK_1..4` | Normal attack sequence | | `ACT_ATTACK_TOSS` | Throw / ranged attack | | `ACT_TAKEOFF` / `ACT_LANDON` | Flight transitions | | `ACT_WOUNDED` | Hit reaction | | `ACT_GROUNDDIE` | Death on ground | | `ACT_REVIVE` | Revive animation | | `ACT_PICKUP` / `ACT_PICKUP_MATTER` | Pick-up gestures | | `ACT_EXP_WAVE..ACT_EXP_DANCE` | Expression animations | | `ACT_EXP_FASHIONWEAPON` | Fashion weapon expression | | `ACT_USING_TARGET_ITEM` | Use item animation | ### Animation Name Convention For skill actions: ``` {action_prefix}_{weapon_suffix}_施放起_ ← attack launch (ground) {action_prefix}_{weapon_suffix}_施放落_ ← attack land (ground) {action_prefix}_{weapon_suffix}_吟唱_ ← cast/charging // Air variants: {action_prefix}_{weapon_suffix}_空中翅膀_施放起_ ← wing-type air attack {action_prefix}_{weapon_suffix}_空中飞剑_施放起_ ← flysword-type air attack {action_prefix}_{weapon_suffix}_空中翅膀_吟唱_ {action_prefix}_{weapon_suffix}_空中飞剑_吟唱_ ``` Multi-section suffix appended: `szAct += "_" + suffix` (from `GetSkillSectionActionName`). --- ## 3. Skill Trigger Logic (No Target Required) ### Action Config Loading `BuildActionList()` in `CECPlayer`: - Reads `PLAYER_ACTION_INFO_CONFIG` from `elementdataman` - Builds `_default_actions[]` array for standard action types - Builds `_default_skill_actions` dictionary `(uint skillId → PLAYER_ACTION_INFO_CONFIG)` - `PlayerSkillAction.NUM_WEAPON_TYPE` weapon suffix variants per skill ### Trigger Chain (local, no server) ``` [Local command: trigger skill idSkill] 1. PlaySkillCastAction(idSkill) ← optional: shows charging/casting animation └─ BuildActionName(data, weapon_type, "_吟唱_") └─ PlaySkillCastActionWithName(idSkill, szAct, bHideFX) └─ ShowWeaponByConfig(data) 2. PlaySkillAttackAction(idSkill, nAttackSpeed, ref piAttackTime) ├─ BuildActionName(data, weapon_type, "_施放起_") ├─ GetSkillSectionActionName (multi-section support) ├─ GetComActTimeSpanByName → nTime1, nTime2 ├─ SetPlaySpeed(vScale) if speed adjustment needed ├─ PlaySkillAttackActionWithName(idSkill, szAct, bHideFX, attackEvent) └─ QueueSkillAttackActionWithName(idSkill, szAct2, 0, bHideFX) 3. PlayAttackEffect(idTarget=0, idSkill, skillLevel, nDamage=0, dwModifier, nAttackSpeed, ref attackTime) ├─ Creates CECAttackEvent via CECAttacksMan.AddSkillAttack(...) ├─ Calls PlaySkillAttackAction(...) └─ CECAttacksMan ticks the event each frame (GFX flight + hit) ``` ### Key Method: `PlayAttackEffect` ```csharp // CECPlayer.cs line ~1658 public void PlayAttackEffect(int idTarget, int idSkill, int skillLevel, int nDamage, uint dwModifier, int nAttackSpeed, ref int attackTime) ``` - `idTarget = 0` → target-less skill (still creates attack event for GFX) - `idSkill = 0` → normal melee attack; uses `AddMeleeAttack` - When `idSkill > 0`: uses `AddSkillAttack`, triggers `PlaySkillAttackAction` ### CECSkill Class (used by CECHostPlayer.Skill.cs) - `GetSkillID()`, `GetSkillLevel()`, `GetCastRange()`, `GetType()` - Types: `TYPE_PASSIVE`, `TYPE_PRODUCE`, `TYPE_LIVE`, `TYPE_GOBLIN`, active types - Retrieved via: `GetNormalSkill(id)`, `GetPositiveSkillByID(id)`, `GetPassiveSkillByID(id)` --- ## 4. Weapon Logic ### Attach / Detach ```csharp AttachWeapon() // checks left/right hook availability via CECModel.GetHook() DetachWeapon() // sets m_bWeaponAttached = false ``` ### Hook Position Strings | Method | WEAPON_HANGER_HAND | WEAPON_HANGER_SHOULDER | |---|---|---| | `GetLeftWeaponHookPos` | `_hh_left_hand_weapon` | `_hh_left_shoulder_weapon` | | `GetRightWeaponHookPos` | `_hh_right_hand_weapon` | `_hh_right_shoulder_weapon` | ### Weapon Type Resolution ```csharp int weapon_type = GetShowingWeaponType(); // considers fashion mode int GetWeaponType(int iWeaponType) // maps raw type to canonical type (0-14) int GetWeaponID() // equip pack weapon item ID (0 if shape-changed) ``` ### Fashion Weapon - `InFashionMode()` → `m_bFashionMode` - `CanShowFashionWeapon(weapon_type, fashion_weapon_type)` → checks `FASHION_WEAPON_CONFIG.action_mask` - `ShowWeaponByConfig(PLAYER_ACTION_INFO_CONFIG)` → shows/hides weapon per skill config ### Weapon SFX Maps (`m_aWeaponSFX`, `m_aWeaponHitSFX`) | Weapon Type | Attack SFX | Hit SFX | |---|---|---| | 0,1 | `item/weaponattack/1hshort[a/b/c]` | `item/weaponattack/hitsword[big]` | | 2 | `item/weaponattack/2hlong[a/b/c/d]` | `item/weaponattack/hitmace[big]` | | 3 | `item/weaponattack/1hshort[a/b/c]` | `item/weaponattack/hithammer[big]` | | 4 | `item/weaponattack/2hlong[a/b/c/d]` | `item/weaponattack/hitaxe[big]` | | 5 | `item/weaponattack/1hshort[a/b]` | `item/weaponattack/hithammer` | | 6,7,9 | `item/weaponattack/bow[/b/drawbow]` | `item/weaponattack/hitthrow` | | 8,10 | `item/weaponattack/fist[a/b/c/d]` | `item/weaponattack/hithand` | --- ## 5. SFX System ### SFXManager Located: `Assets/PerfectWorld/Scripts/Sound/SFXManager.cs` - Singleton (`MonoSingleton`) - Loads `Resources/sound.txt` (tab-separated: `id path`) into `_soundTable` - Pool of `_sfxPoolSize` (default 8) `AudioSource` components - Routes all skill SFX through `_sfxMixerGroup` Key method: ```csharp SFXManager.Instance.PlaySkillSfxAtPointAsync(string soundPath, Vector3 position, float delay) ``` Usage in `PlayAttackAction`: ```csharp string soundPath = m_aWeaponSFX[weapon_type][rand % count]; string hitSoundPath = m_aWeaponHitSFX[weapon_type][rand % count]; SFXManager.Instance.PlaySkillSfxAtPointAsync(soundPath, Vector3.zero, iTransTime / 1000f); SFXManager.Instance.PlaySkillSfxAtPointAsync(hitSoundPath, Vector3.zero, iTransTime / 1000f + 0.1f); ``` Movement SFX: `_moveSoundSource` (2D looping AudioSource, assigned in Inspector). --- ## 6. GFX / VFX System ### State-Effect GFX (persistent buffs/debuffs) Base path: `"gfx/策划联入/状态效果/"` ```csharp // Play GFX on player model PlayGfx(string strGFXFile, string szHook, float fScale, uint iShapeTypeMask, bool persist) // Remove GFX RemoveGfx(string szPath, string szHook, uint iShapeTypeMask) // Play GFX on weapon model PlayStateGfxOnModel(CECModel pWeapon, string path, string hook, float fScale) RemoveStateGfxFromModel(CECModel pWeapon, string path, string hook) ``` Active state GFX tracked in: `_stateGfxObjects` (Dictionary keyed by `path+hook`). Weapon hook resolution: ```csharp IsWeaponHookPos(string szHH, out bool bLeft, out CECModel pWeapon) GetWeaponGFXHookPos(CECModel pModel, bool bLeft) ``` ### Skill GFX (projectiles / area effects) Managed by `CECAttacksMan` + `A3DSkillGfxComposerMan`: ``` CECAttacksMan.LoadAllSkillGfxAsync() └─ For each skill: ElementSkill.GetAllGFX(skillId) → (flyGFX, hitGrdGFX, hitGFX) └─ A3DSkillGfxComposerMan.LoadOneComposerAsync(skillId, skillStub, flyPath, hitGrdPath, hitPath) // On-demand (when skill first used): CECAttacksMan.LoadSkillGfxOnDemand(uint skillId) ``` GFX event lifecycle (per `CECSkillGfxEvent`): 1. Spawn fly GFX at caster position 2. Move toward target (or self for targetless) 3. On arrival: spawn hit GFX, optionally spawn ground-hit GFX 4. Mark `m_bFinished = true` → removed from `m_targets` linked list Ticked every frame: `SkillGfxMan.InstanceSub.Tick(dwDeltaTime)` in `CECAttacksMan.Update()`. --- ## 7. Attack Event (CECAttackEvent) Created by `CECAttacksMan`: ```csharp // Melee (idSkill == 0) CECAttacksMan.Instance.AddMeleeAttack(idHost, idTarget, idWeapon, nDamage, dwModifier) // Skill CECAttacksMan.Instance.AddSkillAttack(idHost, idSkillTarget, idTarget, idWeapon, idSkill, skillLevel, dwModifier, nDamage) ``` Key fields on `CECAttackEvent`: - `m_idHost` — attacker entity ID - `m_bFinished` — set true when GFX resolved - `m_bSignaled` — damage applied flag (see `SetApplyDamage`) - `SetSkillSection(nSection)` — for multi-section skills For the animation test scene, pass `idTarget = 0` and `nDamage = 0` — the event will drive GFX travel with no actual damage. --- ## 8. Change Shape / TransformShape ### Entry Point ```csharp await player.TransformShape(byte iShape, bool bLoadAtOnce = false) ``` ### Shape ID Encoding (8-bit) ``` | Bit 7–6 | Bit 5–0 | | TYPE | Model ID | ``` | TYPE value | PLAYERMODEL_TYPE | Meaning | |---|---|---| | `0x00` | PLAYERMODEL_TYPE_NONE | Invalid / legacy (auto-corrected to 0x40) | | `0x40` | PLAYERMODEL_PROFESSION | Class-based transform (mapped via `_GetProfessionTransformModelID`) | | `0x80` | PLAYERMODEL_DUMMY | Skill-transform / dummy model | ### Flow ``` TransformShape(iShape) ├─ SetShape(iShape) ← decode type+ID, fix legacy format ├─ IsShapeChanged()? │ ├─ YES: QueueLoadDummyModel(m_iShape, bLoadAtOnce) │ │ └─ NPCManager.Instance.GetDummyModel(iShapeID) │ │ └─ ApplyShapeModelChange(pDummyModel) │ └─ NO: ApplyShapeModelChange(GetMajorModel()) ← revert to base model └─ ApplyShapeModelChange(pModel) ├─ OnModelChange(pModel) → RefreshCECModel(pModel) │ ├─ CECModel.SetSkeletonBuilder(...) │ ├─ CECModel.SetNamedAnimancerComponent(...) │ ├─ CECModel.SetTransform(...) │ ├─ CECModel.SetCombinedAction(...) │ └─ CECModel.InitializeSkeletonBuilder() ├─ Sync position/rotation from old model ├─ RecreateActionController() └─ PlayAction(ACT_STAND) ``` ### Profession → Transform Model Mapping | Profession | Male | Female | |---|---|---| | PROF_HAG (妖族) | `RES_MOD_ORC_FOX` | `RES_MOD_ORC_FOX2` | | PROF_ORC (妖兽) | `RES_MOD_ORC_TIGER` | `RES_MOD_ORC_PANDER` | | PROF_MONK / PROF_GHOST | `RES_MOD_SHADOW_FISH_M` | `RES_MOD_SHADOW_FISH_F` | | PROF_YEYING (夜影) | `RES_MOD_YEYING_RESHAPE_M` | `RES_MOD_YEYING_RESHAPE_F` | | PROF_YUEXIAN (月仙) | `RES_MOD_YUEXIAN_RESHAPE_M` | `RES_MOD_YUEXIAN_RESHAPE_F` | Revert to original: `TransformShape(0)` → `IsShapeChanged()` returns false → `ApplyShapeModelChange(GetMajorModel())`. --- ## 9. Required Scene GameObjects | GameObject | Component(s) | Notes | |---|---|---| | `NPCManager` | `NPCManager` | Async model loader; must be in scene | | `CECAttacksMan` | `CECAttacksMan` | Manages attack events and skill GFX; assign `SkillStateActionConfig` SO | | `SFXManager` | `SFXManager` | Assign `_moveSoundSource`, `_sfxMixerGroup` in Inspector | | `EC_ManMessageMono` | `EC_ManMessageMono` | Provides `EC_ManPlayer`, `CECNPCMan` | | `ElementDataManProvider` | `elementdataman` provider | Required for skill/action config lookups | | Player Object | `CECHostPlayer` (or subclass) | `parentModel` Transform, `txtName` TMP text | | SkillGfxMan | `A3DSkillGfxMan` | Low-level GFX manager (instantiated via `SkillGfxMan.InstanceSub`) | --- ## 10. Local Command API (No Server) Replace server message handlers with these direct calls: ```csharp // --- Model --- await player.SetPlayerModel(profession, gender); // --- Standard animations --- player.PlayAction((int)PLAYER_ACTION_TYPE.ACT_STAND, true); player.PlayAction((int)PLAYER_ACTION_TYPE.ACT_RUN, true); player.PlayAction((int)PLAYER_ACTION_TYPE.ACT_ATTACK_1, false); // --- Skill cast (charging phase) --- player.PlaySkillCastAction(idSkill); // 吟唱 animation // --- Skill attack (fire phase, no target) --- int attackTime = 0; player.PlaySkillAttackAction(idSkill, attackSpeed, ref attackTime); // --- Full effect chain (animation + GFX + SFX, no target) --- int attackTime = 0; player.PlayAttackEffect( idTarget: 0, // 0 = no target idSkill: idSkill, skillLevel: 1, nDamage: 0, dwModifier: 0, nAttackSpeed: 50, // 50 = default 1x speed ref attackTime); // --- Change shape --- await player.TransformShape(shapeID); // e.g. 0x40 | modelID await player.TransformShape(0); // revert to base // --- Fashion mode toggle --- player.m_bFashionMode = true; player.m_bShowWeapon = true; ``` --- ## 11. Initialization Order for Scene ``` 1. Awake / Start: - NPCManager initializes - CECAttacksMan.SetupAttacksMan() → A3DSkillGfxComposerMan created - SFXManager.Initialize() → loads sound.txt, builds AudioSource pool 2. Player Init: - player.Init(playerInfo) ← sets profession, gender, equips, shape - await player.SetPlayerModel(profession, gender) → NPCManager.GetModelPlayer() → model loaded → CECModel setup (SkeletonBuilder, Animancer, hooks) → AttachWeapon() → RecreateActionController() → BuildActionList() ← requires elementdataman populated → PlayAction(ACT_STAND) 3. Skill GFX preload (background, non-blocking): - CECAttacksMan.LoadAllSkillGfxAsync() 4. Ready — call local commands to trigger animations ``` --- ## 12. Notes & Caveats - **`elementdataman`** must be loaded before `BuildActionList()`. Skills without a matching config entry in `_default_skill_actions` will silently skip the cast/attack animation. - **Animancer** (`NamedAnimancerComponent`) must be present on the model prefab. The `SkeletonBuilder` may build asynchronously; if `InitializePlayerCECModelDelayed` coroutine fires, hooks are not ready until the next frame. - **`CECAttacksMan` is required even for animation-only scenes** because `PlayAttackEffect` references it, and `SkillGfxMan.InstanceSub` is initialized via its `OnDestroy`. - **Target-less GFX**: passing `idTarget = 0` to `AddSkillAttack` creates a valid event; the GFX composer will still instantiate fly/hit effects but travel to `Vector3.zero` unless a target position override is added. - **Multi-section skills**: use `SetSkillSection(nSection)` on the `CECAttackEvent` before passing it to the animation method; `GetSkillSectionActionName` appends the section suffix automatically. - **Shape model caching**: `m_pModels[iShapeType]` caches loaded dummy models per type slot. Re-calling `TransformShape` with the same shape reuses the cached model instantly.