Console::Body

console 0.2.0 · app/console/body.rb · 350 lines · view source

Console::Body -- a kinematic platformer body.

Deliberately not a physics engine. There is no solver, no impulse response, no rigid bodies and no friction model, because the games that would use this do not need one -- a platformer needs gravity, a floor and a wall, and that is a state machine rather than a simulation.

body = Console::Body.new x: 64, y: 400, w: 16, h: 24, gravity: 0.4
def update
  body.vx = input.axis_x * 4
  body.vy += body.gravity
  body.jump 12 if input.pressed?(:accept) && body.grounded?
  body.update map.solids
end
def render
  draw_entity body.entity
end

Two decisions in here are what separate a body that feels solid from one that does not, and both are about NOT taking the obvious path.

SUBSTEPPING. A body falling for two seconds accumulates enough velocity to cross a platform in a single frame, and a single-frame overlap test then reports no collision at all -- the body passes straight through the floor and the game quietly breaks. Every move is therefore split into steps no larger than MAX_STEP, and each step is fully resolved. This is the single most important line of code in the file.

AXIS SEPARATION WITH RETRY. Moving on X and Y independently is what makes you slide along a wall instead of sticking to it: a wall zeroes vx but leaves vy alone, so you keep falling. But resolving X first and then Y once is not enough for corners -- a body moving diagonally into the corner of a step gets blocked on X and then also blocked on Y by the same block, and snags. So when X is blocked, the Y move is retried at the corrected position, and the body walks up onto the step instead of catching on it.

def initialize(options = {})

One options hash, never a positional list.

mruby binds a trailing key: value list to the FIRST optional parameter, so Body.new(x: 1, y: 2) would drop the whole hash into the first positional argument. One hash is the only spelling that works.

Pass entity: to drive an existing entity hash: x/y are read from it at construction and written back on every update, so draw_entity and the animation system keep working on the same object.

def rect
line 92
def entity
line 96
def grounded?
line 100
def landed?

True on the single frame the body transitions from airborne to standing. Edge-triggered on purpose: a while grounded? is wrong, because the body stays grounded for every frame it rests.

def fall_distance

Falling distance, in pixels, accumulated since the body left the ground. This is what a fall-damage or squash-and-stretch effect wants; it is not the instantaneous vy.

def wall?
line 118
def ceiling?
line 122
def wall_dir

:left, :right, or nil. Direction the body was pushed by a wall.

def floor

The solid the body is standing on, or nil. Useful for riding a moving platform, which is the usual reason to want the reference rather than just a boolean.

def roof
line 138
def jump(speed)

Set vertical velocity directly. A jump is this plus a grounded? check -- there is no separate jump arc to learn.

def update(solids = nil)

Advance one frame against a list of solid rects.

Each solid is { x:, y:, w:, h: } plus an optional one_way: flag. Solids are static; see the note in the README about why moving platforms are left to the cart.

def move_axis(axis, delta, solids)

Move along one axis in substeps, resolving after each. Returns true if any step ended up blocked.

def resolve(axis, solids)

Push the body back out of anything it overlaps, along axis only.

Resolving one axis at a time is what produces wall sliding: a horizontal block clears vx but leaves vy untouched, so gravity keeps working and the body slides down the wall rather than sticking to it.

def step_up_offset(solids)

How far the body may lift itself to walk over a low ledge, or 0 (the default) to treat every wall as a wall. See the step-up note in update.

def one_way_ignores?(axis, solid)

True when a solid should be IGNORED for this move.

A one-way platform is solid only from above and only while descending. Both halves matter: without the first, a body rising through a platform is stopped by it; without the second, a body standing on a platform can never jump back up through it.

def overlap?(a, b)
line 336
def sync_entity
line 341