Piledriver Posted July 19 Author Share Posted July 19 (edited) Story time! When developing Oh Shoot I got frustrated with running out of space all the time. I created a "ROM visualizer" to keep tabs on how much space all my chunks of code and graphics were taking up visually. I wrote some scripts to assemble every single code backup I made (I made a LOT of backups since it's easy to screw things up and introduce weird bugs that can be hard to track down). I made another script to create a ROM visualization of each WIP ROM. Then I made an animated gif out of it! This is 3 years of pain and exhilaration condensed into 30 seconds. You can see how I started with a 4KB rom... then 8KB (briefly), then 16KB, and finally 32KB. You can see how my banks filled up and how I dumped code into other banks, and how I constantly have to optimize or chop out less important functionality to make space for new features and fixes. You can see my occasional edits to the screens - last 4 banks. I actually rearranged the title screen graphics data at one point just so they would look nicer in these visualizations. Did you spot that in the gif? Edited July 19 by Piledriver 6 Quote Link to comment Share on other sites More sharing options...
Piledriver Posted July 21 Author Share Posted July 21 Check it out! 1024 screens! When I only had 64 screens I added a gimmick to flip screens upside down at random to give the illusion of extra content. So all these screens in the gif also have flipped counterparts. 4 Quote Link to comment Share on other sites More sharing options...
Piledriver Posted July 25 Author Share Posted July 25 Check this out for demo ROMs of Oh Shoot! 4 Quote Link to comment Share on other sites More sharing options...
Piledriver Posted July 26 Author Share Posted July 26 (edited) Story time! All throughout this project I thought it would be cool to have an AI opponent. I figured the game would be played 100x more if it contained a good 1 player mode. I also thought that would be impossible to implement unless I only had computer opponents in an empty arena. Today I want to talk about the long path to Oh Shoot AI! The game has 2 basic modes that affect AI - MANUAL flight (fly in any direction) and AUTO flight (horizontal speed is constant). I figured it would be much easier to implement MANUAL flight AI so I started there. MANUAL FLIGHT AI I needed two basic things... a way to move the AI ship around in interesting ways. I also needed the ship to detect if a move will result in crashing into the playfield and taking appropriate action. The "interesting movement" I call "state management". The basic idea is the ship moves in a constant direction for some random number of frames. Then it will pick another direction and a new number of frames. This continues a few times and then the ship can change modes ("meander" in completely random directions, "vertical intercept" - move vertically to align with the opponent). This worked ok but the ship would constantly crash into playfield obstacles. I added horizontal and vertical "bumpers". If a bumper detects an obstacle immediately vertical to the ship the ship's vertical input will be nullified, likewise for the horizontal bumpers. If a ship's motion is completely nullified by the bumpers the ship goes into "meander" mode and picks a new direction - this reduces the time an AI ship is stuck on an obstacle. This bumper approach as opposed to hardware sprite/playfield collision detection allows for smooth sliding along walls as opposed to the more jerky bump into walls and bounce back approach. The bumpers are invisible in the game, but you can see them in these mockups. They are points adjacent to the ships bounding box that defines a span of tiles that need to be scanned for playfield obstacles. Notice how the ship is aligned with the tiles determines how many blocks need to be scanned (orange zone). Here are some of the cases for the manual bumpers: Horizontal bumper with ship vertically aligned. Horizontal bumper with ship not vertically aligned. Vertical bumper with ship horizontally aligned. Vertical bumper with ship not horizontally aligned. Here the bumper scan has detected an obstacle so movement in that direction is nullified to prevent crashing. In this case because the ship is already in the same tile column as the bumpers there is no need to scan for anything. AUTO FLIGHT AI This case is more difficult because the ship has to detect playfield obstacles in advance and then respond appropriately to them. In the game these scan zones are invisible, but here in the mockup you can see the ship scanning a 4 block wide region ahead of it. I call this a "front scan". The green bands are "corridors". A corridor is a band of screen with nothing in it. It must be a minimum of two tiles tall to be considered a corridor. Here the front scan has detected an obstacle! On its next turn(*) it will determine the shortest direction to a corridor. In subsequent frames the ship will continue to move vertically in the direction of the nearest corridor until it fully enters the corridor. This mode also makes use of bumpers, but vertical bumpers only, and these vertical bumpers are extended to always cover 4 tiles when in a corridor. This prevents the ship from moving into oncoming playfield obstacles. This is the AI simplified a fair bit. There's other aspects to the AI as well such as avoiding missiles, avoiding incoming player in AUTO mode, firing logic, etc. I also let the human players use some aspects of the AI such as bumpers, front scans and auto playfield avoidance, as well as auto missile dodging. I call AI for human players "Assistance". One detail I wanted to point out is that when a human player with "assistance" is moving vertically the front scan width is cut in half. This makes it much easier for a human to maneuver through complex playfields without triggering an unwanted auto playfield evacuation. There are two major details I didn't cover - how to scan a bunch of blocks for playfield obstacles FAST and how to find the nearest corridor FAST! I'll save those for another day. Edited July 27 by Piledriver 4 Quote Link to comment Share on other sites More sharing options...
Piledriver Posted July 28 Author Share Posted July 28 (edited) Story time part 2! Last time I explained how the AI interacted with the playfield. To do this I need to scan rectangular regions of playfield for obstacles. That's what I want to discuss today. Lets simplify the problem to only determining if a given tile in a playfield row is empty or solid. PF0 is not used so any tile in there is empty. My first "solution" was something like this: 1. Convert ship horizontal position to a tile column (There are 0-39 playfield tiles or playfield columns). 2. if (column is 00-03) - index is in the unused pf0, return 0 3. if (column is 04-11) - remap column to 0-7 (subtract 04) then reverse column (column = 7 - column), convert this to an empty byte but with a 1 in the bit position of the column, now and this with PF1 and return result. 4. if (column is 12-19) - remap column to 0-7 (subtract 12), convert this to an empty byte but with a 1 in the bit position of the column, now and this with PF2 and return result. 5. if (column is 20-27) - remap column to 0-7 (subtract 20) then reverse index (index = 7 - index), convert this to an empty byte but with a 1 in the bit position of the column, now and this with PF2 and return result. 6. if (column is 28-35) - remap column to 0-7 (subtract 28), convert this to an empty byte but with a 1 in the bit position of the column, now and this with PF1 and return result. 7. if (column is 36-39) - column is in the unused mirrored pf0, return 0 8. if result is 0 the pf tile is empty - otherwise it isn't. I think I got this explained right....? The details don't matter what matters is this is WAY too complicated!!! The game might have to scan up to 20 blocks in a single frame so this was the wrong approach. This was another one of those moments where I thought I was trying to do something impossible on Atari. I learned you don't get too far with 2600 programming if you give up easily. Anyways I was able to express the complexities above in some tables. In the solution I came up with you take the ship's horizontal column value and use that to index into two tables... this gives you values (masks) that you and with PF1 and PF2 respectively... then or those values together and you have 0 if the column is empty or non zero otherwise! Here's the two tables I mentioned. I put them side by side to make them easier to compare. TABLE_PF1MASKSWIDTH1_BANK2: TABLE_PF2MASKSWIDTH1_BANK2: .byte #%00000000 .byte #%00000000 ;0 .byte #%00000000 .byte #%00000000 ;1 .byte #%00000000 .byte #%00000000 ;2 .byte #%00000000 .byte #%00000000 ;3 .byte #%10000000 .byte #%00000000 ;4 .byte #%01000000 .byte #%00000000 ;5 .byte #%00100000 .byte #%00000000 ;6 .byte #%00010000 .byte #%00000000 ;7 .byte #%00001000 .byte #%00000000 ;8 .byte #%00000100 .byte #%00000000 ;9 .byte #%00000010 .byte #%00000000 ;10 .byte #%00000001 .byte #%00000000 ;11 .byte #%00000000 .byte #%00000001 ;12 .byte #%00000000 .byte #%00000010 ;13 .byte #%00000000 .byte #%00000100 ;14 .byte #%00000000 .byte #%00001000 ;15 .byte #%00000000 .byte #%00010000 ;16 .byte #%00000000 .byte #%00100000 ;17 .byte #%00000000 .byte #%01000000 ;18 .byte #%00000000 .byte #%10000000 ;19 .byte #%00000000 .byte #%10000000 ;20 .byte #%00000000 .byte #%01000000 ;21 .byte #%00000000 .byte #%00100000 ;22 .byte #%00000000 .byte #%00010000 ;23 .byte #%00000000 .byte #%00001000 ;24 .byte #%00000000 .byte #%00000100 ;25 .byte #%00000000 .byte #%00000010 ;26 .byte #%00000000 .byte #%00000001 ;27 .byte #%00000001 .byte #%00000000 ;28 .byte #%00000010 .byte #%00000000 ;29 .byte #%00000100 .byte #%00000000 ;30 .byte #%00001000 .byte #%00000000 ;31 .byte #%00010000 .byte #%00000000 ;32 .byte #%00100000 .byte #%00000000 ;33 .byte #%01000000 .byte #%00000000 ;34 .byte #%10000000 .byte #%00000000 ;35 .byte #%00000000 .byte #%00000000 ;36 .byte #%00000000 .byte #%00000000 ;37 .byte #%00000000 .byte #%00000000 ;38 .byte #%00000000 .byte #%00000000 ;39 It's hard to explain but these tables encapsulate all the complexities of the mirrored playfield and reversed bit order of PF1 and PF2. The result though is I can check the value of a playfield block fast! Recall that I needed to scan horizontal spans of playfield that were 1,2,3, or 4 tiles wide. I made another version of these tables that give me the results for 3 tiles in a row. If I needed to scan 2 blocks or 4 blocks I could generate masks for that by combining values from these tables. This is a tad slower but it's a tradeoff and I don't need to have tables for 2 and 4 width PF spans. There was another big but simple optimization. For the front scan I have to scan Playfield spans that are 4 tiles wide by 2-3 tiles tall. Any easy way to simplify this is to or together the PF1 bytes from the rows of the span and likewise with the PF2 bytes. Then do the masking step after that. With this row combining step and the tables I was able to greatly reduce the processing time on playfield scans! I had a bit of a failure doing the AUTO flight AI. I kept thinking the ship should be able to handle just about any obstacle. I spent a lot of time on the "far scanner". It was a pretty neat idea - detect distant objects early enough to have time to avoid them. I actually implemented this and it worked... except I couldnt make it fast enough and the screen would roll so I had to yank it out. Here's a mockup of one of my designs to give you some idea what I was up to. Edited July 28 by Piledriver 2 Quote Link to comment Share on other sites More sharing options...
Piledriver Posted July 28 Author Share Posted July 28 (edited) Story time part 3! Let's talk about corridor escape implementation. Here is a typical screen showing corridors in green. Note that there are empty rows that are too small vertically for your ship to safely fit in and hence are not considered corridors and are not green in this image. It is actually somewhat complicated to determine if a row is in fact part of a corridor because of the requirement that a corridor be at least two rows tall. So I do the slower more time consuming corridor assessments during the screen loading process and save that data in the "corridor map". The corridor map consists of 3 bytes and each bit indicates if a row is a corridor (0) or not (1). I only use 22 bits of this (one bit per row). This corridor map gives me a huge speed boost compared to trying to figure out if a row is in a corridor during the AI code. There is still a problem however.... Here a ship gets a positive front scan! Evasive action is required! During the "escape plan" the ship scans up and down to find the nearest corridor. You can see the rows scanned by the blue marks I put on the screen. I only need to scan every second row because corridors must be two rows tall at least so I can get away with a lower resolution scan. There is only enough time for 3 corridor scans up and 3 corridor scans down. In the image above the corridor scan has gone to its maximum extent in both directions and has not hit the boundaries at the top and bottom of the screen, nor has it found a corridor. Most scans find the best direction to go, but this scan is inconclusive! Initially in this case I couldn't get the information I needed to make the best decision for the ship's escape direction so I thought it was reasonable to pick a random direction. So on occasion the ship would make the wrong call and move towards a dead end and crash. This was really bothering me and I thought I might need some elaborate solution if one was even possible. Then I realized that in all of these "bad call" situations there was a large object at the top or bottom of the screen. It turns out I had all the data I needed conveniently in a single byte - the top corridor map. If the top corridor map was solid (all 8 bits are 1) then there is a large object at the top of the screen so when in doubt evacuate down. If there wasn't a large object at the top of the screen then when in doubt evacuate up. Problem solved! No real processing required! Edited August 7 by Piledriver 2 Quote Link to comment Share on other sites More sharing options...
Piledriver Posted July 28 Author Share Posted July 28 Story time part 4! Despite all the optimizations I already mentioned I needed MORE optimizations! The AI ships don't need to perform a front scan every frame. So I have two AI modes - "low power" and "high power" and the ships alternate between those every frame with only one ship in high power mode and the other in low power mode. A ship must be in high power mode to perform the following tasks - a front scan, missile scan and missile dodge initiation, changing to a new state (attack vs meander). At this point I had cut the processing down to bare essentials without degrading functionality. STILL not good enough! As I already mentioned my game was way out of NTSC spec by using 286 lines to accommodate all that AI processing and wouldn't run on one of my TVs! Occasionally I was way over processing time... in that case I would freeze the other ship in place for one frame. This was barely noticeable and it allowed me to chop the number of scanlines I needed down to 276. This is still much higher than the standard NTSC 262 lines. It's ok to be somewhat out of spec and still work on most TVs. I did a LOT of optimizing and I still just barely squeaked through! That's what happens when you add advanced features that the 2600 was never designed for. With Atari it's not all about how smart you are. It's about how much punishment you can take and keep coming back for more! 2 Quote Link to comment Share on other sites More sharing options...
Piledriver Posted July 29 Author Share Posted July 29 (edited) Check out the box art concept to final version animation. Edited July 29 by Piledriver 3 Quote Link to comment Share on other sites More sharing options...
Piledriver Posted July 31 Author Share Posted July 31 Special thanks to my Oh Shoot testers (you know who you are)! I'm going to consider testing complete (although you are still welcome to make comments if you wish). The feedback I got is the game is fun and bug-free! I also noted that I need simple and clear instructions on the game modes! 3 Quote Link to comment Share on other sites More sharing options...
Piledriver Posted July 31 Author Share Posted July 31 Story time! Early in this project I was interested in sound. I didn't have a clue how things worked in Atari land and decided to make a feature rich sound engine. The sound engine could load and play sounds in either channel (basic stuff), but I also allowed the sound frequency to in crease or decrease over time... I also allowed the volume of the sound to increase or decrease over time. I even made "compound" sounds which would play back a list of sounds. I think I was using 12 bytes of ram per sound channel... so 24 bytes in total for sound! Eventually I got a bit more Atari wise and revisited the sound code. I gutted most of the functionality - I only allowed sound frequency to increase over time... I cut the variable volume and compound sounds and then I squished 2 sound values into a single byte. This cut the audio ram down to 4 bytes from 24! I didn't even miss any of the stuff I cut out! In short the sounds in this game are very basic, but they get the job done. 2 Quote Link to comment Share on other sites More sharing options...
Piledriver Posted July 31 Author Share Posted July 31 (edited) Story time! One day I was brainstorming about how I could spice up the kernel a bit. I explored striped foregrounds and backgrounds. Even animated backgrounds that looked like waterfalls or lavafalls. They looked kinda cool but somewhat distracting and I considered it a lower priority feature (which obviously didn't make it in in the end). While I was poking around in the kernel code I looked at the code that always sets the playfield row thickness/countdown value to 4 giving each playfield row a uniform thickness. I thought I could sort of deform the screen if I altered that thickness on some rows. For things to look good and for the screen to be anchored at the top and bottom for every row that I made 1 "pixel" thicker I had to make another row 1 pixel thinner. This gave me some interesting deformed looking screens. I came up with a "ripple map" where I store the thickness of each row in a table... the thicknesses can be only 3,4,and 5... I could go with values beyond 3 and 5 but then things looked really zany (in a bad way). I realized I could make an animated rippled effect if I advance the starting index into the table every frame and then loop back to the beginning of the table... but this would require special case code in the kernel! I guess it couldn't be done.... kidding. I just made a second copy of the table right after the first so the index will point to the same ripple data I want without having to do any complex looping logic. A cool looking effect that has an impact on gameplay... and it was a relatively cheap effect to implement. Check out the result of this! I had a few more ripple maps with more exaggerated ripples for screens with plenty of open space... but I had to cut those in the end. Even on a solid screen with no visible ripple the effect is still running but with a ripple table that is filled with all the same values - 4. Seems wasteful, but this allowed me to have a single general purpose kernel and not have to make multiple kernels one with the effect on and one with it off. I have seen this effect in a few other Atari games - Smurf and Go Fish come to mind. I don't think it had any impact on the gameplay in those games though. Edited August 7 by Piledriver 2 Quote Link to comment Share on other sites More sharing options...
Piledriver Posted July 31 Author Share Posted July 31 (edited) Story time! One part of my game that folks don't seem to like too much or find confusing is the select screen. I'll just go through the history of the Oh Shoot select screen. On almost all of the original 2600 games in the select screen all you get is a number and you have to look this number up in the instruction manual's "game select matrix" to figure out what game settings correspond to the game select number. This kinda sucks! But it took very little rom space to implement. This is exactly how the select screen started off in Oh Shoot. I also used the player 2 score slot to display the max score required to win. Later on when I had more space thanks to bank switching I wanted to put all the info on the screen so the player knows what they'll be playing. I even bank switch out of the select/AI bank after I display the text and then go to bank0 to display some ships moving in a way to indicate the speed and flight mode to the player. You can not directly change settings like "flight type" and "speed". You can only change the game select number and then the corresponding settings are displayed on the screen. I think this may initially be confusing for some players? By the way the number of controllers on the screen indicates the number of human players in the game selection. Later still I came up with subsettings based on the number of players. Subsettings determine how difficult a computer opponent is, whether a human player has some sort of dodge assistance, or if the game is in an exhibition mode. Initially I saw these as "Easter eggs" and to engage them you had to press the controllers in certain directions at the time you press start. Fortunately I realized this was a terrible idea and I found some way to change these subsettings in the select screen and display them. You can see the current state of the select screen with subsettings displayed on the bottom row. Here are the instructions on the subsettings. The subsettings are based on the number of players. Press left on either controller to cycle through the left player's subsettings likewise press right for the right player. The exhibition modes do not pertain to a player and can be cycled by pressing left or right on either controller. Here is what the subsettings mean: A: Assistance - helps the corresponding player avoid obstacles A+: Assistance+ - helps the corresponding player avoid obstacles and missiles E: Easy computer opponent N: Normal computer opponent H: Hard computer opponent FX: Full screen scrolling and warp effects enabled. Only available in 2 player mode EX: Exhibition mode - computers play forever - sound effects disabled EX+: Exhibition mode+ - computers play forever - sound effects enabled This works and all the information is displayed on the screen but it is pretty cryptic. I wanted this game to be accessible so anyone could pick this up and pretty much know what to do without a manual. I don't think this will be possible however - out of space. I only had room for this text/graphics in the select screen: On the plus side a beginner could just change the game select number and accept the default subsettings. Ideally I could do something like the menus that were added to classic games like Combat, Outlaw, Asteroids etc. With these menus you don't fuss with some game select number... you directly set all the variables for the game upfront and based on that you see what the game select number is. Here is Combat with a nice menu: Here's a link to those enhanced games: But check this out! Combat is 2KB. Combat with this fancy menu is 8KB! I even did one of my "x-rays" on the Combat with menu rom: Most of those 8KB are used! I could do something like this but then I think I would have to switch to a 64KB ROM or cut out a bunch of my screens... big pain in the butt! I don't think it is practical to attempt to improve the select screen beyond what it is right now. Edited August 7 by Piledriver 2 Quote Link to comment Share on other sites More sharing options...
Piledriver Posted August 1 Author Share Posted August 1 (edited) I want to talk about my title screen next! I had a code sample that draws a large object using the 6 character kernel. I did some brainstorming on how I could use this in my title screen. I came up with having large detailed (and animated!) versions of the ships menacing each other. Here's the concept drawing vs the end result. I developed the title screen as a standalone "tech demo" using a full 4KB! Half the title screen bank is the graphics! Most of the rest of the bank is code to draw the large objects in the correct positions, orientations, and frame of animation. I have a separate subroutine to draw every frame of every ship in every frame of animation and orientation. That's 16 nearly identical subroutines right there! Not very elegant, but it worked. I also have fade in and fade out thanks to the large and well organized palette of the 2600. The floaty motion of the ships is pretty cool. I created a table mapping the entire sin range to 256 values in C#. That's a lot of bytes! Turns out I didn't need such a high resolution of sin values. I could get away with a range of only 64. Less than that and the floaty pattern of the ships got jerky and weird. Here's the artwork the title screen is based on. Would you hang these on your wall? And the end results. Here's all the work in progress steps of the title screen artwork. Finally here's the end result. I used fixed point values as input to the "sin function". This allowed me to have the ships bob up and down at different rates that are determined at random when a new pair of ships are displayed. There might be an Easter egg on the title screen. Edited August 1 by Piledriver 3 Quote Link to comment Share on other sites More sharing options...
Piledriver Posted August 1 Author Share Posted August 1 (edited) Ya know... as important as the title screen is in a game... I think in this game the explosions were far more important since you see them ALL the time. That's why I made so many versions until I finally made something I was happy with. Hard to believe I actually thought this looked great at one time. This one is bigger! Had to cut a few frames though. This is the only explosion where I had a special case to do integer scaling in the kernel giving me a larger explosion with a low rom footprint, but burning precious kernel cycles! The flashing colours draws your eyes in. The constant colour explosions were easy to miss. This was just a brief in between step for the next explosion. At this point I had the full sized explosion! Actually I think this one was too tall vertically. Next step was adding some detail. I didn't like it much though... The final version. I liked the circular shape. You don't see many circles on the 2600. Also the colours rapidly flash between dark and bright which catches your attention immediately even if you're not looking directly at the screen. I also liked the dissipating puff of smoke on the last frame. I wasnt able to clip these large explosions when they happened on the sides of the screen... so I just nudged them over so no clipping is required. Kinda cheesey, but I think it was my best option. I actually attempted to add more frames onto the final explosion (and failed). I also considered making an even much larger explosion than the final one! This would have introduced flicker and would have been a clipping nightmare. It was time to move on. Not every Atari game can do something like this... especially if the explosions occur in a formation of ships. Because Oh Shoot is graphically a fairly simple game with only two sprites on screen it was pretty easy for me to make them bigger. Lets compare them all side by side... Edited August 7 by Piledriver 4 Quote Link to comment Share on other sites More sharing options...
+Karl G Posted August 1 Share Posted August 1 16 hours ago, Piledriver said: Most of those 8KB are used! I could do something like this but then I think I would have to switch to a 64KB ROM or cut out a a bunch of my screens... big pain in the butt! I don't think it is practical to attempt to improve the select screen beyond what it is right now. How much spare space do you have to work with, if any? I think you could do a more intuitive menu in far, far less space than that if you are sparing with the text. 1 Quote Link to comment Share on other sites More sharing options...
Piledriver Posted August 1 Author Share Posted August 1 2 hours ago, Karl G said: How much spare space do you have to work with, if any? I think you could do a more intuitive menu in far, far less space than that if you are sparing with the text. This is my AI/Select screen bank. The yellow rectangle is all the space I have left unless I cut something. 1 Quote Link to comment Share on other sites More sharing options...
Piledriver Posted August 1 Author Share Posted August 1 Check out the very first sketches of Oh Shoot going back about 3 years now! Back then I was only planning to make a simple "Combat ripoff". Just for fun I cartoonified the original sketch. 2 Quote Link to comment Share on other sites More sharing options...
Piledriver Posted August 2 Author Share Posted August 2 (edited) I want to go into more detail on my screen encoding (done in a C# app) and loading (done in the game). Today I'll discuss screen loading. This image is half of the 32KB ROM. This is 4 banks of data containing the 1024 screens in the game and the loading code is duplicated in each bank. Each bank is divided into 4 subbanks. The loading code is in the first subbank cutting into the available screen data a bit. An individual subbank holds 64 screens. There are two main components to a subbank - the screens and the chunk palette. A screen is a list of 2-15 one byte indexes to chunks. The screen data grows from near the top of the subbank as more screens are added and the chunk palette grows from the bottom towards the top. As I designed the screens I had to make sure the screen data and chunk palette didn't overlap or there would be corrupted data. You can see the buffer between these two regions as series of solid bytes. Here's a snippet of the chunk palette: .byte #%00000111 ; Line 47 (250) .byte #%00111111 .byte #%00010001 ; rle .byte #%00001111 ; Line 48 (251) .byte #%01111111 .byte #%00000000 ; Line 49 (252) .byte #%00000111 .byte #%00100001 ; rle .byte #%00000000 ; Line 50 (253) .byte #%00000010 .byte #%00000000 ; Line 51 (254) .byte #%11100000 .byte #%00010001 ; rle .byte #%00001000 ; Line 52 (255) .byte #%00000000 A chunk is a run length encoded piece of a screen. A chunk consists of 2 bytes of playfield data (PF1 and PF2) and a nibble of run length encoding data allowing a chunk to be up to 15 rows tall. I didn't want to waste a full byte on these nibbles so I combine two chunks into 5 bytes. This makes loading a tad more complicated but I'm not wasting any space. Here's 7 screens: zScreen000: .byte #5, #255, #5, #255, #4, #254, #5 zScreen001: .byte #253, #252, #251, #250, #249, #12, #248, #249, #247, #246 zScreen002: .byte #5, #255, #5, #254, #4, #255, #5 zScreen003: .byte #5, #254, #5, #254, #4, #255, #5 zScreen004: .byte #5, #254, #10, #255, #5 zScreen005: .byte #5, #245, #10, #245, #5 zScreen006: .byte #10, #245, #11 Each screen is a list of chunk indexes. Notice how in this example the index of the chunks is either a large number (200+) or a small number (< 16)? This is from an optimization I made. A chunk is 2.5 bytes. This adds up quickly as you add more chunks. I came up with an optimization for empty spaces. Instead of making all these chunks of empty space of various numbers of rows... if the chunk index is under 16 I just use the index as the run length value and the chunk playfield data is just empty space. This was not a huge optimization, but still this allowed me to make a handful of complex screens in each of the 16 subbanks that I wouldn't have been able to do otherwise. Every bit helps eh? So now we mostly know how the screen data is laid out... mostly. So how do I actually load a screen from a subbank into RAM?? This is pseudocode, but it starts with something like this: LOADSCREEN(bank=X, subbank=Y, screennumber=Z) ;where bank is 4,5,6 or 7, subbank is 0,1,2, or 3, and screennumber is between 0 and 63. I'm going to gloss over a bit of detail, but lets just jump to the subbank we want. So from there how do I get a pointer to the screen we want? The screens are of variable size so I cant just do something like address = screennumber*16. I could have an array of pointers one for each screen. That would require 2 bytes of extra storage per screen. At 1024 screens that's 2KB just for screen pointers! That's the size of the whole Combat game right there! This is where I got creative to save rom. What I did was for each subbank theres 64 screens but I only store addresses for every 8th screen. So I set my screen pointer close to where it should be (within 8 screens away). I need a way to get to the screen I want. I have another table which stores the size of every screen in a nibble. So with the sizes of all the screens between my current screen and destination I can loop through those sizes offsetting the screen pointer by those sizes until I reach the desired screen. Pretty weird eh? Lets do some analysis on this on a per subbank basis... One pointer per screen method of getting desired screen pointer -requires 128 bytes of pointer table! -very fast to get a pointer to desired screen One pointer per 8th screen method of getting desired screen pointer -requires 16 bytes of pointer table and 32 bytes of screen size data = 48 bytes total overhead! -plus extra overhead for the code required to get to the desired screen -very slow to get a pointer to the desired screen This was an interesting tradeoff. It didn't really matter that locating a screen is very slow - I have an entire frame to load and process a screen, so it's worth it to save a few dozen bytes per subbank. These small gains helped a lot to give me enough bytes to get creative with the screen design and not just have a handful of chunky blocks on each screen. Getting the screen pointer was the hard part. The next step is just looping through all the chunk indexes of a screen, loading the corresponding chunks, and expanding each chunk into the section of RAM reserved for the screen. There is a variant of the screen load function that just loads a single row into RAM. I found a few bytes to spare for the screen pointer, so I have that cached! When I need to load a single row for scrolling purposes I don't need to go through the entire screen searching process again. I still need to jump to the correct bank though. Edited August 7 by Piledriver 2 Quote Link to comment Share on other sites More sharing options...
Piledriver Posted August 2 Author Share Posted August 2 (edited) Now lets get into how these screens are encoded! Let's focus on one subbank or one screen set of 64 screens. Step 1. draw 64 screens. I only had to draw half of each screen of course since the end result is mirrored on the Atari. There are restrictions of course so I can't just draw whatever I want. Here's the restrictions: -every screen is composed of chunks... every chunk must be stored in the chunk palette but can be reused many times -reusing chunks is the key to keeping the byte count down -an entire set of screens with chunk palette must be under 1KB... actually even a tad less than 1KB because of some overhead. -each screen can only have 15 chunks maximum (the playfield can only be changed up to 15 times per screen) Breaking any of these restrictions will cause corrupt data and random game crashes/glitches. I automated all this to make it super easy for the artist (who is me). My screen bitmaps to code converter app reports if I break one of these constraints. Here's a report snippet on one bank of good screen data: 004-000: screen= 388 PF= 120 Total= 508 (513 target 5 available) 004-001: screen= 567 PF= 235 Total= 802 (825 target 23 available) 004-002: screen= 546 PF= 317 Total= 863 (881 target 18 available) 004-003: screen= 570 PF= 297 Total= 867 (889 target 22 available) It reports how many bytes are used by the screens in a subbank and also how many bytes are used on the playfield (chunk palette). It also attempts to let me know how many bytes are still available in each subbank so I can beef up the screen data up to the limit. Here is a report snippet on a single screen: ==================================================================== == SCREEN: 15 Chunks:13 ==================================================================== .......1.1....................1.1.......[1] .......111....................111.......[2] .......111....................111....... .......1.1....................1.1.......[1] ........................................[4] ........................................ ........................................ ........................................ .................1.11.1.................[1] .................111111.................[2] .................111111................. .................11..11.................[1] .................111111.................[3] .................111111................. .................111111................. .................11..11.................[1] ............1.1..111111..1.1............[1] ............111..111111..111............[2] ............111..111111..111............ ............1.1..11..11..1.1............[1] ............111..111111..111............[2] ............111..111111..111............ 00000001010000000000[01] (04) screens: 15, 22, 00000001110000000000[02] (02) screens: 15, 22, 00000001010000000000[01] (04) screens: 15, 22, 00000000000000000101[01] (10) screens: 07, 10, 11, 15, 20, 21, 22, 23, 33, 57, 00000000000000000111[02] (11) screens: 07, 10, 11, 15, 20, 21, 22, 23, 33, 00000000000000000110[01] (42) screens: 07, 10, 11, 15, 17, 20, 21, 22, 23, 26, 33, 34, 36, 44, 47, 00000000000000000111[03] (11) screens: 07, 10, 11, 15, 21, 22, 23, 00000000000000000110[01] (42) screens: 07, 10, 11, 15, 17, 20, 21, 22, 23, 26, 33, 34, 36, 44, 47, 00000000000010100111[01] (03) screens: 11, 15, 20, 00000000000011100111[02] (11) screens: 00, 11, 15, 20, 28, 33, 57, 00000000000010100110[01] (11) screens: 00, 11, 15, 20, 28, 33, 57, 00000000000011100111[02] (11) screens: 00, 11, 15, 20, 28, 33, 57, I can see at a glance that this screen has 13 chunks. In the column to the right of the screen is a number in square brackets. This is the run length data - how many rows tall the chunk is. Then I get a report on every chunk that appears in the screen. The name of a chunk is a string of every bit of playfield data (PF1 and PF2) and how many rows tall the chunk is in square brackets. Then in round brackets I get a count of how many times this chunk appears in this set of 64 screens. Finally I can see which other screens this chunk appears in. Here's another: ==================================================================== == SCREEN: 18 Chunks:11 ==================================================================== ........................................[9] ........................................ ........................................ ........................................ ........................................ ........................................ ........................................ ........................................ ........................................ ..............1.1.1..1.1.1..............[2] ..............1.1.1..1.1.1.............. ..............11.11..11.11..............[1] ..............1...1..1...1..............[1] ..............1....11....1..............[1] ..............1..........1..............[2] ..............1..........1.............. ......1.1.1....1.1....1.1....1.1.1......[1] ......11.11....1.1....1.1....11.11......[1] ......1...1....1.1....1.1....1...1......[1] .......1.1.....1...11...1.....1.1.......[2] .......1.1.....1...11...1.....1.1....... ......1...1....1...11...1....1...1......[1] 00000000000000101010[02] (01) screens: 18, 00000000000000110110[01] (01) screens: 18, 00000000000000100010[01] (01) screens: 18, 00000000000000100001[01] (01) screens: 18, 00000000000000100000[02] (01) screens: 18, 00000010101000010100[01] (01) screens: 18, 00000011011000010100[01] (01) screens: 18, 00000010001000010100[01] (01) screens: 18, 00000001010000010001[02] (01) screens: 18, 00000010001000010001[01] (01) screens: 18, This screen has a huge rom footprint! I had extra space so I made this wasteful screen - the "ghost castle". I can see there's 10 chunks of non-empty space. And of those chunks they all appear only in this screen! So there's no amortized savings in chunk palette space. The rom cost of this screen then is 11 bytes for the chunk indexes + 10x2.5 bytes for the chunk palette entries = 36 bytes total! An average screen is less than 16 bytes. I also get a report on the chunk palette. Here is a snippet of that. 00000000000000000010[01] (08) screens: 36, 44, 47, 00000000000000000011[01] (08) screens: 06, 14, 34, 36, 47, 00000000000000000011[02] (02) screens: 17, 44, 00000000000000000100[01] (06) screens: 27, 39, 51, 53, 61, 00000000000000000101[01] (10) screens: 07, 10, 11, 15, 20, 21, 22, 23, 33, 57, 00000000000000000110[01] (42) screens: 07, 10, 11, 15, 17, 20, 21, 22, 23, 26, 33, 34, 36, 44, 47, If I need to trim my byte count I can look at this report and find chunks that are only used in one place. Then I could modify or delete the wasteful screen. I needed to improve the quality of some screens so my report generator also puts out a collage of all the screens so I can easily spot screens I don't like and where they are located. Edited August 7 by Piledriver 2 Quote Link to comment Share on other sites More sharing options...
Piledriver Posted August 2 Author Share Posted August 2 (edited) Lets talk about missile dodging! A ship with missile dodge assistance has a top and bottom radar to detect missiles. If an enemy missile is detected by the top radar the ship automatically moves down. If a missile is detected by the bottom radar the ship will automatically move up. Here you can see a missile dodge sequence. Top radar detects a missile. Ships avoids by dodging downwards. With harder computer players their radars increase in size making them able to respond to missiles sooner. The hard computer opponent also has a smaller gap between radars making it a more "slippery" opponent. Humans with assistance and computer players in exhibition mode always have the same standard mid level radars. Easy computer opponents have useless radars so its basically a shooting gallery. Edited August 7 by Piledriver 2 Quote Link to comment Share on other sites More sharing options...
Piledriver Posted August 3 Author Share Posted August 3 (edited) 128 bytes is a crumb of RAM! I made a RAM label visualizer to help me see what's going on. The first blue section is the p0 data, the red section after that is p1 data. The dark grey section is my screen in RAM. The very dark section at the end is my function call stack. I don't want to go off the deep end on what every bit is for. But there's some interesting points. The game has two basic modes - FX mode and AI mode. I made the FX first (all the scrolling and ripple effects). This required many bytes of RAM. I knew it was impossible to combine AI and FX so when in AI mode I use the same bytes of RAM that I was using in the FX mode. You can put multiple labels on the same memory location. Check out byte #118 - it has 7 labels and is used for that many different functions! I needed about 30 bytes of RAM for the title screen. I put labels on various bytes of RAM with a ts_ prefix so I know where it's used. I had to be careful which bytes I used for the title screen or cause data corruption. I don't want to overwrite one of the score bytes for example... or any byte of the screen in RAM since the game bounces back and forth between title screen and a "game over" screen with the screen displayed. The title screen only appears when a game is not in play... so I could use any gameplay related bytes for the title screen. A lot of these labels are actually pretty bad now. For example p0AircraftType used to just store the ship type but later I used every bit in that byte. It's hard to come up with a good name for a bunch of sometimes unrelated bits of data. I have all these macros and bitmask definitions to abstract data storage and retrieval. It's kind of like getters and setters in C#. This makes the code much more readable and maintainable. Edited August 7 by Piledriver 2 Quote Link to comment Share on other sites More sharing options...
Piledriver Posted August 3 Author Share Posted August 3 (edited) Initially my jet looked like this: It was hard to hit it with wavy shots, so I changed it to this: Albert made a comment about the graphics - he thought they were glitched. I said it's just bad Atari graphics; however, his comment kinda stuck in my head. Eventually I revisited the jet graphics. Here's the end result. And here's a WIP animation. Edited August 7 by Piledriver 3 Quote Link to comment Share on other sites More sharing options...
Piledriver Posted August 5 Author Share Posted August 5 (edited) Story time! Today I want to discuss how the ships changed over time. They actually have names by the way. This is how they appeared originally. I had some interesting ideas for the Destroyer. I wanted the shot trajectories to be different based on your input. If you press up or down while firing your shot does a wide zig-zaggy path and doesn't cross the midpoint of the ship's position from the time it fired (first two paths below). If you weren't pressing up or down the shot would do a half-wide zig-zag path that would be centered on your ship's initial firing position. I even added memory so if the last direction you pressed was up for example the shot would do the first path in the image below. This seemed cool for a while but eventually I decided I didn't like how this ship's controls were more complex and inconsistent with the other ships. Also this more complicated ship type consumed extra memory! I simplified the destroyer's shots so they basically follow the top zig zag path but shifted down by half the amplitude (sorry, no illustration of this). I sort of liked it... for a while. The problem was my shot always seemed to go everywhere the enemy wasn't. I was kinda thinking that its ok if this ship sucks because your opponent will get stuck with it just as much as you on average... even playing against an absent second player directly in front of me I would miss multiple times and crash into them! I had a random idea once to have a ship that can't shoot but can ram into the enemy without dying. I borrowed that idea for the Destroyer... so if your zig-zaggy shots miss and you crash into the enemy you will survive - they won't. It didn't take me long to realize this sucked! My next attempt to salvage the Destroyer was to have rapid zig zag shots with a high amplitude. I liked this (maybe because I usually play with shots that pass through playfield). Eventually I realized this shot type was too difficult to dodge and was quite unbalanced. I wanted a shot path that was effective at targets directly in front of you but had some zig zagginess as well. I figured the best way to get this was with a table. I generated this by hand - no math here! And here it is in the field. I'm finally satisfied! It can be a challenge to use this effectively in tight spaces in "a" difficulty where playfield stops your shots. The sharp vertical drop in this shot is effective against foiling missile radars. The Destroyer shot type was the hardest one to get right by far... assuming I did get it right. You might think the banshee's shot would move faster vertically in fast ship mode. You would be wrong - that would take extra code. Besides it plays well the way it is. I think so at least. The Spectre's shots have a random chance of moving in a random vertical direction. There is also a slim chance per missile movement step that it will deliberately move toward the enemy vertically. In other words the Spectre missile has a weak attraction for the enemy ship. The Banshee shots move further vertically than Spectre shots if steered that way, but the Spectre shots have a "fire and forget" advantage - you don't have to steer those. Here you can see how the Spectre shots (there are multiple shot paths overlaid) have a slight tendency to move towards the enemy. Once upon a time shot life was different from shot cooldown time... although only for one ship - the helicopter... I mean the Ranger. The Ranger shot life was half the life of other shots so its shots would die after travelling half the length of the screen. However! It's cooldown was 75% of its life span. What this means is you could release the fire button and let the shot travel half a screen, or you could hold the fire button down and a new shot would fire as soon as the cooldown period has elapsed and your old missile will vanish. What this means is the Ranger could hold the button for more than double the fire rate of the other ships but a very short range. This was kinda neat but I wanted to get rid of all these complexities. On Atari simple is good! The final Ranger has half the range and double the fire rate of the other ships consistently. How does a helicopter fly through space anyways? I guess the same way Mario can shoot fireballs underwater. Edited August 7 by Piledriver 2 Quote Link to comment Share on other sites More sharing options...
Piledriver Posted August 5 Author Share Posted August 5 (edited) Today I want to talk about screens - not the technical aspect, but the art. My initial barebones game only had run length encoding. I could only make 16 screens with limited detail. Then with a chunk palette I could make 64 screens (still in a 4KB rom). And then with 32KB I had a lot more freedom. I wanted to highlight a few screens. Check out my Atari inspired screens: It's a space game so I had to have some cool space station looking stuff. Here's a bunch of other screens I think are cool. When I was a kid I loved biking to swamps and catching all kinds of bizarre creatures, taking them home, and observing them in our outdoor aquarium. One day I was at the swamp and realized I had forgotten my brother's birthday party! Whoops! One of my favourite swamp monsters is the water tiger! One look at this beast and you know what they're all about - nothing good! Here's some more swamp critters - water scorpion and juvenile salamander. While working on "Oh Shoot!" some wasps just moved into our house - lots of them! 1000s? I admired their tenacity, but they fared poorly against our vacuum cleaner. I immortalized them in my game. Edited August 7 by Piledriver 3 Quote Link to comment Share on other sites More sharing options...
Piledriver Posted August 6 Author Share Posted August 6 (edited) Lets talk color. The 2600 has a large well organized color palette. The colors are crystal clear on an emulator on my computer screen. The problem is the colors may not look like you expect on a CRT TV. Here's some bad examples: The ghosting is so bad on some of these colors you can't even tell what you're looking at! Here's some good color combos on my CRT (Sony Trinitron): There's still ghosting, but you can tell what you're looking at, and from a distance it looks really good to me. Just because the colors look good on one CRT doesn't mean it will look good on another CRT! Two of my CRTs were kind of dark so I brightened my colors to look better on those TVs. I was able to go into the TV settings and adjust the brightness, but not everyone can figure that out. I had this cheapie red CRT that looked great with some colors but with the pink and purple combos I could see static on the screen and hear this annoying buzz... but only on that TV! At one point I had large set of 32 different color palettes! I kept adjusting some of the palettes to try to get them to look good on all my TVs but it just wasn't happening. I went for quality over quantity and cut the palettes down to 16. I also borrowed and sometimes modified palettes from other games - blue maze from Ms. Pacman, Chopper Command, Missile Command, Mr. Run and Jump, etc. I figured if the colors looked good in those games those same colors would look good in my game too. I'm color blind by the way. I don't think this is a very big handicap but I do perceive color a bit differently than others. Maybe we all perceive color a tad differently? Or even a lot differently? Shrug? Who knows. Anyways here's my final palettes. Maybe I should have included ships on this collage as well. I tried to order the palette changes so they aren't too jarring to the eye... like going from black directly to a bright white screen. I also tried to sort the colors as best I could. I think it makes transitions from one palette to the next look nicer than just a completely random color change. I implemented the black and white switch functionality early on. I felt pressure to cut that feature many times for a few extra bytes and cycles, but somehow it survived to the end of the project. The intention of the Color/BW switch is to make games look good on a black and white TV. Adventure is a perfect example of doing that. In my implementation I just mask out the color components of my ... colors. My implementation isn't perfect. I think a few palettes have ships that are the same shade of grey in black and white. Edited August 7 by Piledriver 3 Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.