Jump to content

vidak's Blog

  • entries
  • comments
  • views

Guerilla Game #5.1



Okay. I was off to the wrong start with my first kernel. On the WIP thread for this game in the forums we established what was really going on as Commando 2600 drew the screen. I will repeat the "discovery" here. Commando draws the screen in bands, what SpiceWare calls in Stay Frosty, "sections". So what we are able to do as we move down the screen is forget that we have drawn the playfield and Player1. This is one of the advantages of the 2600's "racing the beam". You are able to draw as many copies of the player graphics down the screen each line as you like, so long as you have enough memory to set everything up in the code. Perhaps an illustration would help:

TOP OF THE SCREEN--------------------------------------------------                                            ENEMY GRAPHICS (GRP1)-------------------------------------------------------------------       TREE GRAPHICS (GRP1)--------------------------------------------------------------------                           ENEMY GRAPHICS (GRP1)--------------------------------------------------------------------(NUSIZ-ed GRP1):           TREE GRAPHICS             TREE GRAPHICSBOTTOM OF SCREEN----------------------------------------------------

The great benefit of drawing the screen like this is that it multiplies the amount of objects you can draw on the screen. The player character, GRP0, can move all around all of these images on the screen. None of the rules of the 2600 hardware are broken.

I immediately attempted to go about programming a kernel that could do this. Unfortunately, because I am a novice I ran into a big conceptual issue. I couldn't imagine how it would be possible to draw different repeated kernel bands down the screen that would (a) get the 2600 to forget about GRP1, and change its data; while (b) not forgetting about GRP0 (the player character). Luckily SpiceWare provided me with the answer. He had already solved this problem in commented source code in Stay Frosty.

In the rest of this blog post I will explain how he achieves a free-moving GRP0 among bands of repeated GRP1s and playfield graphics. The simple answer for how SpiceWare achieves a GRP0 that can move between bands of repeated GRP1 is that he uses a mask to allow the GPR0 graphics to pass through on the correct lines. The kernel is programmed to calculate and draw the GRP0 graphics, but for the inclusion of the calculation of a mask which will either permit or block the graphics to the drawn. The big picture is easy to understand, but actually practically implementing this a little more complicated.

I How Stay Frosty Does It

The reason I am making this blog post is that I am having trouble understanding the complex process of setting up the pointers for GRP0 and GRP1 in SpiceWare's multi-band kernel.

A Macros

The kernel is implemented using DASM's ability to assemble code using macros.

What is a macro? A macro is different from a "function", "method", or "subroutine". When you type a macro into a file for DASM to assemble, you are not writing code which the Atari will end up running. You are using a set of instructions which DASM interprets and then uses to produce code that will be assembled for the Atari. The exact definition of a macro is that it is a sequence of characters that DASM will take and them map onto another sequence of characters. The purpose of having macros is to help reduce the need to repeat code mindlessly, or to reduce the conceptual complexity of producing the final characters which then go on to form the final code which is executed.

In this case the macro that SpiceWare has written is a sequence of characters which DASM is then told to repeat five times in order to construct an entire kernel to draw the five-banded screen. This can be seen in the source code here:

Line 577:        SECTION 4                 SECTION 3                 SECTION 2                 SECTION 1                         SECTION 0

The macro "SECTION" is defined like this. You can find a fairly accurate description of the acceptable syntax for macros in DASM here: http://www.macs.hw.ac.uk/~pjbk/scholar/dasm.html

        MAC SECTION .SEC    SET {1}<CODE>        ENDM

What happens in the above code is that we define the macro SECTION, and we define it so that it has one argument (SET {1}), and we use what I assume is ".SEC" as a pseudo-variable to pass this argument into the code. Basically what DASM does is replace .SEC in the code with the argument we pass to the macro. The manual I linked above says we cannot use these arguments to write recursive macro code, but we can call macros recursively. Anyway there is no such recursive concepts in Stay Frosty.

B Kernel Basics

This is the basic code that draws the main character:

        lda (FrostyImagePtr+.SEC*2),y         and (FrostyMaskPtr+.SEC*2),y           sta GRP0                               lda (FrostyColorPtr+.SEC*2),y          sta COLUP0                   

As you can see, there are pointers for the image, mask, and colour. The pseudo-variable that is passed into the code from the macro is also present. It is also multiplied by 2 with macro code. This means that if the number 4 is passed as an argument into the macro, the number 8 will be added to the pointer variable. Bear mind mind that each of these pointer variables are being used by Indirect Indexed instructions.

What happens when you type in LDA (Variable),Y is that the CPU fetches the data in the variable, and uses it as the location of 16 bit address in memory. The variable is in zero page memory. The variable is only 8 bits long, so it uses the next higher byte next to the variable in memory as the higher byte of the 16 bit address. Then, the number contained in the Y index register is added to this 16 bit address. That memory location is then fetched. Here is a graphical illustration of how this works:


The Zero Page memory location that contains the 16 bit address is called a pointer. It POINTS to the 16 bit location in memory that contains the real data we want to load. Because of the use of macros in this code, there is an extra step. We are adding the value of (.SEC x 2) to the address contained in the pointer. So we have the POINTER + (.SEC x 2) + Y which contains the final memory location which contains the data we want to load.

So, in order to understand how we are getting the data we want to load as graphics, we need to understand how this operation works.

We need to understand how we obtain the values for three elements:

  1. The Y Index Register
  2. The actual pointer
  3. The (.SEC x 2) macro code

C The Y Index Register

The easiest element to trace inside the code is how the Y Register is set. This code at the beginning of the SECTION macro sets the Y Register:

LINE 138:     ldy Heights+.SEC     ;     load Y with height of current section

So the Y Register holds the number of scanlines we need to draw in each section. It is decremented every time we finish each scan line. We form the value of the Y Register by loading the variable Heights and adding the argument we pass into the code from the macro. So we have 2 elements we need to trace in order to understand the Y register's value. We can obtain the value of .SEC easily: it is a number between 0 and 4. It is the band of the screen we are currently drawing. If we are on band 4, .SEC equals 4.

Where does Heights come from? Heights is formed in the PrepLevel "subroutine" macro.

                       ldy #SECTIONS*DATA_BYTES_PER_SECTION-4-1+2+2                       ; -4 for the 4 unused platform bytes                       ; +2 for fireball size info                       ; +2 for level control initLINE 2200:     .plLoop                               lda (LevelDataPtr),y                       sta Heights,y                       dey                       bpl .plLoop

The level data pointer forms the variable Heights. We loop through this loop (5 x 11 - 4 - 1 + 2 + 2) times. Which is 54 times. The explanations for the minor adjustments are in the above code. We have unused platform bytes, and some game logic encoded in the ROM along with the level data pointer information.

We now have one element we need to trace in order to understand how Heights is formed. That is the level data pointer variable. Where is this variable formed? It is formed just above the above code in the PrepLevel macro "subroutine":

LINE 2187:        lda CurrentLevel                  and #%00111111                  asl                  tay                  lda LevelData,y                  sta LevelDataPtr                  lda LevelData+1,y                  sta LevelDataPtr+1

We form the level data pointer from the CurrentLevel variable. After some masking and shifting using the AND and ASL operations (which I assume is used to make sense of the data), we turn it into an index register. We do not need to loop through this set of operations because we can infer that CurrentLevel can only be a small number of finite states--every single level in the game. This process above illustrates the process of forming a pointer quite well. A pointer is a 16 bit memory address, and we only have 8 bit variables through which to form this address. Therefore we need to set a pointer by setting two 8 bit variables. You can see this when we use STA LevelDataPtr and STA LevelDataPtr+1.

CurrentLevel is used as an index in order to draw data from LevelData, which is a label for information in ROM. LevelData* is in ROM like this:

LINE 2338:     LevelData                         .word Level1                         .word Level2                         .word Level3                         .word Level4                         .word Level1                         .word Level2                         .word Level3                         .word Level4

There are therefore 8 levels in this game - levels 1 to 4 repeated once. You'll notice that under each ROM label there are 54 bytes of data. This agrees with the number of times .plLoop repeats.

So where is CurrentLevel formed? In a few places.

At the beginning of the game, CurrentLevel is set to zero:

LINE 1295:        lda #0                  sta CurrentLevel     ; set initial level

But CurrentLevel can be incremented when a level is completed:

          ; level cleared, advance to next levelLINE 1325:        ldx CurrentLevel                  inx                  cpx #8                  bne SaveLevel                  ldx #0          SaveLevel                          stx CurrentLevel                  jsr PrepLevel

This ends the tracing needed in order to understand how the Y Register in the kernel is obtained. To recap, this is how we obtain Y:

CurrentLevel (Values 0--> LevelData (Repeats through Level1 to Level4 twice)-> LevelDataPtr (54 bytes of information)-> Heights

D The Actual Pointer

I don't have enough time today to pick through everything! I will return to this tomorrow.

Please look forward to me finishing up the kernel tomorrow.


* I am now in such a state of concentration that the word "Level" looks strange and doesn't make sense.

  • Like 1


Recommended Comments

After you get done learning everything you need to know, maybe one day you'll be able to help out with batari Basic. We need somebody with assembly language knowledge who has the time and energy to jump in and add new amazing features.

  • Like 1
Link to comment
Add a comment...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Recently Browsing   0 members

    • No registered users viewing this page.
  • Create New...