Game Build Breakdown

Shadow Ronin Combat Feedback: Making Sword Hits Feel Fast in Three.js

Shadow Ronin is useful as a course because melee combat is unforgiving. If the hit window, camera, sound and invulnerability timing disagree, the player feels it instantly.

AdvancedThree.jsMelee Combat13 min read

Production context

This guide studies Shadow Ronin, a published Supagames game. Repository source: biggames/shadow-ronin/index.html.

The snippets below are intentionally smaller than the production file. They isolate transferable systems so you can reuse the idea in your own browser game without copying every detail of the original project.

1. Model Combat as Timed States

A sword swing is not a single boolean called attacking. It has wind-up, active frames, recovery and optional cancel windows. Once those states are explicit, the game can answer questions cleanly: can the player move, can damage happen, can a combo continue, and can a parry interrupt the enemy?

This pattern also keeps animation and damage synchronized. The blade may look wide, but damage should only be active for a short readable interval.

const katanaState = {
  phase: "idle",
  timer: 0,
  combo: 0,
  activeFrom: 0.14,
  activeUntil: 0.28,
};

function isBladeActive(attack) {
  return attack.phase === "swing"
    && attack.timer >= attack.activeFrom
    && attack.timer <= attack.activeUntil;
}

2. Parry Timing Needs Mercy Without Lying

Parry systems feel better when the visible window and the mechanical window are close but not brutally identical. A small buffer before contact and a small coyote window after input let humans react to animation without making every mistake free.

Store timestamps rather than chaining timeouts. The update loop can then compare current time with the last parry press, enemy attack time and projectile distance.

const PARRY_BUFFER = 0.16;
const PARRY_GRACE = 0.1;

function canParry(now, enemyStrikeTime, lastParryPressedAt) {
  const pressedEarlyEnough = now - lastParryPressedAt <= PARRY_BUFFER;
  const strikeIsCurrent = Math.abs(now - enemyStrikeTime) <= PARRY_GRACE;
  return pressedEarlyEnough && strikeIsCurrent;
}

3. Feedback Should Reuse Prepared Resources

Fast combat cannot create every slash trail, spark, hit label and shock ring during the exact frame of impact. Prepare shared geometries, materials and pools during setup. The hit code should move existing objects, set color/opacity and start lifetimes.

This is the same reason HTML overlays are useful for health and shield bars. They can communicate damage instantly without changing the scene lighting setup or forcing a new shader variant.

const sparkPool = Array.from({ length: 40 }, () => createSparkMesh());

function emitSparks(position, normal) {
  for (let i = 0; i < 8; i += 1) {
    const spark = sparkPool.find((item) => !item.userData.active);
    if (!spark) return;
    spark.position.copy(position);
    spark.userData.velocity.copy(normal).multiplyScalar(4 + Math.random() * 5);
    spark.userData.active = true;
  }
}

4. Shield and Health Are Different Signals

Shadow Ronin separates regenerating shield from vitality. That lets the game punish repeated mistakes without ending every small error. The HUD should show both, but the damage function should be the source of truth: shield absorbs first, health receives overflow, invulnerability prevents rapid repeated hits.

This pattern is valuable for action games with crowds. It creates a readable rhythm of mistake, recovery and danger instead of instant collapse.

function applyDamage(amount) {
  if (player.invulnerableFor > 0) return;
  const absorbed = Math.min(player.shield, amount);
  player.shield -= absorbed;
  player.health -= amount - absorbed;
  player.invulnerableFor = 0.75;
  publishHudState(player);
}

5. Build checklist

  • Use named melee phases rather than one attacking flag.
  • Give parries a small buffer and grace window so timing feels human but still skillful.
  • Pool slash, spark and hit effects; impact code should not allocate fresh objects.
  • Separate shield, health and invulnerability so enemy contact does not multi-hit in one frame.
Previous: Arena of Gods boss-rush design Next: Iron Vanguard mech loop