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, sodraw_entityand 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 optionalone_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
axisonly.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