This is the third article in a series that takes the RPG from the “How to Make an RPG” project and adds a little more polish to one element.
In the previous polish phase we updated the numbers in the game to use bitmap fonts, so they’re much sharper and easier to read.
In this polish phase we’re going to:
- Add chunkier damage and heal numbers
- Use custom sprites for attack events (miss, dodge etc)
Here’s a comparison of the changes.
It looks much crisper natively. I failed to reproduce it perfectly using a gif :(.
Reference
Here’s a screenshot of the Final Fantasy 7 combat state.
There are two different number fonts here: one on the battle field and one in the dialog box at the bottom.
The ‘4’ numeral from each font is enlarged on the left.
Top ‘4’
The top ‘4’ is a jumping number; a number that appears to jump out from the head of a character.
- 8x11 pixels
- Full black border
- Easy to read on any background
Being easy to read on any background is important because this number appears on the field of combat and the background could be any color.
Bottom ‘4’
The bottom ‘4’ font is used to display the current health and mana.
- Smaller at 7x8 pixels
- Drop shadow
- Displayed inside a dialog box
The smaller size allows more data to be squeezed into the combat panel. The panel background is known so a drop shadow is fine.
Bitmap Font Engine
Dinodeck has a true-type text rendering library. It’s hard to make true-type fonts render crisply at low resolutions therefore I wrote a second font rendering library called BitmapFont.lua.
The BitmapFont library uses the same interface as the Dinodeck library. I’ve been hacking on this code for a while and this layer of polish uses the most recent version. You can always get the cutting edge code here. Since the last update, non-monospaced fonts are now supported.
What Are We Going to Change?
We’ll update all numbers on the field of combat as well some of the status text.
Here is how it looks right now.
There are a couple of things to note in these images:
- Numbers / Text isn’t very readable
- Numbers are monospaced, so the gap between the ‘1’ digit is too big
- Dropshadow instead of an outline
- The top two enemies are slightly blurred as they’re not aligned correctly to the pixel grid
Let’s polish up these issues!
Sourcing a Replacement Font
The image below shows the combat font we’ll be using. I drew this pixel by pixel but it’s based very closely on the Final Fantasy font. (If you have a similar font with a permissive license, let me know and I’ll be happy to promote it!).
Numbers spacing is handled in the definition file.
My pixeling skill aren’t very advanced. It would be better with:
- Nicer letter shapes
- Larger glyphs to better suit the resolution.
Code Changes
Code changes are restricted to the combat state and a few classes used by the combat state.
Adding The Font
To add new combat damage numbers we need to add a new font.
Bitmap fonts are made up of two files - an image file and a Lua file that describes the glyphs. The two files we’re adding are:
- damage_font.png
- DamageFontDef.lua
These files are added to /art/font
and /code/font
directories.
I’ve also updated BitmapText.lua with the most recent version of the code.
Storing Fonts
At this point we have two fonts; a general text font and this new combat font. Let’s put the font objects somewhere together so they’re easier to manage.
Here’s how the current font is stored:
gNumberFont = BitmapText:Create(NumberFontDef)
function SetupNewGame()
gStack = StateStack:Create()
gWorld = World:Create()
We’ll modify this to store all fonts under a global gGame.Font
table. While we’re there let’s add the gStack
and gWorld
variables under the same table. This reduces the amount of noise in the global space.
gGame =
{
Font =
{
default = BitmapText:Create(NumberFontDef),
damage = BitmapText:Create(DamageFontDef)
},
Stack = {},
World = {}
}
function SetupNewGame()
gGame.Stack = StateStack:Create()
gGame.World = World:Create()
These polish phases cater primarily to the end user but here I’m also doing a bit of code clean up.
Moving the gWorld
and gStack
variables isn’t just a local change. I’ve updated all code that accesses these variables as well as the save system.
Both bitmap fonts are now easily accessible, so let’s move on.
An Aside: Fixing up Blurry Enemies
In the screenshots the goblin sprites were blurred because they weren’t on the pixel boundaries. This can be fixed by tweaking the CombatState
code as below:
function CombatState:CreateCombatCharacters(side)
-- code omitted
local x = math.floor(pos:X() * System.ScreenWidth())
local y = math.floor(pos:Y() * System.ScreenHeight())
char.mEntity.mSprite:SetPosition(x, y)
char.mEntity.mX = x
char.mEntity.mY = y
The math.floor
functions are new here.
Updating the Combat Font
The font for the combat numbers is set in JumpingNumbers.lua
here’s the current implementation.
function JumpingNumbers:Render(renderer)
local x = self.mX
local y = math.floor(self.mCurrentY)
local n = tostring(self.mNumber)
local font = gGame.Font.default
font:AlignText("center", "center")
font:DrawText2d(renderer, x + 1, y - 1, n, Vector.Create(0,0,0, self.mColor:W()))
font:DrawText2d(renderer, x, y, n, self.mColor)
end
And the fix is to change this line from
local font = gGame.Font.default
To
local font = gGame.Font.damage
That’s it for the numbers.
This screenshot shows the nicer chunkier damage font, no longer monospaced.
Speed Tweak
The damage numbers move quickly, which is good; it gives a nice fast pace, but the number can be tricky to read.
To address this, I’ve added a 0.2 second pause when the number reaches the peak of it’s ascent.
Attack Event Text
Numbers are done, it’s time to move on to the attack events.
Text is controlled by CombatTextFx
. It displays text using the Dinodeck text calls. We’re going to replace the text with sprites cut from the damage_number.png file. To do this we’ll make a new class: CombatSpriteFx
.
The event sprites are stored in damage_font.png
. We’ll add a file called DamageSpriteDef.lua
to describe how to cut the sprites out of this atlas. This file will live in the same directory as DamageFontDef.lua.
-- DamageSpriteDef.lua
DamageSpriteDef =
{
texture = "damage_font.png",
-- Given in pixels
sprites =
{
["miss"] =
{
x = 80,
y = 0,
width = 24,
height = 14
},
["counter"] =
{
x = 104,
y = 0,
width = 47,
height = 14
},
["dodge"] =
{
x = 152,
y = 0,
width = 37,
height = 14,
}
}
}
We’ll create and load these sprites under the Game.Font
table as damageSprite
. The definition contains the pixel coordinates of each sprite. We need a couple of util functions to transform the def into sprites.
function PixelCoordsToUVs(tex, def)
local texWidth = tex:GetWidth()
local texHeight = tex:GetHeight()
local x = def.x / texWidth
local y = def.y / texHeight
local width = def.width / texWidth
local height = def.height / texHeight
return {x, y, x + width, y + height}
end
function CreateSpriteSet(def)
local texture = Texture.Find(def.texture)
local spriteSet = {}
for k, v in pairs(def.sprites) do
local sprite = Sprite.Create()
sprite:SetTexture(texture)
sprite:SetUVs(unpack(PixelCoordsToUVs(texture, v)))
spriteSet[k] = sprite
end
return spriteSet
end
Here’s how the sprites are loaded in the main file.
gGame =
{
Font =
{
-- code omitted
damageSprite = CreateSpriteSet(DamageSpriteDef)
},
After creating the sprites we need to create a new special effect to use them in combat.
The text effects are created by AddTextEffect
, we’ll add a new function call AddSpriteEffect
. Here it is:
function CombatState:AddSpriteEffect(actor, sprite)
local character = self.mActorCharMap[actor]
local entity = character.mEntity
local x = entity.mX
local y = entity.mY
local effect = CombatSpriteFx:Create(x, y, sprite)
self:AddEffect(effect)
end
Finally let’s update the code so the new sprite effect is called.
function CombatState:ApplyMiss(target)
self:AddSpriteEffect(target,
gGame.Font.damageSprite['miss'])
end
function CombatState:ApplyDodge(target)
-- code omitted
self:AddSpriteEffect(target,
gGame.Font.damageSprite['dodge'])
end
Here’s the end result! Definitely a step towards better combat events.
Source
There’s a github with an updated project here.