Game speed
Mega Man is one of those games that just
runs too fast.
It technically
is screen refresh–limited,
but its "natural" speed of one frame per refresh is too fast to be humanly playable.
Ilari explained it: "There are games that fail to sensibly limit themselves, and altough the effective framerate saturates at some point, that saturation point is unplayably fast."
A faster CPU setting would reduces load times, but not increase the speed of gameplay.
To produce an encode that runs slower and is more pleasant to watch,
without messing up the sound effects too much,
you can use the "soundhack" script under
Resources.
By the way, for casual play, you can fix the game's speed with
Brad Smith's
speed patch.
Emulator settings and boot
In the
JPC-RR assembly window,
put the Mega Man image on Hdd and FreeDOS on fda.
Clear the "Modules" box—this game doesn't need need sound card, FPU, MIDI.
At the prompt
Press F8 to trace or F5 to skip FDCONFIG.SYS/AUTOEXEC.BAT
press F8 to run fdconfig.sys, then answer n to all of the prompts.
dos=high[Y,n]?n
lastdrive=z[Y,n]?n
buffers=20[Y,n]?n
files=40[Y,n]?n
device=himem.exe[Y,n]?n
shell=cmd80x86.com command.com /K autoexec.bat[Y,n]?n
A:\>\autoexec.bat [Yes=ENTER, No=ESC] ? n
The setup menu (where you choose VGA and disable the joystick)
is not frame-oriented, unlike the game itself.
It's just a loop that polls a global variable that is set by the keyboard interrupt handler.
This means that you can provide inputs much faster than one per frame—but you have to go slow enough that the loop notices one of your inputs before you provide the next one.
The setupmenu.lua script under
Resources can automatically optimize this menu.
Between stages
You can press and release Space right after finishing the setup menu.
You don't have to wait for the title screen to appear.
The stage select screen recognizes inputs when you release a key.
So if you want to select Sonic Man, you should press Left and Enter
before the stage select appears, then release at the earliest moment at which the game recognizes the input.
This is not necessarily on a frame boundary, so you need to use sub-frame inputs.
Find the earliest frame at which the input is recognized, then load a savestate
and go back to one frame earlier.
Try pressing/releasing the Up key 9 times before releasing Left and Enter.
If it works, try again with pressing/releasing Up 8 times, and so on.
Each press/release of Up takes about 1/10th of a frame—this
is an easy way push inputs later in a frame, if they are not recognized at the beginning of a frame.
The same rule applies at the beginning of each stage.
It may appear that you can start moving 1 frame before the stage appears onscreen.
But usually you can start moving 1.x frames before the stage appears,
by using sub-frame inputs in the frame prior.
Sub-frame inputs don't seem to help at the "weapon get" screen.
After defeating a Robot Master, jumping to collect the card key
will shorten the animation of it falling to you.
Boss order
The
Volt skip requires an E Tank from
Sonic Man's stage, so Sonic must come before Volt.
Therefore the only orders worth considering are:
- Sonic, Volt, Dyna
- Sonic, Dyna, Volt
- Dyna, Sonic, Volt
Current runs use SVD. SDV seems strictly inferior to SVD,
because Dyna Man's stage benefits from the Force Field
but Volt Man's stage doesn't benefit from the Nuclear Detonator.
DSV is worth considering: the Dyna stage and boss fight would be slower,
but the Sonic boss fight would be faster.
Mechanics
Shooting
Press and release Space in the same frame in order to shoot every frame.
Physics
Mega Man accelerates horizontally at 2 pixels/frame/frame
up to a maximum speed of 8 pixels/frame.
Releasing an arrow key, or pressing the opposite direction,
sets your horizontal speed to 0 immediately (instant deceleration).
In most places, vertical acceleration due to gravity is 3 pixels/frame/frame.
Mega Man stops accelerating when he reaches a speed of 15 pixels/frame downwards.
When jumping down a ledge, it's usually best to jump
as early and as high as possible,
so that you're already falling down at maximum speed when you reach the ledge.
Mega Man's apparent position lags 1 frame behind his actual position
(use the HUD script under
Resources to see this).
When landing from a jump, you can jump again
as soon as Mega Man's within-tile
y position is 3;
e.g. a
y position of 819 is 51 3/16 when expressed in terms of tiles.
If you do it right, Mega Man will jump again
without ever entering his running animation.
This matters, for example, on backwards conveyors,
which slow you down as long as you are on the ground.
Some tiles are programmed to push Mega Man horizontally.
Solid pusher tiles (conveyor belts) only push while you are standing on them;
passable pusher tiles (like underwater in Sonic Man's stage)
push as you pass through them.
On either side of every conveyor there are single passable tiles
that push in the same direction as the conveyor.
Examples are highlighted in the screenshot below.
Avoid touching these tiles when they push backwards.
Some places have lower gravitational acceleration than 3 pixels/frame/frame,
for instance underwater in Sonic Man's stage.
Whenever gravity is
zero or negative,
you can affect Mega Man's vertical speed by pressing the Up and Down keys.
This is only useful in one place: the vertical fan shaft in Sonic Man's stage.
Hold the Up key to accelerate to maximum speed 1 frame faster.
Sub-tile alignment
Tiles are 16 pixels wide.
Because Mega Man accelerates in units of 2 pixels,
This means that under normal circumstances, if you have even pixel alignment, it will stay even;
and if you have odd alignment, it will stay odd.
The only way to change your alignment is to run into a wall
(going left makes the alignment even; going right makes it odd)
or grab a ladder (makes it even).
Having the right sub-tile alignment may allow you to,
for example,
start accelerating horizontally 1 frame earlier when exiting a vertical shaft.
Changing your x position by any amount other than 8 pixels
requires slowing down, so it's only worth doing when you are otherwise
stopped from making immediate progress.
To adjust your position by:
- 2 pixels: press Right, release Right, frame advance.
- 4 pixels: press Right, release Right, frame advance, frame advance, press Right, release Right, frame advance.
- 6 pixels: press Right, frame advance, frame advance, release Right, frame advance.
- 8 pixels: just keep running; this is Mega Man's running speed.
Vertical camera recentering delay
Whenever Mega Man lands a jump from a lower to a higher platform,
he freezes in place while the camera recenters on him vertically.
It doesn't happen when jumping downward.
The following animation stabilizes the camera
to show the freezing effect.
The only difference between the two examples is that
the top one avoids low-to-high jumps as much as possible.
Vertical camera recentering delay
also affects the tops of ladders.
You can diminish the delay by jumping at the top of the ladder,
which partially scrolls the camera up,
leaving it less distance to move when you touch the ground again.
it doesn't help unless Mega Man has a certain amount of headroom.
Horizontal camera recentering delay
The only time horizontal camera recentering delay matters
is after each of the Robot Master refights in Wily's stage.
At the moment the camera starts moving, you want to be:
- on the half of the screen nearest the exit gate
- moving at full speed
- with sub-tile alignment of 0 or 8
If you have bad alignment, the camera will take an extra frame to recenter itself. More info:
Forum/Posts/478286.
Item drops
An item drop iterates the
RNG once and chooses the item based on the most significant byte:
0 ≤ x < 4 | 1-up |
4 ≤ x < 24 | large health |
24 ≤ x < 48 | large weapon energy |
48 ≤ x < 88 | small health |
88 ≤ x < 128 | small weapon energy |
128 ≤ x < 256 | nothing |
Weapon select screen
You can switch weapons without losing time.
To switch to Sonic Wave, for example:
press S, press Esc, frame advance; release S, release Esc, frame advance.
Mega Man will continue moving at full speed during both of these frames.
Volt skip
You can skip most of Volt Man's stage
using the E Tank from Sonic Man's stage
to heal past a death barrier.
Take damage from a spark while facing right,
so that you stagger backwards into the wall.
Press Escape on the frame before you lose all your health.
On the next frame the weapon select menu will be open and you will have zero health.
Use an E Tank and then select a weapon to close the weapon select menu.
The same trick would work to pass a death barrier in Dyna Man's stage,
but you would have to farm a second E Tank and
it's not quite worth it.
Weapons
Damage chart
Enemy | HP | P | S | V | D |
---|
Guard dog | 8 | 1 | 1 | 1 | 1 |
Sewer Rat | 2 | 1 | 0 | ∞ | ∞ |
Sonic Man | 32 | 1 | 0 | 2 | 16 |
Volt Man | 32 | 1 | 6 | 0 | 2 |
Dyna Man | 32 | 2 | 1 | 6 | 0 |
Crorq | 32 | 1 | 6 | 2 | 4 |
Wily | 32 | 1 | 2 | 8 | 4 |
Nuclear Detonator
When you shoot the Nuclear Detonator, it starts a weapon_counter
timer.
After 16 frames, you can manually detonate it.
After 96 frames, it will detonate automatically.
Enemy AI
RNG
The random number generator is duplicated for each stage,
a copy of it appearing in each .bin file.
It is the same algorithm everywhere (a linear congruential generator),
but with a different seed in each stage.
uint16_t rng() {
static uint16_t rng_state = RNG_SEED;
rng_state = rng_state * 0xe51d + 0x3619;
return rng_state;
}
stage | seed |
---|
SECUR | 0x7536 |
SONIC | 0x5f27 |
VOLT | 0x3a05 |
DYNA | 0x9d86 |
WILEY | 0xd975 |
The fixed RNG per stage is convenient for TASing.
It means that RNG from earlier stages does not carry over into later stages;
the RNG is always in a known state at the beginning of a stage.
When the game samples from the RNG,
it tends to use the high-order bits,
perhaps as mitigation against the weaknesses of a linear congruential generator.
The RNG iterates at least once per frame.
Enemy AI, item drops, and other random events may iterate it more.
Data structures
Enemies, pickups, and other objects use a common 28-byte data structure.
Each stage (.bin file) contains an array of these data structures,
with one element for each thing that can appear in the stage.
Fields may have different meanings for different types of enemies.
struct actor {
/* +0 */ uint16_t x_pos;
/* +2 */ uint16_t y_pos;
/* +4 */ struct frm *current_frm; // pointer to current bitmap and hitbox
/* +6 */ uint8_t hp;
// damage_amount is how much Mega Man is damaged by contact.
// If the actor is a pickup, damage_amount instead indicates what kind:
// 1 = 1-up, 2 = E Tank, 3 = large health, 4 = small health, 5 = big weapon, other = small weapon
/* +7 */ uint8_t damage_amount;
/* +8 */ uint8_t unknown_0; // ???
/* +9 */ uint8_t flags; // (flags&0x80)!=0 => spawned/active
// The next 4 fields define a bounding box which the enemy will stay inside of.
/* +10 */ uint16_t x_max;
/* +12 */ uint16_t x_min;
/* +14 */ uint16_t y_min;
/* +16 */ uint16_t y_max;
/* +18 */ uint8_t counter_0; // Used for animation, and for timing the death explosion.
/* +19 */ uint8_t counter_1; // Sewer Rats use this counter while walking.
/* +20 */ uint8_t counter_2; // Sewer Rats use this counter while standing.
/* +21 */ int8_t facing; // negative = facing left; otherwise facing right
/* +22 */ uint16_t unknown_1; // ???
// Volt Man and Dyna Man use extra_0 and extra_0 to store horizontal and vertical speed.
/* +24 */ int16_t extra_0;
/* +26 */ int16_t extra_1;
};
The 18-byte data structure that current_frm
points to contains the actor's current bitmap,
and also hitbox dimensions.
struct frm {
/* +0 */ int16_t box_left; // <= 0
/* +2 */ int16_t box_right; // >= 0
/* +4 */ int16_t box_top; // <= 0
/* +6 */ int16_t box_bottom; // >= 0
/* +8 */ int16_t unknown_0; // ???
/* +10 */ int16_t unknown_1; // ???
/* +12 */ int16_t unknown_2; // ???
/* +14 */ int16_t unknown_3; // ???
/* +16 */ int16_t unknown_4; // ???
};
Guard dog
The guard dog's AI subroutine starts at offset 0x56c in SECUR.BIN.
The guard dog in the intro stage is unlike other enemies
in that it can only be damaged once per frame.
Sewer Rat
The Sewer Rat AI subroutine starts at offset 0x1912 in SONIC.BIN.
static uint8_t global_flag_7de;
void ai_rat(struct actor *rat)
{
if ((rat->flags & 0x80) == 0)
// Ignore if not currently spawned.
return;
}
blit(rat, rat->current_frm);
// A global flag at BIN_AREA+0x7de, referred to in many AI
// subroutines.
if ((global_flag_7de & 0x80) != 0)
return;
if (rat->hp == 0) {
// Rat is dead, now playing explosion animation. The animation
// lasts for 5 frames, but there are only 3 bitmaps.
// counter_0 == 5: O
// counter_0 == 4: o
// counter_0 == 3: o
// counter_0 == 2: o
// counter_0 == 1: .
// counter_0 == 0: done
rat->counter_0--;
if (rat->counter_0 != 0) {
// Death animation is finished.
unspawn_actor_and_drop_item(rat); // Unsets rat->flags & 0x80
} else if (rat->counter_0 == 3 || rat->counter_0 == 0) {
// Advance explosion animation.
rat->current_frm--;
}
} else {
// Rat is alive. counter_1 is the walk counter; counter_2 is the
// stand counter.
int16_t x_vel = 0;
if (rat->current_frm == FRM_RAT_STANDING && --rat->counter_2 != 0) {
// Rat is in standing state. Roll a random walk counter for
// when we transition to walking state. NB: the counter is
// re-rolled every frame while standing, but only the final
// one before transitioning to walking matters.
uint16_t r = rng();
rat->counter_1 = (((r << 4) | (r >> 12)) & 0x1f) + 32; // rand(32, 64)
} else {
// Rat is in walking state.
rat->counter_1--;
if (rat->counter_1 != 0) {
// Still walking.
x_vel = 4;
if (rat->counter_1 % 2 == 0)
rat->current_frm = FRM_RAT_WALKING_EVEN;
else
rat->current_frm = FRM_RAT_WALKING_ODD;
} else {
// Done walking. Roll a random stand counter.
rat->counter_2 = (rng() >> 13) & 0x07 + 8; // rand(8, 16)
rat->current_frm = FRM_RAT_STANDING;
}
}
// Reverse velocity if facing left.
if (rat->facing < 0) {
x_vel = -x_vel;
}
if (check_collision(rat, x_vel)) {
// Will the rat hit something?
rat->facing = ~rat->facing;
}
rat->x_pos += x_vel;
// Keep within this rat's boundary.
if (rat->x_pos > rat->x_max)
rat->facing = -1;
else if (rat->x_pos < rat->x_min)
rat->facing = 0;
// Now check for collision with player bullets.
// check_player_shot_collision returns the number of bullets
// that hit the rat on this frame.
uint8_t num_hits = check_player_shot_collision(rat);
uint8_t i;
for (i = 0; i < num_hits; i++) {
if (current_weapon == WEAPON_P) {
rat->hp--;
}
if (rat->hp <= 0
|| current_weapon == WEAPON_V
|| current_weapon == WEAPON_D) {
rat->hp = 0;
// Initialize death animation.
rat->current_frm = FRM_DEATH_BUBBLE_LARGE;
rat->counter_0 = 5;
break;
}
}
}
}
Volt Man
Volt Man's AI subroutine starts at offset 0x2204 in VOLT.BIN,
or offset 0x235b in WILEY.BIN.
Volt Man's initial jump is random.
The x speed is random in {−4, −6} (or {+4, +6} in the Wily-stage refight).
The y speed is random in {−14, −16}.
The x speed doesn't matter, but the smaller jump is 2 frames faster.
Dyna Man
Dyna Man's AI subroutine starts at offset 0x1fdb in DYNA.BIN,
or offset 0x2a1a in WILEY.BIN.
Dyna Man's initial jump is random.
The x speed is random in {−4, −5, −6, −7}.
The y speed is random in {−7, −9, −11, −13}.
Ideally, you want him to jump towards you as fast as possible
(so he gets close enough to use the Force Field on him),
and low enough that he remains in reach of Mega Man's jump.
Underexplored glitches
Lack of collision with Force Field
Sometimes, in the corridor before Volt Man in Wily's stage,
the Force Field will pass through enemies rather than destroying them.
It's not known why this happens or under what circumstances,
except that it has something to do with having used the Nuclear Detonator.
You can see it in Lizstar's AGDQ 2019 run:
Death warp
Trigger tiles are special map tiles that do something when Mega Man passes over them,
for example spawning the enemies for the next room or activating a checkpoint.
You can see the trigger tiles with the
HUD script
or in the annotated
Maps.
A minor death warp is possible with a least one checkpoint,
where the checkpoint is located a tiny bit farther ahead in the stage.
Trigger tile reentrancy
I found a bug having to do with activating a trigger tile twice.
After the fan shaft in Sonic Man's stage,
there is a trigger tile in the downward tube at coordinates (79, 15)
that spawns the Sewer Rat and makes the wall blasters start shooting.
The
Sewer Rat AI is supposed to work like this:
walk for
rand(32, 64)
frames, stand for
rand(8, 16)
frames, repeat.
If you jump straight down into the room (touching the trigger tile only once),
that is the behavior you'll see.
But if you walk off the edge at an angle (touching the trigger tile twice),
you'll instead see the rat walk for about 256 frames before starting
its normal cycle.
It happens because the rat gets reinitialized after it has already started its AI cycle,
and an integer underflow occurs.
Abusing the glitch doesn't help here,
because you want the rat to stand anyway
so it's easier to damage-boost past it.
But possibly there are other such bugs in other places.
The trigger tile callback in question starts at offset 0x9f5 in SONIC.BIN.
The portion having to do with the Sewer Rat is:
rat->flags |= 0x80; // spawn
rat->hp = 2;
rat->damage_amount = 6;
rat->current_frm = FRM_RAT_WALKING_EVEN;
rat->x_pos = 0x3f8; // tile position 63 8/16
rat->y_pos = 0x1db; // tile position 29 11/16
rat->x_min = 0x3e8; // tile position 62 8/16
rat->x_max = 0x4a0; // tile position 82 8/16
The first time Mega Man hits the trigger tile,
the rat is in the walking state with
counter_1
= 0.
The first call to the
Sewer Rat AI
underflows
counter_1
= 255 and transitions to the standing state.
But then if Mega Man hits the trigger tile a second time,
it forces the rat back into the walking state,
but without resetting
counter_1
.
So now the rat will walk for 255 frames.
Wall clip
While climbing a ladder, hold Right and J (jump).
At the top of the ladder, release Up, and you'll clip one tile into the solid wall on the right.
It doesn't work going left.
Out of bounds
After traversing the death barrier in Volt Man's stage,
if you go left rather than right
you'll end up out of bounds,
off the left edge of the stage.
There are some invisible solid platforms there.
Maps
Static maps don't tell the whole story.
For example, in Sonic Man's stage,
if you use the Nuclear Detonator to break into the large secret chamber,
the game will build a wall inside it,
preventing you from going all the way through.
Annotations (see map/main.go under
Resources for details about flags):
- center gray: tile ID
- upper-left magenta: Flags0: animation/destructibility
- upper-right yellow: Flags1: solidity, 0x80=trigger, 0x40=masked, 0x01=climbable
- lower-left red: Flags2: contact damage
- lower-right cyan: Flags3: physics, upper nibble is horizontal push, lower nibble is vertical gravity
Memory addresses
Base memory addresses differ depending on whether you
have loaded HIMEM during boot.
As far as we can tell, it's faster not to load HIMEM.
Without HIMEM:
SEG_000 | 0x2f970 |
MAP_AREA | 0x3859e |
BIN_AREA | 0x5f870 |
With HIMEM:
SEG_000 | 0x5380 |
MAP_AREA | 0xdfae |
BIN_AREA | 0x35280 |
camera_x | word SEG_000 + 0x8c04 |
camera_y | word SEG_000 + 0x8c0a |
camera_target_x | word SEG_000 + 0x8c16 |
camera_target_y | word SEG_000 + 0x8c1c |
megaman_x | word SEG_000 + 0x10db7 |
megaman_y | word SEG_000 + 0x10db9 |
megaman_frm (current bitmap) | ptr SEG_000 + 0x10dbb |
megaman_hp | byte SEG_000 + 0x10dbd |
megaman_x_vel | word SEG_000 + 0x10dcf |
megaman_y_vel | word SEG_000 + 0x10dd1 |
player_shot_1.x | word SEG_000 + 0x10dd3 |
player_shot_1.y | word SEG_000 + 0x10dd5 |
player_shot_2.x | word SEG_000 + 0x10def |
player_shot_2.y | word SEG_000 + 0x10df1 |
player_shot_3.x | word SEG_000 + 0x10e0b |
player_shot_3.y | word SEG_000 + 0x10e0d |
weapon_counter (used for Nuclear Detonator timer) | byte SEG_000 + 0x10de7 |
explosion_counter | byte SEG_000 + 0x10f15 |
num_lives | byte SEG_000 + 0x115b1 |
num_etanks | byte SEG_000 + 0x115b2 |
stages_finished 0x40=SECUR 0x01=SONIC 0x02=VOLT 0x04=DYNA | byte SEG_000 + 0x115b3 |
current_weapon 0=P 1=S 2=V 3=D | byte SEG_000 + 0x115b5 |
weapon_energy_S | byte SEG_000 + 0x115b7 |
weapon_energy_V | byte SEG_000 + 0x115b8 |
weapon_energy_D | byte SEG_000 + 0x115b9 |
setup_menu_settings | byte SEG_000 + 0x18b90 |
setup_menu_settings
bit 0x08 is a global invincibility flag.
It can be convenient to set that bit when you're trying something out
without wanting to worry about damage.
stage_width | word MAP_AREA + 0x0 |
stage_height | word MAP_AREA + 0x4 |
The per-stage variables are relative to BIN_AREA
and vary across stages.
variable | | SECUR | SONIC | VOLT | DYNA | WILEY |
---|
rng_state | word BIN_AREA + | 0x2fc | 0x7d5 | 0xbf4 | 0xb01 | 0xb00 |
ACTORS array | BIN_AREA + | 0x1ac | 0x2ba | 0x340 | 0x3a4 | 0x4a0 |
SONIC 1st wall HP | byte BIN_AREA + | N/A | 0x793 | N/A | N/A | N/A |
SONIC 2nd wall HP | byte BIN_AREA + | N/A | 0x799 | N/A | N/A | N/A |
Some of the notable actors in each stage:
SECUR guard dog | ACTORS[10] |
VOLT Volt Man | ACTORS[40] |
DYNA Dyna Man | ACTORS[60] |
WILEY Volt Man | ACTORS[24] |
WILEY Dyna Man | ACTORS[47] |
Resources
Unorganized resources and reverse engineering tools,
including a HUD script and an automatic optimization script.
git clone https://www.bamsoftware.com/git/megamanpc.git