Tutorial XXIII: Prettifying the HUD
Nobody wants to see boring white on black fonts in a brand new game they just bought. We'll need to make a nicer HUD if people are going to play this game. Of course, we could simply change the font style and color, but Scraggle has a much better looking PacMania bitmap font waiting in the
Bitmap Font Pack 2.
It would be pretty dumb to save this huge image as a BMP, it would take up over 1 MB of memory, and customers want games to eat up as little memory as possible. We'll save it as a PNG, these files use lossless file compression, so the file will be smaller with no quality loss.
As I said in Tutorial 2, PNG images use a different kind of transparency than most of our other images are using, so now might be a good time to talk about
color channels and
alpha transparency.
BMP images use 24-bit color. There are 3 color channels, one for red, green, and blue. Each channel uses 1 byte (8 bits) to represent it's color intensity, so it has 256 (2^8) different intensities of color. Therefore a 24-bit BMP image can display 16777216 unique colors. This is more colors than the human eye can see, so it's called
Truecolor. There's really no reason to go higher and give the channels more than 8 bits each, it's
really really really hard to see the difference between RGB(100, 100, 100) and RGB(101, 100, 100). If you're a 2D artist, work in Truecolor and you'll never need to use dithering again. You might have come across the
screen depth() function in Dark Basic Professional, this function returns how many bits of color your game is using. (Truecolor technically uses 32 bits, you'll see why in a moment.) All modern games use either Truecolor or Hicolor (16-bit, 65536 unique colors), so those are the only legal screen depths allowed in Dark Basic Professional.
PNG images use 32-bit color. The extra 8 bits are for an extra channel, but this time it's not for a color. This extra channel is called
alpha, and deals with transparency. If the alpha of a pixel is set to 0, it will be opaque. If you set it to 255, it will be completely invisible. All the numbers in between are used when you want the pixel to be partially transparent. We used alpha transparency to make a cool fade in effect for when the ghosts respawn, remember? But
set sprite alpha applies transparency to the entire sprite, PNG lets you give every pixel it's own transparency value. PNG is far superior to BMP transparency because...
It uses a lot less memory
You don't have to use
set image colorkey to tell Dark Basic Professional which pixels are transparent, the data is built into the PNG file
You don't have to sacrfice a color to transparency (you can't use the image colorkey color anywhere else in the BMP because it will automatically turn transparent)
There's 256 levels of transparency in PNG images, in BMP images there's only 2 levels
Each pixel can have it's own level of transparency independent of all the others.
It looks cooler
Download the attached image
Save it to the Media folder
Ok, we have the image, but how do we load and display it? Good news, Scraggle already wrote all the code to do that.
Include BMF.dba as a new source file
Add this code to BMF.dba
Rem *** Include File: BMF.dba ***
Rem Created: [date] [time]
Rem Included in Project: Pac-Man.dbpro
function initBMfonts()
Dim BMleft(127)
Dim BMwidth(127)
endfunction
`*********************************
` LOAD BITMAP FONT
`*********************************
Function LoadBMfont(FileName as string,FontImageNumber)
load image FileName,FontImageNumber,1
Font = 32
For y = 0 to 9
for x = 0 to 9
GetImage( FontImageNumber, Font + FontImageNumber, x*32, y*32, 32, 32 )
inc Font
next x
next y
Delete Image FontImageNumber
endfunction
`*********************************
` GET IMAGE WITH TRANSPARENCY
`*********************************
function GetImage(Image1,NewImage,Xstart,Ystart,Xsize as dword,Ysize as dword)
`Find unused memblocks
Memblock1 = 1
repeat
inc Memblock1
until memblock exist(Memblock1) = 0
NewMemblock = 2
repeat
inc NewMemblock
until memblock exist(NewMemblock) = 0
`Set up variables
Local Width as Dword
Local Height as Dword
Local Depth as Dword
Local Red as Byte
Local Green as Byte
Local Blue as Byte
Local Alpha as Byte
`Do it!
make memblock from image Memblock1,Image1
Width = memblock dword(Memblock1,0)
Height = memblock dword(Memblock1,4)
Depth = memblock dword(Memblock1,8)
make memblock NewMemblock,(Width*Height)+12
Write memblock Dword NewMemblock,0,Xsize
Write memblock Dword NewMemblock,4,Ysize
Write memblock Dword NewMemblock,8,Depth
Position = (Width * (Ystart)*4) + (Xstart*4) + 12
NewPosition = 12
for y = 1 to Ysize
for x = 1 to Xsize
Blue = memblock byte(Memblock1,Position)
Green = memblock byte(Memblock1,Position+1)
Red = memblock byte(Memblock1,Position+2)
Alpha = memblock byte(Memblock1, Position+3)
write memblock byte NewMemblock,NewPosition,Blue
write memblock byte NewMemblock,NewPosition+1,Green
write memblock byte NewMemblock,NewPosition+2,Red
write memblock byte NewMemblock,NewPosition+3,Alpha
inc Position,4
inc NewPosition,4
next x
inc Position , (width*4) - (Xsize*4)
next y
make image from memblock NewImage,NewMemblock
delete memblock Memblock1
delete memblock NewMemblock
endfunction
`*********************************
` TRIM BITMAP FONT
`*********************************
Function TrimBMFont(FontNo)
Local BlankColumn as boolean
Local Alpha as byte
Local Chr as integer
Local FontWidth as integer = 32
Local FontHeight as integer = 32
For Chr = 33 to 126
Make Memblock From Image 1 , FontNo + Chr
for x = 1 to FontWidth
BlankColumn = Yes
for y = 1 to FontHeight - 1
if BMleft(Chr) = 0
Position = (y * FontWidth * 4) + (x * 4) + 15
Alpha = memblock byte(1 , Position)
if Alpha > 0
BlankColumn = No
y = FontHeight - 1
endif
endif
next y
if BlankColumn = No
BMleft(Chr) = x
x = FontWidth
endif
next x
for x = FontWidth - 1 to 1 step - 1
BlankColumn = Yes
for y = 1 to FontHeight - 1
if BMwidth(Chr) = 0
Position = (y * FontWidth * 4) + (x * 4) + 15
Alpha = memblock byte(1 , Position)
if Alpha > 0
BlankColumn = No
y = FontHeight
endif
endif
next y
if BlankColumn = No
BMwidth(Chr) = x - BMleft(Chr)
x = 1
endif
next x
delete memblock 1
next Chr
BMleft(32) = 0
BMwidth(32) = FontWidth / 2
endfunction
`*********************************
` DISPLAY BITMAP FONT
`*********************************
Function BMFont(X,Y,S as string,ImageNo,SpriteNo,Kern)
for k = 1 to len(S)
Chr = asc(mid$(S,k))
if k > 1
sprite SpriteNo + k ,NewX - BMleft(Chr) ,Y ,ImageNo + Chr
NewX = (NewX) + BMwidth(Chr) + Kern
else
sprite SpriteNo + k ,X - BMleft(Chr) ,Y ,ImageNo + Chr
NewX = X + BMwidth(Chr) + Kern
endif
next k
endfunction
You may have noticed that the arrays for this code are dimensioned in their own function I added to Scraggle's code, this function will be called by our main source file to make sure the bitmap font code has the memory allocated as soon as it needs it.
Switch to Main.dba
Add this code to Main.dba, below the arrays
We still need to call LoadBMfont() to load the PacMania image, but before we do that we need to port Scraggle's code so it works with our dynamic media system. Right now the code wants us to supply it with image and sprite numbers, but with some editing we can get it to find its own media numbers via Free.dba and Media Allocate.dba
What is interesting about Scraggle's code is that you can have any amount of bitmap fonts loaded at once, as long as the id numbers of the images and sprites do not intersect. If we were to use arrays to hold the media numbers of all the characters for all the fonts, they'd have to be 2D and scalable. This actually can be done, but not with the usual dynamic array commands.
To add a row or column of elements to a 2D array, all you have to do is dimension it again with the new bounds. You don't have to undim it first, and the new array will even remember its old values.
i.e.
dim a(5,5) as integer
for x = 0 to 5
for y = 0 to 5
a(x,y) = x*y
next y
next x
dim a(10, 10) as integer
for x = 0 to 10
for y = 0 to 10
text x*20, y*20, str$(a(x,y))
next y
next x
wait key
Porting Scraggle's code to make it an elaborate multi-font system is a bit of overkill, so we won't use these techniques. We only need 1 font, so a multi-font system is redundant. But tricks like this are still good to know.
The first step in porting Scraggle's code is to give it a free memblock function. GetImage() is finding memblock id numbers manually, but we can just replace that manual code with calls to free_mem().
Switch to Free.dba
Put this code in Free.dba
function free_mem()
local id as dword
repeat
inc id
until memblock exist(id) = 0
endfunction id
Switch to BMF.dba
Delete the "`Find unused memblocks" statementblock
Put this code after "`Do it!"
Put the code in the second box before "make memblock NewMemblock,(Width*Height)+12"
Ok, now the memblock numbers are generated via a function, but we still need to do the same for the images and sprites. Let's start with the images. The image id numbers start with FontImageNumber and end with FontImageNumber + 131. It's not a good idea to try to get a chunck of 100 sequential unused id numbers in a dynamic media system, we'd be better off converting FontImageNumber to an array and letting each of the 100 indexes remember the real id number by calling free_img().
FontImageNumber(0) will be the PacMania image itself, generated in LoadBMfont() by load_img(). Because FontImageNumber(0) will now be generated inside the function instead of being an argument, it doesn't need to be on the parameter list.
Add this array to initBMfonts()
Delete FontImageNumber from the LoadBMfont() argument list
Replace "load image FileName,FontImageNumber,1" with this code
dim FontImageNumber(131) as dword
FontImageNumber(0) = load_img(FileName, 1)
The functions GetImage(), TrimBMFont(), and BMFont() need to access this array too. We can delete the argument corresponding to FontImageNumber(0) from their argument lists also, they'll access the global array instead.
Delete Image1 from the GetImage() argument list
Delete "FontImageNumber" from the GetImage() call parameter list in LoadBMfont()
Search and Replace "Image1" for "FontImageNumber(0)"
Delete FontNo from the TrimBMFont() argument list
Search and Replace "FontNo" for "FontImageNumber(0)"
Delete ImageNo from the BMFont() argument list
Search and Replace "ImageNo" for "FontImageNumber(0)"
Take a look at the line "Make Memblock From Image 1 , FontImageNumber(0) + Chr"
FontImageNumber is an array now, not a variable. This code wanted the image with an id of (FontImageNumber + Chr) before, but now that id is stored
inside the FontImageNumber array. What we really want is the value of index Chr in the array. This line should read "Make Memblock From Image 1 , FontImageNumber(Chr)", we'll need to go through the code and put parentheses after all the referencess to FontImageNumber and put the added number as the index.
Change the line "GetImage(Font + FontImageNumber, x*32, y*32, 32, 32 )" to "GetImage(FontImageNumber(Font), x*32, y*32, 32, 32 )"
Change the line "Delete Image FontImageNumber" to "Delete Image FontImageNumber(0)"
Change the line "Make Memblock From Image 1 , FontImageNumber(0) + Chr" to "Make Memblock From Image 1 , FontImageNumber(Chr)"
Change the line "sprite SpriteNo + k ,NewX - BMleft(Chr) ,Y ,FontImageNumber(0) + Chr" to "sprite SpriteNo + k ,NewX - BMleft(Chr) ,Y ,FontImageNumber(Chr)"
Change the line "sprite SpriteNo + k ,X - BMleft(Chr) ,Y ,FontImageNumber(0) + Chr" to "sprite SpriteNo + k ,X - BMleft(Chr) ,Y ,FontImageNumber(Chr)"
Ok, the arrays are in place, but at this point all the indexes except 0 equal 0. The GetImage() function needs use the dynamic media functions to set the FontImageNumber id numbers, otherwise they will all be 0 cause a crash. There's no point for LoadBMfont() to call GetImage() with FontImageNumber(Font) because it will equal 0 anyway. It would be much more useful to call GetImage() with the index number of the image we want, and GetImage() can set the respective slot in FontImageNumber to the number free_img() returns.
Change the line "GetImage(FontImageNumber(Font), x*32, y*32, 32, 32 )" to "GetImage(Font, x*32, y*32, 32, 32 )"
Change the line "make image from memblock NewImage,NewMemblock" to "make image from memblock FontImageNumber(NewImage),NewMemblock"
Put this code before the line you just changed
FontImageNumber(NewImage) = free_img()
Did you notice something about TrimBMFont()? It's not using dynamic memblock numbers! How sneaky. Let's fix that.
Add this code to TrimBMfont(), right after the variable declarations
Change the line "Make Memblock From Image 1 , FontImageNumber(Chr)" to "Make Memblock From Image TrimMemblock, FontImageNumber(Chr)"
Change the lines "Alpha = memblock byte(1 , Position)" to "Alpha = memblock byte(TrimMemblock, Position)" (There's two of these lines)
Change the line "delete memblock 1" to "delete memblock TrimMemblock"
local TrimMemblock as dword
TrimMemblock = free_mem()
We're putting the call to free_mem() outside the loop because even if we put it inside, the number would never change. A memblock gets deleted at the end of the loop and instantly created again at the top, so it's better to optimize the code and only make it call free_mem() once, not 94 times.
The next step is to make the sprite numbers dynamic. This is going to be hard because it's not all one array. If we did make one array, it would hold the sprite id numbers like this in memory:
Score: 500Level: 1Lives: 3
Let's say the score gets to 1000, the extra digit will overwrite the "L" in "Level". We'd have to be very careful and insert an extra slot in the array so we don't overwrite other text's sprites. But that is very confusing so we'd be better off making a 2D array like this:
Text 1: "S" "c" "o" "r" "e" ":" "5" "0" "0"
Text 2: "L" "e" "v" "e" "l" ":" "1" " " " "
Text 3: "L" "i" "v" "e" "s" ":" "3" " " " "
It's a little wasteful because extra memory is allocated for the shorter texts to keep the 2D array rectangular (see the blanks after text 2 and 3), but it's easier than using pointers to save memory.
Add this code to initBMfonts()
dim BMSprites(,) as dword
The first index of the array will refer to which text we are making sprites for in BMFont(), and the second refers to which character in that text. The dword values hold the sprite id numbers.
Now let's change the BMFont() function so it can work with this array. Instead of calling BMFont() with SpriteNo (the first sprite id number), we'll call it with the number of the text we want.
Search and Replace "SpriteNo + k" for "BMSprites(TextNo, k)" in BMFont()
Change the SpriteNo argument in BMFont() to TextNo
We also need a way to make the array bigger by adding a text or making an existing text bigger than the array currently is. But before we do that, we need some variables to remember how bit the array is. (Remember empty arrays start with size -1)
Add this code to BMF.dba
Add the code in the second box to initBMfonts()
global texts as integer
global chars as integer
Now we can simply increment the number of texts and re-dim the array whenever we need a new hud element. Our hud code in the actual game needs to know the index number of the text so it can call BMFont() with it, so let's return the index of the new text when we create it.
function NewText()
inc texts
dim BMSprites(texts, chars) as dword
endfunction texts
That handles new texts, what what about new characters? This is even simplier, we'll simply check if the length of S in BMFont() is greater than the width of our array (the chars variable). If it is, we simply set chars to the new length and re-dim the array.
Add this code to BMFont(), right at the top
if len(S) > chars
chars = len(S)
dim BMSprites(texts, chars) as dword
endif
Now the arrays are the right size, but we still need to fill them with id numbers. This is very simple, right before we create or change a sprite with the sprite command we'll check if BMSprites(TextNo, k) is 0, if it is we use free_spr to change it a sprite id number and prevent crashing.
Add this code to BMFont(), before "if k > 1"
if BMSprites(TextNo, k) = 0 then BMSprites(TextNo, k) = free_spr()
We're almost done. One last function we should add is the ability to delete texts. It would be much easier to make a function that deletes all the texts instead of them individually. (We'd need a free_text() function for that, and coding that would be a bit of overkill)
Add this code to BMF.dba
function DeleteTexts()
local x as dword
local y as dword
if chars < 0 or texts < 0 then exitfunction
for x = 0 to chars
for y = 0 to texts
if BMSprites(x, y) then delete sprite BMSprites(x, y)
next y
next x
empty array BMSprites(x, y)
texts = -1
chars = -1
endfunction
This code simply loops through each sprite id number in the array, deleting it if the sprite exists. Then once all the sprites have been deleted the array gets emptied and reset. (If the array is already empty the function exits)
One last porting task is to make Scraggle's code work with 64*64 fonts. It was originally designed for 32*32, but with some search and replacing we can easily port it.
Search and Replace "32" for "64" (DO NOT say yes on the lines "Font = 32", "BMleft(32) = 0", and "BMwidth(32) = FontWidth / 2", keep them at 32)
However, 64*64 is quite large for our game. Let's use scale sprite to make the sprites smaller.
Add this code to BMFont(), before "next k"
scale sprite BMSprites(TextNo, k), 50
Those two arrays we saw eariler - BMleft() and BMwidth - hold the number of pixels to offset each sprite. This still works in 64*64 world, so the sprites will be very spaced out when scaled down to 32*32. To fix this we'll just divide the array values by 2, that will make the space half as big (because 32 is half of 64
).
Change the line "BMleft(Chr) = x" to "BMleft(Chs) = x / 2"
Change the line "BMwidth(Chr) = x - BMleft(Chr)" to "BMwidth(Chr) = (x - BMleft(Chr)) / 2"
Change the line "BMwidth(32) = FontWidth / 2" to "BMwidth(32) = FontWidth / 4"
Ok, code porting is done.
Your code should now look like this:
(BMF.dba)
Rem *** Include File: BMF.dba ***
Rem Created: [date] [time]
Rem Included in Project: Pac-Man.dbpro
global texts as integer
global chars as integer
function initBMfonts()
Dim BMleft(127)
Dim BMwidth(127)
dim FontImageNumber(131) as dword
dim BMSprites(,) as dword
texts = -1
chars = -1
endfunction
`*********************************
` LOAD BITMAP FONT
`*********************************
Function LoadBMfont(FileName as string)
FontImageNumber(0) = load_img(FileName, 1)
Font = 32
For y = 0 to 9
for x = 0 to 9
GetImage(Font, x*64, y*64, 64, 64 )
inc Font
next x
next y
Delete Image FontImageNumber(0)
endfunction
`*********************************
` GET IMAGE WITH TRANSPARENCY
`*********************************
function GetImage(NewImage,Xstart,Ystart,Xsize as dword,Ysize as dword)
`Set up variables
Local Width as Dword
Local Height as Dword
Local Depth as Dword
Local Red as Byte
Local Green as Byte
Local Blue as Byte
Local Alpha as Byte
`Do it!
Memblock1 = free_mem()
make memblock from image Memblock1,FontImageNumber(0)
Width = memblock dword(Memblock1,0)
Height = memblock dword(Memblock1,4)
Depth = memblock dword(Memblock1,8)
NewMemblock = free_mem()
make memblock NewMemblock,(Width*Height)+12
Write memblock Dword NewMemblock,0,Xsize
Write memblock Dword NewMemblock,4,Ysize
Write memblock Dword NewMemblock,8,Depth
Position = (Width * (Ystart)*4) + (Xstart*4) + 12
NewPosition = 12
for y = 1 to Ysize
for x = 1 to Xsize
Blue = memblock byte(Memblock1,Position)
Green = memblock byte(Memblock1,Position+1)
Red = memblock byte(Memblock1,Position+2)
Alpha = memblock byte(Memblock1, Position+3)
write memblock byte NewMemblock,NewPosition,Blue
write memblock byte NewMemblock,NewPosition+1,Green
write memblock byte NewMemblock,NewPosition+2,Red
write memblock byte NewMemblock,NewPosition+3,Alpha
inc Position,4
inc NewPosition,4
next x
inc Position , (width*4) - (Xsize*4)
next y
FontImageNumber(NewImage) = free_img()
make image from memblock FontImageNumber(NewImage),NewMemblock
delete memblock Memblock1
delete memblock NewMemblock
endfunction
`*********************************
` TRIM BITMAP FONT
`*********************************
Function TrimBMFont()
Local BlankColumn as boolean
Local Alpha as byte
Local Chr as integer
Local FontWidth as integer = 64
Local FontHeight as integer = 64
local TrimMemblock as dword
TrimMemblock = free_mem()
For Chr = 33 to 126
Make Memblock From Image 1 , FontImageNumber(Chr)
for x = 1 to FontWidth
BlankColumn = Yes
for y = 1 to FontHeight - 1
if BMleft(Chr) = 0
Position = (y * FontWidth * 4) + (x * 4) + 15
Alpha = memblock byte(TrimMemblock, Position)
if Alpha > 0
BlankColumn = No
y = FontHeight - 1
endif
endif
next y
if BlankColumn = No
BMleft(Chr) = x / 2
x = FontWidth
endif
next x
for x = FontWidth - 1 to 1 step - 1
BlankColumn = Yes
for y = 1 to FontHeight - 1
if BMwidth(Chr) = 0
Position = (y * FontWidth * 4) + (x * 4) + 15
Alpha = memblock byte(TrimMemblock, Position)
if Alpha > 0
BlankColumn = No
y = FontHeight
endif
endif
next y
if BlankColumn = No
BMwidth(Chr) = (x - BMleft(Chr)) / 2
x = 1
endif
next x
delete memblock TrimMemblock
next Chr
BMleft(32) = 0
BMwidth(32) = FontWidth / 4
endfunction
`*********************************
` DISPLAY BITMAP FONT
`*********************************
Function BMFont(X,Y,S as string, TextNo,Kern)
if len(S) > chars
chars = len(S)
dim BMSprites(texts, chars) as dword
endif
for k = 1 to len(S)
Chr = asc(mid$(S,k))
if BMSprites(TextNo, k) = 0 then BMSprites(TextNo, k) = free_spr()
if k > 1
sprite BMSprites(TextNo, k),NewX - BMleft(Chr) ,Y ,FontImageNumber(Chr)
NewX = (NewX) + BMwidth(Chr) + Kern
else
sprite BMSprites(TextNo, k),X - BMleft(Chr) ,Y ,FontImageNumber(Chr)
NewX = X + BMwidth(Chr) + Kern
endif
scale sprite BMSprites(TextNo, k), 50
next k
endfunction
function NewText()
inc texts
dim BMSprites(texts, chars) as dword
endfunction texts
function DeleteTexts()
local x as dword
local y as dword
if chars < 0 or texts < 0 then exitfunction
for x = 0 to chars
for y = 0 to texts
if BMSprites(x, y) then delete sprite BMSprites(x, y)
next y
next x
empty array BMSprites(x, y)
texts = -1
chars = -1
endfunction
Now we can finally prettify the HUD by calling the functions in BMF.dba.
First, we need to load the PacMania font into the system.
Switch to Init Game.dba
Add this code to load_misc()
`load the bitmap font
loadBMfont("PacMania.png")
TrimBMfont()
Next, we need variables to hold the text numbers returned by NewText() so we can tell BMFont() which text we want to modify.
Switch to Game.dba
Add these variables to Game.dba
global text_score as dword
global text_level as dword
global text_lives as dword
Next we will need to create these texts via NewText(). This code will have to go into the ready() function because menus will delete these texts (because they'll get in the way), we need to reset the font system every time a player wants to play.
Add this code to ready(), after the "Ready!" sprite gets displayed
`reset text system
DeleteTexts()
text_score = NewText()
text_level = NewText()
text_lives = NewText()
BMFont(640, 32, "Score: " + str$(score), text_score, 0)
BMFont(640, 96, "Level: " + str$(level), text_level, 0)
BMFont(640, 160, "Lives: " + str$(lives), text_lives, 0)
Since sprites do not have to be redrawn manually every frame, we don't need to update these texts constantly, just when they change. That means we can delete the hud() function.
Delete the hud() function
Delete the call to hud() in game_loop()
Add this code to inc_score()
Add the code in the second box to level_up()
Add the code in the third box to inc_score(), right after the play sound command
Add the code in the third box to get_eaten(), right after the "dec lives" line
Compile and Run
`update hud
BMFont(640, 32, "Score: " + str$(score), text_score, 0)
`update hud
if text_level then BMFont(640, 96, "Level: " + str$(level), text_level, 0)
`update hud
BMFont(640, 160, "Lives: " + str$(lives), text_lives, 0)
That's it. Compile and run the program to see our new stylish hud.