The How to Make an RPG book teaches you how to make three different small RPGs. The first RPG, called “Dungeon”, tasks the player with escaping a dungeon after being captured. Once you’ve made this game you’ll probably want to put your own stamp on it.
Recently, a reader asked about extending the “Dungeon” game in two ways and I thought it might nice to share my solutions as an article (in case anyone else was hoping to add the same features).
The extensions:
- Stop the player direction from being reset when the movement key is released.
- Stop the player snoring when the guard enters his house.
Dungeon Game
Here’s a few screenshots of the dungeon game to give you a feel of what we’re dealing with.
Resetting the Facing
Check out the current behavior:
If the player presses the left arrow key they face left, if they release the key then the player faces the camera.
There are two ways to change this behavior:
- Reset the facing behavior but after a short pause
- Never reset the facing behavior
We’ll try both ways. The player movement code is controlled by a simple state machine. There are only two states involved:
The two states are represented by the classes MoveState
and WaitState
. Every time a state is entered an Enter
function is called and each time a state is exited an Exit
function is called.
On entering the MoveState
the “Walk” animation is run on the player entity. When we enter the WaitState
we play the idle animation.
Let’s look at the core code for entering the WaitState
.
function WaitState:Create(character, map)
local this =
{
mCharacter = character,
mMap = map,
mEntity = character.mEntity,
mController = character.mController,
mFrameResetSpeed = 0.05,
mFrameCount = 0
}
-- code omitted
end
function WaitState:Enter(data)
self.mFrameCount = 0
end
function WaitState:Update(dt)
-- If we're in the wait state for a few frames, reset the frame to
-- the starting frame.
if self.mFrameCount ~= -1 then
self.mFrameCount = self.mFrameCount + dt
if self.mFrameCount >= self.mFrameResetSpeed then
self.mFrameCount = -1
self.mEntity:SetFrame(self.mEntity.mStartFrame)
self.mCharacter.mFacing = "down"
end
end
-- code omitted
end
Ok, looks like there’s already code to do everything we want!
On entering the WaitState
the mFrameCount
is set to 0. In the update loop a timer increases it until it reaches mFrameResetSpeed
and the animation is set to idle. The reset speed is currently set to 0.05
seconds; almost instantaneous.
Solution 1: A Longer Reset Speed
If we want the player to continue facing in the same the direction for a longer period we need to increase the mFrameResetSpeed
in the WaitState
. Let’s try a full second.
function WaitState:Create(character, map)
local this =
{
mCharacter = character,
mMap = map,
mEntity = character.mEntity,
mController = character.mController,
mFrameResetSpeed = 1.00, -- changed to 1sec
mFrameCount = 0
}
-- code omitted
end
And here’s how it looks:
Solution 2: Never Reset
In this case we want the WaitState
to maintain the player’s current facing. We need to set the player frame to the first frame of the last walking-animation. If we don’t do this the player may end up waiting with his leg held mid-air; which looks odd!
Here are the code changes:
function WaitState:Update(dt)
-- If we're in the wait state for a few frames, reset the frame to
-- the starting frame.
if self.mFrameCount ~= -1 then
self.mFrameCount = self.mFrameCount + dt
if self.mFrameCount >= self.mFrameResetSpeed then
self.mFrameCount = -1
local faceFrame = 1
if self.mCharacter.mFacing == "right" then
faceFrame = self.mCharacter.mAnims.right[1]
elseif self.mCharacter.mFacing == "left" then
faceFrame = self.mCharacter.mAnims.left[1]
elseif self.mCharacter.mFacing == "up" then
faceFrame = self.mCharacter.mAnims.up[1]
elseif self.mCharacter.mFacing == "down" then
faceFrame = self.mCharacter.mAnims.down[1]
end
self.mEntity:SetFrame(faceFrame)
end
end
-- code omitted
I still wait 0.05 seconds before changing the frame. If it’s done instantly it can occasionally look a little “snappy”.
Feel free to play about with this; with such a short walk cycle it might be best to continue playing the animation and only stop when it next changes to the first frame. (or pass through the remaining frame time from the movement state and wait until that’s finished).
Here’s the change:
After playing around with this - I prefer it! In the future, I’ll add this to my polish article series.
Waking the Player
The game begins with the player asleep in bed. We know the player is sleeping because an animated ZZZ effect plays above his head. Then a guard breaks through the door and shouts “Got You!” but the ZZZ effect continues, making it appear as if the player is a very deep sleeper.
Ideally the player should stay in bed but stop snoring when woken. The ZZZ effect is added when the player enters a SleepState
and removed when they exit the state.
We could add a new state InBedButNotAsleepState
but that seems a little overkill! Instead let’s add code to the cutscene to turn off the snoring effect.
Here’s the SleepState:Exit
function.
function SleepState:Exit()
self.mEntity:RemoveChild("snore")
end
We’re going to break this out into a separate function that we’ll call from the cutscene.
function SleepState:StopSnore()
self.mEntity:RemoveChild("snore")
end
function SleepState:Exit()
self:StopSnore()
end
It’s fine to call RemoveChild
multiple times, if the child isn’t there it doesn’t cause an error.
Now we need to call this function when the guard comes through the door. Here’s the part of the intro cutscene where the guard enters.
SOP.RunAction("AddNPC",
{"player_house", { def = "guard", id = "guard1", x = 30, y = 28}},
{GetMapRef}),
SOP.Wait(1),
SOP.Play("door_break"),
SOP.NoBlock(SOP.FadeOutScreen()),
SOP.MoveNPC("guard1", "player_house",
{
"up", "up",
"left", "left", "left",
}),
We want to insert a storyboard operation that calls StopSnore
on the player’s controller. To do this an operation that’s not introduced until later in the book: Function
. Function
let’s you call any random function during a cutscene. Here’s the definition.
-- In Storyboard.lua
function SOP.Function(func)
return function(storyboard)
func()
return EmptyEvent
end
end
Then we can use this to turn off the snoring.
SOP.RunAction("AddNPC",
{"player_house", { def = "guard", id = "guard1", x = 30, y = 28}},
{GetMapRef}),
SOP.Wait(1),
SOP.Play("door_break"),
SOP.Function(function()
-- This is super brute-force
local storyboard = gStack.mStates[#gStack.mStates]
local map = GetMapRef(storyboard, "player_house")
local sleeper = map.mNPCbyId["sleeper"]
local sleepState = sleeper.mController.mCurrent
sleepState:StopSnore()
end),
SOP.NoBlock(SOP.FadeOutScreen()),
SOP.MoveNPC("guard1", "player_house",
{
"up", "up",
"left", "left", "left",
}),
Now ideally, there’d be a lot more error checking or standard functions to navigate the game state but instead I just brute force my way to the SleepState
.
Once I get the SleepState
it’s as simple as calling StopSnore
and then the player stops snoring as soon as the guard breaks in.
A Note About Code
Note how easy it was to make these small game changes. There’s a reason for that!
The architecture for the game is solid. Partly because it uses State Machines and partly because it’s built using experience won from working with many mature codebases.
Closing
I hope this has given you an insight into the games in the book and how to modify them to make them your own!
Enjoyed this article? Share or link to it, to support the site and help fellow developers.