TASVideos

Tool-assisted game movies
When human skills are just not enough

Game Resources / DOS / Mega Man

Table of contents [expand all] [collapse all]

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.

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
For some reason, running fdconfig.sys and declining all the options shortens load times between stages. More info: =forum/p/479329#479329, =forum/p/478684#478684.

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: 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.

An animation of Mega Man jumping forward over a backwards conveyor, comparing the earliest possible jumps with jumps that are 1 frame slower.

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.

A screenshot of a conveyor belt, with the non-solid tiles at either end highlighted with arrows to show that they push in the same direction as the conveyor.

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.

An animation of Mega Man jumping across platforms, demonstrating how he freezes in place whenever the camera recenters itself.

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.

A split-screen animation comparing Mega Man jumping at the top of a ladder, and not jumping. The jumping one is a tile and a half faster.

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/p/478286#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.

A screenshot of Mega Man falling toward the death barrier in Volt Man's stage, with the death tiles highlighted.

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 subrouting 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 subrouting 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.

An animation of Mega Man entering the bat cave in Sonic Man's stage, dying with F10, and respawning a few tiles down and to the left of where he was.

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.

An animation of Mega Man reaching the top of the long ladder in Dyna Man's stage, and clipping into the solid tile on the right.

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.

An animation of Mega Man traversing the death barrier in Volt Man's stage using the E Tank trick, and moving left to stand atop the pole at the left of the stage.

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):

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


Combined RSS Feed
GameResources/DOS/MegaMan last edited by Sand on 2019-01-30 01:24:38
Page info and history | Latest diff | List referrers | View Source