Files

465 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
**Skill test workflow (AnimatedModifier / LogPanel / SkillPanel):** see [`AnimationSceneSkillTest.md`](AnimationSceneSkillTest.md).
---
## 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/形象/<class><gender>/躯干/<class><gender>.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` |
| 45 | 巫师男/女 |
| 69 | 妖族 (妖精, 妖兽男) |
| 1011 | 刺客男/女 |
| 1215 | 羽族 (羽芒, 羽灵) |
| 1619 | 灵族 (剑灵, 魅灵) |
| 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<SFXManager>`)
- 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 76 | Bit 50 |
| 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.