Jump to content
IGNORED

Drawing tile maps, finding sprite position in grid, counting down time and random numbers


ggn

Recommended Posts

So a friend sent me a private message asking me questions about the three topics in the subject. Instead of answering him in private I thought I'd just make a post about it so other people can also learn a couple of things!

 

The message described a map of 16x16 pixel tiles forming a 16 by 8 tile grid. So, 288x160 pixels.

 

As usual, the code and assets is up on github and you can download it using the usual ways.

 

(Standard disclaimer: if the rest of this post seems alien to you then please go and read the pinned tutorials in this subforum. Hopefully things will become more clear then. If you're still confused, drop a line below!)

 

Firstly, I needed some tile assets and instead of drawing my own hideous things and get depressed I just went into opengameart.org and grabbed a random 16x16 tileset. Originally it had way too many colours so I decreased them to 256 and saved the result as a 8-bit BMP file. I could have gone down to 16 but I didn't want to degrade the quality too much. Also I extracted a 16x16 tile to be used as our main sprite. Because I obviously ran out of paletted colours since they were spent in the background I just made the sprite 16-bit on the grounds that a) this is a demonstration, b) it shouldn't burn too much bandwidth anyway.

 

Then I created a new project from the console

 

build gridsprite new
  ___           _             ___          _      _
 | _ \__ _ _ __| |_ ___ _ _  | _ ) __ _ __(_)__ _| |_
 |   / _` | '_ \  _/ _ \ '_| | _ \/ _` (_-< / _|_   _|
 |_|_\__,_| .__/\__\___/_|   |___/\__,_/__/_\__| |_|
          |_|
C:\svn\raptor\RB_~1\include\template\assets.txt
C:\svn\raptor\RB_~1\include\template\rapapp.s
C:\svn\raptor\RB_~1\include\template\rapinit.s
C:\svn\raptor\RB_~1\include\template\rapu235.s
C:\svn\raptor\RB_~1\include\template\template.bas
C:\svn\raptor\RB_~1\include\template\ASSETS\partipal.bmp
C:\svn\raptor\RB_~1\include\template\ASSETS\fonts\f_16x16.bmp
C:\svn\raptor\RB_~1\include\template\ASSETS\fonts\f_8x16.bmp
C:\svn\raptor\RB_~1\include\template\ASSETS\fonts\f_8x8.bmp
9 File(s) copied
Project gridsprite created successfully!

Great! Let's start butchering it up. First stop is rapapp.s where we change sound engine to Zerosquare's:

 

player equ 0      ;0=Zerosquare's player, 1=U-235 player

Next stop: assets.txt - we need to tell the system to import our graphics:

 

abs,tiles,gfx_clut,ASSETS\GFX\hyptosis_tile-art-batch-1.bmp
abs,scrbuf,gfx_noclut,ASSETS\GFX\playfield.bmp
abs,sprite,gfx_noclut16,ASSETS\GFX\sprite.bmp

First is our huge spritesheet, with palette export. Then comes a placeholder image that will serve as our screen buffer (where we'll draw the tiles). This could have been blank but sometimes it helps having a non-blank image (for debugging). Lastly, our small 16bit sprite.

 

Now we need to define our graphics objects for raptor, which means editing the dreaded rapinit.s file. Ugh! So we need to add two objects to the existing file, one will display the screen buffer and the other will display the sprite. We do that by copying the template (the block of text which is commented out) and adjusting only, a few parameters, namely the sprite_gfxbase field to point to the addresses of our graphics (scrbuf and sprite from above), the BIT DEPTH (8 and 16 respectively), sprite_bytewid (288 and 16*2 respectively), sprite_width (288/16), sprite_height (160/16), sprite_gwidth (288/16). The other stuff, don't know, don't care, leave as is, it's "fine"!

 

After the unpleasant stuff we're now ready to start codin' - woohoo!

 

Drawing the tiles

First of all, the tile map. This was mostly reused code from another project (drawmap). It has been however modified for the parameters of our problem here and the code slightly modified to be more readable (and faster!). The original post for drawmap is here: https://atariage.com/forums/topic/263485-rb-manic-miner/?do=findComment&comment=3724346

 

To begin, we need an array to store our grid tiles. This will hold numeric values.

dim map[map_height][map_width] as short

Notice how we need to define height*width. In most cases it won't hurt to define that the other way, but if we then would like to fill in the indices as a huge stream of values it can get quite weird. Just trust me on this one, I don't like it either as I'm used to think of width*height when defining arrays :).

 

The routine also needs a few definitions:

 

' The screen to draw the map's width in bytes.
' Think of this as <width in pixels>*<bits per pixel>/8
const dest_screen_width_in_bytes=288
' Our tile's height
const tile_height=16
' Our tile's width in bytes.
' Think of this as <width in pixels>*<bits per pixel>/8
const tile_width_in_bytes=16
' The screen to read the tile's width in bytes.
' Think of this as <width in pixels>*<bits per pixel>/8
const src_screen_width_in_bytes=960
' Map width inside the map array
const map_width=18
' Map height inside the map array
const map_height=10

dim x as short
dim y as short
dim c as short
dim tilex as short
dim tiley as short

These are really definitions coming from our map size as a grid, our spritesheet dimensions, our tile dimensions and our screen buffer dimensions. I think it's pretty straightforward. (by all means, shout if something is not clear!)

 

To populate the map I didn't want to spend ages drawing an actual map and exporting the values, so I opted to draw the tiles in the order they appear on the spritesheet. So we fill our map using a simple loop.

 

' Fill map with some tiles
for y=0 to map_height-1
    for x=0 to map_width-1
        map[y][x]=60*y+x
    next x
next y

 

There we go, our map is now ready to be displayed. This could be filled using proper values from a tile editor but this is out of scope for this post. (Perhaps in another I'll get something like Tiled to export maps.) This loop will display the map to our screen buffer:

 

' Draw map
for y=0 to map_height-1
    for x=0 to map_width-1
        c=map[y][x]
        tilex=(c % (src_screen_width_in_bytes/tile_width_in_bytes))
        tiley=(c/(src_screen_width_in_bytes/tile_width_in_bytes))
        drawtile(x,y)
    next x
next y

Easy, just loop through all possible x and y values for width and height, find the coordinates of the tile from our spritesheet and the screen coordinates the tile will be shown and tell drawtile to go draw the thing!

 

So the only thing missing is drawtile routine. Brace yourselves:

' Draws a 16 x map_height tile on screen.
' x is multiplied by map_width and y is multiplied by map_height

sub drawtile(x as SHORT, y as SHORT)
    local i as short
    local screen_y_offset as LONG
    local screen_x_offset as LONG
    local screen_address as LONG

    local tile_y_offset as LONG
    local tile_x_offset as LONG
    local tile_address as LONG

    tile_y_offset=tiley*(src_screen_width_in_bytes*tile_height)
    tile_x_offset=tilex*tile_width_in_bytes
    tile_address=(LONG)strptr(tiles)+tile_x_offset+tile_y_offset

    screen_y_offset=y*(dest_screen_width_in_bytes*tile_height)
    screen_x_offset=x*tile_width_in_bytes
    screen_address=(LONG)strptr(scrbuf)+screen_x_offset+screen_y_offset

    for i=0 to tile_height-1
        ' Our tiles are 16 pixels wide in 8bpp mode, which means they are 16 bytes wide in RAM. Hence we need 4 LPOKEs per line.
        lpoke screen_address,lpeek(tile_address)
        lpoke screen_address+4,lpeek(tile_address+4)
        lpoke screen_address+8,lpeek(tile_address+8)
        lpoke screen_address+12,lpeek(tile_address+12)
        screen_address+=dest_screen_width_in_bytes
        tile_address+=src_screen_width_in_bytes
    next i
end sub

Just think of it as something that takes the values we filled above and turns it into pixels on screen. The only hardcoded bit here is that it assumes that the tile width is 16 pixels and we're using 8bpp mode, otherwise it's quite generic. Again, if people want me to remove that restriction, ask!


Randomness

 

This has been discussed in various threads on this subforum and people have been reporting various degrees of success, so let's clear the air a bit with some theory and code. First of all, there is no easy way to get pure randomness out of deterministic systems like the Jaguar - boot time is pretty much the same, there's no easy register to sample and get something random, and even then who knows how "random" it will be. So let's throw that problem to the user!

 

' Initialise our random values
' The trick here is to keep re-runninf randomize with a running counter
' and wait till the user presses something. So in essence the user is generating
' the randomness
RLOCATE 0,180
RPRINT "Press any button to begin countdown"
do
    randomize(c)
    c+=539
    ZEROPAD()
loop until zero_left_pad BAND (~(Input_Pad_C1|Input_Pad_C2|Input_Pad_C3))

What does this dumb loop do? Very simple, it just waits for someone to press a button on the joypad.

 

....of course while doing that it keeps a running counter which is used to change the random function's initial seed :). Unless we're dealing with a person in possession of extraordinary abilities, there's probably no way they can hit the button at the exact moment twice. And even then our counter will not start from 0 each time this is called. So let's say we have a pretty good random seed going on here!

 

Finding our coordinates

Now we want to know if we have a character running around the screen on top of the tile map where they are. Remember, our sprite's coordinates are from 0 to 288-16 for x and 0 to 160-16 for y, but our map is 16x10. How can we convert from one system to another?

 

do
    vsync
    ZEROPAD()
    if (zero_left_pad band Input_Pad_Up) and sprite_y>0 then
        sprite_y--
    endif
    if (zero_left_pad band Input_Pad_Down) and sprite_y<160-16 then
        sprite_y++
    endif
    if (zero_left_pad band Input_Pad_Left) and sprite_x>0 then
        sprite_x--
    endif
    if (zero_left_pad band Input_Pad_Right) and sprite_x<288-16 then
        sprite_x++
    endif
    rlist[2].x=(16+sprite_x)<<16
    rlist[2].y=(16+sprite_y)<<16
    RLOCATE 0,192
    print "x=";sprite_x;", y=";sprite_y;", map x=";int((sprite_x+8)/16);", map y=";int((sprite_y+8)/16)
    RLOCATE 0,180
loop

This is a sort of main loop. We check the joypad for up/down/left/right and update the coordinates of the sprite (rlist[2] means the second raptor object, which we defined as our sprite). The print statement then converts the sprite_x and sprite_y coordinates into grid coordinates simply by dividing by the grid width and height respectively (which is conveniently 16 in both cases). If we do that calculation we will notice something strange. Here is a screenshot from the binary which has our character (the heart if you haven't figured it out!) within a grid square. Let's say that the grid coordinates are x=3, y=2.

 

image.png.4e873ae21b33c7851bdaf1e63a79b720.png

There is no problem here, when we do the divisions described above we will get our x=3,y=2 nicely. Now let's consider another example:

image.png.25b21e5ad34fece39dcc504527aacdb2.png

So now we're just passed right of the x=3, y=2 grid. So then we'd want our coordinates to be x=4, y=3. The thing is - we'll still get x=3, y=2! Why this is happening? Well, let's have a look at the sprite itself:

image.png.b629a0c6a293c10306c45688ef64271c.png

Our sprite_x and sprite_y coordinates actually point to the top left corner of the sprite. This means that if we place our sprite at coordinates (0,0) and keep moving the sprite to the right, sprite_x/16 will transition from 0.something to 1.something only when sprite_x is larger than 16. This means that the top left corner will be at (16,0), so graphically our heart will be at the middle of the tile. So in order to fix this, we have to add half the sprite width and height to sprite_x and sprite_y before we perform the divisions. Easy! Now our coordinates will map a bit better to the grid coordinates!

 

Getting a countdown timer for PAL/NTSC

One last thing before this post comes to a finish, is how to create a countdown timer.

 

The Jaguar hardware has no easy way to count the time unless you start using DSP timers or something equally bizarre. And you probably don't need that much precision or want to mess around with those timers. But at least we do have the Vertical BLank interrupt, or VBL for short. This is an interrupt that fires at a fixed rate when the screen is ready to be drawn from the beginning. This happens every 50 times for PAL and 60 times for NTSC. So if we can count the VBL ticks then we have a sort of clock! rb+ has a command which waits until such an interrupt is generated:

VSYNC

so let's use that! But first we need to know if our system is PAL or NTSC. Turns out there's a hardware register that gets set with the correct value. So we can use that to see how many VBL ticks equals one second:

' Determine screen frequency
dim ticks_per_second as short
ticks_per_second=50     ' assume pal
if (dpeek(CONFIG) band VIDTYPE)<>0 then
    ticks_per_second=60 ' nope, it's ntsc
endif

Now we can set a countdown timer:

 

' Initialise our countdown timer
dim countdown_ticks as short
countdown_ticks=10*ticks_per_second

Then it's a simple matter of having a loop that VSYNCs and ticks our timer variable down until it's zero:

do
    vsync
    if countdown_ticks>0 then
        print "Seconds left: ";(float)countdown_ticks/(float)ticks_per_second
        countdown_ticks--
    else
        RPRINT "It's over!!!!!!!!!!!!!!!!!!!!!!!!!!"
    endif
loop

 

And that's it!

 

Hopefully this has been helpful to people, I really tried to make it as accessible as possible for people that are learning things. Let me know what you think about it, and more importantly: ask if something is not clear. See you around!

image.png

Edited by ggn
  • Like 3
Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.
Note: Your post will require moderator approval before it will be visible.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

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

×   Your previous content has been restored.   Clear editor

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

Loading...
  • Recently Browsing   0 members

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