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/16 13:22] – appledog | sd:part_ii_writing_games_in_assembly_language [2026/05/03 02:36] (current) – created appledog | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | = Part II: Writing Games in Assembly Language | + | [[Writing Games in Assembly Language]] |
| - | + | ||
| - | == Introduction | + | |
| - | At the end of [[SD-8516 Stellar BASIC]] we wrote a game called [[sdb: | + | |
| - | + | ||
| - | However, if this is your first time writing an assembly language program, you might want to familiarize yourself with the architecture of the SD-8516 first: | + | |
| - | + | ||
| - | * [[SD-8516 Assembly Language]] | + | |
| - | + | ||
| - | Onwards and upwards. | + | |
| - | + | ||
| - | == First Steps: 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!", | + | |
| - | </ | + | |
| - | + | ||
| - | === Using the editor | + | |
| - | Enter this into ed by typing: | + | |
| - | + | ||
| - | f robots.asm | + | |
| - | a | + | |
| - | + | ||
| - | Enter the program then type '' | + | |
| - | + | ||
| - | === Assembly | + | |
| - | To assemble a program named '' | + | |
| - | + | ||
| - | as robots.asm r | + | |
| - | + | ||
| - | === 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. | + | |
| - | + | ||
| - | == Game 1: Robot | + | |
| - | Our first game will be a version of '' | + | |
| - | + | ||
| - | === Part 1. Program Structure | + | |
| - | We begin any project by mapping out the structure of the program and talking about the subroutines, | + | |
| - | + | ||
| - | For '' | + | |
| - | + | ||
| - | * 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] ; Store 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 '' | + | |
| - | + | ||
| - | === Part 2. Draw the 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! | + | |
| - | + | ||
| - | === 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 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. | + | |
| - | + | ||
| - | ==== Moving the Player | + | |
| - | <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' | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | move_down: | + | |
| - | LDAL [@PY] | + | |
| - | INC AL | + | |
| - | CMP AL, #23 | + | |
| - | JNC @md_ok | + | |
| - | LDAL #23 | + | |
| - | md_ok: | + | |
| - | STAL [@PY] | + | |
| - | RET | + | |
| - | + | ||
| - | move_up: | + | |
| - | LDAL [@PY] | + | |
| - | DEC AL | + | |
| - | CMP AL, #1 | + | |
| - | JC @mu_ok | + | |
| - | LDAL #1 | + | |
| - | mu_ok: | + | |
| - | STAL [@PY] | + | |
| - | RET | + | |
| - | + | ||
| - | move_right: | + | |
| - | LDAL [@PX] | + | |
| - | INC AL | + | |
| - | CMP AL, #39 | + | |
| - | JNC @mr_ok | + | |
| - | LDAL #38 | + | |
| - | mr_ok: | + | |
| - | STAL [@PX] | + | |
| - | RET | + | |
| - | + | ||
| - | </ | + | |
| - | + | ||
| - | ==== Quit Command | + | |
| - | The player can also quit the game by pressing q: | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | quit_game: | + | |
| - | ; CLS | + | |
| - | LDAH $10 | + | |
| - | INT $10 | + | |
| - | + | ||
| - | ; Print "QUIT GAME" | + | |
| - | LDBLX @msg_quit | + | |
| - | LDAH $66 | + | |
| - | INT $05 | + | |
| - | LDAH $64 | + | |
| - | INT $05 | + | |
| - | + | ||
| - | RET | + | |
| - | + | ||
| - | msg_quit: | + | |
| - | .bytes "QUIT GAME", 0 | + | |
| - | </ | + | |
| - | + | ||
| - | ==== Moving the Robot | + | |
| - | Moving the robot is similar to the player, except that we do not have commands from the robot (it has no keyboard to type), so we must simply try to move it towards the player. | + | |
| - | + | ||
| - | We will begin by determining if the player is to the left or the right, and then move left or right towards the player. Then we will check if the player is above or below us and then move up or down towards the player. | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | move_robot: | + | |
| - | ; X axis: move RX toward PX | + | |
| - | LDAL [@RX] | + | |
| - | LDBL [@PX] | + | |
| - | CMP AL, BL | + | |
| - | JZ @mr_check_y | + | |
| - | + | ||
| - | ;; If carry is set, then AL is bigger than BL and we should decrease RX's position. | + | |
| - | JC @mr_x_dec | + | |
| - | + | ||
| - | + | ||
| - | ; fall-through RX < PX | + | |
| - | ; In this case since the RX is less than PX, we should increase it to move towards the player. | + | |
| - | INC AL | + | |
| - | JMP @mr_x_done | + | |
| - | mr_x_dec: | + | |
| - | DEC AL | + | |
| - | mr_x_done: | + | |
| - | STAL [@RX] ; save the robot' | + | |
| - | + | ||
| - | mr_check_y: | + | |
| - | ; Y axis: move RY toward PY | + | |
| - | LDAL [@RY] | + | |
| - | LDBL [@PY] | + | |
| - | CMP AL, BL | + | |
| - | JZ @mr_done | + | |
| - | + | ||
| - | JC @mr_y_dec | + | |
| - | + | ||
| - | ; RY < PY | + | |
| - | INC AL | + | |
| - | JMP @mr_y_done | + | |
| - | + | ||
| - | mr_y_dec: | + | |
| - | DEC AL | + | |
| - | + | ||
| - | mr_y_done: | + | |
| - | STAL [@RY] ; save the robot' | + | |
| - | + | ||
| - | mr_done: | + | |
| - | RET | + | |
| - | </ | + | |
| - | + | ||
| - | After checking the relative location of the player vs. the robot, the robot attempts to move towards the player. | + | |
| - | + | ||
| - | After this, you can update the main loop: | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | main_loop: | + | |
| - | CALL @draw_map | + | |
| - | CALL @get_input | + | |
| - | CALL @move_robot | + | |
| - | + | ||
| - | RET | + | |
| - | </ | + | |
| - | + | ||
| - | === Part 4. Win condition | + | |
| - | The game is over when the robots collide with you. Here's the new main loop: | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | main_loop: | + | |
| - | CALL @draw_map | + | |
| - | CALL @get_input | + | |
| - | CALL @move_robot | + | |
| - | + | ||
| - | ; Check collision: PX==RX && PY==RY | + | |
| - | ; The collision check falls through to game_over only if both axes match, otherwise loops back. | + | |
| - | LDAL [@PX] | + | |
| - | LDBL [@RX] | + | |
| - | CMP AL, BL | + | |
| - | JNZ @main_loop | + | |
| - | LDAL [@PY] | + | |
| - | LDBL [@RY] | + | |
| - | CMP AL, BL | + | |
| - | JNZ @main_loop | + | |
| - | JMP @game_over | + | |
| - | + | ||
| - | RET | + | |
| - | + | ||
| - | </ | + | |
| - | + | ||
| - | That little bit at the end checks for collision. As an exercise to the reader, you can try to move it into a subroutine. | + | |
| - | + | ||
| - | Next let's write the game-over ending: | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | game_over: | + | |
| - | ; CLS | + | |
| - | LDAH $10 | + | |
| - | INT $10 | + | |
| - | + | ||
| - | ; Print "OH NO! THE ROBOT CATCHES YOU." | + | |
| - | LDBLX @msg_game_over1 | + | |
| - | LDAH $66 | + | |
| - | INT $05 | + | |
| - | LDAH $64 ; newline | + | |
| - | INT $05 | + | |
| - | + | ||
| - | ; Print "GAME OVER." | + | |
| - | LDBLX @msg_game_over2 | + | |
| - | LDAH $66 | + | |
| - | INT $05 | + | |
| - | LDAH $64 | + | |
| - | INT $05 | + | |
| - | + | ||
| - | RET | + | |
| - | + | ||
| - | ; ============================================================================ | + | |
| - | ; String data | + | |
| - | ; ============================================================================ | + | |
| - | msg_game_over1: | + | |
| - | .bytes "OH NO! THE ROBOT CATCHES YOU.", 0 | + | |
| - | msg_game_over2: | + | |
| - | .bytes "GAME OVER.", | + | |
| - | msg_quit: | + | |
| - | .bytes "QUIT GAME", 0 | + | |
| - | </ | + | |
| - | + | ||
| - | At this time, your '' | + | |
| - | + | ||
| - | === Part 5. Music and Sound | + | |
| - | Let's add a little sound effect to the game (just like in BASIC!) | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | robot_beep: | + | |
| - | LDELM @robot_sfx | + | |
| - | LDAH $52 | + | |
| - | INT $11 | + | |
| - | RET | + | |
| - | + | ||
| - | robot_sfx: | + | |
| - | .bytes "T120 W1 V5 O2 L24 B", 0 | + | |
| - | </ | + | |
| - | + | ||
| - | Now add '' | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | main_loop: | + | |
| - | CALL @draw_map | + | |
| - | CALL @get_input | + | |
| - | CALL @move_robot | + | |
| - | CALL @robot_beep | + | |
| - | + | ||
| - | ... | + | |
| - | </ | + | |
| - | + | ||
| - | There, now you can PLAY music strings just like in BASIC. This is good! | + | |
| - | + | ||
| - | === robots.asm Complete Listing | + | |
| - | <codify armasm> | + | |
| - | // robots.asm | + | |
| - | + | ||
| - | ; Variables | + | |
| - | .equ PX $00 ; player X | + | |
| - | .equ PY $01 ; player Y | + | |
| - | .equ RX $02 ; robot X | + | |
| - | .equ RY $03 ; robot Y | + | |
| - | + | ||
| - | .equ VIDEO_MODE | + | |
| - | + | ||
| - | ; Address statement. | + | |
| - | .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: | + | |
| - | ; Initialize variables | + | |
| - | LDAL #19 | + | |
| - | STAL [@PX] | + | |
| - | LDAL #11 | + | |
| - | STAL [@PY] | + | |
| - | LDAL #1 | + | |
| - | STAL [@RX] | + | |
| - | STAL [@RY] | + | |
| - | + | ||
| - | main_loop: | + | |
| - | CALL @draw_map | + | |
| - | CALL @get_input | + | |
| - | CALL @move_robot | + | |
| - | CALL @robot_beep | + | |
| - | + | ||
| - | ; Check collision: PX==RX && PY==RY | + | |
| - | ; The collision check falls through to game_over only if both axes match, otherwise loops back. | + | |
| - | LDAL [@PX] | + | |
| - | LDBL [@RX] | + | |
| - | CMP AL, BL | + | |
| - | JNZ @main_loop | + | |
| - | LDAL [@PY] | + | |
| - | LDBL [@RY] | + | |
| - | CMP AL, BL | + | |
| - | JNZ @main_loop | + | |
| - | JMP @game_over | + | |
| - | + | ||
| - | RET | + | |
| - | + | ||
| - | ;;;;;;;;;;;;;;;;;;;;;;;;;;;; | + | |
| - | ;; Draw the map. | + | |
| - | ;;;;;;;;;;;;;;;;;;;;;;;;;;;; | + | |
| - | draw_map: | + | |
| - | ; VSTOP | + | |
| - | LDAL $81 | + | |
| - | STAL [@VIDEO_MODE] | + | |
| - | + | ||
| - | ; CLS | + | |
| - | LDAH $10 ; CLS | + | |
| - | INT $10 | + | |
| - | + | ||
| - | ; Draw top/bottom border: ' | + | |
| - | LDBL #' | + | |
| - | LDXL #0 | + | |
| - | + | ||
| - | dm_top_bottom: | + | |
| - | LDYL #0 | + | |
| - | LDAH $11 ; write char AL at XL, YL | + | |
| - | INT $10 | + | |
| - | + | ||
| - | LDYL #24 | + | |
| - | LDAH $11 ; write char | + | |
| - | INT $10 | + | |
| - | INC XL | + | |
| - | CMP XL, #40 | + | |
| - | JNC @dm_top_bottom | + | |
| - | + | ||
| - | ; 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 | + | |
| - | + | ||
| - | ; 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 | + | |
| - | + | ||
| - | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | + | |
| - | ;; Get input from the player. | + | |
| - | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | + | |
| - | get_input: | + | |
| - | ; Blocking GETKEY -> AL | + | |
| - | LDAH $02 | + | |
| - | INT $10 | + | |
| - | + | ||
| - | ; Uppercase: if AL >= ' | + | |
| - | CMP AL, #97 | + | |
| - | JNC @gi_check_keys | + | |
| - | CMP AL, #123 | + | |
| - | JC @gi_check_keys | + | |
| - | SUB AL, #32 | + | |
| - | + | ||
| - | 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 | + | |
| - | + | ||
| - | move_left: | + | |
| - | LDAL [@PX] | + | |
| - | DEC AL | + | |
| - | CMP AL, #1 | + | |
| - | JC @ml_ok | + | |
| - | LDAL #1 | + | |
| - | ml_ok: | + | |
| - | STAL [@PX] | + | |
| - | RET | + | |
| - | + | ||
| - | move_down: | + | |
| - | LDAL [@PY] | + | |
| - | INC AL | + | |
| - | CMP AL, #23 | + | |
| - | JNC @md_ok | + | |
| - | LDAL #23 | + | |
| - | md_ok: | + | |
| - | STAL [@PY] | + | |
| - | RET | + | |
| - | + | ||
| - | move_up: | + | |
| - | LDAL [@PY] | + | |
| - | DEC AL | + | |
| - | CMP AL, #1 | + | |
| - | JC @mu_ok | + | |
| - | LDAL #1 | + | |
| - | mu_ok: | + | |
| - | STAL [@PY] | + | |
| - | RET | + | |
| - | + | ||
| - | move_right: | + | |
| - | LDAL [@PX] | + | |
| - | INC AL | + | |
| - | CMP AL, #39 | + | |
| - | JNC @mr_ok | + | |
| - | LDAL #38 | + | |
| - | mr_ok: | + | |
| - | STAL [@PX] | + | |
| - | RET | + | |
| - | + | ||
| - | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | + | |
| - | ;; Move the robot. | + | |
| - | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | + | |
| - | move_robot: | + | |
| - | ; X axis: move RX toward PX | + | |
| - | LDAL [@RX] | + | |
| - | LDBL [@PX] | + | |
| - | CMP AL, BL | + | |
| - | JZ @mr_check_y | + | |
| - | JC @mr_x_dec | + | |
| - | ; RX < PX | + | |
| - | INC AL | + | |
| - | JMP @mr_x_done | + | |
| - | mr_x_dec: | + | |
| - | DEC AL | + | |
| - | mr_x_done: | + | |
| - | STAL [@RX] | + | |
| - | + | ||
| - | mr_check_y: | + | |
| - | ; Y axis: move RY toward PY | + | |
| - | LDAL [@RY] | + | |
| - | LDBL [@PY] | + | |
| - | CMP AL, BL | + | |
| - | JZ @mr_done | + | |
| - | JC @mr_y_dec | + | |
| - | ; RY < PY | + | |
| - | INC AL | + | |
| - | JMP @mr_y_done | + | |
| - | mr_y_dec: | + | |
| - | DEC AL | + | |
| - | mr_y_done: | + | |
| - | STAL [@RY] | + | |
| - | + | ||
| - | mr_done: | + | |
| - | RET | + | |
| - | + | ||
| - | ;;;;;;;;;;;;;;;;;;;;;;;; | + | |
| - | ;; Game Over. | + | |
| - | ;;;;;;;;;;;;;;;;;;;;;;;; | + | |
| - | game_over: | + | |
| - | ; CLS | + | |
| - | LDAH $10 | + | |
| - | INT $10 | + | |
| - | + | ||
| - | ; Print "OH NO! THE ROBOT CATCHES YOU." | + | |
| - | LDBLX @msg_game_over1 | + | |
| - | LDAH $66 | + | |
| - | INT $05 | + | |
| - | LDAH $64 ; newline | + | |
| - | INT $05 | + | |
| - | + | ||
| - | ; Print "GAME OVER." | + | |
| - | LDBLX @msg_game_over2 | + | |
| - | LDAH $66 | + | |
| - | INT $05 | + | |
| - | LDAH $64 | + | |
| - | INT $05 | + | |
| - | + | ||
| - | RET | + | |
| - | + | ||
| - | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | + | |
| - | ;; Player quit game | + | |
| - | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | + | |
| - | quit_game: | + | |
| - | ; CLS | + | |
| - | LDAH $10 | + | |
| - | INT $10 | + | |
| - | + | ||
| - | ; Print "QUIT GAME" | + | |
| - | LDBLX @msg_quit | + | |
| - | LDAH $66 | + | |
| - | INT $05 | + | |
| - | LDAH $64 | + | |
| - | INT $05 | + | |
| - | + | ||
| - | RET | + | |
| - | + | ||
| - | robot_beep: | + | |
| - | LDELM @robot_sfx | + | |
| - | LDAH $52 | + | |
| - | INT $11 | + | |
| - | RET | + | |
| - | + | ||
| - | ; ============================================================================ | + | |
| - | ; String data | + | |
| - | ; ============================================================================ | + | |
| - | msg_game_over1: | + | |
| - | .bytes "OH NO! THE ROBOT CATCHES YOU.", 0 | + | |
| - | msg_game_over2: | + | |
| - | .bytes "GAME OVER.", | + | |
| - | msg_quit: | + | |
| - | .bytes "QUIT GAME", 0 | + | |
| - | robot_sfx: | + | |
| - | .bytes "T120 W1 V5 O2 L24 B", 0 | + | |
| - | </ | + | |
| - | + | ||
| - | == Game 2: Rogueima | + | |
| - | Game programming is not that difficult, as long as you establish the scope of your game and understand all the individual parts, it can be considered a robotic, mechanical exercise of coding. Even if there are some parts you don't understand, you can learn-as-you-go so long as you have a conception of what it should be like. | + | |
| - | + | ||
| - | But it's important not to bite off more than you can chew. Let's continue on our journey of learning Assembly for the SD-8516 by moving from '' | + | |
| - | + | ||
| - | === Scope and Planning | + | |
| - | As always, the first step is scope and planning. What exactly are we trying to accomplish with this game? And, why Assembly, why not BASIC? The simple answer is that in a typical roguelike game, although it is character based, it requires data structures and computations that are difficult to perform in BASIC. In it's current form. Stellar BASIC is not set up to really make a big, professional-quality game. That will change in the future as it gets improved over time, but for now, if you want to make something ' | + | |
| - | + | ||
| - | Not that there' | + | |
| - | + | ||
| - | Ok, here's what we need. | + | |
| - | + | ||
| - | * Everything in the '' | + | |
| - | * A status display showing our character information | + | |
| - | * A map that we can explore | + | |
| - | * Some form of 'fog of war' or ' | + | |
| - | * The ability for monsters to randomly appear | + | |
| - | * The ability to fight and defeat monsters | + | |
| - | * The ability to pick things up and use them | + | |
| - | * The ability to view an inventory | + | |
| - | * The ability to save your game | + | |
| - | + | ||
| - | Once we have all these things, the game will be considered finished. Beyond the above, there is actually quite a lot of work that would need to be done to make a game like NetHack (or even, like bsdgames' | + | |
| - | + | ||
| - | === Step 1. Drawing the Screen | + | |
| - | The easiest first step is to draw a status bar. In classic rogue, the status bar is on the bottom of the screen. But this means the map is now 80 x 23 (or 80x22 on some displays) characters, and that is incredibly unbalanced. Let's try a different approach and put the status on the right. This is how Temple of Apshai did it, not to mention Ultima 4, and many other games. The Bard's Tale series, technically, | + | |
| - | + | ||
| - | Second, let's use Mode 6 for this game, since it is the classic terminal interface you would expect from a game like Rogue or NetHack. | + | |
| - | + | ||
| - | ; ************************************** | + | |
| - | ; * * * | + | |
| - | ; * * * | + | |
| - | ; * * * | + | |
| - | ; * *********** | + | |
| - | ; * * * | + | |
| - | ; * * * | + | |
| - | ; * * * | + | |
| - | ; * * * | + | |
| - | ; * * * | + | |
| - | ; ************************************** | + | |
| - | + | ||
| - | The above is not to scale, but is the idea. The left side will hold the map, and the right side will hold the status. Status, for now, will include Name, Strength, and Hit Points. That's all we need to start. Therefore we will need new variables to hold Name, Strength, and Hit Points, and we will need to write that into the status bar after we draw the map border. | + | |
| - | + | ||
| - | To the front of the code we will add variable space for the player' | + | |
| - | + | ||
| - | .equ PX $00 ; player X | + | |
| - | .equ PY $01 ; player Y | + | |
| - | .equ RX $02 ; robot X | + | |
| - | .equ RY $03 ; robot Y | + | |
| - | .equ player_name | + | |
| - | .equ pname_zero | + | |
| - | .equ player_hp | + | |
| - | .equ player_str | + | |
| - | + | ||
| - | An interesting question is what things like " | + | |
| - | + | ||
| - | Therefore, our initialization becomes: | + | |
| - | + | ||
| - | init_game: | + | |
| - | LDA #10 | + | |
| - | STAL [@player_str] | + | |
| - | STAL [@player_hp] | + | |
| - | + | ||
| - | You may notice we defined the variables differently here. Above, we added a .bytes imperative to reserve space. The label above the .bytes becomes the label of the memory address where those bytes are assembled. Instead, we could have defined the variables like this: | + | |
| - | + | ||
| - | .equ player_str | + | |
| - | .equ player_hp | + | |
| - | .equ player_name | + | |
| - | .equ pname_zero | + | |
| - | .equ free_space | + | |
| - | + | ||
| - | Next, when drawing the map, we will use the following modified routine: | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | draw_map: | + | |
| - | ; VSTOP | + | |
| - | LDAL $86 ; Mode 6 VSTOP | + | |
| - | STAL [@VIDEO_MODE] | + | |
| - | + | ||
| - | ; CLS | + | |
| - | LDAH $10 ; CLS | + | |
| - | INT $10 | + | |
| - | + | ||
| - | ; Draw top/bottom border: ' | + | |
| - | LDBL #' | + | |
| - | LDXL #0 | + | |
| - | + | ||
| - | dm_top_bottom: | + | |
| - | LDYL #0 | + | |
| - | LDAH $11 ; write char AL at XL, YL | + | |
| - | INT $10 | + | |
| - | + | ||
| - | LDYL #24 | + | |
| - | LDAH $11 ; write char | + | |
| - | INT $10 | + | |
| - | + | ||
| - | CMP XL, #59 ; if XL >= 10, then set carry. | + | |
| - | JNC @dm_skip_ms | + | |
| - | + | ||
| - | LDYL #10 ; status/text window separator | + | |
| - | LDAH $11 ; write char | + | |
| - | INT $10 | + | |
| - | + | ||
| - | dm_skip_ms: | + | |
| - | INC XL | + | |
| - | CMP XL, #80 ; Mode 6 is 80 chars wide. | + | |
| - | JNC @dm_top_bottom | + | |
| - | + | ||
| - | ; Draw left/right borders: ' | + | |
| - | LDBL #' | + | |
| - | LDYL #0 | + | |
| - | dm_left_right: | + | |
| - | LDXL #0 ; left border | + | |
| - | LDAH $11 ; write char | + | |
| - | INT $10 | + | |
| - | + | ||
| - | LDXL #59 ; middle border | + | |
| - | LDAH $11 ; write char | + | |
| - | INT $10 | + | |
| - | + | ||
| - | LDXL #79 ; right side border | + | |
| - | LDAH $11 ; write char | + | |
| - | INT $10 | + | |
| - | + | ||
| - | + | ||
| - | INC YL | + | |
| - | CMP YL, #25 | + | |
| - | JNC @dm_left_right | + | |
| - | + | ||
| - | ; 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 #6 ; Turn off VSTOP | + | |
| - | STAL [@VIDEO_MODE] | + | |
| - | + | ||
| - | RET | + | |
| - | </ | + | |
| - | + | ||
| - | The essential changes are, we section off the map differently, | + | |
| - | + | ||
| - | Because we're using Mode 6, we need to initialize into Mode 6. Start becomes: | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | start: | + | |
| - | ; Set mode 6. | + | |
| - | LDAH $40 ; Set video mode | + | |
| - | LDAL #6 ; to 6 | + | |
| - | INT 0x10 | + | |
| - | + | ||
| - | CALL @init_game | + | |
| - | + | ||
| - | JMP @main_loop | + | |
| - | + | ||
| - | init_game: | + | |
| - | ; Initialize variables | + | |
| - | LDAL #19 | + | |
| - | STAL [@PX] | + | |
| - | LDAL #11 | + | |
| - | STAL [@PY] | + | |
| - | LDAL #1 | + | |
| - | STAL [@RX] | + | |
| - | STAL [@RY] | + | |
| - | + | ||
| - | LDAL #10 | + | |
| - | STAL [@player_str] | + | |
| - | STAL [@player_hp] | + | |
| - | + | ||
| - | CALL @get_player_name | + | |
| - | + | ||
| - | RET | + | |
| - | </ | + | |
| - | + | ||
| - | Next, the game won't be any fun unless we can name our hero. Let's ask the player their name. To do this, we will need an INPUT function. Let's use IO_INPUT: | + | |
| - | + | ||
| - | ; ---------------------------------------------------------------------------- | + | |
| - | ; AH=$68: IO_INPUT - Read line of input from user | + | |
| - | ; Input: | + | |
| - | ; Output: ELM = pointer to input string (null-terminated, | + | |
| - | ; B = numeric value (parsed via atoi) | + | |
| - | ; CF = 0 on success, 1 on empty input | + | |
| - | ; Notes: | + | |
| - | ; | + | |
| - | ; | + | |
| - | ; ENTER is not echoed. | + | |
| - | ; Max input length limited by PATB_TBUF size. | + | |
| - | ; ---------------------------------------------------------------------------- | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | get_player_name: | + | |
| - | LDELM @what_is_your_name | + | |
| - | LDAH $10 ; write string | + | |
| - | INT 0x10 | + | |
| - | + | ||
| - | LDAH $68 ; IO_INPUT | + | |
| - | INT 0x05 | + | |
| - | ; player name is now at ELM. | + | |
| - | + | ||
| - | ; Ensure player name is no longer than 16 characters. | + | |
| - | MOV FLD, ELM | + | |
| - | ADD FLD, #17 | + | |
| - | LDAL #0 | + | |
| - | STAL [FLD] | + | |
| - | + | ||
| - | ; Copy player name into variable space. | + | |
| - | LDFLD @player_name | + | |
| - | name_copy_loop: | + | |
| - | LDAL [ELM, +] | + | |
| - | STAL [FLD, +] | + | |
| - | JNZ @name_copy_loop | + | |
| - | + | ||
| - | RET ;; return. | + | |
| - | + | ||
| - | what_is_your_name: | + | |
| - | .bytes "What is your name? ", 0 | + | |
| - | </ | + | |
| - | + | ||
| - | ==== Drawing the Status Area | + | |
| - | When a function gets too big I like to break it up into smaller functions, that way it's easier to think about the program. It seems draw_map is getting too big, so I will break it into '' | + | |
| - | + | ||
| - | Secondly, let's move all these functions into a file called draw.asm, and the main file can be changed from robots.asm to rogueima.asm. You can keep an old copy of robot.asm if you want, but we aren't going to use it anymore. | + | |
| - | + | ||
| - | < | + | |
| - | + | ||
| - | So let's get all the drawing functions done. | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | draw_map: | + | |
| - | CALL @draw_borders | + | |
| - | CALL @draw_world | + | |
| - | CALL @draw_player | + | |
| - | CALL @draw_mobs | + | |
| - | CALL @draw_stats | + | |
| - | CALL @draw_msgs | + | |
| - | RET | + | |
| - | + | ||
| - | draw_borders: | + | |
| - | ; Put the old draw_map code in here. | + | |
| - | RET | + | |
| - | + | ||
| - | draw_player: | + | |
| - | ; Put the old draw-player code in here. | + | |
| - | RET | + | |
| - | + | ||
| - | draw_mobs: | + | |
| - | ; put the old draw robot code in here. | + | |
| - | + | ||
| - | draw_stats: | + | |
| - | ; We will work on this next. | + | |
| - | RET | + | |
| - | + | ||
| - | draw_msgs: | + | |
| - | ; We will work on this after. | + | |
| - | RET | + | |
| - | + | ||
| - | draw_world: | + | |
| - | ; We will work on this last. | + | |
| - | RET | + | |
| - | </ | + | |
| - | + | ||
| - | Our plan of attack is to write draw stats, then msgs, then world. For each one, we will need to define some sort of variable to hold the data. For STATUS, we already have everything we need; name, strength, and health. We'll throw in " | + | |
| - | + | ||
| - | To write at a specific locatoin, we will need to use the set_cursor routine: | + | |
| - | + | ||
| - | ; ============================================================================ | + | |
| - | ; INT 0x10, AH=15h - Set Cursor Position | + | |
| - | ; Input: | + | |
| - | ; X = column (0-based) | + | |
| - | ; Output: Clamps X and Y to valid range. | + | |
| - | ; Notes: | + | |
| - | ; ============================================================================ | + | |
| - | + | ||
| - | We will also use INT 0x05 IO_PUTNUM, to draw numbers up to 65,535: | + | |
| - | + | ||
| - | ; ---------------------------------------------------------------------------- | + | |
| - | ; INT 0x05, AH=$63: IO_PUTNUM - Output number to screen | + | |
| - | ; Input: | + | |
| - | ; AL = 0 unsigned, 1 signed (we will let the caller decide) | + | |
| - | ; Output: Number written to screen as ASCII digits | + | |
| - | ; ---------------------------------------------------------------------------- | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | draw_stats: | + | |
| - | ; Set cursor | + | |
| - | LDA $1500 | + | |
| - | LDX #61 | + | |
| - | LDY #2 | + | |
| - | INT 0x10 | + | |
| - | + | ||
| - | LDELM @str_name | + | |
| - | LDA $1800 ; write string | + | |
| - | INT 0x10 | + | |
| - | + | ||
| - | ; Draw player' | + | |
| - | ; This will draw right after the 'Name: ' above. | + | |
| - | ; That's why we added spaces to the strings (below). | + | |
| - | LDELM @player_name | + | |
| - | LDA $1800 ; write string | + | |
| - | INT 0x10 | + | |
| - | + | ||
| - | ;;;;;;;;;;;;;;;;;;;; | + | |
| - | LDA $1500 ; set cursor | + | |
| - | LDX #61 | + | |
| - | LDY #4 | + | |
| - | INT 0x10 | + | |
| - | + | ||
| - | LDELM @str_str | + | |
| - | LDA $1800 ; write string | + | |
| - | INT 0x10 | + | |
| - | + | ||
| - | ; Draw player' | + | |
| - | LDA $6300 ; print unsigned word (will print after string above). | + | |
| - | LDB [@player_str] | + | |
| - | INT 0x10 | + | |
| - | + | ||
| - | ;;;;;;;;;;;;;;;;;;;; | + | |
| - | LDA $1500 ; set cursor | + | |
| - | LDX #61 | + | |
| - | LDY #5 | + | |
| - | INT 0x10 | + | |
| - | + | ||
| - | LDELM @str_hp | + | |
| - | LDA $1800 ; Write string | + | |
| - | INT 0x10 | + | |
| - | + | ||
| - | LDA $6300 ; Write number (unsigned) | + | |
| - | LDB [@player_hp] | + | |
| - | INT 0x05 | + | |
| - | + | ||
| - | + | ||
| - | ;;;;;;;;;;;;;;;;;;;; | + | |
| - | LDA $1500 ; Set cursor | + | |
| - | LDX #61 | + | |
| - | LDY #7 | + | |
| - | INT 0x10 | + | |
| - | + | ||
| - | LDELM @str_score | + | |
| - | LDA $1800 ; write string | + | |
| - | INT 0x10 | + | |
| - | + | ||
| - | LDA $6300 ; print number | + | |
| - | LDB [@player_score] | + | |
| - | INT 0x05 | + | |
| - | + | ||
| - | + | ||
| - | ;;;;;;;;;;;;;;;;;;;; | + | |
| - | LDA $1500 ; set cursor | + | |
| - | LDX #61 | + | |
| - | LDY #8 | + | |
| - | INT 0x10 | + | |
| - | + | ||
| - | LDELM @str_time | + | |
| - | LDA $1800 ; write string | + | |
| - | INT 0x10 | + | |
| - | + | ||
| - | LDA $6300 ; print number | + | |
| - | LDB [@game_time] | + | |
| - | INT 0x05 | + | |
| - | + | ||
| - | RET | + | |
| - | + | ||
| - | str_name: | + | |
| - | .bytes "Name: ", 0 | + | |
| - | + | ||
| - | str_str: | + | |
| - | .bytes "STR: ", 0 | + | |
| - | + | ||
| - | str_hp: | + | |
| - | .bytes "HP: ", 0 | + | |
| - | + | ||
| - | str_time: | + | |
| - | .bytes "T: ",0 | + | |
| - | + | ||
| - | str_score: | + | |
| - | .bytes " | + | |
| - | </ | + | |
| - | + | ||
| - | Finally, there' | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | move_right: | + | |
| - | LDAL [@PX] | + | |
| - | INC AL | + | |
| - | CMP AL, #59 | + | |
| - | JNC @mr_ok | + | |
| - | LDAL #58 | + | |
| - | mr_ok: | + | |
| - | STAL [@PX] | + | |
| - | RET | + | |
| - | + | ||
| - | </ | + | |
| - | + | ||
| - | ==== MVP 1: Base game | + | |
| - | Now that we have status drawing nicely, let's remove the draw_robot function. We will keep the stub, but we won't draw the robot or check for collision. You can also remove move_robots and change it to move_mobs (add a stub; you can make all these changes to your own copy). What's left is a simple game where you can move the player around. But nothing interesting happens, so let's increment game time every tick; add a CALL @inc_game_time (or similar) to the game loop (at the end): | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | inc_game_time: | + | |
| - | LDA [@game_time] | + | |
| - | INC A | + | |
| - | STA [@game_time] | + | |
| - | RET | + | |
| - | </ | + | |
| - | + | ||
| - | We'll also add a way for the player to quit out, since we changed the game loop. Change quit_game to this: | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | quit_game: | + | |
| - | ; CLS | + | |
| - | LDAH $10 | + | |
| - | INT $10 | + | |
| - | + | ||
| - | ; Print "QUIT GAME" | + | |
| - | LDBLX @msg_quit | + | |
| - | LDAH $66 | + | |
| - | INT $05 | + | |
| - | + | ||
| - | POP ELM ; destroy return address of CALL from main loop | + | |
| - | RET ; exit program. | + | |
| - | </ | + | |
| - | + | ||
| - | We now have, essentially, | + | |
| - | + | ||
| - | === Rogueima MVP 1 | + | |
| - | * Source code so far: [[Rogueima I MVP 1]] | + | |
| - | + | ||
| - | === The World Map | + | |
| - | Now let's work on displaying a world map. To display a world map, we will need three important things: | + | |
| - | + | ||
| - | # A source of truth for the world map | + | |
| - | + | ||
| - | Okay, we need one important thing. The actual map. Let's start with a very simple world map which will help us test out a few things. | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | map1_id: | + | |
| - | .bytes 1 | + | |
| - | map1_name: | + | |
| - | .bytes " | + | |
| - | map1_up: | + | |
| - | .bytes 5, 5 | + | |
| - | map1_down: | + | |
| - | .bytes 25, 15 | + | |
| - | map1_dim: | + | |
| - | .bytes 80, 25 | + | |
| - | map1_data: | + | |
| - | .bytes "################################################################################" | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "##### | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "# | + | |
| - | .bytes "################################################################################" | + | |
| - | </ | + | |
| - | + | ||
| - | This map was constructed to be very simple and allow us to test: | + | |
| - | + | ||
| - | * Having a map in the first place (it works!) | + | |
| - | ** Reading and storing basic maps if we need to | + | |
| - | ** Having metadata in the map data to store info (ex. up and down stairs) | + | |
| - | * Opening and closing doors | + | |
| - | * Rendering and Lighting | + | |
| - | + | ||
| - | (Note, the @ and > are added for visual reference. They will be ignored by the game). | + | |
| - | + | ||
| - | === Maps vs. Objects | + | |
| - | You may wonder where we are going to read in and store this map data. Why, we're going to read in and store this map data using the map data itself! For the intent of the game, we can check the actual map data for the location of walls and stairs. This simplifies a huge chunk of the game; the map is the map, easy. | + | |
| - | + | ||
| - | To proceed, we need to introduce a major concept: Variables. | + | |
| - | + | ||
| - | == Game Objects using NVAR | + | |
| - | It's true that we could reserve a flat chunk of memory, set up a " | + | |
| - | + | ||
| - | NVAR stands for named variable system. It stores data starting at $FFFF and grows downwards towards program space. This lets you efficiently use the space in bank 0 split between code and data. Note that there are no memory protections here; it's up to you not to accidentally over-write your code and data. To do this, check PATB_VARTOP ($01EEE4; 3 bytes). This points to the top of variable storage. If you want to reserve 4k of memory for variables for example, don't add more variables if PATB_VARTOP falls below $F000 ($F000 to $FFFF is 4k). For our purposes we will ignore this, since our code and data is not expected to be more than a few kilobytes. We should have plenty of room to spare. | + | |
| - | + | ||
| - | To use the NVAR system in your assembly language programs you need to know four simple functions: | + | |
| - | + | ||
| - | ; Interrupt 0x05: Langauge Services | + | |
| - | ; | + | |
| - | ; | + | |
| - | ; | + | |
| - | ; | + | |
| - | + | ||
| - | === Setting variables with NVAR_SET | + | |
| - | <codify armasm> | + | |
| - | ; NVAR_SET exmaple: | + | |
| - | LDELM @int_name | + | |
| - | LDFLD @int_data | + | |
| - | LDAH $15 ; NVAR_SET | + | |
| - | INT 0x05 ; INT 0x05 -- Language Services | + | |
| - | + | ||
| - | LDELM @str_name | + | |
| - | LDFLD @str_data | + | |
| - | LDAH $15 ; NVAR_SET | + | |
| - | INT 0x05 ; INT 0x05 -- Language Services | + | |
| - | + | ||
| - | LDELM @float_name | + | |
| - | LDFLD @float_data | + | |
| - | LDAH $15 ; NVAR_SET | + | |
| - | INT 0x05 ; INT 0x05 -- Language Services | + | |
| - | + | ||
| - | RET | + | |
| - | + | ||
| - | ; data section: | + | |
| - | int_name: | + | |
| - | .bytes " | + | |
| - | int_data: | + | |
| - | .bytes " | + | |
| - | + | ||
| - | str_name: | + | |
| - | .bytes " | + | |
| - | str_data: | + | |
| - | .bytes "giant rat", 0 | + | |
| - | + | ||
| - | float_name: | + | |
| - | .bytes " | + | |
| - | float.data: | + | |
| - | .bytes " | + | |
| - | </ | + | |
| - | + | ||
| - | This example code stores an int named x, a string named mon_name(2), | + | |
| - | + | ||
| - | //Variable names are unique; setting a variable with the same name will delete the old variable first. Garbage collection is done automatically at delete time.// | + | |
| - | + | ||
| - | === Getting variables with NVAR_GET | + | |
| - | Getting variables is the next step. | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | LDELM @int_name | + | |
| - | LDAH $14 ; NVAR_GET | + | |
| - | INT 0x05 ; INT $5 -- Language Services | + | |
| - | JC @var_not_found | + | |
| - | </ | + | |
| - | + | ||
| - | The code above sets FLD to point to the data section of the variable. This is the address where the variable is stored. | + | |
| - | + | ||
| - | === Deleting variabls with NVAR_DELETE | + | |
| - | You can also delete variables. | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | LDELM @int_name | + | |
| - | LDAH $16 ; NVAR_DELETE | + | |
| - | INT 0x05 ; INT $5 -- Language Services | + | |
| - | JC @var_not_found | + | |
| - | </ | + | |
| - | + | ||
| - | The code above removes the variable " | + | |
| - | + | ||
| - | === Clear all variables with NVAR_CLEAR | + | |
| - | This will erase all variables: | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | LDELM @int_name | + | |
| - | LDAH $17 ; NVAR_CLEAR | + | |
| - | INT 0x05 ; INT $5 -- Language Services | + | |
| - | </ | + | |
| - | + | ||
| - | === Organizing Game Objects | + | |
| - | + | ||
| - | Now that we have a simple storage system we can organize game objects. | + | |
| - | + | ||
| - | Let's first create a list of items for the player to pick up. Treasures. Gold. | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | itype_prefix: | + | |
| - | .bytes " | + | |
| - | ixpos_prefix: | + | |
| - | .bytes " | + | |
| - | iypos_prefix: | + | |
| - | .bytes " | + | |
| - | idata_prefix: | + | |
| - | .bytes " | + | |
| - | items_length: | + | |
| - | .bytes 0 | + | |
| - | </ | + | |
| - | + | ||
| - | These two values will be used to store data about objects (items) in the game in parallel arrays: | + | |
| - | + | ||
| - | * '' | + | |
| - | * '' | + | |
| - | * '' | + | |
| - | + | ||
| - | Finally, we need to keep track of how many items are on the " | + | |
| - | + | ||
| - | We might as well throw in the monster variable names too. Why not? We won't use it yet, but we can add it now as a second example of how this works: | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | mtype_prefix: | + | |
| - | .bytes " | + | |
| - | mxpos_prefix: | + | |
| - | .bytes " | + | |
| - | mypos_prefix: | + | |
| - | .bytes " | + | |
| - | mstr_prefix: | + | |
| - | .bytes " | + | |
| - | mhp_prefix: | + | |
| - | .bytes " | + | |
| - | mon_length: | + | |
| - | .bytes 0 | + | |
| - | </ | + | |
| - | + | ||
| - | The difference is that monsters have strength and hp (two data fields) instead of a " | + | |
| - | + | ||
| - | === obj.sda | + | |
| - | Let's put the variable defs in obj.sda. We will also create a temporary scratch area: | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | vb_name: | + | |
| - | .bytes " | + | |
| - | vb_dat: | + | |
| - | .bytes " | + | |
| - | </ | + | |
| - | + | ||
| - | Now let's get to work. We will need a random number generator to produce random locations for objects: | + | |
| - | + | ||
| - | ; ============================================================================ | + | |
| - | ; INT 0x13, AH=01h - Random Number in Range | + | |
| - | ; Input: | + | |
| - | ; Output: A = random number from 0 to B-1 | + | |
| - | ; ============================================================================ | + | |
| - | + | ||
| - | Calling this function with B=80 (80 columns) for example, will produce a value from 0 to 79. | + | |
| - | + | ||
| - | For our purposes, the screen has a range of 57x22. We add 1 to the number produced. | + | |
| - | + | ||
| - | We will also need a strcpy to copy prefixes. | + | |
| - | + | ||
| - | ; ============================================================================ | + | |
| - | ; INT 0x12, AH=01h - strcpy | + | |
| - | ; Input: | + | |
| - | ; FLD = destination pointer | + | |
| - | ; Output: none; pointers (unchanged) | + | |
| - | ; ============================================================================ | + | |
| - | + | ||
| - | This will be a bit long, and I have already thought of ways to shorten this a bit while writing it, but, here's the general idea: | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | create_gold: | + | |
| - | ; Create a gold object. | + | |
| - | + | ||
| - | ; 1a. Set data for type | + | |
| - | LDB #1 ; object #1 is by convention a pile of gold. | + | |
| - | LDFLD @vb_dat | + | |
| - | LDAH $5 ; INT 0x12 itoa B--> | + | |
| - | INT 0x12 ; convert ingener in B to string at FLD | + | |
| - | + | ||
| - | ; 1b. Set name for type | + | |
| - | LDELM @itype_prefix | + | |
| - | LDFLD @vb_name | + | |
| - | LDAH $01 ; strcpy | + | |
| - | INT 0x12 ; string services | + | |
| - | + | ||
| - | MOV FLD, ELM ; Set target of itoa to the end of the varname | + | |
| - | DEC FLD ; point to the zero | + | |
| - | LDB [@items_length] ; get current items_length | + | |
| - | MOV I, B ; save B in I for parallel arrays. | + | |
| - | LDAH $5 | + | |
| - | INT 0x12 ; append the length -- auto-adds zero terminator. | + | |
| - | + | ||
| - | INC B | + | |
| - | STB [@items_length] ; store new length | + | |
| - | + | ||
| - | ; save variable for type | + | |
| - | LDAH $15 | + | |
| - | LDELM @var_name | + | |
| - | LDFLD @var_data | + | |
| - | INT 0x05 ; NVAR_SET | + | |
| - | + | ||
| - | ;; Now that we'e saved type, lets get its X, Y and Data. | + | |
| - | LDAH $00 ; 16 bit random number | + | |
| - | INT 0x13 ; math servics | + | |
| - | MOV G, B ; save random number in G | + | |
| - | + | ||
| - | INT 0x13 | + | |
| - | MOV X, B ; save random number in X | + | |
| - | + | ||
| - | INT 0x13 | + | |
| - | MOV Y, B ; save random number in Y | + | |
| - | + | ||
| - | MOD G, #100 ; gold value 0-99 | + | |
| - | INC G ; gold value 1-100 | + | |
| - | + | ||
| - | MOD X, #57 ; x value 0-57 | + | |
| - | INC X ; x value 1-58 | + | |
| - | + | ||
| - | MOD Y, #22 ; y value 0-22 | + | |
| - | INC Y ; y value 1-23 | + | |
| - | + | ||
| - | ;; save X value | + | |
| - | LDELM @ixpos_prefix | + | |
| - | LDFLD @var_name | + | |
| - | LDAH #01 ; strcpy | + | |
| - | INT 0x12 | + | |
| - | + | ||
| - | ADD FLD, #3 ; point to 0 at end of ix_ | + | |
| - | MOV B, I ; return i to B for itoa | + | |
| - | LDAH $05 ; itoa | + | |
| - | INT 0x12 ; write index of parallel array ix_ | + | |
| - | + | ||
| - | LDFLD @var_data | + | |
| - | MOV B, X | + | |
| - | INT 0x12 ; write data of X value to string | + | |
| - | + | ||
| - | LDELM @var_name | + | |
| - | LDAH $05 ; var set | + | |
| - | INT 0x05 ; save X position. | + | |
| - | + | ||
| - | ;; save Y value | + | |
| - | LDELM @iypos_prefix | + | |
| - | LDFLD @var_name | + | |
| - | LDAH #01 ; strcpy | + | |
| - | INT 0x12 | + | |
| - | + | ||
| - | ADD FLD, #3 ; point to 0 at end of iy_ | + | |
| - | MOV B, I ; return I to B for itoa | + | |
| - | LDAH $05 ; itoa | + | |
| - | INT 0x12 ; write index of parallel array iy_ | + | |
| - | + | ||
| - | LDFLD @var_data | + | |
| - | MOV B, Y | + | |
| - | INT 0x12 ; write data of Y value to string | + | |
| - | + | ||
| - | LDELM @var_name | + | |
| - | LDAH $05 ; var set | + | |
| - | INT 0x05 ; save Y position. | + | |
| - | + | ||
| - | ;; save GOLD amount. | + | |
| - | LDELM @idata_prefix | + | |
| - | LDFLD @var_name | + | |
| - | LDAH #01 ; strcpy | + | |
| - | INT 0x12 | + | |
| - | + | ||
| - | ADD FLD, #6 ; point to 0 at end of idata_ | + | |
| - | MOV B, I ; return I to B for itoa | + | |
| - | LDAH $05 ; itoa | + | |
| - | INT 0x12 ; write index of parallel array idata_ | + | |
| - | + | ||
| - | LDFLD @var_data | + | |
| - | MOV B, G ; gold amount | + | |
| - | INT 0x12 ; write data of gold amount as string | + | |
| - | + | ||
| - | LDELM @var_name | + | |
| - | LDAH $05 ; var set | + | |
| - | INT 0x05 ; save gold amount. | + | |
| - | + | ||
| - | delete_item: | + | |
| - | ; Delete all four parallel-array entries for item at index I. | + | |
| - | ; Input: | + | |
| - | ; Clobbers: A, B, ELM, FLD | + | |
| - | ; AH=$16 on INT 0x05 = NVAR_DELETE | + | |
| - | + | ||
| - | ;; itype_< | + | |
| - | LDELM @itype_prefix | + | |
| - | LDFLD @vb_name | + | |
| - | LDAH $01 | + | |
| - | INT 0x12 ; strcpy prefix -> vb_name | + | |
| - | ADD FLD, #6 ; past " | + | |
| - | MOV B, I | + | |
| - | LDAH $05 | + | |
| - | INT 0x12 ; itoa I onto end | + | |
| - | LDELM @vb_name | + | |
| - | LDAH $16 | + | |
| - | INT 0x05 ; NVAR_DELETE | + | |
| - | + | ||
| - | ;; ix_< | + | |
| - | LDELM @ixpos_prefix | + | |
| - | LDFLD @vb_name | + | |
| - | LDAH $01 | + | |
| - | INT 0x12 | + | |
| - | ADD FLD, #3 ; past " | + | |
| - | MOV B, I | + | |
| - | LDAH $05 | + | |
| - | INT 0x12 | + | |
| - | LDELM @vb_name | + | |
| - | LDAH $16 | + | |
| - | INT 0x05 | + | |
| - | + | ||
| - | ;; iy_< | + | |
| - | LDELM @iypos_prefix | + | |
| - | LDFLD @vb_name | + | |
| - | LDAH $01 | + | |
| - | INT 0x12 | + | |
| - | ADD FLD, #3 ; past " | + | |
| - | MOV B, I | + | |
| - | LDAH $05 | + | |
| - | INT 0x12 | + | |
| - | LDELM @vb_name | + | |
| - | LDAH $16 | + | |
| - | INT 0x05 | + | |
| - | + | ||
| - | ;; idata_< | + | |
| - | LDELM @idata_prefix | + | |
| - | LDFLD @vb_name | + | |
| - | LDAH $01 | + | |
| - | INT 0x12 | + | |
| - | ADD FLD, #6 ; past " | + | |
| - | MOV B, I | + | |
| - | LDAH $05 | + | |
| - | INT 0x12 | + | |
| - | LDELM @vb_name | + | |
| - | LDAH $16 | + | |
| - | INT 0x05 | + | |
| - | + | ||
| - | RET | + | |
| - | </ | + | |
| - | + | ||
| - | Actually, it wouldn' | + | |
| - | + | ||
| - | The difficulty with item length is that if we have 10 items and delete #5, we would have to copy item #10 into item #5 to maintain sanity with item length. Lets change item_length to item_id and use it as an ID marker only. We can loop through to I when looking for an item and re-use numbers later -- something to figure out later. For now we just want something where we can create an object in the game. | + | |
| - | + | ||
| - | Oh, one important thing. We need an is_freespace() function. This will return AL=1 if the space is occupied and AL=00 if it's free (i.e. a space. On the map.) | + | |
| - | + | ||
| - | Here is the is_freespace function. The create_gold function or its refactor should contain a check for if it's generated on a free or occupied space. | + | |
| - | + | ||
| - | <codify armasm> | + | |
| - | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | + | |
| - | ;; is_freespace() | + | |
| - | ;; | + | |
| - | ;; ELM = pointer to map character data. | + | |
| - | ;; X, Y = location to test | + | |
| - | ;; I, J width and height of map. | + | |
| - | ;; | + | |
| - | ;; Sets carry if space is not free. | + | |
| - | ;; Clears carry is space is free. | + | |
| - | ;; | + | |
| - | is_freespace: | + | |
| - | ; calculate index into map data | + | |
| - | MOV B, Y | + | |
| - | MUL B, I ; ex. B = Y * map_width | + | |
| - | ADD B, X ; B = Y*80 + X | + | |
| - | + | ||
| - | LDELM @map1_data | + | |
| - | ADD ELM, B ; ELM -> tile at (X, Y) | + | |
| - | + | ||
| - | LDAL [ELM] ; AL = tile byte | + | |
| - | CMP AL, $20 ; is it a space? | + | |
| - | JZ @is_freespace_yes | + | |
| - | + | ||
| - | SEC ; not free, set carry and exit. | + | |
| - | RET | + | |
| - | + | ||
| - | is_freespace_yes: | + | |
| - | CLC ; clear carry (i.e. free space=no error) | + | |
| - | RET | + | |
| - | </ | + | |
| - | + | ||
| - | //more to come// | + | |
sd/part_ii_writing_games_in_assembly_language.1776345728.txt.gz · Last modified: by appledog
