written by slither (silverslither), last updated 2025-02-08.
This page documents regular movement in NTSC SMB1 or SMB2J. Collision is not covered in detail. For that, consult Dan Salvato's SMB1 Tile Collision Flowchart. In this article, the player will always be referred to as Mario. Unless otherwise stated, all points also apply to Luigi. All variable names are from doppelganger's SMB1 Disassembly.
At any point, Mario may be moving left or right, determined by the sign of his speed
(Player_X_Speed
). His speed is measured in subpixels/frame. In non-water areas, his speed is hard
capped at ±40 when RunningTimer
is set, or soft capped at ±24 otherwise. In water areas, his speed
is always hard capped at ±24, and soft capped at ±16 on the ground. Note that a "soft cap" refers to the fact
that in practice it is possible to exceed this cap by holding either no directional inputs or the opposite of
the directional input corresponding to the direction Mario is traveling. An exception exists during the pipe
entrance cutscenes (where GameEngineSubroutine == 7
) where Mario's speed is hard capped to 12.
Mario's subspeed (Player_X_MoveForce
) is only used in calculating acceleration, and does not
contribute to updating his position.
RunningTimer
is always set to 10 when B is held, Mario is on the ground and the directional inputs
held correspond to the direction Mario is moving (Left_Right_Buttons == Player_MovingDir
, where
Left_Right_Buttons
is set to SavedJoypadBits & 0x03
). It decrements by 1 every
frame these conditions aren't met.
Mario's horizontal acceleration is stored in FrictionAdderHigh
and FrictionAdderLow
,
following this algorithm:
$00 := 0 if Player_State != 0 then /* if Mario is in the air */ if Player_XSpeedAbsolute < 25 then $00 := $00 + 1 if RunningSpeed then /* RunningSpeed is set if Player_XSpeedAbsolute >= 28 and is only unset if Left_Right_Buttons == Player_MovingDir and Player_XSpeedAbsolute < 28 */ $00 := $00 + 1 end end else /* if Mario is on the ground */ if AreaType == 0 then /* if in water area */ $00 := $00 + 1 else if Left_Right_Buttons != Player_MovingDir then $00 := $00 + 1 if RunningSpeed then $00 := $00 + 1 end else if !(A_B_Buttons & #B_Button) and !RunningTimer then /* If B button is not pressed and RunningTimer is not set */ $00 := $00 + 1 end end FrictionAdderLow := FrictionData[$00] /* [0xe4, 0x98, 0xd0] for Mario, [0xb4, 0x68, 0xa0] for Luigi */ FrictionAdderHigh := 0 if PlayerFacingDir != Player_MovingDir then /* Double Acceleration */ FrictionAdderLow := FrictionAdderLow << 1 /* Rolls bit 7 into carry */ FrictionAdderHigh := FrictionAdderHigh << 1 /* Rolls carry bit in */ end
Note that pressing L+R sets PlayerFacingDir
to 3 (0b00000011), but Player_MovingDir
is
based solely on the sign of Mario's speed. This means that when L+R is pressed, Mario's horizontal acceleration
will always be double it's original value.
Mario's horizontal acceleration is then applied first to subspeed then to his speed depending on the sign of his
speed and directional inputs (note that if Mario's speed is zero, his subspeed will not be updated if no buttons
are held; see the last entry of #Speed Resets). His speed is then capped to his
maximum speed which depends on if RunningTimer
is set.
As Mario accelerates faster when he is facing the opposite direction he is going, one can deliberately take
advantage of this, performing a trick called a fast acceleration. TASes usually perform what's called a L+R fast
acceleration, where the inputs L+R, R+A
are performed at the beginning of a section. RTA runners,
who are either console runners physically unable to press the Left and Right D-pad buttons together, or emulator
players who follow a no L+R rule present on most speedrun leaderboards can still perform a fast acceleration, by
performing the inputs L, [nothing], R+A
. Note that [nothing] means no Left, Right, or A inputs, as
at least one other button has to be held (see the last entry of #Speed Resets). The
same idea is then applied by both RTA runners and TASers to gain maximum running speed quicker.
As there exists no possible acceleration value that sets FrictionAdderLow
to 0 (on NTSC), Mario's
subspeed will fluctuate even when his speed is capped. His subspeed can be frozen by releasing all directional
inputs while in the air, useful for manipulating the speed at which Mario will accelerate. A common example is
releasing right for one frame in order to maintain a subspeed of 0xF0 following a L+R fast acceleration,
allowing Mario to reach the maximum running speed slightly faster.
Mario's speed is reset to zero when:
Mario's speed and subspeed are reset to zero when:
RunningTimer
to be set. This can be exploited as you can obtain running speed in the
air after grabbing a powerup. This is used in the category "1-1 as Fire Mario" where the fire flower grab is
called a darG grab.
In SMB2J, depending on whether the most significant bit of the frame counter is set, wind pushes Mario by one
pixel every two or four frames (FrameCounter & 0b10000000 ? 2 : 4
). This can be exploited as a
method to clip into walls with much more leniency than normal, or even to fully clip while Mario is traveling
downwards, which is otherwise impossible.
Vertical movement behaves much like horizontal movement, where a positive speed (Player_Y_Speed
)
represents downwards movement and a negative speed represents upwards movement. Unlike with horizontal movement,
Mario's subspeed (Player_Y_MoveForce
) is applied to his subpixel value, as vertical movement would
be jarring otherwise.
Mario's gravity is only set under 4 different conditions:
if Player_State == 0 or (SwimmingFlag != 0 and (JumpSwimTimer != 0 or Player_Y_Speed >= 0)) then /* If Mario is on the ground, or Mario is swimming and either JumpSwimTimer is set or his speed is nonnegative */ JumpSwimTimer := 32 /* Reset timer */ y_register := 0 Player_YMF_Dummy := 0 /* Reset y subpixel */ JumpOrigin_Y_Position := Player_Y_Position /* Used for an edge case at the very beginning of Mario's jump where Player_Y_Speed < 0 and A_B_Buttons & PreviousA_B_Buttons & 0b10000000 == 0 */ Player_State := 1 for i := 0; i < 4 do if Player_XSpeedAbsolute < [9, 16, 25, 28][i] then break end y_register := y_register + 1 end if SwimmingFlag != 0 then /* If Mario is swimming */ y_register := 5 if Whirlpool_Flag != 0 then /* If Mario is in a whirlpool */ y_register := y_register + 1 end end VerticalForce := JumpMForceData[y_register] /* [0x20, 0x20, 0x1e, 0x28, 0x28, 0x0d, 0x04] for Mario, [0x18, 0x18, 0x18, 0x22, 0x22, 0x0d, 0x04] for Luigi */ VerticalForceDown := FallMForceData[y_register] /* [0x70, 0x70, 0x60, 0x90, 0x90, 0x0a, 0x09] for Mario, [0x42, 0x42, 0x3e, 0x5d, 0x5d, 0x0a, 0x09] for Luigi */ Player_Y_MoveForce := InitMForceData[y_register] /* [0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00] */ Player_Y_Speed := PlayerYSpdData[y_register] /* [0xfc, 0xfc, 0xfc, 0xfb, 0xfb, 0xfe, 0xff] */ if SwimmingFlag != 0 and Player_Y_Position < 20 then /* Check if above bounds in water areas */ Player_Y_Speed := 0 end end
Note that upwards and downwards gravity are stored in VerticalForce
and
VerticalForceDown
respectively. Only VerticalForce
is ever applied to Mario's speed
and subspeed. VerticalForceDown
is copied to VerticalForce
when Mario is in the air
and either his speed is greater or equal to zero, or the A button has been released.
Since Mario's downwards gravity is set to a low value (0x28) through screen transitions, he appears to fall very slowly. Since bouncing on enemies does not set his gravity, this is used in the SMB1 A Button Challenge TAS to cross large gaps in 1-1 and 8-2 without jumping.
Mario's speed is set in the following conditions:
PlayerYSpdData[y_register]
, and his subspeed is set to
InitMForceData[y_register]
(see pseudocode above for exact values).
The vertical speed cap for all sprite objects (including Mario) is implemented poorly. It checks for whether the
object's speed and subspeed both exceed their respective intended caps (SprObject_Y_Speed[x_register] >= $02
and SprObject_Y_MoveForce[x_register] >= 0x80
), forgetting to account for when it's speed is above
the cap, but it's subspeed is below the cap (SprObject_Y_Speed[x_register] > $02
and
SprObject_Y_MoveForce[x_register] < 0x80
). This allows Mario (and all other sprite objects) to exceed
the intended speed cap, with precise subspeed manipulation (e.g. differing jump heights, ceiling bonks, enemy bounces).
This is generally useless on NTSC, however it can be used in the PAL version to clip into the floor.