The long-press left click charge wind-up only affects the upper body — whether ground WASD running (lower-body locomotion in progress) or airborne legs executing a flight animation, the charge wind-up overlays well. After release, the release (fling) connects into the follow-through. Short taps retain the original basic attack combo, with no effect on feel.
— Core requirement
1. Final spec settled after three iterations
| Version | User feedback | Revisions |
|---|---|---|
| V1 first version | — | Long press → One, release → Two → connects to Three; auto-release on timeout |
| V2 after PIE | Held down without releasing, so it auto-completed. It should keep charging the whole time | Long press without release → stays in One, plays to frame 75 → 65↔75 oscillation (charge breathing motion); force-release if cumulative exceeds 2 s |
| V3 tuning | Two is too slow | Two playback speed ×1.5 → ×1.5 again (final 2.25); acceptance satisfied |
2. Short tap vs long press detection window
Left click is a single key, routed with a 0.25 s detection window:
- Released within 0.25 s of pressing → Short tap → follows original basic attack combo (DoAttack → PlayMontage)
- Released after more than 0.25 s of pressing → Long press → follows the charge chain (One → Two → Three)
- Long press beyond 2 s still not released → Force release (charge auto-releases)
// CharacterBase.cpp · Left click input guard
void ACharacterBase::OnAttackPressedKey()
{
if (bIsCharging) return; // Do not re-trigger during charge
bWasChargingReleased = false;
GetWorldTimerManager().SetTimer(
ChargeKeyPressTimer, this,
&ACharacterBase::OnChargeDetected, 0.25f, false); // Detect after 0.25s
}
void ACharacterBase::OnAttackReleasedKey()
{
if (!bWasChargingReleased) {
// Released within 0.25s → short tap, follow basic attack
GetWorldTimerManager().ClearTimer(ChargeKeyPressTimer);
DoAttack(); return;
}
// Long press release → trigger charge release
if (bIsCharging) { ReleaseCharge(); }
}
3. AnimGraph four-layer structure (the upper-body layering key)
The original ABP AB_角色蓝图 only contained 1 Slot(DefaultSlot); after adding layered Slots it was extended to four layers:
Locomotion(StateMachine)
└─> Slot(DefaultSlot) ← Original full-body montage still goes here, unaffected
├─> SaveCachedPose(ChargeBasePose)
│ ├─> UseCachedPose A ──> LayeredBoneBlend.BasePose
│ └─> UseCachedPose B ──> Slot(UpperBody).Source
└─> Slot(UpperBody).Pose ──> LayeredBoneBlend.BlendPoses_0
LayeredBoneBlend.Pose ──> LocalToComponentSpace ──> ModifyBone ──> TwoBoneIK×2 ──> AnimDynamics ──> KawaiiPhysics×5 ──> Root
Why use SaveCachedPose + UseCachedPose:
- Inside DefaultSlot is the full-body Locomotion (walk / run / airborne leg animation); it is the "base".
- SaveCachedPose saves a copy of the base Pose for LayeredBoneBlend.BasePose to use (keeping the lower body on the full-body Pose).
- Slot(UpperBody) is the new layering entry; LayeredBoneBlend only blends the bones above the spine (Branch Filter filled with spine bone names), so the charge stacks only on the upper body while the legs continue walk/flight animations.
4. Three-segment animation and timing
| Animation | Frame count | Duration | Playback speed | Loop |
|---|---|---|---|---|
| AM_CL_Attack_01_One (charge wind-up) | 76 | 2.533 s | 1.25× | Yes (65↔75 oscillation) |
| AM_CL_Attack_01_Two (fling) | 30 | 1.0 s | 2.25× | No |
| AM_CL_Attack_01_Three (follow-through) | 45 | 1.5 s | 1.0× | No |
Key point: this animation sequence has no corresponding montage; it is played by calling PlayAnimation directly. When the charge wind-up One plays to frame 75 and the key is still held → the AnimBP Event BlueprintUpdateAnimation in the AnimBP drives a per-frame Tick, playing back and forth between progress 65↔75 (charge breathing effect).
5. Event-driven chain in the AnimBP
// AB_角色蓝图's AnimGraph Event BlueprintUpdateAnimation (per-frame Tick)
// Inputs: Player → IsCharging(bool), ChargeProgress(0-1 float)
// Actions:
// if IsCharging=true → Set Slot(UpperBody) Pose = PlayAnimation(One)
// play to frame 75 → Reverse → Forward oscillation between 65↔75
// if IsCharging=false and bJustReleased=true → Set Slot(UpperBody) Pose = PlayAnimation(Two, then Three)
// else → Slot(UpperBody) cleared → upper body returns to full-body Locomotion
6. Issues and fix list
- Isolated InputKey node: in the Blueprint Attack graph,
K2Node_InputKey_2is an isolated node; the actual left click input is driven entirely by C++ (bBindLegacyAttackKey → BindKey(LeftMouseButton, IE_Pressed)). Do not modify the Blueprint isolated node. - LayeredBoneBlend branch filter bone names: the Branch Filter must be filled with spine bone names (
spine_01 / spine_02 / spine_03); if not filled, the entire body is covered by the charge animation. - C++ and AnimBP data sync: the two variables
IsCharging/ChargeProgressin the Player Blueprint are written by C++ and read by the AnimBP. AnimBP Event BlueprintUpdateAnimation reads every frame and should not be read only once in Event Blueprint Initialize. - Charge timing independent of the montage system: because the charge animation is not a montage (no Slot Montage), the timing logic is managed separately on the C++ side TimeManager; the AnimBP only drives LayeredBoneBlend according to C++ state.
- Dash during charge must be interrupted first: the dash guard
if (bIsCharging) { StopCharge(); }; otherwise two AnimGraph sources drive the same Slot simultaneously, causing animation tearing.