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.
Package Contents
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.
- Create a player GameObject with a
Rigidbody2D(Body Type: Dynamic, Gravity Scale ~3-5, Freeze Rotation Z) and a trigger-freeCollider2Dfor the body. - Add CharacterGroundChecker, CharacterMovement, PlayerJump and PlayerController β see Movement & Jump.
- Add a
PlayerInputcomponent (Input System package) and CharacterInputController β see Input Setup. - Add an
Animatorwith the required parameters and PlayerAnimationStatesController β see Animator Setup. - (Optional) Add CharacterDashRoll for a dash β see Dash / Roll.
- Build your attack hitboxes as child objects and add CharacterHitboxController + MeleeDamageDealer β see Combat & Hitboxes.
- Add HealthComponent to the player (and to anything else that should be damageable) β see Health & Damage.
- (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:
| Action | Keys |
|---|---|
| Move | Arrow Keys (Up / Down / Left / Right) |
| Jump | Z or Space |
| Attack | X |
| Dash | C |
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
| Component | Purpose |
|---|---|
Rigidbody2D | Required by everything below. Set Body Type to Dynamic, freeze Z rotation. |
CharacterGroundChecker | Detects ground contact via Physics2D, with a coyote-time buffer. |
CharacterMovement | Horizontal acceleration/deceleration and the "external force" API used by jump/dash/knockback. |
PlayerJump | Buffered jump with variable height (cut short on release). |
PlayerController | Aggregates the rig above and tracks combo/attack/facing state. Other scripts reference this one component instead of wiring each piece individually. |
CharacterGroundChecker fields
| Field | What it does |
|---|---|
| Ground Layer | LayerMask of what counts as "ground" β set this to your Ground/Platform layer(s). |
| Dynamic Friction | Leave 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 2D | A PhysicsMaterial2D with low/zero friction. Create one via Assets > Create > 2D > Physics Material 2D. |
| Grounded Physics Material 2D | Your normal gameplay friction material (can be the same one already on the Rigidbody2D). |
CharacterMovement fields
| Field | Default | What it does |
|---|---|---|
| Initial Speed | 2 | Speed snapped to instantly when starting to move from a standstill. |
| Max Speed | 8 | Top horizontal speed. |
| Acceleration | 30 | Units/secondΒ² applied while moving towards the target speed. |
| Deceleration Factor | 1.5 | Multiplier on Acceleration used when stopping (no input held). |
PlayerJump / CharacterJump fields
| Field | Default | What it does |
|---|---|---|
| Buffer Jump Window | 0.15s | How long a jump press pressed slightly too early is remembered, executing as soon as landing/coyote conditions allow. |
| Jump Force | 16.5 | Upward velocity impulse on jump. |
| Cutting Factor | 0.5 | Multiplies remaining upward velocity when the jump button is released early β gives variable jump height. |
| Air Control Factor | 0.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.
- Add a
PlayerInputcomponent (from the Input System package) to the player GameObject. - Set its Behavior to
Send Messages. -
Assign (or create) an Input Actions asset with a
Playeraction map containing these actions:Action name Type Suggested 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 DashButton Left Shift / East Button AttackButton Left Mouse Button / West Button - Add CharacterInputController to the same GameObject. Its reference fields auto-fill via
GetComponenton Reset if left empty. Set Action Map Name to match the action map you created above (default:"Player").
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.
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.
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.
| Field | Default | What it does |
|---|---|---|
| Dash Speed Multiplier | 2 | Dash speed = CharacterMovement.MaxSpeed Γ this value. |
| Dash Duration | 0.3s | How long the dash lasts before control returns to normal movement. |
| Buffer Dash Window | 0.15s | If Dash is pressed mid-air, the input is remembered for this long β landing within the window auto-executes the dash. |
| Player ref | auto | Used 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
| Parameter | Type | Set when |
|---|---|---|
IsRunning | Bool | Horizontal input is held and movement isn't disabled. |
IsFalling | Bool | Vertical velocity is negative and airborne. |
IsGrounded | Bool | Mirrors CharacterGroundChecker.IsGrounded. |
WantsJump | Bool | Vertical velocity is positive and airborne (rising). |
XVelocity | Float | Absolute horizontal velocity β drive a blend tree with this. |
IsBufferedJump | Bool | True 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 dash | Trigger | Fired 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
| Field | Default | What it does |
|---|---|---|
| Attack State Tag | Attack | The 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 Window | 0.85 | Normalized 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. |
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.
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:
| Field | What it does |
|---|---|
| Id | String matched against CharacterHitboxController.Activate(id) calls β must be unique per hitbox. |
| Target Layers | LayerMask 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:
| Component | Purpose |
|---|---|
CharacterHitboxController | Collects all child CharacterHitbox components, exposes Activate/Deactivate by Id, and mirrors hitbox X offsets when the player turns. |
MeleeDamageDealer | Listens 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:
| Field | What it does |
|---|---|
| Default Damage | Used for any hitbox Id without an explicit entry below. |
| Default Knockback | Vector2. X = horizontal force (always positive β direction is resolved automatically from attacker/target positions). Y = vertical force, positive = upward. |
| Default Damage Type | Physical / Magic / Fire β extend the DamageType enum in Core/DamageInfo.cs if you need more. |
| Hitbox Configs | Array of overrides: HitboxId (must match the CharacterHitbox.Id exactly), Damage, Knockback, DamageType. |
(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.
| Field | Default | What it does |
|---|---|---|
| Max Health | 100 | Starting and maximum health value. |
| Destroy On Death | true | Automatically 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
| Component | Adds |
|---|---|
PlayerHitSequence | Plays the "Player-Hit" reaction animation on every effective hit (see Animator parameters above). |
PlayerHitCounter | Tracks 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).
| Field | What it does |
|---|---|
| Target | Transform 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.
| Field | Default |
|---|---|
| Slow Time Scale | 0.15 |
| Slow Duration / Slow Recovery | 0.08s / 0.35s |
| Shake Amplitude / Shake Recovery | 3 / 0.4s |
| Camera Shake | Drag 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.
| Field | Default |
|---|---|
| Hit Stop Time Scale / Duration | 0.05 / 0.05s |
| Shake Amplitude / Recovery | 1.2 / 0.15s |
| Hit VFX Prefab | Instantiated at the target's position on a normal hit. |
| Blocked Hit VFX Prefab | Instantiated 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.
| Field | What it does |
|---|---|
| Hit VFX Prefab | Instantiated on effective damage, oriented opposite to the hit direction (a "splash" effect). |
| Spawn Offset | Local offset from the player's position β use to align with the torso. |
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
| Script | Type | Summary |
|---|---|---|
| DamageInfo | struct | Amount, source, type, direction and knockback for a single hit. |
| IDamageable | interface | CurrentHealth / MaxHealth / IsDead / TakeDamage(DamageInfo). |
| IKnockbackable | interface | ApplyKnockback(direction, knockback). |
| IDefendable | interface | TryDefend(DamageInfo) β DefenseResult. Implement for your own block/parry. |
| IVariableVulnerability | interface | IsInvulnerable β checked by HealthComponent before applying damage. |
| IFacingDirectionProvider | interface | FacingDir (+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
| Script | Summary |
|---|---|
| CharacterMovement | Horizontal acceleration/deceleration, speed/air-control multipliers, and the external-force API (ApplyConstantExternalForce, FreezeInAir, etc.) used by jump, dash and knockback. |
| CharacterGroundChecker | Grounded detection with coyote time and dynamic friction swapping. |
| CharacterJump | Buffered, variable-height jump. Base class β subclass it for non-player characters. |
| PlayerJump | Player-facing subclass of CharacterJump. |
| CharacterDashRoll | Buffered dash with turn-interrupt. |
| CharacterInputController | Input System "Send Messages" bridge: OnMove / OnJump / OnDash / OnAttack. |
Player
| Script | Summary |
|---|---|
| PlayerController | Aggregates the rig, tracks combo state (AttackCombo), attack/defend flags, facing direction, invulnerability. The main reference point for other scripts. |
| PlayerHitCounter | Consecutive-hit streak with timeout, for UI. |
Combat
| Script | Summary |
|---|---|
| CharacterHitbox | One attack's trigger collider(s) + OnHit event. |
| CharacterHitboxController | Activate/Deactivate by Id, OnAnyHit / OnHitProcessed events, flip mirroring. |
| MeleeDamageDealer | Resolves damage/knockback per hitbox Id and applies it via IDamageable/IKnockbackable. |
| HealthComponent | Health, damage resolution (defense + invulnerability checks), knockback, death/heal events. |
| PlayerHitSequence | Plays the "Player-Hit" Animator state on every effective hit taken. |
Combat/FX
| Script | Summary |
|---|---|
| CameraShake2D | Self-contained decaying-noise camera shake. |
| HitImpactEffect | Time-slow + shake when the player takes damage. |
| HitConnectEffect | Hit-stop + shake + VFX when the player's attack lands. |
| PlayerReceiveHitEffect | VFX spawn when the player takes damage. |
Animations
| Script | Summary |
|---|---|
| PlayerAnimationStatesController | Drives 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
| Event | Raised by | Signature | Fires when |
|---|---|---|---|
| OnDirectionChanged | CharacterMovement | (Vector2 newInput, Vector2 previousInput) | Movement input vector changes. |
| OnGroundedChanged | CharacterGroundChecker | (GroundChangedEvent) | Grounded state flips. |
| OnAnyHit | CharacterHitboxController | (string hitboxId, GameObject target) | A hitbox overlaps a valid target, before damage is resolved. |
| OnHitProcessed | CharacterHitboxController | (string hitboxId, GameObject target, bool wasDefended) | After damage has been resolved for that hit. |
| OnDamaged | HealthComponent | (DamageInfo info, float effectiveDamage) | Damage is applied (effectiveDamage may be 0 if fully blocked). |
| OnDied | HealthComponent | (DamageInfo info) | Health reaches zero. |
| OnHealthChanged | HealthComponent | (float current, float max) | Health changes for any reason (damage or Heal()). |
| OnCountChanged / OnTimerChanged / OnReset | PlayerHitCounter | (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
| Version | Notes |
|---|---|
| 1.0 | Initial release: movement, jump, dash, combo melee combat, health/damage, hit reactions, hit feedback FX. |