Image Map Image Map
Page 1 of 21 1234511 ... LastLast
Results 1 to 10 of 204

Thread: MagiDuck, a DOS / CGA text mode game project

  1. #1

    Default MagiDuck, a DOS / CGA text mode game project

    Hi!

    I thought I'd share this little project I've been working on in QuickBasic 7.1 PDS and Assembly language.

    magiduck_ss_140520_02.jpg

    It's a simple platform game running in 40x25 CGA text mode, tweaked to show 50 rows of half height glyphs. Combined with using the ASCII 222 it gives the game a resolution of 80x50 pixels. I chose this resolution to avoid CGA snow completely. Works in DosBox-X anyway

    The engine supports "smooth" scrolling using two 4K off screen buffers. One for drawing changed tile areas at the current scroll offset and another one a blitted copy of that with sprites drawn over it every frame. The sprite animation routine supports multi-part sprites.

    Currently most of the graphics routines are made in Assembly and I'm getting framerates of 19-22 in DosBox @ 270 cycles. I'm fairly pleased with that, considering my humble programming experience.

    My goal is to have this running on an 4.77mhz 8088 / 256k RAM system with framerates above 15 and have a decent game to play too. I guess I was hoping to get some insights from here, to find out if this is actually possible by showing some stuff. This forum has been a big help already though, especially Trixter's and Deathshadows's stuff

    I'll make little updates along the way to this thread and my blog: http://bluepandion.tumblr.com/

    And here's my Assembly scrolling routine to show some actual code too, sorry if this is too horrible to look at:
    Code:
    ;---------------------------------------------------------------------------
    ;Textmode Tile buffer scroll routine for 40x50 mode
    ;
    ;Version 7.2
    ;
    ;
    ;---------------------------------------------------------------------------
    
    	push bp				
    	mov bp, sp				;Get stack pointer
    
    	push ds
    	push si
    	push es
    	push di
    	
    ;----------------------
    ; Parameter stack offsets
    ; Order is inverted from qbasic CALL ABSOLUTE parameter order
    
    ;00 bp
    ;02 ds
    ;04 si
    ;06 0a es
    ;08 0c di
    
    ;10 Qbasic return segment
    ;12 Qbasic return offset
    
    ;06 Tile buffer offset
    ;08 Tile buffer segment
    ;10 Screen Buffer offset
    ;12 Screen Buffer segment
    ;14 Tile Buffer scroll offset
    
    ;-----------------------------------------------------------------
    	
    	mov ax, [bp + 14]		;AX = Scroll offset
    	mov bx, 4096			;
    	sub bx, ax				;BX = 4000 - Scroll offset
    	
    ;---------------------------------------------------------------
    							;BLIT Tile buffer to screen buffer
    							
    ;Screen buffer is written linearly from 0 to 1999
    
    ;But SI (Read Offset) first goes from 2000-offset to 2000 ...
    
    	mov ds, [bp + 8]		;Change read offset to Tile buffer
    	mov si, [bp + 6]
    		
    	mov es, [bp + 12]		;Change write offset to Screen buffer
    	mov di, [bp + 10]
    	
    	add si, bx
    	
    	mov cx, ax				;Loop counter, AX = Scroll offset
    	shr cx, 1				;CX / 2
    	rep	movsw				;Blit
    
    ;... And then from 0 to 2000-offset
    
    	mov ds, [bp + 8]		;Change read offset to Tile buffer
    	mov si, [bp + 6]
    	
    	mov cx, bx				;Loop counter, BX (1999 - scroll offset)
    	shr cx, 1				;CX / 2
    	rep	movsw				;Blit
    
    ;---------------------------------------------------------------
    exit:
    	pop di
    	pop es
    	pop si
    	pop ds
    	
    	pop bp					;Return stack pointer
    	
    	retf 8
    I'll post some more routines later if anyone's interested, maybe it's better not to bloat this post with too much stuff.
    Cheers!

  2. #2

    Default

    Looks awesome so far!

    If I understand correctly, for each frame you need to do:
    * Modify changed tiles in first buffer
    * Copy first buffer to second buffer
    * Draw sprites on second buffer
    * Copy second buffer to screen

    Have you thought of doing hardware scrolling instead, by modifying the CGA start address registers? That gives you the ability to put your framebuffer anywhere in CGA RAM with a resolution of one character (i.e. two pixels). It's a bit more fiddly but a lot faster - I think you'd be able to get up to 60fps on a 4.77MHz 8088 with that method, and have the backgrounds as complicated as you like without any slowdown.

  3. #3

    Default

    Quote Originally Posted by reenigne View Post
    Looks awesome so far!

    If I understand correctly, for each frame you need to do:
    * Modify changed tiles in first buffer
    * Copy first buffer to second buffer
    * Draw sprites on second buffer
    * Copy second buffer to screen

    Have you thought of doing hardware scrolling instead, by modifying the CGA start address registers? That gives you the ability to put your framebuffer anywhere in CGA RAM with a resolution of one character (i.e. two pixels). It's a bit more fiddly but a lot faster - I think you'd be able to get up to 60fps on a 4.77MHz 8088 with that method, and have the backgrounds as complicated as you like without any slowdown.
    Thanks!

    Yes, that's excactly how the buffering works currently.
    I actually have a version of the engine with CGA hardware scrolling too. It was very much faster indeed, but it had some issues too:
    - Just drawing the changed tile areas, there was flicker every time at the part of the screen that changed. Vertical scrolling is easy, but horizontal scrolling always ends up drawing on visible areas of the screen.
    - When drawing sprites, things get more complicated. You need to copy sprite backgrounds to memory from video memory, which is considerably slower than RAM.
    - Clearing previous sprites and drawing new ones will, again cause more flicker...

    After some fighting, I made a dirty rectangle replacement of a system. That divided the screen into 64 zones, 5x6 pixels each. Sprites and tile drawing routines marked those zones for copying to screen from a 4K buffer. So the updating the screen had these stages:
    * Modify changed tiles in first buffer. Flag changed zones.
    * Copy all flagged zones to second buffer.
    * Draw sprites to second buffer. Flag zones that the sprites occupy. (Use flag value 2 so these zones will be cleared twice, in case the sprites move)
    * Copy all flagged zones from second buffer to screen.
    * Decrease all zone flags if > 0.

    This did work, but ended up halving the frame rate and the scrolling just looked really jittery because the CGA scroll offset ended up lagging just abit every time... Maybe it was just too complicated compared to a 2K REP MOVSW. Much of the logic was inside Qbasic, which may have slowed it even further.

    I actually tried to study how Commander Keen 4 CGA version does its scrolling by dropping DosBox cycles to 50 while playing... Keen seems to copy the whole 16K of a screen from a buffer every frame, you can see the screen appear line by line when scrolling. The game runs really well at 300 cycles too. That's why I'm thinking block copying might be the best way to go when having to deal with sprites too.

  4. #4

    Default

    Yeah, it's definitely trickier to get flicker-free updates without a second buffer - you have to pay attention to where the raster beam is and write your drawing code so that it either always stays ahead of the beam or always stays behind it (but still finishes before the raster beam starts the next frame). That probably means doing all the screen updates in a single top-to-bottom pass.

    But even if you don't use CGA hardware scrolling directly, you might still be able to speed up your code considerably by using some variations on that technique. Instead of modifying tiles in the first buffer and then blitting to the second buffer, copy from the first buffer to the second buffer with an offset start address (i.e. pretend you're "hardware scrolling" the first buffer) so that you only need to update the edges. You'll probably need to do the copy in two chunks so you can use a circular buffer. The second buffer could be in CGA memory (there's enough space for 4 pages in this mode) so that after you've blitted the background to it and drawn the sprites on top you can just change the start address to flip between the pages.

  5. #5

    Default

    Quote Originally Posted by mangis View Post
    I thought I'd share this little project I've been working on in QuickBasic 7.1 PDS and Assembly language.
    I agree with reenigne, this looks awesome! I hope you don't mind a few suggestions for improvement?

    Code:
    ; Order is inverted from qbasic CALL ABSOLUTE parameter order
    Are you calling the ASM procedures using CALL ABSOLUTE? This can be a bit kludgy because it requires you to store the machine code somewhere (DATA statements are often used) and then read it in and you also need to do DEF SEG before CALL ABSOLUTE. All this overhead can be avoided. Let me know if you need help with this.

    This code;
    Code:
    	mov ds, [bp + 8]		;Change read offset to Tile buffer
    	mov si, [bp + 6]
    can be replaced with this (shorter and more efficient);
    Code:
    	lds si, [bp + 6]
    Likewise, this;
    Code:
    	mov es, [bp + 12]		;Change write offset to Screen buffer
    	mov di, [bp + 10]
    is more efficiently done like this;
    Code:
    	les di, [bp + 10]
    and so on.

    Shorter instructions are almost always faster on 8088 so instead of this (2 bytes);
    Code:
    	mov cx, ax				;Loop counter, AX = Scroll offset
    you can do it like this (1 byte);
    Code:
    	xchg cx, ax
    Of course this is assuming you don't need to preserve AX (which you don't in this particular case).

    Finally, if I remember correctly, you don't need to preserve ES but I'm not 100% sure on this (maybe someone else can confirm).

  6. #6
    Join Date
    Aug 2006
    Location
    Chicagoland, Illinois, USA
    Posts
    4,565
    Blog Entries
    1

    Default

    Quote Originally Posted by mangis View Post
    I actually tried to study how Commander Keen 4 CGA version does its scrolling by dropping DosBox cycles to 50 while playing... Keen seems to copy the whole 16K of a screen from a buffer every frame, you can see the screen appear line by line when scrolling. The game runs really well at 300 cycles too. That's why I'm thinking block copying might be the best way to go when having to deal with sprites too.
    Good catch on the great engine in Keen 4. It is copying entire 16K to the screen, but the engine achieves it's (relatively) high speed because it is doing almost nothing inbetween copies. The engine maintains a larger buffer for the playfield that is larger than the screen, and it copies only the visible 16K portion on every update. When the playfield is shown scrolling to the right, what is actually happening is that the visible 16k "window" is scrolling to the left. Only the left edge of the playfield is drawn... then the sprites are animated by replacing the background behind them and redrawing them, then the visible 16K portion is copied, and the cycle repeats.

    In fact, this is what Andrew was referring to:

    Quote Originally Posted by reenigne View Post
    Instead of modifying tiles in the first buffer and then blitting to the second buffer, copy from the first buffer to the second buffer with an offset start address (i.e. pretend you're "hardware scrolling" the first buffer) so that you only need to update the edges.
    In other words, work "smarter" not "harder" Only draw/redraw exactly what you have to.
    Offering a bounty for:
    - Documentation and original distribution disks for: Panasonic Sr. Partner, Corona PPC-400, Zenith Z-160 series
    - Music Construction Set, IBM Music Feature edition (has red sticker on front stating IBM Music Feature)
    - Any very old/ugly IBM joystick (such as the Franklin JS-123)

  7. #7

    Default

    Quote Originally Posted by reenigne View Post
    But even if you don't use CGA hardware scrolling directly, you might still be able to speed up your code considerably by using some variations on that technique. Instead of modifying tiles in the first buffer and then blitting to the second buffer, copy from the first buffer to the second buffer with an offset start address (i.e. pretend you're "hardware scrolling" the first buffer) so that you only need to update the edges. You'll probably need to do the copy in two chunks so you can use a circular buffer.
    My first buffer already is circular / wrapping. The first routine I posted copies the buffer in two chunks just as you explained, if I understood correctly. All the blue areas you see in the game are actually tiles just like everything else, so there is no limit in complexity currently. The solid blues and simple colours are just easier on the eyes IMHO.

    The tile routine also only draws as much as is needed. In most cases either a 40x2 or a 2x48 byte area. To accommodate strange (less than tile width/height) drawing sizes the routine is abit complicated though:
    Code:
    ;============================================================================
    ;
    ; Tile area drawing routine     v. 7.02
    ;
    ; 40x50 mode drawing. 2 Pixels per byte.
    ;
    ; Draws an area from Tile Map to Tile Buffer, using Tile Bank graphics.
    ;
    ;============================================================================
    
    ; Parameter stack offsets
    ; Order is inverted from qbasic CALL ABSOLUTE parameter order
    
    ;00 bp
    ;02 Qbasic return segment
    ;04 Qbasic return offset
    
    ;06 tileBank offset
    ;08 tileMap offset
    ;10 tileBuffer offset
    ;12 tileBuffer Segment
    ;14 tile read y
    ;16 tile read x
    ;18 area Height
    ;20 area Width
    ;22 Write area offset
    ;24 Tilemap read offset
    
    ;============================================================================
    
    push bp
    mov bp,sp
    
    ;---------------------------------------------------------------------------
    
    begin:
    
    mov es, [bp + 12]               ;ES = Tilebuffer seg
    mov di, [bp + 10]               ;DI = Tilebuffer ofs
    add di, [bp + 22]               ;DI + Write area offset
    inc di                          ;DI + 1, for attribute cell
    
    add [bp + 10], 4096             ;[BP + 10] = Tilebuffer Wraparound
    
    mov ds, [bp + 12]               ;DS = Tilebuffer seg
    mov si, [bp + 08]               ;SI = Tilemap ofs
    add si, [bp + 24]               ;SI + Tilemap Read offset
    
    mov bh, [bp + 18]               ;BH = Height
    
    mov dl, [bp + 16]               ;DL = Tile read X
    mov dh, [bp + 14]               ;DH = Tile read Y
    
    
    ;============================================================================
    
    mov [bp + 14], si               ;[BP + 14] = SI
    
    
    loopy:  ;....................................................................
    
    mov cx, [bp + 20]               ;CX = Width
    
    mov w[bp + 24], 0               ;[BP + 24] = Tile map read carriage return value.
    
    CMP di, [bp + 10]               
    JL nowrap1                      ;IF DI > (Tilebuffer ofs + 3999) THEN
        sub di, 4096                    ;DI - 4000
    nowrap1:                        ;END IF
    
    
        mov ax, 0
        mov al, ds:[si]
                                    ;Get Tile Bank Pointer
        mov si, ax                      
        shl si, 5                       ;SI = Tile index * 32
        mov ah, 0                       ;Tile pixel read offset =
        mov al, dl                      ; Tileread X
        shl dh, 2                       ; + (
        add al, dh                      ;    Tileread Y * 4 )
        add si, ax                      ;SI + Tile pixel read offset
        add si, [bp + 06]               ;   + Tile Bank offset
        shr dh, 2                       ;Tileread Y / 4
    
    loopx:  ;....................................................................
    
    CMP dl, 4                       
    JNZ noXmapInc                   ;IF Tileread X > 3 THEN
        mov dl, 0                       ;Tileread X = 0
        inc w[bp + 24]                  ;[BP + 24] (Carriage return value)
        
        mov si, [bp + 14]               ;SI = Tilemap read offset [BP + 14]
        inc si                          ;SI + 1
        
        xor ax, ax                      ;AX = 0
        mov al, ds:[si]                 ;AL = Tile Index
        
        mov [bp + 14], si               ;Store SI back to [BP + 14]
        
                                        ;Get Tile Bank Pointer
        mov si, ax                      
        shl si, 5                       ;SI = Tile index * 32
        mov ah, 0                       ;Tile pixel read offset =
        mov al, dl                      ; Tileread X
        shl dh, 2                       ; + (
        add al, dh                      ;    Tileread Y * 4 )
        add si, ax                      ;SI + Tile pixel read offset
        add si, [bp + 06]               ;   + Tile Bank offset
        shr dh, 2                       ;Tileread Y / 4
    noXmapInc:                      ;END IF
    
    movsb
    inc di
    inc dl                          ;Tileread X + 1
    
    CMP di, [bp + 10]               
    JL nowrap                       ;IF DI > (Tilebuffer ofs + 3999) THEN
        sub di, 4096                    ;DI - 4000
    nowrap:                         ;END IF
    
    LOOP loopx ;^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    
    mov dl, [bp + 16]               ;Tileread X = [BP + 16] (Starting value)
    
    add di, 80                      ;DI + 80
    mov ax, [bp + 20]               ;AX = Width
    shl ax, 1                       ;AX * 2
    sub di, ax                      ;DI - Width * 2
    
    mov si, [bp + 14]               ;SI = Tilemap read offset
    sub si, [bp + 24]               ;SI - Tilemap Carriage return value
    mov [bp + 14], si
    
    inc dh                          ;Tileread Y + 1
    CMP dh, 8
    JNZ noYmapInc                   ;IF Tileread Y > 7 THEN
        mov dh, 0                       ;Tileread Y = 0
            
        mov si, [bp + 14]               ;SI = Tilemap read offset [BP + 14]
        add si, 20                      ;SI + 20
            
        xor ax, ax                      ;AX = 0
        mov al, ds:[si]                 ;AH = Tile Index
        
        mov [bp + 14], si               ;Store SI back to [BP + 14]
        
    noYmapInc:                      ;END IF
    
    dec bh                          ;Height - 1
    CMP bh, 0
    JZ exit                         ;IF Height = 0 THEN EXIT
    JMP loopy  ;^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    
    ;============================================================================
    exit:
    pop bp
    retf 20
    Quote Originally Posted by reenigne View Post
    The second buffer could be in CGA memory (there's enough space for 4 pages in this mode) so that after you've blitted the background to it and drawn the sprites on top you can just change the start address to flip between the pages.
    I tried your idea last night and it worked. I removed the second buffer and replaced it with video memory addresses. This was a really simple fix to make and the game runs about 3 fps faster now and 4K less memory used!

    Here's a video comparing it to the old routine:


    This certainly got me thinking if there's still more to be done with video memory...


    Quote Originally Posted by Krille View Post
    I hope you don't mind a few suggestions for improvement?

    Are you calling the ASM procedures using CALL ABSOLUTE? This can be a bit kludgy because it requires you to store the machine code somewhere (DATA statements are often used) and then read it in and you also need to do DEF SEG before CALL ABSOLUTE. All this overhead can be avoided. Let me know if you need help with this.
    Thanks, I certainly don't mind improvements and these were great tips! I'll update all my routines to use these and will study some more on the subject.

    Do you mean avoiding CALL ABSOLUTE by making a quick library with LINK? Wow, I forgot all about that... I was just so happy to get anything in Assembly working at all. But yeah, this would be a great improvement and I have some tutorials in store for that so I might get it to work. At the moment, the Assembly routines are loaded from .COM-files into strings.

    I'm using A86 as my assembler. I'm a bit worried about using strange stuff like .PROC because they've caused me errors in many compliers I've tried. But this should be worth the trouble.

    Quote Originally Posted by Trixter View Post
    It is copying entire 16K to the screen, but the engine achieves it's (relatively) high speed because it is doing almost nothing inbetween copies. The engine maintains a larger buffer for the playfield that is larger than the screen, and it copies only the visible 16K portion on every update. When the playfield is shown scrolling to the right, what is actually happening is that the visible 16k "window" is scrolling to the left. Only the left edge of the playfield is drawn... then the sprites are animated by replacing the background behind them and redrawing them, then the visible 16K portion is copied, and the cycle repeats.

    In fact, this is what Andrew was referring to:

    In other words, work "smarter" not "harder" Only draw/redraw exactly what you have to.
    Ah, I see. I'm surprised to hear it has a buffer larger than the screen, that would mean it can only copy 80 byte lines with REP and have an additional loop for rows. Maybe that's doesn't slow it all that much then.

    Can I ask how you know so much about Keen 4 code, since the source hasn't been released yet? I'm just really glad you explained this, because I've been trying to understand the engine from brief and confusing explanations in "Masters of Doom" and some Wikipedia articles.

    I think my engine works in a similar way since only one pixel wide tile areas are usually drawn at the screen edges when scrolling. Copying the scroll buffer to video memory also clears old sprites at the same cost. But yeah, I'm sure there's still room for improvement. Currently the biggest CPU-hogs seem to be object behavior and sprite drawing, instead of scrolling.

  8. #8

    Default

    That game looks pretty cool. Seems to have a lot of potential.
    I have a Major in Post-Apocalyptic Economics.

  9. #9
    Join Date
    Mar 2006
    Location
    Massachusetts, USA
    Posts
    1,799

    Default

    I have nothing to add on the programming front, but your graphics tiles are very impressive looking. Good luck with your game, I hope to play it someday on real CGA.
    My Retro Computing and Vintage Gaming Blog : http://nerdlypleasures.blogspot.com/

  10. #10

    Default

    Will this run on an 8088?
    I have a Major in Post-Apocalyptic Economics.

Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •