Animations & Hitboxes

All character actions are defined as one or more animations. An animation is an ordered list of frames with each frame containing information about the sprite that will be drawn to the screen and hitboxes which define the exact collision geometry of the attack. Hitboxes can be tricky to work with at first because they are defined relative to a default position of [0,0] with the character facing left (i.e. player 2).

In the previous section there was a sample program which would simply punch if the two players were with in 80 pixels of each other. That's a pretty naive approach though, every character has a different shape and size so the same distance can't be used for different opponents. Instead we can look at the hitbox data and punch as soon as the hitboxes would overlap.

// This function will return the Hitbox associated with the given animation frame and adjusted to
// the player's *current* position.
function getAdjustedHitbox(player, animationName, animationFrame) {
  let animation = player.animations[animationName];
  let hitbox = animation.frames[animationFrame].hitbox;
  if (player.flipX) {
    hitbox = hitbox.flipX();
  }
  return hitbox.translate(player.position);
}

// Given an animation which has attack frames, this function returns the index of the frame with
// the first attack hitbox.
function findFirstAttackFrame(animation) {
  for (let ii = 0; ii < animation.frames.length; ++ii) {
    if (animation.frames[ii].hitbox.attack) {
      return ii;
    }
  }
}

// Remembers the last state of `punchHeavy`. This is a little more sophisticated than the
// `battle.frame % 2` trick.
let didPunch = false;
// This is the index of the first attack frame of `punchHeavy`
let myAttackFrame = findFirstAttackFrame(player.animations.punchHeavy);

module.exports.frameHandler = function() {
  // Get current opponent hitbox and what our hitbox would be if we threw a heavy punch right now
  let opponentHitbox = getAdjustedHitbox(opponent, opponent.animationName, opponent.animationFrame);
  let attackHitbox = getAdjustedHitbox(player, 'punchHeavy', myAttackFrame);

  // Check to see if our punch would connect
  let intersects = false;
  if (opponentHitbox.vulnerable) {
    intersects = opponentHitbox.vulnerable.some(hitbox => attackHitbox.attack.intersects(hitbox));
  }
  if (intersects) {
    // Press and release punch heavy every frame
    joystick.punchHeavy = !didPunch;
    didPunch = !didPunch;
    return;
  } else {
    // If the punch wouldn't connect then walk closer
    joystick.direction = player.position.x < opponent.position.x ? RIGHT : LEFT;
  }
  // Reset punch heavy flag
  didPunch = false;
};

This program will attempt pixel-perfect punches no matter which opponent you are against. It doesn't matter if an attack connects by 1 pixel or by 20 pixels, a connected attack always counts the same.

Pixel-perfect hitbox.