Skip to content

Player Grappling

DarcyMF edited this page Oct 7, 2026 · 6 revisions

Grapple System

Introduction

This page documents the Sprint 3 implementation in PR #196, PR #211 and PR #229. Earlier revision-specific test results are retained as historical evidence.

The Grapple System lets the player fire a hook at terrain and swing from it as a pendulum. It reuses the projectile pipeline built for the bow, so the grapple is a physical arrow that travels and lands rather than an instant raycast.

Swinging is driven by Box2D physics rather than scripted motion, so momentum carries naturally into jumps and releases.

Controls

Input Action
Inventory selection controls Select a rope arrow
Hold right click Charge the grapple and display its trajectory preview
Release right click Fire the grapple
Press and release right click while attached Let go of the rope
Space while attached Release with an upward boost
A/D while attached Pump the swing
W/S while attached Climb or descend

Rope arrows are not consumed. Once attached, the player does not need to hold right click to remain connected. A charge cannot start while attached, so letting go of right click on the next click drops the rope instead of firing. The same release also cancels a rope that is still in flight.

Architecture

ItemUseComponent routes the selected rope arrow into the grapple charge and release path. The bow and grapple share BowCharge, keeping their charge response consistent.

GrappleArrowComponent handles projectile contact and attachment. GrappleComponent owns the rope path, attachment state, swing and climbing behaviour. Rendering components display the aiming arc, travelling rope and attached path.

The projectile and rope have separate responsibilities: terrain contact identifies an attachment, while the grapple component manages the player constraint and wrapped path.

flowchart LR
    Input["Input / ItemUseComponent"] -->|charge and release| Grapple["GrappleComponent"]
    Grapple --> Charge["BowCharge"]
    Grapple --> Factory["ProjectileFactory"]
    Factory --> Arrow["GrappleArrowComponent"]
    Arrow -->|contact and attachment| Grapple
    Grapple --> Rope["Rope constraint and wrapped path"]
    Grapple --> Renderer["GrappleRenderComponent"]
Loading

Charging and Firing

Pressing right click with a rope arrow selected sends grappleDrawStart, and letting go sends grappleDrawRelease. These are separate from the bow's chargeStart and chargeRelease so a rope draw can never fire the bow, or the other way round. GrappleComponent answers an accepted draw with grappleChargeStart and grappleChargeFire, which PlayerAnimationController plays as the draw and release animations. A press that is ignored (still on cooldown, or already attached) sends neither, so no animation plays for it.

Launch speed comes from the same BowCharge curve as the bow. The multiplier rises in a straight line from 0.3 for a tap to 1.5 after 1.5 seconds held, so with the base speed of 22 the arrow leaves at about 6.6 to 33 units per second. The range stays at 50.

Like every other arrow, the grapple arrow arcs under gravity (ARC_GRAVITY_SCALE, 0.4 of world gravity) instead of flying straight.

Aim Preview

While a rope arrow is charging, GrappleIndicatorRenderComponent draws a dotted white arc with a larger dot where it lands. It simulates the launch point, gravity and current charge speed that the real arrow would get if released at that moment, so the arc stretches as the charge builds. It stops at the first solid surface or at the maximum range. It disappears as soon as the arrow is fired or the charge is cancelled. The shared arc simulation lives in ArcIndicatorRenderComponent.

Attachment Point

The arrow turns about its centre as it flies, so its hitbox stays under the drawn sprite. When it hits terrain, GrappleArrowComponent anchors the rope to the nearest point on the surface it hit, not to wherever the arrow's centre happened to be, so the rope ends on the wall rather than in the air beside it. A hit within 1.2 units of the player is ignored.

Grapple Lifecycle

  1. Holding right click starts an accepted charge and its draw animation.
  2. The trajectory preview displays the expected flight arc.
  3. Releasing fires the arrow with charge-dependent speed.
  4. The rope follows the arrow during flight and records bends around terrain.
  5. A valid terrain contact attaches the rope and preserves the wrapped path.
  6. Attachment clears the firing cooldown.
  7. Jumping releases the rope with an upward boost.
  8. Respawning clears charging, the flying arrow and the attached rope.

A request rejected by cooldown does not start the draw animation. Attachment persists independently of whether right click remains held.

Swing Physics

The rope is a Box2D RopeJoint, which only caps how far the player can get from the anchor. It goes slack when the player is closer, so an anchor below the player does not force movement around a fixed-radius arc. An earlier version used a DistanceJoint, which behaves like a rigid rod and swung the player round the anchor even when it was underneath them. collideConnected is true so the player still collides with the surface they grappled.

Swing force is applied along the arc, perpendicular to the line from the anchor to the player. The direction is flipped when needed so the left key always pushes left and the right key always pushes right, including when the player is above the anchor, where a fixed quarter turn would push the wrong way:

Vector2 tangent = new Vector2(-r.y, r.x).nor();
boolean pointsTheWrongWay =
    Math.abs(tangent.x) > MIN_TANGENT_X ? tangent.x * direction < 0 : direction < 0;
if (pointsTheWrongWay) {
  tangent.scl(-1f);
}

When the player is almost level with the anchor the arc is nearly vertical and tangent.x is too small to pick a side, so it falls back to a fixed convention (MIN_TANGENT_X). Movement remains physics-driven so momentum carries into release.

Movement Integration

While attached, PlayerActions redirects walking input into the swing rather than calling updateSpeed(). Without this, the impulse that snaps horizontal velocity to walking speed would cancel the pendulum every frame. The same guard applies in stopWalking(), sprint() and stopSprinting().

Air control is reduced to 10% of ground control so a fast landing from a swing isn't immediately wiped when a movement key is pressed.

Jumping while attached releases the rope and adds an upward impulse scaled by ROPE_JUMP_MULTIPLIER on top of existing velocity, so swinging fast and jumping at the bottom of the arc launches the player properly.

Terrain Wrapping and Rope Climbing

The rope tracks bends during projectile flight and preserves them when attachment completes. Its path remains consistent across the transition from a flying arrow to an attached rope.

Corner-climbing targets are tracked separately from rope bends. This allows the player to negotiate terrain corners without replacing the contact geometry that describes the wrapped rope.

When extending climbing behaviour, preserve the distinction between the player’s movement target and the rope’s persistent contact path.

Tuning

Constant Class Value Purpose
SWING_FORCE GrappleComponent 7 Tangent force multiplier, scaled by player mass
MAX_SWING_SPEED GrappleComponent 7 Cap on pumped speed
SWING_DAMPING GrappleComponent 0.5 How fast the swing decays
Release damping GrappleComponent Saved pre-grapple value Restore drag after letting go
GRAPPLE_COOLDOWN GrappleComponent 2s Firing cooldown; cleared on attachment
ROPE_JUMP_MULTIPLIER PlayerActions 0.7 Upward kick when jumping off
AIR_CONTROL PlayerActions 0.1 Steering while airborne
GRAPPLE_ARROW_SPEED ProjectileFactory 22 Base speed before charge scaling
GRAPPLE_ARROW_RANGE ProjectileFactory 50 Distance from live shooter position
MIN_SPEED_FACTOR BowCharge 0.3 Speed multiplier for a tap
MAX_SPEED_FACTOR BowCharge 1.5 Speed multiplier at full charge
MAX_CHARGE_SECONDS BowCharge 1.5 Hold time for a full charge
ARC_GRAVITY_SCALE ArrowProjectileComponent 0.4 Share of world gravity acting on every arrow
SPAWN_OFFSET GrappleComponent 0.6 How far in front of the player, in player widths, the arrow appears
MIN_TANGENT_X GrappleComponent 0.05 Below this the swing side comes from a fixed convention
MIN_TRAVEL GrappleArrowComponent 1.2 Hits closer to the player than this do not attach

Testing

Automated coverage includes:

  • Charge acceptance, firing and cooldown behaviour.
  • Shared bow and grapple charge-curve behaviour.
  • Slack rope and swing direction.
  • Wall-face attachment from different approach angles.
  • Flight-time wrapping and bend preservation.
  • Climbing around terrain corners.
  • Respawn cleanup.
  • Rope, preview and arrow rendering.
  • The preview only drawing while a rope arrow is charging, and its launch speed following the charge.
  • The 16-contact path limit.

GrappleArrowAnchorIntegrationTest uses real arrows, real Box2D bodies and a real wall, and checks the rope ends on the wall face for arrows fired from several angles.

Relevant classes include BowChargeTest, GrappleComponentFiringTest, GrappleComponentRopeTest, GrappleArrowAnchorIntegrationTest, GrappleIndicatorRenderComponentTest, GrappleFlightRopeTest, GrappleCornerClimbTest and GrappleContactLimitTest.

Manual check Expected result
Short and long charges Different projectile speeds consistent with the preview
Fire at a wall from a shallow and a steep angle The rope ends on the wall surface, not in the air beside it
Press and release right click while attached The rope drops and no new arrow is fired
Press right click again while the grapple is on cooldown No draw animation plays
Fall towards an anchor below the player Rope allows slack until its length constrains movement
Fire past terrain corners Rope wraps during flight and retains bends on attachment
Climb around a corner Player progresses around terrain without losing the wrapped path
Respawn while charging, during flight or while attached No charge, projectile or rope remains active

Known Limitations

Wrapping is constrained by supported terrain geometry and the contact limit. Physics and rendering tests cover specific layouts; unusual terrain arrangements and combinations of movement, collision and climbing still require level playtesting.

An arrow that hits terrain closer than 1.2 units to the player does not attach, so a point-blank shot at a wall right next to the player can miss.

The aim preview is an approximation. It steps its own simulation, and Box2D integrates the real arrow a little differently, so on long shots the arrow can land slightly below the landing dot.

Relevant Development Links

PR Work
#196 Regression coverage, collision fixes, maintainability and Sonar coverage reporting
#211 Charged grapple, rope physics, flight wrapping and corner climbing
#229 Movement, melee and animation overhaul

Contribution Attribution

The grapple system, swing physics, rope rendering, camera aiming, arrow switching and associated tests were implemented by Darcy Franklin (DarcyMF). The projectile pipeline the grapple arrow reuses was built by Dhiren Malik (deerainn) as part of the ranged attack system.

That original credit includes historical arrow cycling; current selection integrates the inventory system. Later work includes Rohan Shirkhedkar's hold/SOLID-layer integration and new terrain wrapping, bend persistence and W/S climbing; Tristan Sheedy's damping restoration; and Jared Smith's moving-platform integration. New grapple geometry and climb tests accompany Rohan's merged commits.

Dhiren Malik added the player-action/grapple interaction regressions and merged-input/ledge coverage; these complement the grapple component and geometry tests without reassigning their authorship.

Sprint 3 work includes Darcy Franklin’s charged firing, trajectory preview, slack-rope behaviour and wall attachment; Rohan Shirkhedkar’s flight-rope tracking, preserved bends and corner climbing; and Dhiren Malik’s lifecycle, contact-limit and rendering regressions.

Clone this wiki locally