Changing the dungeon game.

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:

  1. Stop the player direction from being reset when the movement key is released.
  2. 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.

Starting the Dungeon JRRG game.

Waking up.

Entering the dungeon.

Resetting the Facing

Check out the current behavior:

The current facing 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:

  1. Reset the facing behavior but after a short pause
  2. 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 movement state machine.

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:

The current facing behavior.

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:

The current facing behavior.

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!