Font Module for games
Posted: Mon Nov 27, 2023 5:27 am
I've created a small font module that takes either installed fonts or a font file and converts it to sprites for use in games. It is a bit limited in that you can't change the colour or size of the font unless you create a whole other set of sprites in that colour or size. To optimise this I have allowed the creation of only selected parts of the font like the numbers only or uppercase etc, or even just selected characters. My only problem with it is that is a bit slow. Does anyone know how to optimise DisplayStringSprite in particular?
UPDATED 20231127: added optimisation for displaying strings (20-25% improvement)
UPDATED 20231129: added further optimisation for displaying strings (further 20-25% improvement)
Module:
Speed testing:
UPDATED 20231127: added optimisation for displaying strings (20-25% improvement)
UPDATED 20231129: added further optimisation for displaying strings (further 20-25% improvement)
Module:
Code: Select all
EnableExplicit
DeclareModule Fonts
#Max_Fonts = 10
#Font_Num_ASCII_Characters = 125
#Font_Num_Characters = 95
#Font_Num_Characters_Numbers = 10
#Font_Num_Characters_Uppercase = 26
#Font_Num_Characters_Lowercase = 26
#Font_Num_Characters_Symbol = 33
#Font_Characters_Select = 1 ; used if you just want to select a few
#Font_Characters_Numbers = 2
#Font_Characters_Uppercase = 4
#Font_Characters_Lowercase = 8
#Font_Characters_Symbol_1 = 16
#Font_Characters_Symbol_2 = 32
#Font_Characters_Symbol_3 = 64
#Font_Characters_Symbol_4 = 128
#Font_Characters_All = 256
Structure Text_Array_Structure
Index.u[0]
EndStructure
Structure Font_Character_Structure
Sprite_ID.i
Image_ID.i
Width.i
Height.i
Loaded.i
EndStructure
Structure Font_Structure
Name.s
Filename.s
Size.i
Character.Font_Character_Structure[#Font_Num_Characters]
Crop_Top_Ratio.d
Crop_Bottom_Ratio.d
Crop_Left_Ratio.d
Crop_Right_Ratio.d
Colour.i
Back_Colour.i
Available.i ; 0 = select, 1 = numbers, 2 = uppercase, 4 = lowercase, 8 = symbol 1, 16 = symbol 2, 32 = symbol 3, 64 = symbol 4, 128 = all
Select_Available.s
Border.i
Border_Colour.i
Loaded.i
EndStructure
Dim *Fonts.Font_Structure(#Max_Fonts)
Global Dim Characters.i(#Font_Num_ASCII_Characters)
Declare Initialise(Array *Fonts.Font_Structure(1))
Declare.s GetChar(Char.i)
Declare.i GetCharNum(Char.s)
Declare.i GetNum(ASCII.i)
Declare.i CreateFont(Array *Fonts.Font_Structure(1), Font_ID.i, Flags.i=0)
Declare.i GetCharacterWidth(Array *Fonts.Font_Structure(1), Font_ID.i, Char.s)
Declare.i GetCharacterHeight(Array *Fonts.Font_Structure(1), Font_ID.i, Char.s)
Declare DisplayCharacterSprite(Array *Fonts.Font_Structure(1), Font_ID.i, Char.s, x.i, y.i)
Declare DisplayCharacterImage(Array *Fonts.Font_Structure(1), Font_ID.i, Char.s, x.i, y.i)
Declare DisplayStringSprite(Array *Fonts.Font_Structure(1), Font_ID.i, Text.s, x.i, y.i, Spacing.i=0)
Declare DisplayStringImage(Array *Fonts.Font_Structure(1), Font_ID.i, Text.s, x.i, y.i, Spacing.i=0)
Declare.i GetStringWidth(Array *Fonts.Font_Structure(1), Font_ID.i, Text.s)
Declare Shutdown(Array *Fonts.Font_Structure(1))
EndDeclareModule
Module Fonts
Procedure Initialise(Array *Fonts.Font_Structure(1))
Protected.i c
Protected.i Ch
For c = 0 To #Max_Fonts
*Fonts(c) = AllocateStructure(Font_Structure)
Next c
Restore Font_Character_ASCII_Map
For c = 0 To #Font_Num_ASCII_Characters
Read.i Ch
Characters(c) = Ch
Next c
EndProcedure
Procedure.s GetChar(Char.i)
Select Char
Case 0 To 9: ProcedureReturn(Chr(48+Char))
Case 10 To 35: ProcedureReturn(Chr(55+Char))
Case 36 To 61: ProcedureReturn(Chr(61+Char))
Case 62 To 77: ProcedureReturn(Chr(Char-30))
Case 78 To 84: ProcedureReturn(Chr(Char-20))
Case 85 To 90: ProcedureReturn(Chr(6+Char))
Case 91 To 94: ProcedureReturn(Chr(32+Char))
EndSelect
EndProcedure
Procedure.i GetCharNum(Char.s)
Protected ASCII.i = Asc(Char)
Select ASCII
Case 48 To 57:ProcedureReturn(ASCII-48)
Case 65 To 90:ProcedureReturn(ASCII-55)
Case 97 To 122:ProcedureReturn(ASCII-61)
Case 32 To 47:ProcedureReturn(ASCII+30)
Case 58 To 64:ProcedureReturn(ASCII+20)
Case 91 To 96:ProcedureReturn(ASCII-6)
Case 123 To 126:ProcedureReturn(ASCII-32)
EndSelect
EndProcedure
Procedure.i GetNum(ASCII.i)
Select ASCII
Case 48 To 57:ProcedureReturn(ASCII-48)
Case 65 To 90:ProcedureReturn(ASCII-55)
Case 97 To 122:ProcedureReturn(ASCII-61)
Case 32 To 47:ProcedureReturn(ASCII+30)
Case 58 To 64:ProcedureReturn(ASCII+20)
Case 91 To 96:ProcedureReturn(ASCII-6)
Case 123 To 126:ProcedureReturn(ASCII-32)
EndSelect
EndProcedure
Procedure.i CheckCreateCharacter(Array *Fonts.Font_Structure(1), Font_ID.i, c.i)
; Checks whether to create the character based on the settings provided
Protected.i Result = 0
If *Fonts(Font_ID)\Available & #Font_Characters_Select
If FindString(*Fonts(Font_ID)\Select_Available, GetChar(c)):Result = 1:EndIf
EndIf
If *Fonts(Font_ID)\Available & #Font_Characters_Numbers And c>=0 And c<=9:Result = 1:EndIf
If *Fonts(Font_ID)\Available & #Font_Characters_Uppercase And c>=10 And c<=35:Result = 1:EndIf
If *Fonts(Font_ID)\Available & #Font_Characters_Lowercase And c>=36 And c<=61:Result = 1:EndIf
If *Fonts(Font_ID)\Available & #Font_Characters_Symbol_1 And c>=62 And c<=77:Result = 1:EndIf
If *Fonts(Font_ID)\Available & #Font_Characters_Symbol_2 And c>=78 And c<=84:Result = 1:EndIf
If *Fonts(Font_ID)\Available & #Font_Characters_Symbol_3 And c>=85 And c<=90:Result = 1:EndIf
If *Fonts(Font_ID)\Available & #Font_Characters_Symbol_4 And c>=91 And c<=94:Result = 1:EndIf
If *Fonts(Font_ID)\Available & #Font_Characters_All:Result = 1:EndIf
ProcedureReturn Result
EndProcedure
Procedure.i CreateFont(Array *Fonts.Font_Structure(1), Font_ID.i, Flags.i=0)
Protected Char.s
Protected c.i
Protected Crop_Left.i
Protected Crop_Right.i
Protected Crop_Top.i
Protected Crop_Bottom.i
If *Fonts(Font_ID)\Filename <> ""
If FileSize(*Fonts(Font_ID)\Filename) = -1
Debug "Font file does not exist: " + *Fonts(Font_ID)\Filename
ProcedureReturn 0
EndIf
RegisterFontFile(*Fonts(Font_ID)\Filename)
EndIf
LoadFont(Font_ID, *Fonts(Font_ID)\Name, *Fonts(Font_ID)\Size, Flags)
Restore Font_Characters_Numbers
For c = 0 To #Font_Num_Characters-1
Read.s Char
If CheckCreateCharacter(*Fonts(), Font_ID, c)
; create temp image so that the font can be selected
Temp_Image = CreateImage(#PB_Any, 1, 1, 32)
StartDrawing(ImageOutput(Temp_Image))
DrawingFont(FontID(Font_ID))
*Fonts(Font_ID)\Character[c]\Width = TextWidth(Char)
*Fonts(Font_ID)\Character[c]\Height = TextHeight(Char)
StopDrawing()
FreeImage(Temp_Image)
If *Fonts(Font_ID)\Crop_Top_Ratio > 0 And *Fonts(Font_ID)\Crop_Top_Ratio < 1
Crop_Top = *Fonts(Font_ID)\Character[c]\Height * *Fonts(Font_ID)\Crop_Top_Ratio
EndIf
If *Fonts(Font_ID)\Crop_Bottom_Ratio > 0 And *Fonts(Font_ID)\Crop_Bottom_Ratio < 1
Crop_Bottom = *Fonts(Font_ID)\Character[c]\Height * *Fonts(Font_ID)\Crop_Bottom_Ratio
EndIf
If *Fonts(Font_ID)\Crop_Left_Ratio > 0 And *Fonts(Font_ID)\Crop_Left_Ratio < 1
Crop_Left = *Fonts(Font_ID)\Character[c]\Width * *Fonts(Font_ID)\Crop_Left_Ratio
EndIf
If *Fonts(Font_ID)\Crop_Right_Ratio > 0 And *Fonts(Font_ID)\Crop_Right_Ratio < 1
Crop_Right = *Fonts(Font_ID)\Character[c]\Width * *Fonts(Font_ID)\Crop_Right_Ratio
EndIf
*Fonts(Font_ID)\Character[c]\Width = *Fonts(Font_ID)\Character[c]\Width - (Crop_Left + Crop_Right)
*Fonts(Font_ID)\Character[c]\Height = *Fonts(Font_ID)\Character[c]\Height - (Crop_Top + Crop_Bottom)
*Fonts(Font_ID)\Character[c]\Sprite_ID = CreateSprite(#PB_Any, *Fonts(Font_ID)\Character[c]\Width, *Fonts(Font_ID)\Character[c]\Height, #PB_Sprite_AlphaBlending)
If Not *Fonts(Font_ID)\Character[c]\Sprite_ID
Debug "CreateFont: sprite creation failed"
Debug "It must be run after the following:"
Debug "- InitSprite()"
Debug "- OpenScreen() or OpenWindowedScreen()"
ProcedureReturn 0
EndIf
If StartDrawing(SpriteOutput(*Fonts(Font_ID)\Character[c]\Sprite_ID))
DrawingMode(#PB_2DDrawing_AllChannels)
DrawingFont(FontID(Font_ID))
DrawText(-Crop_Left, -Crop_Top, Char, *Fonts(Font_ID)\Colour, *Fonts(Font_ID)\Back_Colour)
If *Fonts(Font_ID)\Border
DrawingMode(#PB_2DDrawing_AllChannels | #PB_2DDrawing_Outlined)
Box(0, 0, *Fonts(Font_ID)\Character[c]\Width, *Fonts(Font_ID)\Character[c]\Height, *Fonts(Font_ID)\Border_Colour)
EndIf
StopDrawing()
EndIf
*Fonts(Font_ID)\Character[c]\Image_ID = CreateImage(#PB_Any, *Fonts(Font_ID)\Character[c]\Width, *Fonts(Font_ID)\Character[c]\Height, 32)
If StartDrawing(ImageOutput(*Fonts(Font_ID)\Character[c]\Image_ID))
DrawingMode(#PB_2DDrawing_AllChannels)
DrawingFont(FontID(Font_ID))
DrawText(-Crop_Left, -Crop_Top, Char, *Fonts(Font_ID)\Colour, *Fonts(Font_ID)\Back_Colour)
If *Fonts(Font_ID)\Border
DrawingMode(#PB_2DDrawing_AllChannels | #PB_2DDrawing_Outlined)
Box(0, 0, *Fonts(Font_ID)\Character[c]\Width, *Fonts(Font_ID)\Character[c]\Height, *Fonts(Font_ID)\Border_Colour)
EndIf
StopDrawing()
EndIf
*Fonts(Font_ID)\Character[c]\Loaded = 1
EndIf
*Fonts(Font_ID)\Loaded = 1
Next c
ProcedureReturn 1
EndProcedure
Procedure DisplayCharacterSprite(Array *Fonts.Font_Structure(1), Font_ID.i, Char.s, x.i, y.i)
Protected ASCII.i = Asc(Char)
Protected.i CharNum = GetCharNum(Char)
If *Fonts(Font_ID)\Character[CharNum]\Loaded
DisplayTransparentSprite(*Fonts(Font_ID)\Character[CharNum]\Sprite_ID, x, y)
EndIf
EndProcedure
Procedure DisplayCharacterImage(Array *Fonts.Font_Structure(1), Font_ID.i, Char.s, x.i, y.i)
; draws a character to the output as an image
Protected.i ASCII = Asc(Char)
Protected.i CharNum = GetCharNum(Char)
If *Fonts(Font_ID)\Character[CharNum]\Loaded
DrawAlphaImage(ImageID(*Fonts(Font_ID)\Character[CharNum]\Image_ID), x, y)
EndIf
EndProcedure
Procedure.i GetCharacterWidth(Array *Fonts.Font_Structure(1), Font_ID.i, Char.s)
Protected.i ASCII = Asc(Char)
Protected.i Return_Width = 0
Protected.i CharNum = GetCharNum(Char)
If *Fonts(Font_ID)\Character[CharNum]\Loaded
Return_Width = *Fonts(Font_ID)\Character[CharNum]\Width
Else
Return_Width = 0
EndIf
ProcedureReturn Return_Width
EndProcedure
Procedure.i GetCharacterHeight(Array *Fonts.Font_Structure(1), Font_ID.i, Char.s)
Protected.i ASCII = Asc(Char)
Protected.i CharNum = GetCharNum(Char)
Protected.i Return_Height = 0
If *Fonts(Font_ID)\Character[CharNum]\Loaded
Return_Height = *Fonts(Font_ID)\Character[CharNum]\Height
Else
Return_Height = 0
EndIf
ProcedureReturn Return_Height
EndProcedure
Procedure DisplayStringSprite(Array *Fonts.Font_Structure(1), Font_ID.i, Text.s, x.i, y.i, Spacing.i=0)
; Displays the string on the screen using sprites
; Spacing is used to monospace the text
Protected.i ASCII
Protected.i Width
Protected.i CharNum
Protected.i i
Protected *Text_Array.Text_Array_Structure = @Text
For i = 0 To Len(Text)-1
ASCII = *Text_Array\Index[i]
;CharNum = GetNum(ASCII)
;Debug Chr(ASCII) + " - " + ASCII
CharNum = Characters(ASCII)
If *Fonts(Font_ID)\Character[CharNum]\Loaded
DisplayTransparentSprite(*Fonts(Font_ID)\Character[CharNum]\Sprite_ID, x, y)
Width = *Fonts(Font_ID)\Character[CharNum]\Width
EndIf
If Spacing > 0
x = x + Spacing
Else
x = x + Width
EndIf
Next i
EndProcedure
Procedure DisplayStringImage(Array *Fonts.Font_Structure(1), Font_ID.i, Text.s, x.i, y.i, Spacing.i=0)
; Displays the string to the output using images
; Spacing is used to monospace the text
Protected.s Char
Protected.i Width
Protected.i CharNum
Protected *Text_Array.Text_Array_Structure = @Text
For i = 1 To Len(Text)
;Char = Mid(Text, i, 1)
ASCII = *Text_Array\Index[i]
CharNum = GetNum(ASCII)
If *Fonts(Font_ID)\Character[CharNum]\Loaded
DrawAlphaImage(ImageID(*Fonts(Font_ID)\Character[CharNum]\Image_ID), x, y)
Width = *Fonts(Font_ID)\Character[CharNum]\Width
EndIf
If Spacing > 0
x = x + Spacing
Else
x = x + Width
EndIf
Next i
EndProcedure
Procedure.i GetStringWidth(Array *Fonts.Font_Structure(1), Font_ID.i, Text.s)
Protected.i Return_Width = 0
Protected.s Char
Protected.i CharNum
Protected.i i
Return_Width = 0
For i = 1 To Len(Text)
Char = Mid(Text, i, 1)
CharNum = GetCharNum(Char)
If *Fonts(Font_ID)\Character[CharNum]\Loaded
Return_Width = Return_Width + *Fonts(Font_ID)\Character[CharNum]\Width
EndIf
Next i
ProcedureReturn Return_Width
EndProcedure
Procedure Shutdown(Array *Fonts.Font_Structure(1))
; Note: the screen needs to be open for this to work
Protected.i c1
Protected.i c2
For c1 = 0 To #Max_Fonts
For c2 = 0 To #Font_Num_Characters-1
If *Fonts(c1)\Character[c2]\Loaded
FreeSprite(*Fonts(c1)\Character[c2]\Sprite_ID)
FreeImage(*Fonts(c1)\Character[c2]\Image_ID)
EndIf
Next c2
If *Fonts(c1)\Loaded:FreeFont(c1):EndIf
FreeStructure(*Fonts(c1))
Next c1
EndProcedure
CompilerIf #PB_Compiler_IsMainFile
; Test code
Define.i Window_Width = 800
Define.i Window_Height = 800
Define.i Char_Height
Initialise(*Fonts())
InitSprite()
If OpenWindow(0, 0, 0, Window_Width, Window_Height, "Fonts Test", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
If OpenWindowedScreen(WindowID(0), 0, 0, Window_Width*DesktopResolutionX(), Window_Height*DesktopResolutionY())
With *Fonts(0)
\Name = "Arial" ; if you don't specify a filename it will use an inbuilt font by the nearest name given
\Size = 24
\Colour = RGBA(0, 128, 255, 255)
\Back_Colour = RGBA(0, 0, 0, 0)
\Available = #Font_Characters_All
\Border = 0
\Border_Colour = RGBA(255, 255, 255, 255)
EndWith
If CreateFont(*Fonts(), 0, #PB_Font_Bold)
Char_Height = GetCharacterHeight(*Fonts(), 0, "A")
DisplayStringSprite(*Fonts(), 0, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0, 0)
DisplayStringSprite(*Fonts(), 0, "abcdefghijklmnopqrstuvwxyz !"+#DQUOTE$+"#$%&'()*+,-./", 0, Char_Height)
DisplayStringSprite(*Fonts(), 0, ":;<=>?@[\]^_`{|}", 0, 2*Char_Height)
;StartDrawing(ScreenOutput())
;DisplayStringImage(*Fonts(), 0, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0, 0)
;StopDrawing()
EndIf
;With *Fonts(1)
; \Name = "Afacad Bold"
; \Filename = "Data\Fonts\Afacad\fonts\ttf\Afacad-Bold.ttf"
; \Size = 24
; \Colour = RGBA(255, 128, 0, 255)
; \Back_Colour = RGBA(0, 0, 0, 0)
; \Available = #Font_Characters_All
; ;\Available = #Font_Characters_Select
; ;\Select_Available = "123"
; \Border = 0
; \Border_Colour = RGBA(255, 255, 255, 255)
;EndWith
;If CreateFont(*Fonts(), 1)
; DisplayStringSprite(*Fonts(), 1, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", 0, 40)
;EndIf
Repeat
Event = WaitWindowEvent()
Until Event = #PB_Event_CloseWindow
EndIf
EndIf
Shutdown(*Fonts())
CloseScreen()
CompilerEndIf
DataSection
Font_Characters_Numbers:
Data.s "0","1","2","3","4","5","6","7","8","9" ; 0-9
Font_Characters_Alphabetic_Uppercase:
Data.s "A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z" ; 10-35
Font_Characters_Alphabetic_Lowercase:
Data.s "a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z" ; 36-61
Font_Characters_Symbol_1:
Data.s " ","!",#DQUOTE$,"#","$","%","&","'","(",")","*","+",",","-",".","/" ; 62-77
Font_Characters_Symbol_2:
Data.s ":",";","<","=",">","?","@" ; 78-84
Font_Characters_Symbol_3:
Data.s "[","\","]","^","_","`" ; 85-90
Font_Characters_Symbol_4:
Data.s "{","|","}","~" ; 91-94
Font_Character_ASCII_Map:
Data.i 000, 000, 000, 000, 000, 000, 000, 000, 000, 000 ;9
Data.i 000, 000, 000, 000, 000, 000, 000, 000, 000, 000 ;19
Data.i 000, 000, 000, 000, 000, 000, 000, 000, 000, 000 ;29
Data.i 000, 000, 062, 063, 064, 065, 066, 067, 068, 069 ;39
Data.i 070, 071, 072, 073, 074, 075, 076, 077, 000, 001 ;49
Data.i 002, 003, 004, 005, 006, 007, 008, 009, 078, 079 ;59
Data.i 080, 081, 082, 083, 084, 010, 011, 012, 013, 014 ;69
Data.i 015, 016, 017, 018, 019, 020, 021, 022, 023, 024 ;79
Data.i 025, 026, 027, 028, 029, 030, 031, 032, 033, 034 ;89
Data.i 035, 085, 086, 087, 088, 089, 090, 036, 037, 038 ;99
Data.i 039, 040, 041, 042, 043, 044, 045, 046, 047, 048 ;109
Data.i 049, 050, 051, 052, 053, 054, 055, 056, 057, 058 ;119
Data.i 059, 060, 061, 091, 092, 093, 094 ;126
EndDataSection
EndModule
Code: Select all
; Test font speed
EnableExplicit
XIncludeFile "..\Fonts.pb"
Define.i Width = 800
Define.i Height = 800
Define.i Event
Define.i c
Define.i x
Define.q Time_Start
Define.i Str_Width
Define.i Str_Height
Define.q Time
Define.d Total_Time
Define.d Average_Time
Fonts::Initialise(Fonts::*Fonts())
InitSprite()
If OpenWindow(0, 0, 0, Width, Height, "Test Debug", #PB_Window_SystemMenu | #PB_Window_ScreenCentered)
If OpenWindowedScreen(WindowID(0), 0, 0, Width*DesktopResolutionX(), Height*DesktopResolutionY(), #False, 0, 0, #PB_Screen_WaitSynchronization)
With Fonts::*Fonts(0)
\Name = "Arial"
;\Filename = ""
\Size = 36
\Colour = RGBA(255, 0, 0, 255)
\Back_Colour = RGBA(0, 0, 0, 0)
\Available = Fonts::#Font_Characters_All
\Border = 0
EndWith
With Fonts::*Fonts(1)
\Name = "Arial"
;\Filename = ""
\Size = 36
\Colour = RGBA(0, 255, 0, 255)
\Back_Colour = RGBA(0, 0, 0, 0)
\Available = Fonts::#Font_Characters_All
\Border = 0
EndWith
With Fonts::*Fonts(2)
\Name = "Arial"
;\Filename = ""
\Size = 36
\Colour = RGBA(0, 0, 255, 255)
\Back_Colour = RGBA(0, 0, 0, 0)
\Available = Fonts::#Font_Characters_All
\Border = 0
EndWith
Fonts::CreateFont(Fonts::*Fonts(), 0, #PB_Font_Bold)
Fonts::CreateFont(Fonts::*Fonts(), 1, #PB_Font_Bold)
Fonts::CreateFont(Fonts::*Fonts(), 2, #PB_Font_Bold)
Str_Width = Fonts::GetStringWidth(Fonts::*Fonts(), 0, "0123456789")
Str_Height = Fonts::GetCharacterHeight(Fonts::*Fonts(), 0, "0")
Time_Start = ElapsedMilliseconds()
For c = 1 To 2000
Fonts::DisplayStringSprite(Fonts::*Fonts(), Random(2, 0), "0123456789", Random(Width-Str_Width, 0), Random(Height-Str_Height, 0))
Next c
Time = ElapsedMilliseconds() - Time_Start
Debug "Time: " + Str(Time) + " ms"
Repeat
Event = WaitWindowEvent()
Until Event = #PB_Event_CloseWindow
Fonts::Shutdown(Fonts::*Fonts())
CloseScreen()
EndIf
EndIf