2016/12/20

Development Insight #2: ASM Contest Round 000

I am a showman at heart. I love to create and present things for everyone to marvel at. The attention thrills me. So when the ASM Contest was announced, it felt like the most amazing thing.

In the end, I had to drop out of the contest for personal reasons. But in the beginning, things were different. "This is my time to shine", I thought. You see, for some reason, no one considers me an ASMer, even though that is probably my strongest skill, the skill I've been honing and practicing all these years. Everyone considers me an artist, even though my art skills have stagnated since high school. Or they just consider me one of the many site jokesters/shitposters/whatever. It upsets me. The one thing I've worked hard on, I'm not acknowledged for. I saw the ASM contest as a way to prove to the community that I can do ASM. Of course, it would be nice to win. But recognition of my abilities is what I really wanted.



So after going through various bonus game ideas, I settled on one that I was sure would amaze: the Sonic 1 special stage(s). Essentially a rotating room, where the goal is to nab the Chaos Emerald. Of course, with only 2 weeks, I wasn't sure if I would make the deadline, considering the possible complexity and possible real life events, so I layed the foundation for a backup as well: a quick entry remniscent of Highly Responsive to Prayers, aka Touhou 1. I had already semi-coded it for a previous project (it's in a test ROM that contains Super Desu World WIP bosses, but uses Bonni's Quest graphics...), so it would be trivial to complete. But again, this would be in case I couldn't do the Sonic 1 special stage. I have a feeling it would have scored better...


Moving on: Super Mario World collision and interaction code is a black box full of black magic. So I took the "easy" route (aka the kinda cheating route) and decided to implement everything in levelasm, bypassing pretty much most of SMW's code since Mario is stuck at the center of the screen. I created the player sprite, and the tilemap, and made a quick "Map16" (interaction map). Basically just solid and empty tiles, no (default) slopes. Now, the player sprite must be able to interact with this interaction map. If the tilemap is at 45deg, then previously flat ground will now be sloped. Flat land becomes slopes, slopes become walls, walls become sloped ceilings, sloped ceilings become walls again, and slopes, and back to flat land. So, how does one rotate the collision map? The answer I came up with is this: you don't. Rather than actually rotating the tilemap, create the illusion of a rotating tilemap, by rotating the player! Or rather, the player's hitbox. Visually (onscreen), the player will see a rotating tilemap and a static player sprite. But internally, the tilemap is the static one, and 3 "interaction" points that represent the edges of the player revolve around a 4th point (Mario's feet).

  (1)  \   T          (2)
        \L   R                R
         \ B*              T    B*
          \               ___L____

Above, (1) shows what the player sees onscreen. T being Mario's headpoint, B being Mario's feet, and L/R being obvious. Mario is standing (or trying to stand) on a slope. (2) shows the same thing, but from the internal logic's perspective. Everything revolves around point B (hence the asterisk). Specifically, points T, R, and L have a set distance from point B, but the angle depends on the stage rotation angle. Ground is deemed a slope (or wall/ceiling/whatever) depending on the stage rotation angle as well, among a slew of other factors.

Fun side note: Internally, points T, L, R, and B are named H (for Head), C, D, and P (for Player, as this was the focal point that all other points are derived from). Initially, I was to have 6 points: H, A, B, C, D, and P. ABCD would be a rectangle, with P being at the center of CD and H being slightly above AB. I guess due to complications, I removed points A and B and moved points C and D up. This meant interactions are now handled by single points and not lines, making collision less accurate and more prone to bugs (though I think I did a good job of squashing most of those).


Now this is all fine and dandy... except not really. This bonus game would obviously require mode 7, since that's the only way to rotate something large on the SNES without hogging ROM space or CPU time. Luckily, I've toyed a bit with mode 7 before. Enough to get the jist of the basics. But I have never, ever dealt with anything related to interaction and collision. And now I have to create functioning collision for a rotating tilemap... 👌👌👌. Naturally, I had to read up on collision. Out of everything I read and studied, out of all the methods I tried, none of them worked! Here's why: none of them deal with a rotating tilemap! Specifically, they all rely on static collision boxes (usually rectangular). So I had to hack together a basic interaction method: if the player is inside something, push them out until they aren't. Obviously a bit more complicated than that, since I have to account for angle and watnot. Additionally, each point of interaction has it's own block interaction code (see below). Yeah, the entire interaction code is a mess, though I did a really good job of cleaning and optimizing it, because initially it was a lot worse.


The block interaction is rather limited. Each "Map16" number is directly tied to a given block. I only coded behavior for values 00-0F (actually 00-0E, and 06 is a direct copy of 01, and 01 is actually the base for a lot of other blocks), so any Map16 value outside of this will crash. Here's the list of blocks:

00 - air
01 - solid (can be slope, wall, ceiling, whatever)
02 - coin
03 - muncher
04 - note block
05 - invisible block (when passed, replaced with solid blocks. requires specific coordinates)
06 - round block (same as solid. was meant to have rounded corners for easy walking)
07 - 3-up moon (same as muncher actually, but uses different end-level variables)
08 - blue question block (mimics the diamond thingies in the original. default state)
09 - yellow question block (2nd state)
0A - red question block (3rd state)
0B - green question block (final state. becomes air after this)
0C - rotation inverse block (changes the stage rotation direction)
0D - ON block (speeds up the stage rotation)
0E - OFF block (slows down the stage rotation)

The original Sonic bonus game didn't have many "unique" blocks, so it makes sense that I didn't code that many. Also, I wanted to be able to compress the Map16 table by cutting out the high nibble of each byte, though I didn't do any sort of compression on it in the end I think. Anyways, the routine for each interaction point is given a sort of "index" and jumps to the routine for each block. The block code will then do things with that index. Basic stuff which I'm too lazy to get in depth into.


The one block I will cover is the coin. I coded each block pretty much in the order they're listed, so the coin was the first unique block (as it wasnt solid or empty). It introduced 3 problems that needed to be solved:
  1. How to handle changes to the Map16 (coin is collected and replaced with air)
  2. How to handle animation (coin spins)
  3. How to handle sprites besides Mario (coin sparkles when collected)
2 and 3 could remain unsolved; they weren't necessary and the end product could do without them, as they only added visual flair. But a behavioral aspect like 1 NEEDS to be done. In order to do it, I had to set up a VRAM update routine. And I did: changes to VRAM are stored to a buffer by whatever block needs updating (in this case, the coin) and then actual updating is done during NMI. Only specific changes can be done; it's not an all-purpose buffer. I forgot how it works, but I think the buffer contains pointers to the tile data. Simple and limited but serves its purpose well

There are 2 ways to tackle animation: change the GFX or change the tilemap. The latter is good for animating a few blocks, but animating a whole bunch is a nightmare and you're honestly an idiot if you even try it. So I did the former, following a similar method to SMW where not every block animates on the same frame, to save NMI time. This is noticeable on the question mark blocks if you compare different-colored ones side-by-side.

Problem 3 remains unsolved, because in the end I didn't use any sprites in the stage (besides the background, see below). The intention was to use sprites for the coin sparkles. If Mario is falling through a group of coins, the sparkles would need to spawn at the correct screen coordinates of each coin, and then speed away from and rotate around Mario as required. Thank Jesus Christ Our Lord and Savior that I didn't have to do this, because I figured I can just directly animate the coins via the tilemap to simulate sparkles (the tilemap in VRAM, not the Map16. There's no "coin sparkle" block). Though another slight problem that arose was the need to display the coin sparkle above Mario (like in SMW). Mode 7 doesnt offer tile priority, so you're pretty much screwed... UNLESS you use ExtBG. So I did; the coin sparkles are the only tiles that take advantage of ExtBG. As ZSNES doesn't like ExtBG, I effectively introduced ZSNES incompatibility for such a small detail.


A very... noticeable aspect of the entry is the background. Once you get past the erotic nature of it, you may ask yourself: "Wait... a full background? In a mode 7 level?". Yes, sorta. 2 things:
  1. It's technically not a full background.
  2. It's technically not a background.
It is not a full background, in the sense that it does not take up the whole screen. The screen is windowed; 16px clipped off of each end. I'll explain why I did that.

In Sonic 1, the bonus game tiles are 24x24px. As Sonic's sprite is rather large as well, things look fine. Mario's sprite is noticeably smaller than Sonic's. Also, it's much easier to work with 16x16 tiles. So I went ahead and went with that; recreating the stage layout with 16x16 blocks. So things should work fine, right? Wrong. Everything is "smaller", so you (the player) is able to see more of the stage. Part of the difficulty of the original is not really knowing what's up ahead. So this is a deal-breaker. I had to mask off the edges of the screen. An alternative would have been to zoom in the mode 7 tilemap. But the window serves another purpose: mask the birds looping across the screen. You see, I was too lazy to implement a proper high-X position for them. Laziness...

But remember: you can only have so many sprites on a single scanline. Specifically, enough to span the screen. Mario, the birds, and their wings are sprites, so the background must be a maximum of 256-32px wide. And yes, I just alluded to the fact that the background is made of sprites, thus technically not being a "background".

If you do the math, you'll realize that sprites only have access to 2 pages in VRAM, as opposed to the 4 that regular layers have. This means that constructing a background using unique tiles only gives you a maximum size of 256x128 (or 128x256). So, how did I make a sprite background larger than that? Easy: change the VRAM address for sprites midscreen. Actually, it's not that easy...

Sprites are rendered rather uniquely. Their properties and junk are calculated during the H-blank before the scanline they're set to render on, and cached during the actual rendering of said scanline. This means that messing with, for example, the VRAM pointer during H-blank (including via HDMA) will give undesirable results. So the answer is to mess with it during rendering of the scanline before the sprite's scanline, as the value is cached and thus won't affect that specific scanline, but the calculations during H-blank will read the new value and use it for the next scanline. I forgot how I came to this conclusion; probably extensive research & testing as I remember celebrating vividly once I completed the lewd background.


That pretty much covers everything important. I left out things like controller input and the rather busy but boring code that was the position conversion. The entry ended up scoring less that I had hoped, which disappointed me considering everything I did for it. Though had I known I would score lower than anticipated, I would still have gone ahead, because what I learned during development is priceless. I effectively created a platforming engine in a few days time, which I can put to good use 😄

No comments:

Post a Comment