2D Character Movement Pack

Complete player controller: movement, jump, dash, melee combo combat, health, hit reactions and hit feedback β€” ready to drop into your project.

Overview

This pack gives you a fully working 2D player character: run/jump movement with air control, a buffered jump and dash, a 2D melee combo system with hitboxes and damage, a health component with knockback and defense hooks, hit-reaction animations, and lightweight "game feel" effects (hit-stop, camera shake, time slow) β€” with zero third-party dependencies.

All scripts live under the namespace CloudedStudio.CharacterPack2D and are compiled into their own assembly (CloudedStudio.CharacterPack2D.asmdef), so they won't slow down your project's compile times and won't collide with your own class names.

Who this is for This documentation assumes basic familiarity with the Unity Editor (adding components, wiring Inspector fields, Animation Events) and the new Input System package. No scripting is required to get the default setup running β€” scripting is only needed if you want to extend the combat system (e.g. your own enemy or defense component).

Package Contents

Assets/Clouded Studio/2D Character Movement Pack/Scripts/ β”œβ”€β”€ Core/ Shared contracts & data types β”‚ β”œβ”€β”€ DamageInfo.cs Struct describing a single hit β”‚ β”œβ”€β”€ IDamageable.cs IDamageable + IKnockbackable interfaces β”‚ β”œβ”€β”€ IDefendable.cs IDefendable interface + DefenseResult β”‚ β”œβ”€β”€ IVariableVulnerability.cs IVariableVulnerability interface β”‚ └── IFacingDirectionProvider.cs IFacingDirectionProvider interface β”œβ”€β”€ Movement/ β”‚ β”œβ”€β”€ CharacterMovement.cs Horizontal movement, acceleration, external forces β”‚ β”œβ”€β”€ CharacterGroundChecker.cs Ground detection + coyote time + dynamic friction β”‚ β”œβ”€β”€ CharacterJump.cs Buffered jump, variable jump height, air control β”‚ β”œβ”€β”€ PlayerJump.cs Player-specific jump subclass β”‚ β”œβ”€β”€ CharacterDashRoll.cs Buffered dash/roll with turn-interrupt β”‚ └── CharacterInputController.cs Input System β†’ movement/jump/dash/attack bridge β”œβ”€β”€ Player/ β”‚ β”œβ”€β”€ PlayerController.cs Player rig + combo state + facing direction β”‚ └── PlayerHitCounter.cs Consecutive-hit streak counter (for UI) β”œβ”€β”€ Combat/ β”‚ β”œβ”€β”€ CharacterHitbox.cs A single attack's trigger collider(s) β”‚ β”œβ”€β”€ CharacterHitboxController.cs Activates/deactivates hitboxes, mirrors on flip β”‚ β”œβ”€β”€ MeleeDamageDealer.cs Hitbox hit β†’ damage + knockback pipeline β”‚ β”œβ”€β”€ HealthComponent.cs Health, damage, knockback, death events β”‚ β”œβ”€β”€ PlayerHitSequence.cs Plays the "Player-Hit" reaction animation on damage β”‚ └── FX/ β”‚ β”œβ”€β”€ CameraShake2D.cs Self-contained camera shake (no 3rd-party camera package) β”‚ β”œβ”€β”€ HitImpactEffect.cs Time-slow + shake when the player takes damage β”‚ β”œβ”€β”€ HitConnectEffect.cs Hit-stop + shake when the player's attack lands β”‚ └── PlayerReceiveHitEffect.cs VFX spawn when the player takes damage └── Animations/ └── PlayerAnimationStatesController.cs Drives Animator parameters, hitbox Animation Events

Quick Start

The fastest path to a moving, jumping, attacking player. Each step is expanded later in its own section β€” follow the links if something isn't working.

  1. Create a player GameObject with a Rigidbody2D (Body Type: Dynamic, Gravity Scale ~3-5, Freeze Rotation Z) and a trigger-free Collider2D for the body.
  2. Add CharacterGroundChecker, CharacterMovement, PlayerJump and PlayerController β€” see Movement & Jump.
  3. Add a PlayerInput component (Input System package) and CharacterInputController β€” see Input Setup.
  4. Add an Animator with the required parameters and PlayerAnimationStatesController β€” see Animator Setup.
  5. (Optional) Add CharacterDashRoll for a dash β€” see Dash / Roll.
  6. Build your attack hitboxes as child objects and add CharacterHitboxController + MeleeDamageDealer β€” see Combat & Hitboxes.
  7. Add HealthComponent to the player (and to anything else that should be damageable) β€” see Health & Damage.
  8. (Optional) Add the FX components for hit feedback β€” see Hit Feedback.

Default Controls

The included demo scenes (Demo/PlayerSampleScene.unity and Demo/PlayerPrototypeSampleScene.unity) ship with a ready-to-use PlayerInputs.inputactions asset. These are the default keyboard bindings:

ActionKeys
MoveArrow Keys (Up / Down / Left / Right)
JumpZ or Space
AttackX
DashC

These bindings are defined entirely inside PlayerInputs.inputactions β€” open it with the Input Actions editor if you want to add, remove or rebind any of them.

Movement & Jump

Required components

ComponentPurpose
Rigidbody2DRequired by everything below. Set Body Type to Dynamic, freeze Z rotation.
CharacterGroundCheckerDetects ground contact via Physics2D, with a coyote-time buffer.
CharacterMovementHorizontal acceleration/deceleration and the "external force" API used by jump/dash/knockback.
PlayerJumpBuffered jump with variable height (cut short on release).
PlayerControllerAggregates the rig above and tracks combo/attack/facing state. Other scripts reference this one component instead of wiring each piece individually.

CharacterGroundChecker fields

FieldWhat it does
Ground LayerLayerMask of what counts as "ground" β€” set this to your Ground/Platform layer(s).
Dynamic FrictionLeave on. Swaps the Rigidbody2D's PhysicsMaterial2D between a "grounded" and "airborne" material so the character doesn't stick to walls during a jump.
Airborne Physics Material 2DA PhysicsMaterial2D with low/zero friction. Create one via Assets > Create > 2D > Physics Material 2D.
Grounded Physics Material 2DYour normal gameplay friction material (can be the same one already on the Rigidbody2D).
Why two materials? Box2D caches friction on a contact when it's created. Changing the existing material's friction value at runtime does not make already-active contacts pick up the change β€” which causes a player to feel "stuck" to a wall mid-jump. Swapping the material reference forces Box2D to rebuild the contact and apply the new friction immediately.

CharacterMovement fields

FieldDefaultWhat it does
Initial Speed2Speed snapped to instantly when starting to move from a standstill.
Max Speed8Top horizontal speed.
Acceleration30Units/secondΒ² applied while moving towards the target speed.
Deceleration Factor1.5Multiplier on Acceleration used when stopping (no input held).

PlayerJump / CharacterJump fields

FieldDefaultWhat it does
Buffer Jump Window0.15sHow long a jump press pressed slightly too early is remembered, executing as soon as landing/coyote conditions allow.
Jump Force16.5Upward velocity impulse on jump.
Cutting Factor0.5Multiplies remaining upward velocity when the jump button is released early β€” gives variable jump height.
Air Control Factor0.8 (0-1)How much horizontal acceleration applies when trying to reverse direction mid-air. 1 = full ground-like control, 0 = momentum can't be reversed in the air.

Wire your jump button to call PlayerJump.Jump() on press and PlayerJump.CutJump() on release β€” this is handled automatically if you use CharacterInputController (see next section).

Input Setup

CharacterInputController bridges Unity's Input System to the movement/jump/dash/attack components. It relies on the PlayerInput component's Send Messages notification behavior, so there is no manual event wiring.

  1. Add a PlayerInput component (from the Input System package) to the player GameObject.
  2. Set its Behavior to Send Messages.
  3. Assign (or create) an Input Actions asset with a Player action map containing these actions:
    Action nameTypeSuggested binding
    MoveValue / Vector2 (2D Vector composite, or a Gamepad stick)WASD / Left Stick
    JumpButton β€” add a "Press" Interaction set to "Press and Release" (see note below)Space / South Button
    DashButtonLeft Shift / East Button
    AttackButtonLeft Mouse Button / West Button
  4. Add CharacterInputController to the same GameObject. Its reference fields auto-fill via GetComponent on Reset if left empty. Set Action Map Name to match the action map you created above (default: "Player").
Jump needs a "Press and Release" interaction, or jump cutting won't work A Button action with no Interaction assigned defaults to an implicit Press interaction in Press Only mode. That mode only ever reports the button as pressed β€” it never fires the action's Canceled phase on release, so OnJump never receives isPressed = false and CharacterJump.CutJump() is never called. The jump will always reach full height no matter how briefly you tap the button.

Fix: select the Jump action, add a Press Interaction in the Interactions panel, and set its Trigger Behavior to Press and Release (not the default "Press Only"). This makes the action report both the press and the release, which is what variable jump height needs.
Renaming actions If your project already has an Input Actions asset with different action names, you don't need to rename them β€” just make sure the C# method names that PlayerInput's "Send Messages" looks for match: OnMove, OnJump, OnDash, OnAttack. These are exactly the four public methods CharacterInputController exposes, so as long as your action map calls those names (Input Actions β†’ action β†’ "Name" field), it will work.
Behavior must be "Send Messages" If PlayerInput.Notification Behavior is set to Invoke Unity Events or Invoke C# Events instead, CharacterInputController's methods won't be called automatically. Either switch it to Send Messages, or wire the events manually in the Inspector to call OnMove/OnJump/OnDash/OnAttack.

Using the Project-wide Input Actions asset

If you assign Unity's Project-wide Actions asset (Project Settings > Input System Package) to PlayerInput, the Inspector will warn:

Project-wide actions asset is not recommended to be used with Player Input
because it is a singleton reference and all actions maps are enabled by
default. You should manually disable all action maps on Start() and
manually enable the default action map.

This happens because the Project-wide asset is a single shared instance, so by default every action map on it (Player, UI, anything else you've defined) is active at once β€” input meant for a menu could leak into gameplay and vice versa. This is a known Input System behavior, not specific to this pack.

CharacterInputController already handles this for you: on enable, it disables every action map on PlayerInput.actions and re-enables only the one named in its Action Map Name field β€” exactly the workaround Unity's warning recommends. You can safely ignore the warning once this component is present. If you'd rather not see the warning at all, create a dedicated Input Actions asset for this character instead of using the Project-wide one (Project Settings still lets other systems use their own project-wide actions independently).

Dash / Roll optional

Add CharacterDashRoll to the player (it requires CharacterMovement and CharacterJump on the same GameObject). Triggered automatically by the Dash input action if you're using CharacterInputController, or call CharacterDashRoll.TriggerDash() yourself.

FieldDefaultWhat it does
Dash Speed Multiplier2Dash speed = CharacterMovement.MaxSpeed Γ— this value.
Dash Duration0.3sHow long the dash lasts before control returns to normal movement.
Buffer Dash Window0.15sIf Dash is pressed mid-air, the input is remembered for this long β€” landing within the window auto-executes the dash.
Player refautoUsed to read facing direction for the dash's direction. Auto-fills via GetComponent.

Pressing the opposite direction mid-dash interrupts it early and fires the DashTurn/DashTurnBack Animator triggers (see next section).

Animator Setup

PlayerAnimationStatesController drives your Animator Controller's parameters every frame from movement/combat state, and exposes methods you call from Animation Events to drive hitboxes. Add it to the player alongside an Animator component.

Required parameters

ParameterTypeSet when
IsRunningBoolHorizontal input is held and movement isn't disabled.
IsFallingBoolVertical velocity is negative and airborne.
IsGroundedBoolMirrors CharacterGroundChecker.IsGrounded.
WantsJumpBoolVertical velocity is positive and airborne (rising).
XVelocityFloatAbsolute horizontal velocity β€” drive a blend tree with this.
IsBufferedJumpBoolTrue while a jump press is buffered, waiting to land.
Dash if using dashβ€”CharacterDashRoll calls Animator.Play("Player-Roll") directly β€” name your roll state "Player-Roll", or change the string in CharacterDashRoll.ExecuteDash().
DashTurn / DashTurnBack if using dashTriggerFired when a turn input interrupts the dash.
Player-Hit if using PlayerHitSequenceβ€”PlayerHitSequence calls Animator.Play("Player-Hit") directly on every effective hit β€” name your hit-reaction state "Player-Hit", or change hitStateName on the component.

Attacks, turning and running aren't driven by Trigger parameters β€” PlayerAnimationStatesController calls Animator.Play(...) directly on a relay state for attacks, and on the "Player-Turn" / "Player-TurnBack" / "Player-Run" states for facing changes. Name those states accordingly (or update the corresponding Animator.StringToHash(...) calls in the script if you'd rather use your own names).

Attack States & Movement Lock

FieldDefaultWhat it does
Attack State TagAttackThe Animator Tag (set per-state in the state's Inspector, not a parameter) that every attack state must have. Used to detect "currently attacking" regardless of which combo step is playing.
Attack Combo Accept Window0.85Normalized progress (0-1) through the current attack clip after which a new attack input is allowed to interrupt it and advance the combo. Before this point, attack input is ignored so the current swing always finishes (or reaches its cancel window) first.
The player can't move while attacking Every state tagged with Attack State Tag automatically disables the player's horizontal movement input the instant it's entered, and re-enables it the instant it's exited β€” attacks always root the player in place. Tag only your attack states this way; tagging anything else (e.g. a hit-reaction or dash state) would also lock movement for as long as that state plays.

Hitbox Animation Events

PlayerAnimationStatesController exposes two generic methods β€” ActivateHitbox(string hitboxId) and DeactivateHitbox(string hitboxId) β€” instead of one hardcoded method pair per attack. Add an Animation Event on each attack clip at the frame the hitbox should turn on, pick ActivateHitbox from the function list, and type the hitbox's Id (e.g. Punch01) into the event's string parameter field. Add a second event at the frame it should turn off, picking DeactivateHitbox with the same Id string.

Why a string parameter instead of a method per attack Animation Events can carry a single string/float/int/Object argument, so a generic ActivateHitbox("Punch01") works for any attack you define β€” including ones you add after buying this pack β€” without ever touching PlayerAnimationStatesController's code. Add a new CharacterHitbox child with a new Id, wire up two Animation Events with that Id as the string parameter, and you're done.

These Id strings match the hitbox Id strings used in Combat & Hitboxes below β€” they're exactly the same value, just typed into the Animation Event's parameter field instead of a method name.

Combat & Hitboxes

1. Build the hitbox hierarchy

Create one child GameObject per attack under the player, each with a CharacterHitbox component and one or more trigger Collider2D shapes sized for that attack:

Player β”œβ”€β”€ Hitbox_Punch01 (BoxCollider2D, Is Trigger βœ“) Id = "Punch01" β”œβ”€β”€ Hitbox_Punch02 (BoxCollider2D, Is Trigger βœ“) Id = "Punch02" β”œβ”€β”€ Hitbox_Punch03 (BoxCollider2D, Is Trigger βœ“) Id = "Punch03" └── Hitbox_Punch04 (2x BoxCollider2D, Is Trigger βœ“) Id = "Punch04" ← multiple colliders, one attack
FieldWhat it does
IdString matched against CharacterHitboxController.Activate(id) calls β€” must be unique per hitbox.
Target LayersLayerMask of what this hitbox is allowed to hit (e.g. your Enemy layer).

CharacterHitbox automatically sets all its colliders to isTrigger = true and starts deactivated; you don't need to disable the colliders by hand.

2. Add the controller and damage dealer

On the player root (same GameObject as PlayerController), add:

ComponentPurpose
CharacterHitboxControllerCollects all child CharacterHitbox components, exposes Activate/Deactivate by Id, and mirrors hitbox X offsets when the player turns.
MeleeDamageDealerListens for hits and applies damage/knockback to whatever the hitbox touched.

CharacterHitboxController auto-collects its CharacterHitbox children if the array is left empty in the Inspector β€” you only need to drag them in manually if you want a specific subset.

3. Configure damage per attack

On MeleeDamageDealer, set a default damage/knockback/type, then optionally add per-hitbox overrides:

FieldWhat it does
Default DamageUsed for any hitbox Id without an explicit entry below.
Default KnockbackVector2. X = horizontal force (always positive β€” direction is resolved automatically from attacker/target positions). Y = vertical force, positive = upward.
Default Damage TypePhysical / Magic / Fire β€” extend the DamageType enum in Core/DamageInfo.cs if you need more.
Hitbox ConfigsArray of overrides: HitboxId (must match the CharacterHitbox.Id exactly), Damage, Knockback, DamageType.
Example knockback values (5, 0) β€” pure horizontal push. (3, 8) β€” launches the target up and sideways. (0, 10) β€” straight uppercut.

4. Make targets damageable

Any GameObject the hitboxes should affect needs a HealthComponent (or your own component implementing IDamageable) and ideally a Rigidbody2D if you want knockback to apply (HealthComponent implements IKnockbackable already).

Full hit pipeline

CharacterHitbox (OnTriggerEnter2D)
   β†’ CharacterHitboxController.OnAnyHit
       β†’ MeleeDamageDealer.OnAnyHit
           β†’ IDamageable.TakeDamage(DamageInfo)      on the target
           β†’ IKnockbackable.ApplyKnockback(...)      on the target, if it implements it
           β†’ CharacterHitboxController.NotifyHitProcessed(...)
               β†’ anything listening to OnHitProcessed (PlayerHitCounter, HitConnectEffect, ...)

Health & Damage

Add HealthComponent to the player and to any enemy/destructible that should be able to take damage.

FieldDefaultWhat it does
Max Health100Starting and maximum health value.
Destroy On DeathtrueAutomatically destroys the GameObject when health reaches zero. Turn off for the player if you handle death/respawn yourself.

Reacting to health events

Other systems should subscribe to events instead of polling β€” for example a health bar UI:

void Start()
{
    var health = player.GetComponent<HealthComponent>();
    health.OnHealthChanged += (current, max) => healthBar.fillAmount = current / max;
    health.OnDied += info => PlayDeathSequence();
}

Optional: combo / hit sequencing on the player

ComponentAdds
PlayerHitSequencePlays the "Player-Hit" reaction animation on every effective hit (see Animator parameters above).
PlayerHitCounterTracks a consecutive-hit streak for a combo/score UI. Resets on timeout or when the player takes damage.

Invulnerability & defense (extension points)

HealthComponent.TakeDamage automatically checks for an IVariableVulnerability component (e.g. PlayerController, which already implements it via Invulnerable()/Vulnerable()) and an IDefendable component on the same GameObject, if present. Call PlayerController.Invulnerable() during a dash or hit-stun window to make the player immune to damage, then Vulnerable() to restore it. See Extending: Interfaces for how to add your own block/parry component via IDefendable.

Hit Feedback (FX) optional

These add "game feel" on top of the combat pipeline above. None of them are required for combat to function β€” they only react to events already being fired.

CameraShake2D

A small self-contained shake β€” no Cinemachine or other camera package required. Add it to whichever Transform should actually move (your Camera, or a Cinemachine virtual camera's Transform if you do use Cinemachine).

FieldWhat it does
TargetTransform to shake. Defaults to its own GameObject if left empty.

Call cameraShake.Shake(amplitude, duration) yourself, or assign it to the fields below so the included effects drive it automatically.

HitImpactEffect player takes damage

Add to the same GameObject as HealthComponent (the player). Plays a brief time-slow plus a camera shake whenever the player takes effective damage.

FieldDefault
Slow Time Scale0.15
Slow Duration / Slow Recovery0.08s / 0.35s
Shake Amplitude / Shake Recovery3 / 0.4s
Camera ShakeDrag your CameraShake2D here. Leave empty to skip the shake.

HitConnectEffect player's attack lands

Add to the same GameObject as CharacterHitboxController (the player). More subtle than HitImpactEffect since it fires far more often β€” a short hit-stop plus a small shake, and optional VFX prefabs.

FieldDefault
Hit Stop Time Scale / Duration0.05 / 0.05s
Shake Amplitude / Recovery1.2 / 0.15s
Hit VFX PrefabInstantiated at the target's position on a normal hit.
Blocked Hit VFX PrefabInstantiated instead if the target defended (see IDefendable).

PlayerReceiveHitEffect player takes damage, receiver's perspective

Add to the player alongside HealthComponent. Spawns a VFX prefab at (or near) the player when they take damage β€” distinct from HitConnectEffect, which is the attacker's perspective.

FieldWhat it does
Hit VFX PrefabInstantiated on effective damage, oriented opposite to the hit direction (a "splash" effect).
Spawn OffsetLocal offset from the player's position β€” use to align with the torso.
What was intentionally left out The original effects this pack is based on also drove a URP Volume Vignette/Chromatic Aberration override and a Cinemachine noise-based shake. To keep this pack dependency-free, the Cinemachine shake was replaced with the self-contained CameraShake2D above, and the URP Volume overrides were removed rather than reimplemented (they require a specific Volume profile setup tied to URP). If you use URP and Cinemachine, you can re-add those by subscribing your own script to HealthComponent.OnDamaged / CharacterHitboxController.OnHitProcessed β€” see the Events Cheat Sheet.

Script Reference

Core

ScriptTypeSummary
DamageInfostructAmount, source, type, direction and knockback for a single hit.
IDamageableinterfaceCurrentHealth / MaxHealth / IsDead / TakeDamage(DamageInfo).
IKnockbackableinterfaceApplyKnockback(direction, knockback).
IDefendableinterfaceTryDefend(DamageInfo) β†’ DefenseResult. Implement for your own block/parry.
IVariableVulnerabilityinterfaceIsInvulnerable β€” checked by HealthComponent before applying damage.
IFacingDirectionProviderinterfaceFacingDir (+1/-1). Implemented by PlayerController; read by CharacterHitboxController to mirror hitboxes on turn β€” implement it on your own character type to use the hitbox system without depending on PlayerController.

Movement

ScriptSummary
CharacterMovementHorizontal acceleration/deceleration, speed/air-control multipliers, and the external-force API (ApplyConstantExternalForce, FreezeInAir, etc.) used by jump, dash and knockback.
CharacterGroundCheckerGrounded detection with coyote time and dynamic friction swapping.
CharacterJumpBuffered, variable-height jump. Base class β€” subclass it for non-player characters.
PlayerJumpPlayer-facing subclass of CharacterJump.
CharacterDashRollBuffered dash with turn-interrupt.
CharacterInputControllerInput System "Send Messages" bridge: OnMove / OnJump / OnDash / OnAttack.

Player

ScriptSummary
PlayerControllerAggregates the rig, tracks combo state (AttackCombo), attack/defend flags, facing direction, invulnerability. The main reference point for other scripts.
PlayerHitCounterConsecutive-hit streak with timeout, for UI.

Combat

ScriptSummary
CharacterHitboxOne attack's trigger collider(s) + OnHit event.
CharacterHitboxControllerActivate/Deactivate by Id, OnAnyHit / OnHitProcessed events, flip mirroring.
MeleeDamageDealerResolves damage/knockback per hitbox Id and applies it via IDamageable/IKnockbackable.
HealthComponentHealth, damage resolution (defense + invulnerability checks), knockback, death/heal events.
PlayerHitSequencePlays the "Player-Hit" Animator state on every effective hit taken.

Combat/FX

ScriptSummary
CameraShake2DSelf-contained decaying-noise camera shake.
HitImpactEffectTime-slow + shake when the player takes damage.
HitConnectEffectHit-stop + shake + VFX when the player's attack lands.
PlayerReceiveHitEffectVFX spawn when the player takes damage.

Animations

ScriptSummary
PlayerAnimationStatesControllerDrives Animator parameters every frame, exposes hitbox Activate/Deactivate methods for Animation Events.

Extending: Interfaces

The combat system is built around small interfaces in Core/ so you can add new entity types (enemies, breakables, shields) without touching the pack's scripts.

Make something damageable without using HealthComponent

using CloudedStudio.CharacterPack2D.Core;

public class BreakableCrate : MonoBehaviour, IDamageable
{
    [SerializeField] private float maxHealth = 20f;
    private float current;

    public float CurrentHealth => current;
    public float MaxHealth => maxHealth;
    public bool IsDead => current <= 0f;

    private void Awake() => current = maxHealth;

    public float TakeDamage(DamageInfo info)
    {
        float applied = Mathf.Min(info.Amount, current);
        current -= applied;
        if (IsDead) Destroy(gameObject);
        return applied;
    }
}

Add a block/parry component

Implement IDefendable on the same GameObject as HealthComponent β€” it will be consulted automatically before damage is applied:

using CloudedStudio.CharacterPack2D.Core;

public class ShieldBlock : MonoBehaviour, IDefendable
{
    public bool IsBlocking;
    [Range(0f, 1f)] public float DamageReduction = 1f; // 1 = full block

    public DefenseResult TryDefend(DamageInfo info)
    {
        return IsBlocking
            ? DefenseResult.Success(info.Amount, DamageReduction)
            : DefenseResult.Failed(info.Amount);
    }
}

Custom invulnerability

PlayerController already implements IVariableVulnerability. For a non-player entity, implement it yourself:

using CloudedStudio.CharacterPack2D.Core;

public class EnemyController : MonoBehaviour, IVariableVulnerability
{
    public bool IsInvulnerable { get; private set; }
    public void SetInvulnerable(bool value) => IsInvulnerable = value;
}

Events Cheat Sheet

EventRaised bySignatureFires when
OnDirectionChangedCharacterMovement(Vector2 newInput, Vector2 previousInput)Movement input vector changes.
OnGroundedChangedCharacterGroundChecker(GroundChangedEvent)Grounded state flips.
OnAnyHitCharacterHitboxController(string hitboxId, GameObject target)A hitbox overlaps a valid target, before damage is resolved.
OnHitProcessedCharacterHitboxController(string hitboxId, GameObject target, bool wasDefended)After damage has been resolved for that hit.
OnDamagedHealthComponent(DamageInfo info, float effectiveDamage)Damage is applied (effectiveDamage may be 0 if fully blocked).
OnDiedHealthComponent(DamageInfo info)Health reaches zero.
OnHealthChangedHealthComponent(float current, float max)Health changes for any reason (damage or Heal()).
OnCountChanged / OnTimerChanged / OnResetPlayerHitCounter(int) / (float) / ()Hit-streak count changes / every frame while active / streak resets.

Troubleshooting

The player won't jump, or jumps inconsistently.

Check that Ground Layer on CharacterGroundChecker includes your ground/platform layers, and that the player's Collider2D isn't set to trigger. If the player sticks to walls during a jump, make sure both PhysicsMaterial2D fields are assigned and Dynamic Friction is enabled.

CharacterInputController's methods never get called.

Confirm the PlayerInput component's Behavior is set to Send Messages, and that your action map's actions are literally named Move, Jump, Dash, Attack (PlayerInput derives the message name β€” OnMove, etc. β€” from the action name).

The jump always reaches the same height, even if I tap the button briefly.

This means OnJump is only ever receiving the press, never the release. Confirm with a quick test: temporarily add Debug.Log(value.isPressed) inside CharacterInputController.OnJump β€” if the Console only ever shows True and never False, the Jump action has no Interaction assigned, which defaults to an implicit Press interaction in Press Only mode (no Canceled phase on release). Add a Press Interaction to the Jump action and set its Trigger Behavior to Press and Release β€” see the warning in Input Setup.

Hitboxes never deal damage.

Check, in order: the hitbox's Target Layers includes the target's layer; the target has a Rigidbody2D or the hitbox's collider can resolve the target's root GameObject directly; the target has a component implementing IDamageable (e.g. HealthComponent); the Animation Event is actually calling Activate("YourId") at the right frame (scrub the clip in the Animation window to verify).

Animator parameters throw "parameter does not exist" warnings.

Add every parameter listed in Animator Setup to your Animator Controller β€” PlayerAnimationStatesController sets them every frame regardless of whether your state machine currently uses them.

Damage isn't reduced even though I attached a defense/invulnerability component.

HealthComponent looks up IDefendable / IVariableVulnerability via GetComponent on its own GameObject β€” make sure your custom component is on the same GameObject as HealthComponent, not a child or parent.

Time.timeScale gets stuck below 1.

This can happen if a GameObject with HitImpactEffect or HitConnectEffect is destroyed mid-effect outside of normal play (e.g. stopping Play Mode while a coroutine is running). Both scripts reset Time.timeScale in OnDestroy, but if you see it stuck after a hard stop, just re-enter and exit Play Mode once.

FAQ

Do I need the Input System package?

Only if you use CharacterInputController. Every other script reacts to plain method calls (Jump(), TriggerDash(), SetMovementInput(...), SetAttack()) β€” wire those from the legacy Input Manager or your own input solution if you prefer.

Can I use this for an enemy instead of the player?

Movement/jump/dash/combat scripts are not player-locked except PlayerController, PlayerJump, PlayerHitCounter, PlayerHitSequence and PlayerAnimationStatesController, which assume a single controlled character. For an enemy, drive CharacterMovement/CharacterJump/CharacterHitboxController/MeleeDamageDealer directly from your AI script instead of from PlayerController.

How do I add a 5th combo attack?

Increase Combo Length on PlayerController, add the new CharacterHitbox child + Animator state + Animation Events, and add a matching Activate/Deactivate method pair in PlayerAnimationStatesController following the existing pattern.

Why is there no target-lock / lunge-to-enemy system?

That's intentionally out of scope for this pack β€” it's a general-purpose 2D character controller, not tied to any specific targeting design. Build your own targeting on top of MeleeDamageDealer's pipeline if your game needs it.

Changelog

VersionNotes
1.0Initial release: movement, jump, dash, combo melee combat, health/damage, hit reactions, hit feedback FX.