Files

19 KiB
Raw Permalink Blame History

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.


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
  • CombineActHolderCombinedActionSO — 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:

// 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

// 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

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

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:

SFXManager.Instance.PlaySkillSfxAtPointAsync(string soundPath, Vector3 position, float delay)

Usage in PlayAttackAction:

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/策划联入/状态效果/"

// 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:

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:

// 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

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:

// --- 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.