sd:part_ii_writing_games_in_assembly_language
Differences
This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| sd:part_ii_writing_games_in_assembly_language [2026/04/02 12:07] – appledog | sd:part_ii_writing_games_in_assembly_language [2026/04/03 05:30] (current) – appledog | ||
|---|---|---|---|
| Line 3: | Line 3: | ||
| == Introduction | == Introduction | ||
| - | At the end of [[SD-8516 Stellar BASIC]] we wrote a game called [[sdb: | + | At the end of [[SD-8516 Stellar BASIC]] we wrote a game called [[sdb: |
| + | |||
| + | == Part 1: Assembling and Running a Program | ||
| + | Just like BASIC programs have line numbers, or C programs have #include statements, an assembly language program should start with an '' | ||
| + | |||
| + | <codify armasm> | ||
| + | .address $000100 | ||
| + | |||
| + | start: | ||
| + | LDELM @message | ||
| + | LDAH $18 | ||
| + | INT $10 | ||
| + | RET | ||
| + | |||
| + | message: | ||
| + | .bytes "It works!", | ||
| + | </ | ||
| + | |||
| + | If you enter this code into ed and save it as robots.asm, then you can do: | ||
| + | |||
| + | as robots.asm robots | ||
| + | |||
| + | To run the program, just DLOAD (or LOAD) it and type "SYS 256". This can be a bit confusing; why 256? Without instructions, | ||
| + | |||
| + | * 1. Tell people they have to type SYS and a number. Mysterious. | ||
| + | * 2. use '' | ||
| + | * 3. Include a BASIC stub so people can type '' | ||
| + | |||
| + | For this program I will demonstrate option #3, although #2 is probably just as common. Our program now becomes: | ||
| + | |||
| + | <codify armasm> | ||
| + | .address $000100 | ||
| + | |||
| + | ; BASIC stub: 10 SYS 269 | ||
| + | .bytes $FB, $0A, $00 | ||
| + | .bytes "SYS 269", $00 | ||
| + | .bytes $00, $00 | ||
| + | |||
| + | ; --- Entry point at $00010D (decimal 269) --- | ||
| + | start: | ||
| + | LDELM @message | ||
| + | LDAH $18 | ||
| + | INT $10 | ||
| + | RET | ||
| + | |||
| + | message: | ||
| + | .bytes "It works!", | ||
| + | </ | ||
| + | |||
| + | === Step 1: ED | ||
| + | Enter this into ed by typing: | ||
| + | |||
| + | f robots.asm | ||
| + | a | ||
| + | |||
| + | Enter the program then type '' | ||
| + | |||
| + | === Step 2: Assembly | ||
| + | To assemble a program named '' | ||
| + | |||
| + | as robots.asm r | ||
| + | |||
| + | |||
| + | === Step 3: LOAD and RUN | ||
| + | Finally, enter | ||
| + | |||
| + | dload r | ||
| + | |||
| + | After you '' | ||
| + | |||
| + | 10 SYS 269 | ||
| + | |||
| + | Now you can RUN the program. This is a fun and convenient way to let users launch your assembly program. | ||
| + | |||
| + | == Part 2: Program Structure | ||
| + | Now that we understand how to assemble and run a program, we can talk about our game '' | ||
| + | |||
| + | * A game loop | ||
| + | * A subroutine that draws the map | ||
| + | * A subroutine to get player input | ||
| + | * A subroutine to move the robot | ||
| + | * Collision detection | ||
| + | |||
| + | === Initialization | ||
| + | Just like in the BASIC version, we can start by drawing the map. First, let's define our variables and then the map drawing function. We will include a simple main loop whose only job is to call draw_map once. | ||
| + | |||
| + | <codify armasm> | ||
| + | ; Define variables. | ||
| + | ; This section uses memory-location variables. $00 is memory location $00. | ||
| + | .equ PX $00 ; player x | ||
| + | .equ PY $01 ; player y | ||
| + | .equ RX $02 ; robot x | ||
| + | .equ RY $03 ; robot y | ||
| + | |||
| + | .address $000100 | ||
| + | |||
| + | ; BASIC stub: 10 SYS 269 | ||
| + | .bytes $FB, $0A, $00 | ||
| + | .bytes "SYS 269", $00 | ||
| + | .bytes $00, $00 | ||
| + | |||
| + | start: | ||
| + | ; First, let's initialize our variables. | ||
| + | LDAL #19 | ||
| + | STAL [@PX] ; Store 19 as initial player X location. | ||
| + | LDAL #11 | ||
| + | STAL [@PY] ; Story 11 as player' | ||
| + | LDAL #1 | ||
| + | STAL [@RX] | ||
| + | STAL [@RY] ; The robot starts at 1,1 for now, | ||
| + | |||
| + | main_loop: | ||
| + | CALL @draw_map | ||
| + | RET | ||
| + | </ | ||
| + | |||
| + | Now that we have initialized our main variables and set up a program loop we are ready for the '' | ||
| + | |||
| + | === draw_map | ||
| + | Like in the BASIC version, the '' | ||
| + | |||
| + | <codify armasm> | ||
| + | .equ VIDEO_MODE $01EF00 ; This defines VIDEO_MODE as a memory location variable. | ||
| + | |||
| + | draw_map: | ||
| + | ; VSTOP | ||
| + | LDAL $81 | ||
| + | STAL [@VIDEO_MODE] | ||
| + | |||
| + | ; CLS | ||
| + | LDAH $10 ; CLS | ||
| + | INT $10 | ||
| + | </ | ||
| + | |||
| + | In assembly, we can't easily call a BASIC command, so we do things slightly differently. Setting video mode to $81 sets us up in mode 1 but stops video updates (because bit 7 is set). So $81 means "mode 1, but set bit 7 so we don't update the screen" | ||
| + | |||
| + | Secondly, INT $10 AH=$10 is the clear screen function. We can call that as an interrupt helper here. | ||
| + | |||
| + | ==== Drawing the border | ||
| + | To draw the border we need to set up a loop that draws stars on the top and bottom, left and right, just like in BASIC. Here we will examine the top and bottom loop: | ||
| + | |||
| + | <codify armasm> | ||
| + | ; Draw top/bottom border: ' | ||
| + | LDBL #' | ||
| + | LDXL #0 ; set X location | ||
| + | |||
| + | dm_top_bottom: | ||
| + | LDYL #0 ; set Y location | ||
| + | LDAH $11 ; write char AL at XL, YL | ||
| + | INT $10 | ||
| + | </ | ||
| + | |||
| + | This simply loads the character, sets the x and y locations, and calls write_char_at_xy via INT $10. | ||
| + | |||
| + | <codify armasm> | ||
| + | LDYL #24 | ||
| + | LDAH $11 ; write char (on the bottom of the screen this time) | ||
| + | INT $10 | ||
| + | INC XL | ||
| + | CMP XL, #40 | ||
| + | JNC @dm_top_bottom | ||
| + | </ | ||
| + | |||
| + | Here's the loop: After the top and bottom characters are drawn, we INC XL (which is the column marker). Then we CMP XL, #40. This means "check if it's under 40"-- i.e. 0-39. If we are on 39, we need to draw again. If we just drew on column 39 and then we INC XL to 40, that means don't continue the loop, and we fall-through to the next function; drawing on the left and right sides. | ||
| + | |||
| + | <codify armasm> | ||
| + | ; Draw left/right border: ' | ||
| + | LDBL #' | ||
| + | LDYL #0 | ||
| + | dm_left_right: | ||
| + | LDXL #0 | ||
| + | LDAH $11 ; write char | ||
| + | INT $10 | ||
| + | |||
| + | LDXL #39 | ||
| + | LDAH $11 ; write char | ||
| + | INT $10 | ||
| + | |||
| + | INC YL | ||
| + | CMP YL, #25 | ||
| + | JNC @dm_left_right | ||
| + | </ | ||
| + | |||
| + | This code works just like the top and bottom but on the left and right sides. Instead of looping from 0 to 39 on XL, we will loop from 0 to 24 on YL. As before, we CMP to see if YL is 25. If it is, we fall through. A simple loop, in essence the exact same loop technique we used to draw the map in BASIC. | ||
| + | |||
| + | ==== Draw the player and robots | ||
| + | <codify armasm> | ||
| + | ; Draw player ' | ||
| + | LDBL #' | ||
| + | LDXL [@PX] | ||
| + | LDYL [@PY] | ||
| + | LDAH $11 ; write char | ||
| + | INT $10 | ||
| + | |||
| + | ; Draw robot ' | ||
| + | LDBL #' | ||
| + | LDXL [@RX] | ||
| + | LDYL [@RY] | ||
| + | LDAH $11 ; write char | ||
| + | INT $10 | ||
| + | |||
| + | ; VSTART | ||
| + | LDAL #1 | ||
| + | STAL [@VIDEO_MODE] | ||
| + | |||
| + | RET | ||
| + | </ | ||
| + | |||
| + | Finally we draw the player and the robot, and turn video updates back on (Set mode to 1, but without bit 7 set). As we are done drawing the map, we can now RET. If you save, assemble and run the program now you will see it draws the border, player and robot before exiting. | ||
| + | |||
| + | It's working! | ||
| + | |||
| + | more to come... | ||
| + | |||
| + | == Part 3: Player Input | ||
| + | Let's begin part 3 by adding a new subroutine to our main loop. Our main loop now becomes: | ||
| + | |||
| + | <codify armasm> | ||
| + | main_loop: | ||
| + | CALL @draw_map | ||
| + | CALL @get_input | ||
| + | RET | ||
| + | </ | ||
| + | |||
| + | === get_input | ||
| + | The primary goal of this section is to get input from the player and deal with it. We can do this by using the getkey function: | ||
| + | |||
| + | <codify armasm> | ||
| + | get_input: | ||
| + | LDAH $02 ; getkey() | ||
| + | INT $10 | ||
| + | </ | ||
| + | |||
| + | In this case, the function specification for AH=$02, INT 0x10 is: | ||
| + | |||
| + | ; ============================================================================ | ||
| + | ; AH=02h - Read Character (Blocking) | ||
| + | ; Input: | ||
| + | ; Output: AL = ASCII character | ||
| + | ; | ||
| + | ; | ||
| + | ; Note: X, Y preserved for cursor position | ||
| + | ; This function BLOCKS until a key is pressed | ||
| + | ; ============================================================================ | ||
| + | |||
| + | As we can see, this function returns the ASCII code of the key the player will press in '' | ||
| + | |||
| + | === uppercase or lowercase? | ||
| + | The player might type an uppercase command or a lowercase command. Let's normalize the keys to uppercase before processing them: | ||
| + | |||
| + | <codify armasm> | ||
| + | |||
| + | ; Uppercase: if AL >= ' | ||
| + | CMP AL, #97 | ||
| + | JNC @gi_check_keys | ||
| + | |||
| + | CMP AL, #123 | ||
| + | JC @gi_check_keys | ||
| + | |||
| + | SUB AL, #32 | ||
| + | |||
| + | gi_check_keys: | ||
| + | ... | ||
| + | </ | ||
| + | |||
| + | CMP works as follows: | ||
| + | |||
| + | CMP A, B ; if A >= B, then set carry | ||
| + | |||
| + | Therefore, | ||
| + | |||
| + | CMP AL, #97 ; if AL is greater or equal to #97 (' | ||
| + | |||
| + | So therefore if carry is NOT set, skip past the SUB command. Or else (if it's a lowercase ASCII) subtract 32 to convert from lowercase to uppercase. | ||
| + | |||
| + | The second compare has a protective function. | ||
| + | |||
| + | CMP AL, #123 ; if AL >= 123, set carry. | ||
| + | |||
| + | ; If it's ' | ||
| + | JC @gi_check_keys | ||
| + | |||
| + | |||
| + | So after these two checks, the only values that reach SUB AL, #32 are exactly the bytes from 97 to 122 inclusive; i.e. ' | ||
| + | |||
| + | Visual summary: | ||
| + | ^ AL Value ^ Meaning ^ What Happens ^ | ||
| + | | < 97 | before '' | ||
| + | | 97-122 | '' | ||
| + | | >= 123 | after ' | ||
| + | |||
| + | === Processing player input | ||
| + | We will use the HJKL convention: | ||
| + | |||
| + | <codify armasm> | ||
| + | gi_check_keys: | ||
| + | CMP AL, #' | ||
| + | JZ @move_left | ||
| + | |||
| + | CMP AL, #' | ||
| + | JZ @move_down | ||
| + | |||
| + | CMP AL, #' | ||
| + | JZ @move_up | ||
| + | |||
| + | CMP AL, #' | ||
| + | JZ @move_right | ||
| + | |||
| + | CMP AL, #' | ||
| + | JZ @quit_game | ||
| + | |||
| + | RET | ||
| + | </ | ||
| + | |||
| + | This is easy enough; after a CMP, if the values are equal then the zero flag is set. We will use JZ (jump if zero flag is set) to go to the correct routine. | ||
| + | |||
| + | === processing each command | ||
| + | <codify armasm> | ||
| + | move_left: | ||
| + | LDAL [@PX] ; load variable PX into register AL. | ||
| + | DEC AL ; X = X - 1 (move to the left!) | ||
| + | |||
| + | CMP AL, #1 ; bounds check. Is the player on the border? | ||
| + | JC @ml_ok | ||
| + | |||
| + | LDAL #1 ; If we reach here the player was on the border; set position to 1. | ||
| + | |||
| + | ml_ok: | ||
| + | STAL [@PX] ; pass! Save the player' | ||
| + | RET | ||
| + | </ | ||
| + | |||
| + | This is the move_left command. We begin by loading the player' | ||
sd/part_ii_writing_games_in_assembly_language.1775131642.txt.gz · Last modified: by appledog
