Why SNES Final Fight Sucked

by Gnawtor, 02/08/2020

The SNES port of Final Fight was released in Japan in 1990, one year after the original CPS version debuted in the arcades. Done by an in-house team at Capcom, this effort to deliver an accurate port of the original game was marred by hardware limitations and cartridge storage restrictions. These circumstances unfortunately resulted in one of the most notorious disappointments in video game history, and an easy punching bag for retro gaming bloggers. If you’re the kind of person who would read an article like this you probably already know what grievous sins SNES Final Fight is guilty of, but let’s go over them anyway. First, one of the three playable characters, Guy, was removed. Second, the entire fourth level of the game and its boss were cut. Third, and arguably worst of all, they took away the ability to play in co-operative 2 player mode. And of course there was other collateral damage, such as a noticeable downgrade to the graphics and audio, but that was expected for arcade-to-console ports at the time. Despite these shortcomings, the game-play is remarkably faithful to the CPS version, especially when compared to other contemporary ports of the game (so long as you forget that the Sharp X68000 version exists). With the use of debugging tools, a close examination of both the SNES port and CPS original paints a less damning picture of the port than history has provided.

By default, the code for the original CPS game is incompatible with the SNES. The CPS uses a M68K as its main processor, and a Z80 co-processor for handling music and sound effects. The SNES used a 65816 for its main CPU and Sony’s SPC-700 as its audio processor. Digging deeper into the various game-play system handlers, there is a remarkable degree of similarities between how the CPS1 and SNES versions handle different tasks. The structure where each entity has a class (player, enemy, boss, etc.), a type (fat enemy, knife throwing enemy, one of the Andores, etc.) and a sub-type (Andore, Andore Jr, etc.) is preserved almost exactly the same between the two versions. There are of course some necessary differences in the fine print. For example, the SNES port requires players and NPCs to specify in their RAM structure where their decompressed sprite pattern data is stored while the CPS version has no such system. That being said, the RAM structure for these entities are otherwise quite similarly arranged. Other general game-play loop subroutines, such as the one responsible for decrementing and drawing the in-game round timer, are virtually direct adaptions of the original M68K code into 65816. This evidence strongly suggests that the team tasked with the SNES port had the luxury of working off the original M68K source, giving the in-house port an advantage over third-party ports which play more like approximations of the original game-play with a Final Fight paint job.

Generally speaking, the SNES port does play faithfully to its source material. Although the overall pace of game-play is a bit slower, the controls are very similar, and even quirks like Cody’s infinite combo work the same. But as we all know, the game simply wasn’t the same. Players were limited to selecting Haggar or one of either Cody or Guy, and the maximum number of enemies on the screen is limited to just three. This is where the hardware limitations of the SNES catch up with the ambitions of the port. The CPS can address any part of its graphics ROM at any time while the SNES has no distinction at the cartridge level between program and graphics data. This means that the SNES port had to flash all of its character data to VRAM before they could be drawn, limiting the range of displayable graphics to just what is needed for the current scene, with background tiles and animation patterns loading and unloading as needed. This left room in VRAM for tiles for one player, three enemies, the items and miscellaneous effects, enough background tiles to draw the current area, and then the arrangement data for sprites and the three background layers. This is why the SNES port can only draw up to three enemies at a time while the CPS can draw up to thirteen enemies, despite having less Work RAM to play with than the SNES.

A closer look at the way sprite patterns are constructed reveals the extent to which saving on cartridge space superseded performance. The SNES port runs in a graphics mode that allows for sprites to be either 8×8 or 16×16 pixels in size. Virtually all sprites are optimized to keep empty space at an absolute minimum, with patterns arranged in a staggered fashion to minimize the use of transparent space, bringing down the required number of tiles per pattern. While this decision certainly saved on cartridge storage, it came at a cost to performance. Each on-screen sprite object, regardless of its resolution, is counted towards the SNES’ upper limit of 128 objects. But in places where a 16×16 tile would have otherwise sufficed, if one quadrant were blank (filled entirely with the “transparent” color), it was broken apart into 3 tiles of 8×8 adding significantly to the object overhead. This is at least partially responsible for the noticeable slowdown in parts of the game and sprite flicker caused by too many competing objects.

An OAM pattern for the tire debris animation. Four objects could have been used, but eight were used to save on ROM storage by eliminating two blank tiles. Screenshots taken in No$sns.

Even with these savings on storage space, there simply was not enough room to fit all of the graphics required for the complete game. And so, some hard decisions had to be made. In the end, playable character Guy, fourth level Industrial Area, boss Rolento, three item containers (Dustbin, Billboard, Tel.Booth) and some food and points items fell by the wayside. The level intro and end demo sequences, which take up a surprisingly decent amount of space in the CPS ROM, also did not make it over.

Reverse engineering the SNES port’s code reveals that the intention was to include everything they could. The IDs for the missing player character, level, boss, and item containers do exist along with associated text strings, but generally their pointers or other entries in various tables are dummied out and point to code or data for other entities. The logic gates in the player handler suggests that the decision to only have 2 player characters was made quite early, as many subroutines that check the player’s character ID simply determine if the player is Haggar or not Haggar. Industrial Area’s level ID usually loads data for Slums, however the code that loads level background tiles makes a special exception when the current level is Industrial Area, bypassing the usual tile loading routine and forcing Belger and Andore’s sprites as background tiles for some reason. Industrial Area also has its own set of BRR sample data that gets flashed to the SPC700, and it contains an unused sound effect for Rolento’s grenade that can actually be played in the game’s sound test in Options Mode. Industrial Area has a handful of its arcade palettes buried in the palette region of ROM, and Rolento’s name-bar and portrait are intact. Rolento’s AI pointer in the boss handler points to Abigail’s AI, Dustbin points to Drumcan’s code, Billboard points to Freight’s code, and Tel.Booth points to the code for the glass objects from the second bonus room. Dustbin and Tel.Booth’s name-bar portraits also somehow survived among the other portrait graphics. This is not even an exhaustive list of cut content that still has leftovers in the ROM.

One can even see the high water mark of 2-player mode. While Player 2 has its own RAM section set aside, and some of its variables are even initialized on a new game, there are only a few instances where the player code will check to see if the player in action is 1P or 2P, and then use the direct page to address the player’s RAM. In all other instances, reads and writes to player RAM use 1P’s RAM address by default. In the CPS version, the player entity’s Type determines if they are 1P or 2P while the sub-type determines the character. The name-bar lookup code uses the entity Class/Type/Sub-type organization, but the pointer tables for name-bar data in the SNES version contains entries for a Player 2 entity type. Name-bar data also exists for all three of the player characters across all versions of the SNES port.

This is, as I understand it, why the SNES port of Final Fight wound up the way it did. The shortcomings of this port were not, as some commentators have assumed, due to a lack of effort or aptitude on the part of the developers. The shortage of ROM space was the greatest detriment to the project, being responsible for much content being cut, and performance issues that were brought about by scrounging for what little cartridge storage was even left. While the full details of this port’s development cycle are likely lost to history, I hope this article can at least provide some clearer context to its discussion.