19 KiB
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.
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 |
| 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 pointsNamedAnimancerComponent— Animancer component; clips referenced by nameCombineActHolder→CombinedActionSO— maps action prefix+weapon suffix to clip names
CECPlayerActionController
Located: Assets/PerfectWorld/Scripts/Players/CECPlayerActionController.cs
Channels:
ACT_CHANNEL_UPPERBODY = 0ACT_CHANNEL_LOWERBODY = 1ACT_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 = 2COMACT_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_CONFIGfromelementdataman - Builds
_default_actions[]array for standard action types - Builds
_default_skill_actionsdictionary(uint skillId → PLAYER_ACTION_INFO_CONFIG) PlayerSkillAction.NUM_WEAPON_TYPEweapon 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; usesAddMeleeAttack- When
idSkill > 0: usesAddSkillAttack, triggersPlaySkillAttackAction
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_bFashionModeCanShowFashionWeapon(weapon_type, fashion_weapon_type)→ checksFASHION_WEAPON_CONFIG.action_maskShowWeaponByConfig(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)AudioSourcecomponents - 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):
- Spawn fly GFX at caster position
- Move toward target (or self for targetless)
- On arrival: spawn hit GFX, optionally spawn ground-hit GFX
- Mark
m_bFinished = true→ removed fromm_targetslinked 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 IDm_bFinished— set true when GFX resolvedm_bSignaled— damage applied flag (seeSetApplyDamage)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 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:
// --- 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
elementdatamanmust be loaded beforeBuildActionList(). Skills without a matching config entry in_default_skill_actionswill silently skip the cast/attack animation.- Animancer (
NamedAnimancerComponent) must be present on the model prefab. TheSkeletonBuildermay build asynchronously; ifInitializePlayerCECModelDelayedcoroutine fires, hooks are not ready until the next frame. CECAttacksManis required even for animation-only scenes becausePlayAttackEffectreferences it, andSkillGfxMan.InstanceSubis initialized via itsOnDestroy.- Target-less GFX: passing
idTarget = 0toAddSkillAttackcreates a valid event; the GFX composer will still instantiate fly/hit effects but travel toVector3.zerounless a target position override is added. - Multi-section skills: use
SetSkillSection(nSection)on theCECAttackEventbefore passing it to the animation method;GetSkillSectionActionNameappends the section suffix automatically. - Shape model caching:
m_pModels[iShapeType]caches loaded dummy models per type slot. Re-callingTransformShapewith the same shape reuses the cached model instantly.