This is the final WIP update for MW ULTRA. In hindsight there could have been one or two intermediate updates more, but as often happens, the amount of work needed to bring the game to completion was considerably more than estimated, and there was little extra time for writing the updates!
But now we're finally almost there, with the game content-complete and being tested.
First some bad news, the "scramble tool" mentioned in the last update, that would turn mechanical enemies to your side did not make it through, due to being hard to use especially for flying enemies, and potentially game-unbalancing. But the rage dart gun definitely remains!
The four and half months since the last update involved first getting the game to a state where all exploration and combat could be played out without the story, and the weapon damage amounts would be roughly tuned for both the player and enemies.
At the end of last year, the game had acquired a system where the enemies' damage resistance could be defined for each damage type, including flames, electricity, melee, gunfire and explosives. This allowed rather precise finetuning, but it was easy to get lost in the details. Therefore, a return to a simpler system was in order.
In the beginning of March, the game was finally in the state that it could be played with a reasonable difficulty curve from the beginning to end, but without the story and friendly NPCs. Only the prologue story parts existed at this point; they had already been implemented to test the story scripting capabilities. Now the remaining work was theoretically simple: to fill the remaining disk space with the rest of the story, and also implement the game's endings.
To help with implementing the story, the engine had the following features, similar to Steel Ranger, Hessian and Metal Warrior 4. Below, "script code" means just assembly code organized into resource files for runtime loading.
- Run a piece of script code each frame (tick script)
- Run a piece of script code on each area transition. Used for music overrides and NPCs following the player
- The NPC's could run a specified script code each frame, in addition to their base per-frame update
- In-world objects (trigger areas or operable switches) could run a piece of script code, either once or several times
- Take over the player character's joystick input for scripted movement
- Unhide hidden enemies placed into the level, or turn respawning of certain enemies on or off
Dialogue would typically be implemented as NPC scripts, or if it was just the player character talking, as a tick script. Displaying the dialogue and knowing when it's complete happens by calling into a dialogue subroutine each frame, with the text resource and speaking actor as parameter. The subroutine handles having multiple actors "wait their turn", checking for prerequisites like distance and the speaking actor being stationary first, and displaying the speech bubble sprite and portrait for the speaking actor. When the piece of dialogue has been shown completely, it returns with the carry flag set, and the calling script code knows to proceed to the next step.
Also like in the previous games, there was no virtual machine or proper coroutine support for the scripting, everything was just raw assembly code. This was both good and bad, good in the sense that there was practically no limit in what the story scripts can do, bad in that implementing complex scripting again became cumbersome and errorprone (despite having already been noticed as an issue during Hessian's development), and resulted in increased memory & diskspace use.
Of course, a proper virtual machine resident in the engine would also have reduced available memory, and would have been "unnecessarily" resident also during pure exploration and combat scenes. But for future projects, it's finally time to address these scripting "quality of life" issues early on and properly.
Fast forward to the end of April, and all the story parts, "lore texts" on computer terminals and endings had taken shape. Dialogue had been practically written on the fly with only a story outline to base it on, but it ended up quite fine that way. The outline had existed since Summer 2017, and though at first it was uncertain how well it would integrate into the gameplay, it turned out fine as well, with only some minor parts either simplified or improved on the fly. The total amount of loadable story, enemy & NPC code ended up as 1 megabyte of source code, which is twice as much as in Steel Ranger!
At this point disk space had been mostly exhausted, though there seemed to be plenty free when the story work started. This would mean no extra bitmap graphics, rather the endings would also stay in the same camera angle as the rest of the game. Now it was time to start testing and tweaking the final experience.
A couple of weeks into May also that was done, first producing a beta version, then the first release candidate version. Some rather scary bugs were still found, most of them resulting from optimization:
- On the 2 diskside (D64) version, a charset asset had been forgotten from the other diskside, producing an unwanted "turn disk" prompt just to load it. Diskspace was out, so what to do? Directory track to the rescue, as with Steel Ranger. Now that it was in use, the added free space allowed to improve the endings a little as well.
- Dropping down to the ground with the grapple rope and crouching immediately after would produce rubbish on the screen, due to the player's animation frame being illegal momentarily. This could in the worst case corrupt memory.
- Sprite display code would mark resource file ages to 0, which meant "unloaded", producing rare lockups if resource files would not have been aged at all before needing to deallocate some file due to lack of memory. In this case the engine would not know what to deallocate.
- If the inventory double-click mechanism (as in Steel Ranger) had been disabled, also using items by fire longpress would stop working.
- Player could jump again in mid-air if jumping precisely at the moment of head hitting the ceiling. This was due to zero Y-speed allowing the player to jump one frame late after starting to fall, and the ceiling head bump would need to reset the Y-speed to a small nonzero value instead.
- A small jeep enemy would use wrong actor slots for launching its driver into the air on destruction. There is only room for 7 "complex" / humanoid actors including the player, but the jeep also tried to use the non-complex actor slots if those had run out, resulting in corruption of actor variables and a crash. Typically noticed when firing an RPG into a mass of enemies including several jeeps.
At this point, multiple playtesters have already finished the game, so it should be rather possible, but yet challenging to complete. Now all that remains are some more potential small fixes / tuning, and then finally getting the game out!
It has been a long journey to get to this point, but the end result should be worth it.
In the meanwhile, here's the main theme (title screen) track being played in the sound test program, with rastertime shown, and the very beginning of the game, with first half of the prologue:
1st January 2020:
Project status in short: all enemies are done. All boss fights are done. The final stretch is the non-hostile characters, the story, filling the rest of the game world, testing everything, and producing all the necessary materials for the publishers.
During the enemy work, the game got two more non-standard weapons to the player's arsenal: a special version of the dart gun, which "enrages" enemies and makes them attack each other, and a "scramble tool" which does the same for mechanical enemies. The aim is for these to be one-use and quite rare, so the player has to judge carefully which enemy to "turn" for maximum effect. The turned enemy's attacks still hurt the player too, so it's also important to stay out of the crossfire!
Implementing these was straightforward, however there was one gotcha: when a turned enemy disappears from the screen and then reappears, we should store the "turned" status information, so it can be restored. Otherwise the enemy would attack the player again, as usual, and using the tool would have gone to waste. Thankfully, since the enemies use only a subset of all the weapons available to player, there was a spare bit in the "weapon number" byte stored for each enemy, which now got a use!
Implementing the boss fights was also a relatively smooth experience. The boss enemy code typically breaks the game AI rules a little; the boss usually keeps the player always "locked on" as a target, so there is no possibility for total stealth during the encounters, like for normal enemies. However the player can still hide behind cover to avoid attacks. Like written before in the Steel Ranger WIP updates, boss enemy code tends to be somewhat throwaway and very specific to the area the battle is being fought on (e.g. using hardcoded tile X or Y coordinate checks to determine boss actions), but as long as it serves its purpose, no harm done! In contrast to Steel Ranger, there are very few hardcoded patterns in the boss behaviors; they still react to player actions like ordinary enemies would, but yet have some weaknesses or delays in their strategy so that the player has a chance to win.
The game engine handled the load of the boss fights well for the most part, and no new severe bugs were encountered. It's the final boss fight which stresses the CPU most and uses the most amount of sprites onscreen at once, so that provided a good target for measuring performance and optimizing where possible.
At this stage the optimizations are mostly tied to the low-level code: outputting the logical actor sprites as C64's hardware sprites for the sprite multiplexer, preparing the sorted sprite arrays for the raster interrupts actually displaying the sprites, and then the raster interrupt code itself. Only very minor optimization opportunities are possible on the higher game logic level, like physics and collision.
The objective is to get the total amount of CPU cycles taken by the sprite display code (both main program + raster interrupts) as low as possible, while also trying to keep any multiplexer glitching, which occurs when sprites need to be reused in too "tight" formations, to a minimum.
One of the first observations was that the register $d01d (sprite X-expand bits) was being written for every sprite in the interrupt code, while it changes only rarely. For the most part it stays at all bits 0, since only some bosses and some bullet sprites use expansion. Therefore, it turned out it's OK to only write it at the very end of a multiplexer raster interrupt, after sprite X,Y coordinates, frames and colors have been written.
Doing this optimization, which already reduces the amount of CPU cycles taken by the raster interrupt code, gave possibility to several others. Now $d01d bits can be calculated during the multiplexer preparation to a zeropage temporary variable instead of storing it to a non-zeropage array for every sprite. The multiplexer interrupt code is also reduced in size, so that all entrypoints to it (depending on the starting hardware sprite) fit within a single page, and only the entrypoint jump lowbyte needs to be updated for each sprite raster interrupt.
But this was not yet all that could be done! A subroutine call (JSR + RTS) takes 12 cycles of CPU time, so trying to optimize those away is always a high priority. However MW ULTRA can't afford to unroll or duplicate code, as the level and enemy data need all the free memory they can get. One good opportunity for JSR removal is in the loop which outputs the hardware sprites from a "logical" sprite. The bosses can consist of up to 13 sprites, so 13 subroutine calls could be removed.
In more detail, the problem is this: for every hardware sprite, a finalization step is needed, which checks the coordinate range for being within the screen, and finally "accepts" the sprite for displaying. There are two code paths: a fast path when the logical sprite contains just one hardware sprite, and a slow path when there are several. In the slow case we want to execute this finalization for every sprite, but without a JSR.
The solution turns out to be a small code self-modification. For the fast path, the finalization step is also the end of the logical sprite render process, and it should end with an RTS instruction. But for the slow path, it should loop for more hardware sprites. We put the desired jump address for the loop right after the RTS instruction, and when using the slow path, transform the RTS momentarily into a JMP instruction, and then back into RTS again when done. The self-modification wastes some cycles, but is an overall win compared to repeated JSR + RTS, saving almost 4 rasterlines when the multiplexer is fully loaded, which might already mean the difference of not skipping a frame in some tight situation.
Considering that the player can be firing on full auto, or using the grapple rope, generating more CPU slowdown just from more sprites being on the screen, it can't be guaranteed that e.g. the final boss fight would never slow down. However, especially on PAL machines the slowdowns are rare, and the REU / C128 / SCPU scrolling acceleration (when available) should eliminate them completely.
Some further optimization was possible in the raster interrupt code itself, particularly related to the "being late" check. This means comparing the current rasterline ($d012 value) to see if we were too late writing the $d012 target line for the next interrupt, and should already be executing its code manually right afterward. The failure to observe this leads into a glitching screen and slowdown (like in Commando) and is highly undesirable.
Previously, the code took up to 3 lines advance for checking this situation, which needed complicated math and made the code longer and slower. However, if we always acknowledge the current raster interrupt early, and check being late only after writing the target $d012 line, we only need 1 line of advance, like this, where Y register holds the target line. The next interrupt vector needs to have been written to $fffe / $ffff already:
cpy $d012 ;Late check
EndIrq: dec $01 ;Restore $01 value
lda irqSaveA ;Restore CPU registers
bcc EndIrqDirect ;Act on late check result
EndIrqDirect: jmp ($fffe)
Only two of the raster interrupts need the late check, the multiplexer interrupt, and the scorepanel split. Checks for both were combined into one, so the engine code also became smaller.
Optimizations didn't exactly even stop here: the final step was analyzing the engine code for page-crossing penalties from conditional branches crossing a page. These penalties are quite minor, yet for something repeated e.g. for each multiplexed sprite, they're better to eliminate. Page-crossing penalties from accessing variable arrays and static data had already been eliminated quite thoroughly, but how to best do it for code?
The answer turned out to be the custom-written limited C64 emulator Oldschoolengine2. It had been written for fun for the most part, but now it became definitely useful: by using it to record page crosses and dumping them to a file when execution stops, it was easy to determine where they happened most, and how code should be reorganized. Modifying the code of VICE for instance to record the same information would have been much more involved.
After these final optimizations, only the final stretch remains, to tie everything together into the finished game!
Naturally, free resources are going down: both disksides are below 200 blocks free now, and when populating the levels one has to avoid using so many different enemies at the same time that the dynamic memory allocation area would be exhausted. Also, after the enemies and bosses there are 100 actor types already used, which means just 27 left for the non-hostile story actors (as types 128-255 are used to mean items in the engine + world editor.) It *should* be enough, in contrast Hessian used 75 actor types in total, and Steel Ranger 115.
An interesting comparison is also that Hessian and Steel Ranger saw quite "scary" low-level modifications right to the end of the revision history, for example to the loader. In constrast MW ULTRA's code has had much more calendar time to mature, and therefore it's unlikely that significant late-stage changes would be needed, or that very nasty bugs would be found. This will hopefully result in a more relaxed final testing round.
Until the next update (which might already be the final one before release) ... here's a video of one of the boss fights:
9th November 2019:
Enemy work for MW ULTRA is well underway. Only mission area-related human enemies and boss enemies are missing now.
In the beginning of October, development work was "sidetracked" a little by the upcoming Metal Warrior Quadrilogy box set to be released by Psytronik Software. It contains the four original Metal Warrior games updated for better loading compatibility (EasyFlash .crt downloads included), tuned for better playability, and even a few improved cutscenes here and there. The Psytronik releases of Hessian and Steel Ranger also got the loader + EasyFlash .crt treatment, so there was a fairly good amount of testing involved.
But now that is done, and MW ULTRA work can continue.
The game will come out in 2020 as a joint release by Psytronik Software (disk version) and Protovision (cartridge and download versions.)
Working in MW ULTRA's engine to get the new enemies implemented has been fairly smooth, just a few bugs related to loading code while initializing or spawning enemies needed to be ironed out, and some more AI issues. For the first time, the engine allows mixing enemy code and sprite data into the same file on the disk, which reduces the amount of separate resources to keep track of, and preserves disk space better (less half-filled sectors.) In practice, this was implemented by the sprite editor saving the sprites
also in source code format, and the actual enemy code files just including them.
Once the final human enemies are complete (this is practically just graphics work), it's on to the bosses. There's a plan for there to be 7 boss fights in total. As some of the boss enemies will be fairly large, they may yet present a CPU use challenge. We will see. One way to counteract possible slowdowns is to make the boss arenas only horizontally scrolling, so that the CPU use from scrolling is more predictable.
One thing that needed to be done in preparation for the bosses (and also armored enemies, like gun turrets) was reworking the armor damage reduction handling. Now the game actually recognizes different damage types, such as melee blows, bullets, explosions, flames and electricity, and armor can have different damage reduction for each. Without this, it's quite hard to make a boss enemy that would require several magazines from a firearm, but only a reasonable amount of grenades or rockets.
Once the bosses are also done, then it's on to filling the rest of the game world, and working the game's story in, including non-hostile NPCs, dialogue and any special events. So far disk space is not a concern, but it's only about 250 blocks free on both disk sides, so it's not like one can keep adding content endlessly!
Finally, a couple of videos from along the way:
15th August 2019:
Enemy work for MW ULTRA is going on, along with bugfixes, minor AI adjustments and optimizations.
When one starts to work on the enemies seriously, AI routines get severely stresstested, and some odd behaviors stand out. For example, the player would occasionally get noticed even when in cover, if an enemy was approaching from the left and the player was behind cover, also facing left. This was a simple logic mistake, easily fixed once detected, but also easily missed if only using straightforward frontal attacks on enemies when testing.
How enemy spawning works got implemented. Spawn points are to be placed into the levels with the world editor. These are item actors with an illegal type, that will not be put onscreen as actual items. Instead, they're processed on each area change, and create an enemy at their position, using randomized enemy types and weapons from lists, similar to practically all Covert Bitops games so far. The horizontal position can be changed slightly to produce variation. Spawn points can also be turned on and off by the game's story progression boolean flags. The spawned enemies are removed when leaving the area.
But this is not yet the complete picture. MW ULTRA contains horizontally long areas, and the game world might seem empty if you were to walk to the other end of an area and defeat all the enemies along the way, then walk back, and find it still empty, until you actually cross to the next area. Therefore the spawn points within that same area will also respawn enemies after enough time has passed. This requires some care: the spawn point must be far enough from the player, and the spawn point's previous enemy must actually have been defeated. This is to ensure that the player is never overwhelmed. Staying in one place will never produce a continuous stream of respawning enemies (as can happen in Hessian), so the player can proceed at their own pace.
Along with enemy spawning, item drop logic & rules were also needed. The straightforward answer is for an enemy to drop their weapon, but there are also other pickups like coins, first aid kits and armor. Similar to Hessian, it's about thinking of what the player is likely to need most at the moment, along with some randomness:
- If player doesn't yet have the enemy's weapon, it should definitely be dropped.
- If player is low on health, first aid drop probability should increase.
- If player is already carrying maximum of something or has the same melee weapon, it shouldn't be dropped.
- If the enemy can drop coins, it's a good fallback choice.
Something that both Metal Warrior 4 and Hessian got occasional criticism for was showing only the amount of spare magazines instead of spare rounds, similar to the original Deus Ex game. This is perhaps more realistic, but could create ambiguity. So now MW ULTRA displays rounds in magazine and amount of spare rounds, like most recent shooter games. This required removing the expanded borders in the score panel (40 column mode instead of 38), but was well worth it, as now the selected weapon is also displayed completely thanks to more available space, and there's two chars more available for each line in conversations.
Since the last WIP update, the scrolling also got one more improvement. Rastertime would get very tight on an unaccelerated NTSC C64, leading to occasional visual glitches at the midpoint of the screen when redrawing the screen. The solution is to split the NTSC screen update to 3 parts: top, middle and bottom, instead of just top and bottom. No more glitches!
This means the REU scrolling code path could even be removed in case of severe compatibility trouble or lacking disk space, however it's preferable to keep it in, as it leaves REU-enabled machines much more free CPU time for all non-scrolling code, preventing potential slowdowns.
Now, the enemy work shall continue! Here is a work-in-progress video of the first enemies that were implemented, and fighting against them:
15th July 2019:
The initial pass through the whole of MW ULTRA's world map is complete now! It's about 750 screens split over 21 tilesets and 30 level loads.
Though there are no enemies yet, creating the world meant also designing all movement related puzzles, making sure the movement and level pacing is OK, as well as placing lockers & containers for weapon / item pickups. When enemies and story are added, these might need further adjustments, and it might be nice to add some more secret areas, but the majority of the work should already be done.
But as it has turned out already, development of MW ULTRA is never straightforward, so world map editing is never just world map editing, but also making engine tweaks and fixing bugs.
Since the last update, MW ULTRA changed the data compressor again, to Doynamite by Johan "Doynax" Forslöf. Its advantages are faster decompression, smaller code footprint, and less runtime variable space needed, at the disadvantage of slightly larger compressed data compared to Exomizer 3. Using Doynamite, the custom tile map storage format (written about in the last update) could be replaced with compressing each
room / area individually, making the engine code yet smaller as the custom decompressor code could be removed. Doynamite also
responds well to recompressing the collection of area datas to yield the final compressed level file, making it usually yet smaller on disk.
Pathfinding also needed a second rewrite. The previous algorithm stored bounding boxes of platforms, stairs and ladders for each room, and the AI code would head toward the closest one in the direction of the target. However, it needed quite a bit of storage space, and could make a
wrong turn in some cases. Therefore, the levels now store "junctions" for each horizontal platform, to aid the AI in making decisions to turn left or right. The junctions include stair and ladder endpoints, dead ends, mantleable spots, and too high drops. Fingers crossed for not needing a third rewrite!
When creating the game world progressed, it became apparent that the game will need disk side changes during gameplay - boot / play sides like in Metal Warrior 4 aren't enough to fit all data in. The plan is to have a disk turn at about halfway through the game, with the common "city hub" level data shared on both disk sides to eliminate repeated turns.
Now that the game world is done, it's easier to reflect on the total amount of data and memory needed, and to make a sort of mid-project postmortem on the world data-related decisions.
- Overall, the redraw-based scrolling works well and keeps CPU use lower than traditional scrolling, however there are some "problem areas" when some levels have so much color changes, that the color-RAM scrolling optimizations aren't of much use. On NTSC machines time can be tight in these spots, and so the engine got a REU optimization path for scrolling in addition to C128 & SuperCPU turbo modes. In the REU scrolling mode, the original scrolling routines get patched over by code that writes each room / area to the REU memory on room changes, and as the screen scrolls, the REU DMA transfer is fired for each row to quickly rewrite the screen and color RAM.
- Since the scrolling needs chars arranged in a certain way to match the char code and color code (done automatically by the editor), making charset animations becomes errorprone as the char order changes when the tileset gets edited. It's best to write the charset animation code only after the tileset is final, however "final" is often relative.
- The physics / collision code requires using special tiles to mark the ends of slopes and slope crossings to the map. This speeds up collision checks, but is manual and errorprone, and should not be used again in further Covert Bitops games!
- The "realistic" setting places some constraints on the world design, practically each area should make sense as a place people (even if villains) could actually exist and work in! In practice this turned not a problem, or was even liberating, since it reduces the amount of design possibilities, similarly as the C64's limitations force the scope of the game smaller and manageable.
- As no further world interactions or movement modes will be added (unless it turns out to be something absolutely vital) it's easy to take inventory of the ideas that will remain unused. It looks like there will for example be no lift platforms as in Steel Ranger. One planned idea was to have movable objects like boxes for reaching some hard-to-reach areas, but that looks not to be required either. MW ULTRA is still a traditional disk-based game that needs to keep all the engine code in RAM, and one has to be mindful of its total size. Some more advanced ideas were a hacking minigame (as in the latest Deus Ex games) or even taking control of enemy units, but these will remain just ideas as well. Therefore, the core of the gameplay will remain traditional: movement / exploration, combat (close / ranged / stealth), and advancing the story through character interactions.
That last point prompted the thought that making a C64 game cartridge-only could be the "next step up", almost becoming another platform, since code could be executed out of the cartridge ROM directly, and compromises could almost always be made to favor code speed. For example, speedcode-based scrolling. The amount of player character movements or interactions would also be much less limited, with just the cartridge total size as the limit.
Naturally, MW ULTRA will be finished as planned, being able to run both from disk and cartridge.
The next step after the world map will be the enemies. In addition to just enemy sprite pixeling and behavior coding, the algorithm for spawning enemies needs to be decided on. There will certainly be enemies in fixed positions for each playthrough, but in addition it would be good to add some unpredictability, or enemies reppearing to places that were cleared already, for example the city streets. Care must just be taken to not overwhelm the player with constant respawning.
Until the enemy work gets well underway, there won't be more videos, but in the meanwhile here are the game world screenshots from along the way so far:
12th February 2019:
Time flies! But MW ULTRA has also progressed significantly (if sometimes unpredictably) in the meanwhile.
The world pixeling is well underway, with the game's city streets (AKA the main "hub" area) complete, and also about half of the city building insides done. The game contains two bars / nightclubs you can visit, but something was missing - they should have their distinct musical themes! So, two more songs were composed.
The game's loading sequence is also very likely final now, with the loading picture fading in / out, and after loading there is a menu for reading the game's manual, or proceeding into the actual game. For disk, the disk side change happens at this point and is the only one needed (similar to Metal Warrior 4), unless the game grows so much to actually need game content on both disk sides.
December - January also saw the need to consider the best game world storage data format. It should preferably be small on disk, small when loaded into memory, and fast to decompress into the actual scrollable data used by the game. The initial format was just compressing each room or area individually with Exomizer, but that wasn't yet optimal. The results of the investigation were somewhat surprising, and in the end a new custom lightweight compression format was devised. It's described in the new "rant" published a little while ago - Tile map data compression - and the idea is that this intermediate format responds well to further Exomizer compression.
Speaking of Exomizer, the game now uses Exomizer 3 for better loading performance - the new decompressor is significantly more efficient in the bitstream manipulation.
In between world pixeling and some bugfixing, it also occurred that the actor / world collisions on sloped ground weren't 100% solid yet. This was particularly noticeable when throwing grenades against stairs. Both Hessian and Steel Ranger employ somewhat of a hack - when the actor is traveling downward toward a slope and the "landing" is to be checked, they add the absolute horizontal velocity to the vertical velocity for the check. This prevents glitching through the slopes, but can cause too early landing. Initially MW ULTRA inherited this code.
But a better approach is to move the actor first horizontally, checking for a slope crossing, then vertically, again checking for crossing. If the crossing is horizontal, the physics / collision routine can report a "wall hit" just like when hitting an actual wall, and for example a grenade would then bounce back. Writing the code this way was a success, and the main physics routine also got a little bit faster due to eliminating redundant checks for world collision data.
However - it grew in size.
This prompted a few somewhat mad days of code size optimization, with the aim of at first trying to get the code back to the original size, of course without losing functionality. However, I didn't stop there, and in the end, over 200 bytes were shaved off from the engine code, which I honestly didn't think to be possible at this point. Some observations:
- If there are separate pieces of code doing almost the same thing, try to combine, if just performance considerations permit. For example, the sprite multiplexer has a loop which is executed for every sprite past the first 8. It copies the sprite variables to sorted arrays for the multiplexer IRQ, and also builds a list of IRQs that need to be happen in vertical order. This was executing in two separate sub-loops: "find the first sprite of IRQ" and "find the rest of the sprites for the IRQ." Combining these activities yielded a nice code reduction, with a minimal performance loss of having to check a zeropage flag using a BIT instruction.
- Self-modifying code isn't always the shortest, due to the absolute writes that take 3 bytes per instruction. Several dynamic jumps were converted from selfmodifying to using a zeropage indirect jump instead. However, a JSR cannot be converted this way.
- Sometimes reorganizing data or the meaning of variables allows shorter code. For example, Ian can do a high punch (FIRE+UP) or a low punch (just FIRE.) Originally the "attack state numbers" for these were high punch = 6 and low punch = 7. But reversing these so that high punch is 7 instead allows to LSR the joystick control bits, so that UP ends up in the carry flag, and then do an ADC instruction to determine the new attack state without any branching.
- In a similar vein, the code for spawning bullets or other actors was expecting a 16-bit offset in world coordinates for both X and Y directions. This offset is produced by running the actor draw code in a "fake" manner without producing actual sprites for display. It operates on 8-bit pixel coordinates, which are expanded to 16-bit after the draw code has finished executing. But turns out it's more efficient to keep the spawn offset in 8-bit pixel space until the very end, when the new actor's final world coordinates are calculated and set. This practically reduced code size in every case where actor spawning was being handled.
- Sometimes subroutines just need to be used more efficiently. Case in point, drawing health bars, which could be modified to (mis)use scorepanel text printing routines. The original unoptimized code does its own drawing to the screen & color RAM instead:
; Draw a three-segment health bar
; Parameters: A value, X position, Y color
; Modifies: A,zpSrcLo
DrawHealthBar: sta zpSrcLo
DHB_LastCharOK: adc #19-1
lda zpSrcLo ;Decrement total value for next segment
DHB_MiddleChar: bcs DHB_MiddleCharOK
The optimized routine modifies the char color first, does the drawing, then restores the color to the default white needed by all status texts. There is increased subroutine use, resulting in more CPU cycles, but drawing the bars is infrequent in any case (only when player's health or experience points change):
; Draw a three-segment health bar
; Parameters: A value, X position, Y color
; Modifies: A,zpSrcLo
DrawHealthBar: sty PrintPanelColorValue+1
sty PrintPanelColorValue+1 ;Restore white color for text printing
DHB_Segment: lda zpSrcLo
DHB_LastCharOK: adc #19-1
DHB_MiddleChar: bcs DHB_MiddleCharOK
DHB_Char: jsr PrintPanelChar
With the game engine optimized to good shape, now nothing should prevent just concentrating on the world pixeling. Until the next unexpected thing comes up :)
Here are two new videos, showing the grenade bounce after getting the new physics / collision code in, and an early visit into the Hades Club, familiar from the original Metal Warrior games. Its music uses dual filtered channels for a raw distorted sound, particularly on a 6581 SID.
3rd December 2018:
The engine code for MW ULTRA has been open sourced on github as the c64gameframework project! This doesn't include any of MW ULTRA's content or game-specific code, but just the basics: scrolling, sprites, resource loading, sound playback, and helper routines including movement and collision.
The project includes an intentionally simple sidescroller game example, with a player character, shooting, powerups to pick up, enemies, moving between
areas, and in-memory checkpoint save & restore. Sprite assets & music are taken from Steel Ranger.
As MW ULTRA's release is still a long time away, it's good to contribute the engine code + editors already, in the hope they will be useful or inspiring. Compared to earlier Covert Bitops games, the main differentiating features are the concept of logical sprites (+ graphical collision bounds editing), and the flexible screen redraw-based scrolling, which saves CPU time for screen areas where the char colors don't change.
After the last WIP update, the engine gained one more feature: mastering also to GMod2 cartridge format. This gives the option of potentially also publishing MW ULTRA on cartridge. C64gameframework's example also demonstrates mastering to all of the supported formats: .d64 image, EasyFlash .crt and GMod2 .crt.
This practically means that four variants of the game's loader code must exist:
- Fastloader, using custom drivecode for 1541, 1571 (almost same as 1541), 1581 and CMD FD/HD
- Fallback disk loader using Kernal routines, used for IDE64, devices like SD2IEC, and "fast drive emulation"
- EasyFlash cartridge ROM loader. Also includes flash save support using the EasyAPI
- GMod2 cartridge ROM loader. Implements save support using the 2KB EEPROM
For disk support, the two first variants of the loader code are both included in the "LO" (loader) file that is read immediately after boot, but only one is left in the memory after initialization. The EasyFlash & GMod2 cartridge ROM loading code is rather similar, only the data layout on the cart differs due to different banking ($8000-$9fff and $a000-$bfff for EasyFlash, $8000-$9fff only for GMod2). Cartridge initialization after reset and saving differ much, though.
The game main part relies on the loader routine jump addresses always being the same, and the loader always occupying the same memory space. In practice the loader API is extremely simple, as there are only three routines actually to do with file I/O:
- Open a file
- Read a byte from opened file
- Save a file
The disk loader was written first, so it sets the amount of available space for all of the loader variants. It would be possible to have the memory layout
different in the cartridge builds, but having it the same speeds up the build process, as the main part and loadable code (which depends on the memory layout of the main part) only have to be compiled once. It also makes testing more reliable, as the amount of free memory in the same ingame situation will be the same across the disk / cartridge builds.
In practice, the cartridge loading code is extremely simple: just choose the correct bank, and copy bytes to the "sector buffer" in RAM at $0200-$02ff, similar to how the fastloader operates. Cartridge saving code is more involved, though.
On EasyFlash, cartridge space used for saving must be erased in 64 KB sectors. This sets all the bits in the sector to "1," and they can be changed to 0's while writing, but not back without erasing again. To reduce wear on the flash memory and speed up saving, it's a good idea to use up the whole 64KB sector before starting over and erasing it again.
The EasyFlash save file system uses a 512 byte "directory" in the beginning of the save sector to mark allocations (256 bytes for file numbers per page, then 256 bytes for number of bytes in the file's last page). When a save file is overwritten, the old file must be marked as obsolete. This is based on an idea demonstrated in Per Olofsson's Ultima 4 Remastered EasyFlash I/O code: because writing a $00 byte is always legal without having to erase, it's used to mark obsolete file entries in the save directory. Similarly, $ff (the initial value of bytes in a freshly erased save sector) marks a free page.
When it's time to erase the save sector, the latest versions of all save files currently on the cartridge must be backed up to RAM, under the Basic ROM region ($a000-$bfff.) This presents an additional logic problem: there necessarily isn't enough free RAM for this when the game is running! Therefore the save routine must call into the game main part and ask it to deallocate (purge) loaded resource files, until there is enough free memory. The EasyAPI code that does the actual flash ROM interfacing also occupies a fixed RAM location, adjacent to where the music is located, so there is an additional rule that while saving, the currently loaded music must not take too much memory! In MW ULTRA this is not a problem, as saving always happens in the title screen.
For GMod2 the save support implementation is thankfully much simpler, as the EEPROM does not need to be erased, but can be written to word-by-word (not byte-by-byte!) Therefore the save directory format can also be different: just 3 bytes per file entry (file number, size lowbyte, size highbyte.) However, there's still the trouble of memory in the loader area running a little short, so the stack and the sector buffer are reused creatively during the save. But the GMod2 save routine does not need to deallocate game resource files.
The fastloader save and the cartridge saves operate on the same principle, that a specific save file must always be the same size. In MW ULTRA (same as in Hessian & Steel Ranger) this is not a problem, as there is only the configuration file, a "savegame list" which contains summary information of all saves, and finally the save files themselves, and their sizes never change. On disk, space is preallocated so that the fastloader doesn't need to handle allocation. However, the game must be prepared for the fact that on cartridge initial save files will not exist, so for example the configuration must be able to use valid defaults.
Writing the cartridge images is done using two separate utilities, one for EasyFlash and one for GMod2. You can take a look at both cartridge loaders, and the cart writing utilities in the c64gameframework source tree:
Chester Kollschen from Knights of Bytes also provided some necessary consulting on GMod2 save; this was needed because of a failure to RTFM diligently enough.Thanks!
Now, as the NaNoWriMo 2018 month is over, and the engine code is released, it's time to get back into serious MW ULTRA development, starting with game world pixeling at last! As a summary, here's what's finished at this point:
- Game engine code
- Loading sequence & title screen
- Player animations
- Weapons, combat & stealth mechanics
- Standard enemy & pathfinding AI
- Music & 1st pass of sound effects
- Story (excluding detailed dialogue)
Finally, here's a video of the c64gameframework example game in action:
19th October 2018:
Some time has passed since last update, and the game has also progressed much in the meanwhile, bringing the game's always-resident engine code to completion for now.
After getting the weapons in, it was time to look at how the enemies and NPCs do navigation / pathfinding in the game world. The early Metal Warrior games did not have much movement AI to speak of, the enemies either made random decisions at junctions, or just tried to home in at the player.
Metal Warrior 4 and Hessian both use a kind of "ad-hoc" pathfinding, where the character looks for routes of opportunity at their current location. For example if the target is below, and there is a ladder leading down at the character's feet, it makes sense to climb down.
This works well most of the time, but trouble arises when the character is far from a junction, and doesn't know whether to go left or right first. This could lead to them trying the wrong direction first, appearing less intelligent.
This kind of pathfinding was also initially in use for MW ULTRA. But as the old problems reappeared, it was time to see if something better could be done.
The ideal solution would be a full precalculated navmesh of all walkable / climbable sections in each area, with information on how the sections are connected, but the memory use would easily be too much for the C64. Therefore a simpler "halfway" solution was developed. The world editor now automatically detects the walkable platforms, stairs and ladders in each area, and stores their coordinates / sizes, but doesn't store connection information. Instead, the character AI finds out the next adjacent section during runtime. It also knows the section the target character (typically the player) is on,
and tries to choose the next route step which allows to get closest to the target. Finally, any large drops and platforms the AI shouldn't attempt navigating to, can be tagged in the editor. This is to avoid getting stuck in a loop when the player character for example uses the grapple gun to get above, but the AI cannot follow.
With this system in place, enemies now appear to react intelligently to the player's position, and re-evaluate their route as necessary. For example if the player opts to drop from a large height (and likely take fall damage), the enemy will instead turn around and head for a ladder down if available, to reacquire the player on the platform below. Each navigable section takes 5 bytes of memory to define (type, coordinates, size) and there can be a total of 48 in a single scrolling area. The exact amount can be adjusted later as necessary.
After pathfinding, the next step was to work on "indicators" that appear on top of an enemy's head to let the player know their status. An exclamation mark appears when they see the player. Lethal and non-lethal defeat of an enemy also have their own icons.
Then it was time for another larger feature which had been postponed for a while, the stealth gameplay elements. The enemies already could detect line-of-sight to the target, so it was possible to hide from sight above or below a solid platform, or behind walls. But open areas allowed the enemies to easily detect and fire on the player, so something more was needed. With a few good suggestions from the Lemon64 forum, the game now implements "cover objects" like pillars or closets, that the player can go hiding next to. This is activated by pressing up on the joystick; the player character appears to go a little further in the depth direction. Enemies' sight is now blocked from the side of the object. If an enemy will then walk by, not yet alerted, the player can perform an attack with a surprise damage bonus.
Stealth mechanics also needed the reimplementation of the "make noise" feature from Metal Warrior 4. Footsteps, gunshots and all other sounds define a radius in which they make the enemies suspicious. This is indicated by a question mark indicator. The loudest sounds always alert every enemy on the screen no matter the distance, and like in Metal Warrior 4, for a time all new enemies coming into the view will be automatically suspicious.
The final piece of the stealth gameplay rules was to make it so that a suspicious enemy, or an enemy that has seen the player once, will also automatically look above and below non-solid platforms, while a completely unalerted enemy doesn't.
A "rock" weapon was added, also after Lemon64 discussions, that the player has an infinite supply of. These can be used to deliberately make small noises and distract enemies, but owing to the rule above, that can actually turn out as a very bad idea!
Next up was a small detour back to the soundtrack. The level and bossfight songs were done, yet something was missing. Hessian used to load a short gameover tune whenever you died, which meant a loading break (not good!) Therefore MW ULTRA turns back to what Metal Warrior 4 already did: every level has also its unique gameover theme. There ended up being 12 of them, lasting only a few seconds each. Several use the same chord progression as the "death theme" from the first Metal Warrior game: Am - A# - Am. One or two should never be heard, as it should be impossible to die in some of the levels, but for the sake of completeness they're still there...
For a long time during MW ULTRA development, dying meant reloading. But at last the game gained the pause / gameover menus similar to Hessian and Steel Ranger, and restarting from last entered area became possible. The final game will need some designing in this matter, it might be useful to have enemy-free "save rooms" which checkpoint the player, followed by longer stretches of enemy-filled areas that don't checkpoint. This would avoid respawning in the middle of trouble, with health down to the last recharging segment.
MW ULTRA will have light RPG elements similar to Metal Warrior 3: defeating enemies or completing other tasks will fill the experience bar on the bottom right of the screen. Once the bar is full, a "skill point" for upgrading the player character's skills is awarded. The skills will give damage bonuses, speed up weapon reloading, improve jumping and climbing abilities, allow for more carried weapons / ammunition, and improve damage resistance. Implementing both the XP gain and skill effects was straightforward enough, while the combined skill upgrade and stats screen (shown below) was more work.
After the skill menu, the final thing to work on before writing this update was to prototype the NPC conversation system. In Steel Ranger this was done in the project's final stages, and it led to ugly spaghetti code and even bugs. Doing it earlier this time allowed for a more controlled approach. The plan is to show the speaking character's portrait in the bottom left of the screen, in addition to the familiar speech bubble icon:
Showing the portrait wasn't completely straightforward. The portrait is a "logical sprite" that consists of several sprites, and rendering it in a fixed position on the screen is easy enough. But since the game assumes that all sprites are world objects that move along with the scrolling, a separate bit was needed for the sprite multiplexer to indicate that this is an overlay sprite, don't scroll it! This was actually stuffed into the sprite color byte, where there was still one bit free (color byte also controls sprite flickering, invisibility and X-expansion.)
A thing that recurred with all of the pause menu, skill upgrade screen and the conversation system was the need for a firebutton press to return to the game, but not yet initiate an attack by mistake. The previous games solved this by some ugly hacking of the joystick reading code, but utilizing the status bar / menu state machine, which already has states like "inventory," "pause menu" and "conversation" allowed a better solution this time: define an extra state which we call "resume", whose only purpose is to wait for releasing the fire button, before normal player character control (and attacks) resume.
Next up: some more fun with code size-optimization. Substantial gains are likely not possible at this point though... Then it's off to NaNoWriMo 2018, which might be partially used for working on the game's detailed script, and possibly, also starting the game world pixeling work at last!
In the meanwhile, here are the YouTube videos from the last two months, showing stealth and close combat:
2nd August 2018:
Now also the shorter bossfight and "danger" songs of the soundtrack are done. These 7 additional tunes were expertly composed by Kamil Wolnikowski (Jammer), so now the soundtrack totals 23 subtunes.
The initial set of weapons for the game are complete as well, adding up to 15 of them with the grapple gun counted in.
The disheartening part of implementing weapons, or any new features that go into the always-resident main part of the game, is seeing the code & data size grow. For weapons, there's much data to include: the weapon definition data including bullet speed, duration, damage, which sound effects to play for shooting and for reloading, the actor definitions for the bullets, the bullets' movement code (if special behavior is needed, such as the bouncing of grenades), the weapon sound effect data itself, and also the name texts of the weapons.
Therefore, after adding a bunch of new stuff, it's often beneficial to stand back and see whether the resident code & data could be reduced in size to compensate. In the best case some code doesn't need to be executed at all, leading to both size and CPU time reduction. Other times it's a tradeoff between memory use and execution speed. For example I know that by writing unnecessarily to the 40th (invisible) screen column, the screen redraw code could be reduced by almost half a kilobyte, at the expense of being slower due to the unnecessary writes. This might yet become in handy in a memory emergency.
One of the first things to optimize this time was the sound effect format. The Miniplayer follows the same idea as the Prince of Persia C64 sound routine, so that each sound effect frame starts with a bitmask byte,
telling what further data to read & put into the SID registers this frame. However, in practice changing the waveform and frequency occurs most often. Ideally these should be optimized to not require a separate bitmask byte at all.
Since MW ULTRA's sound effects are single-channel, they cannot use sync & ringmod bits in the waveform, so those bits could be used instead to tell if a frequency change or frequency slide are also going to occur on the same frame.
Frequency-only frames could likewise be optimized for a limited range of frequencies. We just need to be able to distinguish between waveform changes, frequency changes, ordinary bitmask frames, and "delay / wait" steps. To simplify code, the Miniplayer feature of being able to change pulse only would be dropped
as unnecessary. Therefore the final encoding of sound effect frame start bytes becomes:
$00 End of sound effect
$01-$8f Waveform change, bit 2 = frequency follows, bit 4 = frequency slide follows
$90-$9f No waveform change. Bit 1 = init ADSR/pulse follows, bit 2 = frequency follows, bit 4 = freq. slide follows
$a0-$df Set frequency only, possible range $00-$3f (high byte)
$e0-$ff Delay for x frames
Here's the data of one sound effect before and after:
In total, over 100 bytes were saved by the sound effect format change.
More bytes could be eliminated by changing some logic in the code. Due to working on the weapons last, I focused on the weapon & attack code now as well (an earlier size-optimization pass had already removed about 300 bytes from all over the code.)
One example: Ian can do a flying kick move while jumping, but he shouldn't be able to kick twice during a single jump. The unoptimized code transitioned from the "flying kick" state back to the ordinary "jumping / falling / walking" state after the kick animation,
and a variable needed to be set & checked to prevent the second kick. This logic could be changed to stay in the flying kick state until landing instead, in which case a not-insignificant amount of code could be eliminated.
The following piece in the flying kick state code (check for animation end and do state transition)
These optimizations do not save *that* many bytes at once, but the idea is to keep going through the code with a creative mindset, until the bytes saved begin to add up. Another trick to remember is reordering data where possible so that extra instructions accessing it can be kept to a minimum.
After this size-optimization pass it's finally time to start refining the combat AI, and begin implementing the proper stealth mechanics in the game, which might include hiding behind objects. Until next time!
22nd July 2018:
The "long" songs (title, level themes, ending) of the MW ULTRA soundtrack are all done!
After this, before tackling more of the gameplay things (enemy behavior, stealth mechanics), I took an opportunity to test something
that had never featured in Covert Bitops games before: a grapple gun in the style of Batman the Movie and Bionic Commando.
The gun is just a weapon that fires a special kind of bullet: the grappling hook. When the hook's code is active, it does some "magic"
to the player attack state: firing again (or switching weapons) is prevented, and the player character remains in the aiming pose (arm extended) as long as the
hook is flying through the air.
This is really the easy part, but the rope itself presents two math-heavy challenges for the C64's limited instruction set: the arc-like motion when the player is swinging on it, plus drawing it on the screen.
Since MW ULTRA doesn't use char bullets, just sprites, it's logical for the rope to also be all sprites, which are fed into the sprite multiplexer just like any actor sprites.
My hypothesis was that 12 predefined sprite frames with different angles would be enough to get a reasonably accurate rope "line", and it turned out OK.
These sprites are placed at 21 pixel intervals vertically, until the whole rope length is drawn. For more accuracy, one can alternate between two frames when the angle is "inbetween." Finally there are another 12 frames
with only half of the vertical space covered for a shorter final piece. As these are stored compressed, the total memory use of the hook + rope sprite frames is reasonable, about 500 bytes.
Initially the frame to use would be chosen from a 16 x 16 lookup table, indexed by horizontal and vertical distance (scaled by bit-shifting right until both are below 16) but
this was later replaced with real-time math: multiply X distance by 11, then divide by Y distance, and you have the sprite frame between 0 - 11. If there's a remainder
equal or greater to half of the Y distance, then alternate between two frames. Since the divide routine I use only operates on 8 bit values, the X * 11 needs to be
pre-shifted right (along with the divisor) until it fits into 8 bits.
To avoid overflows or the need for 16-bit math the distance from hook to player is calculated at two pixel accuracy both horizontally and vertically; it turns out to not look too bad.
Without the "each actor has its own draw routine" mechanism drawing the rope would have required support in the resident game engine code, but now it doesn't,
practically the engine does not need to even know of its existence at all. The rope is drawn as part of the grappling hook's draw routine, which means that if the hook is too far
outside the screen, the rope doesn't draw at all! Therefore the maximum length needs to be limited.
This covers the rendering, but what about the movement?
It turns out there's no need for trigonometry tables, or using an angle representation for the swing motion, instead applying the Pythagorean theorem
(x^2 + y^2 = l^2, where l is the total rope length) is enough. It's needed in two cases:
- When the grappling hook attachs, the rope length gets calculated from the current X & Y distance between the player and the hook.
- When the player position is updated during the swing, the Y-coordinate is calculated from the rope length and player's current X position (ie. y^2 = l^2 - x^2.)
Solving l or y requires a square root routine. Thanks go to Graham for the "fast square root" routine on Codebase64.
The acceleration to make the player swing back and forth comes simply from the rope frame: the acceleration is strongest on the extreme sides and zero in the middle.
Angles greater than 45 degrees cannot be drawn with the 12 rope sprite frames, so the maximum swing speed needs to be limited proportionally to the current rope length to prevent a too extreme angle happening in the first place.
The control mechanics are the same as in Batman the Movie; while swinging it's possible to either spool the rope in or out to ascend and descend, or apply a small swing force to the left or right.
Pressing fire detaches the hook.
Also similar to Batman the Movie, colliding with a wall during the swing will cause the hook to detach automatically and for the player to start falling. This simply
reuses the ordinary jump collision detection.
When the hook detaches, it starts to spool in automatically; it simply "homes in" toward player at a speed proportional to the distance. If the player is falling, the hook could end up "chasing" the player for
a longer time than looks good, but this can be fixed by adding the player's Y-velocity to the hook (this fix is not yet in use in the video below.) Upon detaching, the player's final swing velocity is applied as the new jump
velocity, so that the transition to jumping or falling is smooth.
Another thing that contributes to the smoothness is making sure that launching the hook can be done at any time: standing, walking or jumping, and it doesn't restrict player motion before the hook attaches.
This is in contrast to Batman the Movie, where Batman stops while the rope spools out.
Being so math-heavy, the hook + rope routines do consume some CPU time, and a maximum length rope also stresses the sprite multiplexer fairly much. Using it with many enemies around could result in a momentary
overload, either slowing down the game or resulting in missing sprites. However, this should be balanced by survival instinct: as the player character can't attack while on the rope, using it in combat is risky and
likely best avoided :)
Here's the end result captured on video:
1st July 2018:
Among other work (mastering also to EasyFlash cartridge format, plus the option for using a second firebutton to jump, both requested on Lemon64),
creating the soundtrack for MW ULTRA is now underway, with 6 songs complete so far.
The soundtrack is shaping up to be a little more atmospheric and less drum-heavy compared to Hessian & Steel Ranger, and could even be considered as a complementary
method of storytelling. MW ULTRA will not have a silent protagonist like the two previous games, but yet we can count on Ian not speaking that much,
and certainly not actually thinking aloud, so the soundtrack aims to (hopefully) provide additional insight into his headspace.
The basic workflow for composing is still the same as before: songs are initially composed in GoatTracker 2, taking into account
the player limitations, then converted. The conversion is now easier, as the Miniplayer converter program produces the output directly in source code format ready for the
build process, instead of having to manually run NinjaTracker 2 and use its packer / relocator function to save the final song data.
For testing on real C64 hardware, and to also check how the sound effects play on top of each song, the game's build process also creates a sound test program,
in which the tunes & sounds can be triggered with keys. When music is playing, sounds are played on the 1st channel only, that is typically reserved for the lowest
volume instruments, such as arpeggios or secondary melodies.
The Miniplayer uses a one-frame pseudo hardrestart somewhat similar to DMC 4. One frame before the next note's start, the gatebit is switched off and ADSR release is set
to maximum (15.) Combined with a silent first frame (typically testbit + gate), this ensures even note starts, but doesn't give time for the sound to automatically
start from zero volume, like a proper two-frame hardrestart would do. Therefore some care is needed in composing, to set the notes to manually release early enough.
Fortunately GoatTracker 2 can be made to mimic this kind of operation with the hardrestart ADSR parameter, and setting "gateoff timer" to 1 in all instruments.
Because notes don't always start strictly from zero volume, they blend to each other more naturally, giving the music a more organic feel. All previous Metal Warrior games that used the earlier music playback routines had 1-frame hardrestart as well, so it's also keeping the tradition :)
The CPU and memory use gains from switching from NinjaTracker 2 to Miniplayer are minimal to tell the truth, but after using the same routine for so long (NinjaTracker 2
was written in 2006) it was good to try something else for a change. The Miniplayer is based on some unfinished player concepts written at the same time or earlier,
the main unique point is having the pulse and filter modulation compare to a destination value, instead of counting frames remaining in the modulation
step. This reduces CPU use slightly, but makes it harder to use step-modulation loops that don't loop exactly (ie. down for 10 frames, up for 20, getting further away from
the start point each time.) That's mostly not a big deal, since both pulse and filter will sound bad if the pulsewidth or cutoff overflows and "wraps" during a long note.
Again, using the SID filter presents a challenge. Each 6581 SID has its unique cutoff frequency curve, and filtering a voice
also lowers its volume a little. Emulation usually doesn't account for this, so there's a risk of making some sounds too loud for emulation users, or too silent for
real hardware. Yet using the filter is an essential part of C64 music, so this time either, leaving it out would not be a real option.
Continuing the idea from Steel Ranger, the game detects the SID model (6581 or 8580) on startup, and will adjust the filter cutoff for 8580 to mimic a typical 6581 (as the
songs are composed for 6581.) This time the cutoff adjustment is no longer just a constant subtraction, but is strongest in the middle of the cutoff range,
using a linear function resembling an inverted triangle wave. It takes only a few assembly instructions, yet is surprisingly effective.
To not spoil "significant" themes like the title screen theme yet, here's a preview of the new "inside buildings" theme. In the original Metal Warrior it was just a repeating
melody, less than one minute total:
11th June 2018:
This update goes more in-depth into the sprite handling and scrolling in MW ULTRA.
Previous Covert Bitops games since Metal Warrior 4 have operated on the principle of each sprite frame having hotspot and "connect-spot" coordinates, and multi-sprite actors being
constructed by chaining these sprites one after one. The hotspot defines the sprite's origin, and the "connect-spot" tells where to place the origin of the next sprite.
For example humanoid actors' chain is: lower body, upper body, weapon.
That particular chain works well, but designing multi-sprite boss enemies quickly gets tedious, as the sprites typically have to be joined from their corners to form
the complete shape. For example the 4-sprite large spherical droid that is the first boss in Steel Ranger. Furthermore, when pixeling the sprites individually it's harder to
get the shape right.
Therefore MW ULTRA improves this system by introducing separate "logical sprites" in addition to physical (hardware) sprites.
Each logical sprite has the hotspot and connect-spot, as before. But it can consist of multiple physical sprites as necessary. To use the Steel Ranger boss as an example,
since it has no separately moving parts, it could be one logical sprite with four physical sprites.
The handling of human actors doesn't change much; since the lower and upper body can animate individually, they still need to be separate logical sprites.
The logical sprites also include collision box information, so collision bounds can now be edited graphically instead of typing them into source code.
Physical sprites within a single logical sprite can have different colors, as well as X-expansion on or off. Each physical sprite is a separate "object" within its resource
file, and duplicate shapes can be detected, so for example a large car with front and rear wheels the same would save memory.
The previous games used a few hardcoded routines for splitting actors into sprites. Typically these were an optimized routine for one sprite actors, a generic routine for
n sprites long sprite chain, and a separate piece of code for the humanoids.
In MW ULTRA, each actor type has its own "draw" routine in addition to the tick update routine, and it will be called to output the actor's logical sprites.
This allows greater freedom and extra logic during the draw if necessary. Furthermore for NPCs, enemies and bosses both the draw and update routines can be
loadable code instead of residing in the main game engine, so the engine's resident code size can be reduced.
The logical sprites are in turn split into physical sprites. In practice, optimization mandated that there's a separate fast path for logical sprites that have only one
physical sprite and max. one collision box. In this case the logical sprite resource object also contains the actual sprite data, which saves resource file memory overhead.
Furthermore, one resource file has a limit of max. 128 resource objects, so that allows placing more sprite frames into the same resource file.
The screenshot below illustrates the new sprite editor in action, editing a large enemy:
Then the second topic, the scrolling.
Arriving at the somewhat unorthodox "whole screen redraw" scrolling in MW ULTRA required some prototyping and experimenting.
Both the first and fourth Metal Warrior games (with Ian as the playable character) opted for color-per-tile graphics and 4x4 char tiles, making it impossible to get colorful
details to the graphics. At the time of writing Metal Warrior 4, it seemed absolutely impossible to get both color-per-char graphics and any sophisticated
AI routines in. Hessian solved the problem by having 8-way scrolling with limited speed, dividing the screen and color RAM update on 3 frames total. Steel Ranger had
multidirectional scrolling, but a smaller scrolling window and much simpler enemy AI code. Plus neither player or enemies had separate weapon sprites, so the total load
on the actor and sprite system was kept lower.
Reusing Hessian's 8-way scrolling was not really an option for MW ULTRA, as it will lag behind in fast falling motion, and generally introduce a jagged feeling
to the scrolling, which would fit badly with the varied scrolling speed (running vs. crouchwalking) and shifting the scrolling center depending on the player character's
facing direction (ie. show more of the world in forward direction.)
Therefore the initial options appeared to be:
- Traditional color-per-char, multidirectional scrolling, but with a limited-size scrolling window.
- Color-per-tile, with 2x2 tiles. The saved CPU time would allow a max.size scrolling window (22 rows) while keeping one row for the status display.
Before starting Steel Ranger's development, I had briefly experimented with redraw-based scrolling, using the tilemap data directly, but it seemed to have too much CPU overhead
and therefore not be promising. But in contrast to traditional scrolling, redrawing could allow to handle different tiles differently for optimization, e.g. some tiles could use a single color RAM value, or even a
single screen RAM value, or skip color RAM writes altogether.
This time Ian would hopefully get the colorful background graphics he deserved, so the first option of the two looked more promising. It would also cut down on scrolling
code size, since only three different color RAM copy loops (up, down, horizontal) would be required. In contrast, an optimized 2x2 color-per-tile scrolling would practically need
separate color shift routines for each of the 8 directions.
However, early tests proved that scrolling that way could consume CPU time so much that slowing down could result in situations where the screen scrolls fast,
so that practically every frame is a scrolling frame, the player fires a full-auto weapon, and there are also enemy actors on the screen. And this was without any AI or enemy firing so far!
Therefore I returned to examining redraw-based scrolling methods, and found a way to optimize the overhead of the tilemap access to acceptable. The key is indexing both
the screen and the tilemap data with the same CPU index register, leaving the other index register for the current tile's number for tile data access. With 2x2 blocks, the
screen indices would skip every second value, ie. 0, 2, 4... so also tilemap data would need to have the same tile row's tiles spaced to every second byte.
This proved not to be a severe problem, as two tilemap rows can be interleaved. Therefore, no wasted memory space.
Each of the visible tile map rows is handled in its own piece of code, so before the redraw, tile map addresses for each row need to be calculated. The redraw
code is modified directly for that, so that absolute address access can be used.
The redraw writes to both the screen RAM and the color RAM at the same time, from the top to the bottom, so a doublebuffered screen is not required, but the redraw takes almost
a whole frame (on NTSC it gets very tight!) At first this seems counterintuitive, but the redraw happens in an IRQ that starts from the middle of the screen, and meanwhile the main
program is already calculating the next frame ahead as much as it can, without any busy-waiting. Given this, it appears that it's better to have a large amount of CPU time taken away,
followed by a frame that's completely free for game logic processing, instead of taking half of every frame for alternatively shifting either the screen or the color RAM,
as Steel Ranger does.
The final thing to decide was: which kind of tiles would be possible? Initially, the redraw supported only two: either color-per-char (charcode & colorcode the same), or a
"default color" tile which skips the color RAM write completely. The default color would only be initialized when entering an area, not during scrolling. And
other colors could not be allowed to "leak" onto the default color areas. This scheme required only one branch per tile, but proved too limiting in practice.
Therefore it was necessary to add also actual color-per-tile tiles. This slows down the redraw slightly, as one more branch per tile is possible. However otherwise it would
likely be impossible to produce the background graphics the game needs.
Doing scrolling by redraw also gives the possibility to easily make arbitrarily large background animations (e.g. opening doors), as the CPU cost is just the same for scrolling,
and there doesn't need to be separate redraw code for the animations, like in the previous Covert Bitops games.
After the redraw code was in place, the world editor (based on Steel Ranger's world editor) needed to be modified to support the tile types, and the char allocation scheme that
matches the charcode to the char's color. The editor needs to analyze the map data and check for which tiles the color RAM writes can be optimized away. When necessary, two copies are
produced from a single tile: one with optimization, the one without. Color-per-tile tiles are actually slowest to draw due to needing to access the tile color separately,
so they are avoided if the right charcodes for color-per-char operation (max. 16 chars with the same color) can still be allocated.
The editor can also show a debug visualization, to see how the tiles will be drawn, which is shown in the screenshot below: (C is "color-per-char" and O is "optimize" which skips color RAM writes)
6th June 2018:
MW ULTRA is the new Covert Bitops C64 game in development!
It's essentially a reinterpretation or reboot of the first Metal Warrior game, but with substantial changes planned to both the story and gameplay.
The genre is still sidescrolling action-adventure, but the aim is to blend shooting, melee fighting and stealth in a
semi-realistic (or "semi-cinematic") way, with the melee fighting taken further by making the characters capable of multiple moves, including different punches and kicks.
Plans for revisiting the first MW game were on the backburner for a long time, including the possibility to use a more powerful platform.
During Steel Ranger's development, ideas how to do it on the C64 materialized instead, along with a very solid rework & expansion of the original's
story. Like in the original, you take the role of Ian, a troubled musician in a not-so-bright near future, but apart from that, expect surprises...
Technically, the game borrows the 2x2 tiles from Steel Ranger, but with a larger scrolling window that is redrawn completely from the tilemap data for each
scrolling update, instead of shifting the screen. Tiles can have their color RAM data updated differently to optimize the CPU use of scrolling,
either color-per-char with char & color code the same (completely inspired by Quod Init Exit II), color-per-tile, or skipping
color RAM update where there is a larger patch of the same color.
The resource management & code loading / relocation from Steel Ranger likewise makes a comeback. But Steel Ranger contained a shortcoming where loadable or
"story script" code couldn't load further resources from the disk, in the fear that it would get relocated in memory due to old resources getting purged, and
the code execution returning to an illegal address afterward, crashing the game. This time the engine should be capable of handling stack return address relocation to fix this!
The music routine is no longer NinjaTracker 2, but the "miniplayer" which was released earlier in 2018 as open source.
The engine work is going along well, with almost all the "whole time resident" code written, but naturally there is still much work to be done. The game is planned to
be finished in 2019, 20 years after the original's release!
Here's the first WIP video, showing player character movement & initial city background graphics. Note: despite being absent in this video, there's certainly going to be all-directional scrolling, and ingame music.