sd:game_3_rogueima_i
Differences
This shows you the differences between two versions of the page.
| sd:game_3_rogueima_i [2026/04/14 06:09] – created appledog | sd:game_3_rogueima_i [Unknown date] (current) – external edit (Unknown date) 127.0.0.1 | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| + | = Game 3. Rogueima I | ||
| + | * [[SD-8516 Assembly Language]] | ||
| + | * [[Part II Writing Games in Assembly Language]] | ||
| + | * This is Part III. | ||
| + | |||
| + | == Introduction | ||
| + | Let's make a new game. This game will be called Rogueima I: Moongate. Or something. It doesn' | ||
| + | |||
| + | === Step 1. Program Scope and Structure | ||
| + | A game like this, if you don't know, is a lot like Robots, except that it looks better (it's in a graphics mode) and it has better music and more in-depth gameplay. | ||
| + | |||
| + | ==== a. Scope | ||
| + | It's typically played on a 320x200 screen. If you look closely from left to right you will see that on the left edge there is a blue border, then five " | ||
| + | |||
| + | There is another kind of //similar game// with a graphics window on the upper left, a chat/ | ||
| + | |||
| + | These layouts are similar to Nethack or Rogue, which was a mostly full-screen character mode game, but which was single player. The three main differecnes between these games besides depth of play are that the first two we mentioned are graphical and have music. | ||
| + | |||
| + | The first kind of game, as I mentioned, has an 11x11 tile window. If you count carefully you will see(from left to right) a border, 11x2 squares used for tiles, another border, and space for sixteen chat characters. Or alternately in the top sections on the right, 15 characters and a border. Lets count: | ||
| + | |||
| + | width = 1 + (5*2) + (1*2) + (5*2) + 1 + 16 | ||
| + | width = 1 + 10 + 2 + 10 + 17 | ||
| + | width = 11 + 12 + 17 | ||
| + | width = 40 | ||
| + | |||
| + | And from top to bottom, | ||
| + | |||
| + | height = 1 + 10 + 2 + 10 + 1 | ||
| + | height = 24 | ||
| + | |||
| + | This exactly represents a 40x24 character screen. The reason for this is interesting. In the early days, some computers had a 40x24 screen and some had a 40x25. | ||
| + | |||
| + | |= The 40x24 camp |= The 40x25 camp | | ||
| + | | Apple I | Commodore PET| | ||
| + | | Apple II | Commodore VIC-20 (extended mode) | | ||
| + | | Atari 8 bit | Commodore 64 | | ||
| + | | Mattel Aquarius | CGA Graphics | | ||
| + | | ABC 80 (Sweden) | MSX (ex. MSX1) | | ||
| + | | Sharp MZ-721 | CP/M Videotex terminals | | ||
| + | | TI-99/4A | Aster CT-80 | | ||
| + | | ColecoVision | | | ||
| + | | Acorn Electron | | | ||
| + | | BBC Micro | BBC Micro in Mode 6 | | ||
| + | |||
| + | Later, 40x25 became more of a standard. After all, when you have a 320x200 screen, 25 8x8 characters will naturally fit on the screen. In 16 color mode this is 32k -- a nice, even number with a little bit of room left over for a palette or some system variables. So it became a very common standard. | ||
| + | |||
| + | And, what about the extra line on a C64 or a PC? It was left blank. The reason is because the tilemap is 2x2 characters (16x16 tiles) so there' | ||
| + | |||
| + | For the curious, the plan there will be to offset the entire game by 4 y-pixels and then re-fill the top and bottom border areas before drawing the bottom-right chat window. A simple fix. | ||
| + | |||
| + | The game itself has a rather large world with sub-maps you can explore, such as dungeons and towns, and sub-games such as " | ||
| + | |||
| + | Now with the scope in mind we are almost ready to outline the structure, with one caveat; A game like this requires significant graphics and sound assets. We will design these //after// the program structure, because the program scope and structure will help expand and bound what we will need in terms of game assets. | ||
| + | |||
| + | ==== b. Structure | ||
| + | |||
| + | For " | ||
| + | |||
| + | * A main screen drawing function. The main display screen remains the same throughout the entire game. It will consist of: | ||
| + | ** Border drawing function. | ||
| + | ** Player status drawing function. | ||
| + | ** Party status (food, gold, score). | ||
| + | ** Chat window. | ||
| + | ** Game view (11x11 tile map of 16x16 tiles). | ||
| + | |||
| + | During play we need: | ||
| + | * Live animation of some tiles (ex. water, monsters, player) | ||
| + | * Background Music | ||
| + | * Player input | ||
| + | * Passage of time effects (moon phase/ | ||
| + | |||
| + | In terms of world design we will need: | ||
| + | * World map with link/entry points to sub maps | ||
| + | * Sub maps like towns, dungeons, other " | ||
| + | |||
| + | In terms of combat and interaction; | ||
| + | * Combat takes place in a " | ||
| + | * There is a talk or interact command which lets us interact with objects and players | ||
| + | * NPCs usually respond to one or two word conversations, | ||
| + | |||
| + | In terms of player inventory, etc. | ||
| + | * Each player can have a simple inventory. Unequipped items can go to a 'party inventory' | ||
| + | |||
| + | === Step 2. Designing the assets. | ||
| + | Musical assets can take quite some time to perfect. For now, we don't need to worry about it, but rather we will keep our eyes and ears open for interesting classical or ' | ||
| + | |||
| + | Fine knacks for ladies would make a good stand-in for Rule Britannia. Handel' | ||
| + | |||
| + | The EGA upset where it's behind the Commodore and Apple II for Ultima 4, but ahead for Ultima 5 is likely because the game was designed for the Apple II originally, but Ultima 5 was designed around EGA tiles so that became the look of the game by then. But honestly you can't go wrong with a decent 80s palette. We will be designing our own custom palette based on these ideas. | ||
| + | |||
| + | ==== Designing the Graphics | ||
| + | Do you know how to do graphics? Here's the idea: | ||
| + | |||
| + | |= |= 01 |= 02 |= 04 |= 08 |= 10 |= 20 |= 40 |= 80 |= 01 |= 02 |= 04 |= 08 |= 10 |= 20 |= 40 |= 80 |= | ||
| + | | 04 | | ||
| + | | 08 | | ||
| + | | FC | | ||
| + | | 76 | | ||
| + | | FF | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | | ||
| + | | FD | 1 | | ||
| + | | 04 | 1 | | ||
| + | | 18 | | ||
| + | | a | | ||
| + | | b | | ||
| + | | c | | ||
| + | | d | | ||
| + | | e | | ||
| + | | f | | ||
| + | | g | | ||
| + | | h | | ||
| + | |= |= 01 |= 02 |= 04 |= 08 |= 10 |= 20 |= 40 |= 80 |= 01 |= 02 |= 04 |= 08 |= 10 |= 20 |= 40 |= 80 |= | ||
| + | |||
| + | This grid represents four 8x8 cells. At the top and bottom hold hexidecimal cell values, which you sum up into the values on the left and right. These become four 8x8 tiles or one 16x16 tile as follows; | ||
| + | |||
| + | C1 = 04, 08, FC, 76, FF, FD, 04, 18 | ||
| + | C2 = 01, 00, 01, 03, 07, 05, 05, 00 | ||
| + | C3 = a, b, c, d, e, f, g, h | ||
| + | C4 = j, k, l, m, n, o, p, q | ||
| + | |||
| + | So for example in Stellar BASIC you could do | ||
| + | |||
| + | DEFCHAR 160, $04, $08, $FC, $76, $FF, $FD, $04, $18 | ||
| + | DEFCHAR 161, $01, $00, $01, $03, $07, $05, $05, $00 | ||
| + | |||
| + | Once you have these numbers, you can use DRAWCHAR to draw the data onto the screen. Of course, we're using Assembly language here, but we can call the same graphics routines BASIC uses. We'll just CALL them when we need to. | ||
| + | |||
| + | For now, here's a great idea! Get some colored markers that represent the palette you wish to use and draw out the art you want to use for your game. If you like, you can use a graph paper notebook to do this. Just tell your mom you need it for your math homework and she'll buy you a new one. A graph paper notebook is an excellent investment for this stage of game develpment. Carry it with you, and practice drawing some bitmaps at lunchtime. You can draw four 8x8 bitmaps or one 16x16. The values on the left and right become the values you will use as data for your tile-set as demonstrated above. | ||
| + | |||
| + | ==== The Player (Example) | ||
| + | |= |= 01 |= 02 |= 04 |= 08 |= 10 |= 20 |= 40 |= 80 |= 01 |= 02 |= 04 |= 08 |= 10 |= 20 |= 40 |= 80 |= | | ||
| + | | a1 | | ||
| + | | b1 | | ||
| + | | c1 | | ||
| + | | d1 | | ||
| + | | e1 | | ||
| + | | f1 | | ||
| + | | g1 | | ||
| + | | h1 | | ||
| + | | a3 | | ||
| + | | b3 | | ||
| + | | c3 | | ||
| + | | d3 | | ||
| + | | e3 | | ||
| + | | f3 | | ||
| + | | g3 | | ||
| + | | h3 | | ||
| + | |= |= 01 |= 02 |= 04 |= 08 |= 10 |= 20 |= 40 |= 80 |= 01 |= 02 |= 04 |= 08 |= 10 |= 20 |= 40 |= 80 |= | ||
| + | |||
| + | I've taken the liberty of using colors here. For 1-color characters or sprites, you calculate pixels as shown above. But for multi-color images you need to understand the screen format. | ||
| + | |||
| + | First let's talk colors. On the EGA 16 color palette: | ||
| + | |||
| + | * 2 = green | ||
| + | * 4 = red | ||
| + | * 6 = yellow/ | ||
| + | * 7 = light gray | ||
| + | * 8 = dark gray | ||
| + | * 15 (0xF) white | ||
| + | |||
| + | However, the 320x200 screen is a 4bpp format; each byte represents two pixels: | ||
| + | |||
| + | ^ ONE BYTE || | ||
| + | | Low Byte | High Byte | | ||
| + | | 4 bits | 4 bits | | ||
| + | | Pixel 1 | Pixel 2 | | ||
| + | | Color 0-15 | Color 0-15 | | ||
| + | |||
| + | Therefore, calculating what bytes to use for this image is a bit tricker. | ||
| + | |||
| + | |||
| + | ==== The Player (Example) | ||
| + | |= |= a1 |= b1 |= a2 |= b2 |= a3 |= b3 |= a4 |= b4 |= a5 |= b5 |= a6 |= b6 |= a7 |= b7 |= a8 |= b8 | | ||
| + | | a1 | | ||
| + | | b1 | | ||
| + | | c1 | | ||
| + | | d1 | | ||
| + | | e1 | | ||
| + | | f1 | | ||
| + | | g1 | | ||
| + | | h1 | | ||
| + | | a2 | | ||
| + | | b2 | | ||
| + | | c2 | | ||
| + | | d2 | | ||
| + | | e2 | | ||
| + | | f2 | | ||
| + | | g2 | | ||
| + | | h2 | | ||
| + | |= |= a1 |= b1 |= a2 |= b2 |= a3 |= b3 |= a4 |= b4 |= a5 |= b5 |= a6 |= b6 |= a7 |= b7 |= a8 |= b8 | | ||
| + | |||
| + | here, you need to combine 2 pixels into a low/high pair. So on row b1, the 5th and 6th columns will combine as 0 in the low byte and 6 in the high byte. This becomes: | ||
| + | |||
| + | 0110 0000 = #96 or 0x60 | ||
| + | |||
| + | you know it's 0x60 because the two high bits are 0x40 and 0x20; $40 + $20 = $60. | ||
| + | |||
| + | However way you intend to encode the graphic information is up to you. In fact, there' | ||
| + | |||
| + | But, there' | ||
| + | |||
| + | === 3. Super Easy Tiles | ||
| + | Actually I'm very lazy and I don't like to carry a notebook around with me or use GIMP/images directly. I like to keep everything in the source code, you see -- makes things more portable. So what if you could to it this way: | ||
| + | |||
| + | <codify armasm> | ||
| + | .tile_player | ||
| + | .bytes #16, #16 | ||
| + | .bytes " | ||
| + | .bytes " | ||
| + | .bytes " | ||
| + | .bytes " | ||
| + | .bytes " | ||
| + | .bytes " | ||
| + | .bytes " | ||
| + | .bytes " | ||
| + | .bytes " | ||
| + | .bytes " | ||
| + | .bytes " | ||
| + | .bytes " | ||
| + | .bytes " | ||
| + | .bytes " | ||
| + | .bytes " | ||
| + | .bytes " | ||
| + | </ | ||
| + | |||
| + | There, now doesn' | ||
| + | |||
| + | < | ||
| + | ;;;;;;;;;;;;;;;;;;;;;;; | ||
| + | ;; draw_tile | ||
| + | ;; IN: ELM -- Address of tile data | ||
| + | ;; X -- X address to draw tile | ||
| + | ;; Y -- Y address to draw tile | ||
| + | ;;;;;;;;;;;;;;;;;;;;;;; | ||
| + | read_tile: | ||
| + | PUSH I ; Save these as we will use them for loop counters. | ||
| + | PUSH J | ||
| + | | ||
| + | PUSH ELM ; Save ELM, X and Y since we will modify them. | ||
| + | PUSH Y | ||
| + | PUSH X | ||
| + | | ||
| + | LDI #16 ; initialize width and height loop counters. | ||
| + | LDJ #16 | ||
| + | | ||
| + | ; So now: I and J are our loop counters. | ||
| + | |||
| + | dt_y_loop: | ||
| + | MOV I, #16 ; Reset the width counter | ||
| + | POP X ; Restore X from stack | ||
| + | PUSH X ; Save for next Y-loop | ||
| + | |||
| + | dt_x_loop: | ||
| + | LDCL [ELM, +] ; Read a charater from the tile data. | ||
| + | CMP CL, #' ' | ||
| + | JNZ @dt_draw | ||
| + | LDCL #0 ; it's a space, convert CL to 0. | ||
| + | |||
| + | dt_draw: | ||
| + | SUB CL, #' | ||
| + | CMP CL, #16 ; if CL >= 16 then set carry. | ||
| + | JC @dt_x_end | ||
| + | |||
| + | LDAH #01 ; Draw Mode3 pixel (at 4bpp) (draws C=color at X, Y) | ||
| + | INT 0x18 | ||
| + | | ||
| + | dt_x_end: | ||
| + | INC X ; Increase X count (for next pixel) | ||
| + | DEC I ; Decrease width count | ||
| + | JNZ @dt_x_loop | ||
| + | | ||
| + | DEC J ; decrease height counter after width loop | ||
| + | JNZ @dt_y_loop | ||
| + | |||
| + | |||
| + | POP X | ||
| + | POP Y | ||
| + | POP ELM | ||
| + | POP J | ||
| + | POP I | ||
| + | | ||
| + | RET ; Return to caller | ||
| + | </ | ||
| + | |||
| + | Now, I haven' | ||
| + | |||
| + | If it takes 10 instructions to process the character and the plot function is another 40 instructions -- as the crow flies -- that's 50 instructions to plot a pixel. If you had to plot every pixel on the screen then, that becomes 320x200 pixels! That means at 1.28 MIPS it might take 3 seconds to draw the screen. This is a problem! What can we do? | ||
| + | |||
| + | The standard tricks are to try blitting (which is a memory copy) and to use a dirty map (i.e. only redraw tiles that have changed). The loop for blitting means we have at least 8 times fewer plots to make, and the dirty map ensures that most of the time we only need to redraw a small number of tiles on the screen -- even if the user is mashing move keys, it's usual to see less than half the map area needing a redraw. So even if we had to redraw 20 or 30 tiles every frame, this amounts to just 480 MEMCOPY instructions. Even at 50 supporting | ||
| + | |||
| + | //Note: Using MEMCOPY we must pixel-align all tiles to pixels divisible by 2. This is because when we copy one byte, we are copying two pixels. Therefore we can only blit to an X divisible by 2. That's usually no problem for a tile-based game. If it draws at 0,0 it will be blittale at 16,16, 32,32, etc.// | ||
| + | |||
| + | Okay, lets set this up. To initialize, we will need to pre-draw all the tiles into a tilesheet image. Each tile will be referred to by number, which means it will be easy to calculate it's location from the base of the data (as each tile is 16x16, or 128 bytes (at 2 pixels per byte). | ||
| + | |||
| + | here's the function to save the data after the framebuffer: | ||
| + | |||
| + | <codify armasm> | ||
| + | ;;;;;;;;;;;;;;;;;;;;;; | ||
| + | ;; copy_to_index | ||
| + | ;; Copies the tile data from 0,0 to 15x15 into the index location (I) | ||
| + | ;; on the tilesheet. We do not need to clamp X and Y, if the tile is | ||
| + | ;; 8x8 it will be copied inside the 16x16 sqaure. | ||
| + | ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; | ||
| + | |||
| + | .equ M3_FRAMEBUFFER $020000 | ||
| + | .equ SHAPES_DATA | ||
| + | |||
| + | copy16_to_index: | ||
| + | PUSH ELM | ||
| + | PUSH FLD | ||
| + | PUSH C | ||
| + | PUSH I | ||
| + | | ||
| + | LDELM @SHAPES_DATA | ||
| + | LDFLD @SHAPES_DATA | ||
| + | MUL I, #128 ; get offset | ||
| + | ADD FLD, I ; get memory location to store tile data | ||
| + | | ||
| + | LDC #16 ; repeat 16 times (one for each row of the tile) | ||
| + | cti_loop: | ||
| + | MEMCOPY ELM, FLD, #8 ; copy first 16 pixels (8 bytes; 4bpp = 16 pixels) | ||
| + | ADD ELM, #160 ; span to next screen line | ||
| + | ADD FLD, #8 ; store tile data row after row. | ||
| + | DEC C | ||
| + | JNZ @cti_loop | ||
| + | |||
| + | POP I | ||
| + | POP C | ||
| + | POP FLD | ||
| + | POP ELM | ||
| + | RET | ||
| + | </ | ||
| + | |||
| + | And now, we need the tile drawing function: | ||
| + | |||
| + | <codify armasm> | ||
| + | ; draw_tile16 - Draw a 16x16 tile to Mode 3 framebuffer | ||
| + | ; Input: I = tile index, X, Y = location to draw (X must be even) | ||
| + | draw_tile16: | ||
| + | PUSH ELM | ||
| + | PUSH FLD | ||
| + | PUSH C | ||
| + | | ||
| + | MOV A, X ; save copy | ||
| + | MOV B, Y | ||
| + | |||
| + | ; Calculate source: SHAPES_DATA + I * 128 | ||
| + | LDFLD @SHAPES_DATA | ||
| + | MUL I, #128 | ||
| + | ADD FLD, I | ||
| + | |||
| + | ; Calculate dest: M3_FRAMEBUFFER + (Y * 160) + (X / 2) | ||
| + | LDELM @M3_FRAMEBUFFER | ||
| + | MUL B, #160 | ||
| + | ADD ELM, B | ||
| + | SHR A, #1 ; X / 2 for byte offset | ||
| + | ADD ELM, A | ||
| + | |||
| + | LDC #16 ; copy 16 rows | ||
| + | dt_loop: | ||
| + | MEMCOPY FLD, ELM, #8 ; 16 pixels = 8 bytes | ||
| + | ADD FLD, #8 ; next tile row | ||
| + | ADD ELM, #160 ; next screen row | ||
| + | DEC C | ||
| + | JNZ @dt_loop | ||
| + | |||
| + | POP C | ||
| + | POP FLD | ||
| + | POP ELM | ||
| + | RET | ||
| + | </ | ||
| + | |||
| + | The draw for a 16x16 sprite is therefore 18 instructions plus 16x5 instructions; | ||
| + | |||
| + | === Map Projection and Dirty Tiles | ||
| + | We don't want to draw a tile that we have already drawn. So, how then do we draw a tile? Well we need to know what tiles are supposed to be there in the first place. We will do this by keeping track of a tile draw area: | ||
| + | |||
| + | <codify armasm> | ||
| + | tile_window: | ||
| + | .bytes 0, | ||
| + | .bytes 0, | ||
| + | .bytes 0, | ||
| + | .bytes 0, | ||
| + | .bytes 0, | ||
| + | .bytes 0, | ||
| + | .bytes 0, | ||
| + | .bytes 0, | ||
| + | .bytes 0, | ||
| + | .bytes 0, | ||
| + | .bytes 0, | ||
| + | |||
| + | tile_window_drawn: | ||
| + | .bytes 0, | ||
| + | .bytes 0, | ||
| + | .bytes 0, | ||
| + | .bytes 0, | ||
| + | .bytes 0, | ||
| + | .bytes 0, | ||
| + | .bytes 0, | ||
| + | .bytes 0, | ||
| + | .bytes 0, | ||
| + | .bytes 0, | ||
| + | .bytes 0, | ||
| + | </ | ||
| + | |||
| + | You may wonder what the '' | ||
| + | |||
| + | * 1. We want to draw a tile. So we set the byte in the tile_area to the tile index of the tile we want to draw (0 to 128). | ||
| + | * 2. Every render cycle (at least 60 times per second) the rendering function checks each byte in '' | ||
| + | |||
| + | Something like; | ||
| + | |||
| + | <codify armasm> | ||
| + | ; render_dirty_tiles - Draw only changed tiles | ||
| + | render_dirty_tiles: | ||
| + | PUSH ELM | ||
| + | PUSH FLD | ||
| + | PUSH A | ||
| + | PUSH B | ||
| + | PUSH C | ||
| + | PUSH I | ||
| + | |||
| + | LDELM @tile_window | ||
| + | LDFLD @tile_window_drawn | ||
| + | LDI #0 ; linear index 0..128 | ||
| + | |||
| + | rdt_loop: | ||
| + | LDAL [ELM] ; tile_window[i] | ||
| + | LDBL [FLD] ; tile_window_drawn[i] | ||
| + | CMP AL, BL | ||
| + | JZ @rdt_skip | ||
| + | |||
| + | STAL [FLD] ; Store new value into drawn map | ||
| + | |||
| + | LDX #8 ; tile area to draw in starts at 8,8 | ||
| + | LDY #8 | ||
| + | LDC #0B0B ; draw an 11x11 set of 16x16 tiles. | ||
| + | |||
| + | ; This area shoud now loop to draw the 11x11 (121 tiles) in the tile area. Starting at X=0 and Y=0 we run two loops. | ||
| + | | ||
| + | FIXME | ||
| + | |||
| + | rdt_skip: | ||
| + | ; we need to keep track of the X and Y position even if we don't draw the tile | ||
| + | ; /add code here// | ||
| + | |||
| + | INC ELM | ||
| + | INC FLD | ||
| + | INC I | ||
| + | CMP I, #127 | ||
| + | JNC @rdt_loop | ||
| + | |||
| + | POP I | ||
| + | POP C | ||
| + | POP B | ||
| + | POP A | ||
| + | POP FLD | ||
| + | POP ELM | ||
| + | RET | ||
| + | </ | ||
| + | |||
| + | That's a very simple routine! | ||
| + | |||
| + | more coming soon | ||
