Roguelike, step 7: scenes and sounds

Previous tutorial – Download files – Next tutorial

Give me victory or give me death – Behind the scenes – A dimly heard din of in the dungeon – Let those who have ears, cheer  

 Give me victory or give me death

I tend to overthink game features.  When thinking about damage in the game, for instance, I wanted to include resistences and vulnerabilities to various types of damage before I had even coded a simple HP system.  Not only that, I also considered ongoing damage, environmental damage and area effects.  In other words, just to get the simple basicAttack() function up and running, I really had to tone it down, keep it simple stupid (K.I.S.S.) and just get a nice clean function implemented.

Thinking about a Victory scene and a Death scene then, has me thinking about animations and sounds and high score tables and all the other different scenes I would like in the game.  An options scene?  For sure.  A loading scene?  Maybe.  A start scene?  Definitely.  How about a character creation scene?  This is a RPG after all, so why not?  Usually at this point, I would tone it down, K.I.S.S., and see if I can implement one of these scenes and then go from there.  But I don’t have to do that because a class called SceneManager already exists.

I discovered SceneManager.lua while reading Gideros Mobile Game Development by Arturs Sosins.  Mr. Sosins is one of the maintainers of Gideros Mobile and SceneManager is just one of many tools available for developers.  Of course, we’re already familiar Mr. Sosins’ work because he wrote the Button class we’ve been using since Step 2.  What SceneManager allows is plug and play scene programming.

In main.lua, if we change

local game = Game.new()
stage:addChild(game)

to

sceneManager = SceneManager.new({
  ["play"] = Game, 
  ["death"] = Death, 
  ["victory"] = Victory, 
})
stage:addChild(sceneManager)
sceneManager:changeScene("play", 1, SceneManager.crossfade)

we will get our game up and running as before.

Let’s go over what sceneManager is doing.  First, when we create an instance of SceneManager, we pass the class a table of names for classes we’re using for each scene.  Second, when we want to change to a scene, we call it with sceneManager:changeScene().  Besides the name of the class, this function expects as arguments a duration value in seconds and a transition option.  The latter is picked from one of these:

--Move functions
 SceneManager.moveFromLeft 
 SceneManager.moveFromRight
 SceneManager.moveFromBottom
 SceneManager.moveFromTop
 SceneManager.moveFromLeftWithFade
 SceneManager.moveFromRightWithFade
 SceneManager.moveFromBottomWithFade
 SceneManager.moveFromTopWithFade
--Overlay functions
 SceneManager.overFromLeft
 SceneManager.overFromRight
 SceneManager.overFromBottom
 SceneManager.overFromTop
 SceneManager.overFromLeftWithFade
 SceneManager.overFromRightWithFade
 SceneManager.overFromBottomWithFade
 SceneManager.overFromTopWithFade
--Fade & flip functions
 SceneManager.fade
 SceneManager.crossFade
 SceneManager.flip
 SceneManager.flipWithFade
 SceneManager.flipWithShade

With all new things, it’s best to pick your options and define them in constants.lua.  That way, they’ll be easy to change latter on.

TRANSITION_TIME @ 1
TRANSITION @ \SceneManager.crossfade\

But before, you hit play, you also have to download SceneManager.lua into your classes folder.  We should also make a scenes folder in our project where we’ll move GameLogic.lua and create Victory.lua and Death.lua.  To call the Victory scene, check in Game:turnOver() if all the monsters are dead.  To call the Death scene, check if the hero is dead.

if #self.monsters.list == 0 then
   sceneManager:changeScene("victory", TRANSITION_TIME, TRANSITION) 
end 
if self.hero.hp < 1 then 
   sceneManager:changeScene("death", TRANSITION_TIME, TRANSITION) 
end

What can we do in these new scenes?  In the “play” scene, we did our whole game.  So we can be equally elaborate or we can keep it simple.  Here, of course, we’ll K.I.S.S..

Victory is a class that gets words on the screen and creates two events.  One is a ENTER_FRAME event that brightens the words from alpha = 0 to alpha = 1.  The other is a MOUSE_UP event that waits for a mouse click or touch.

Victory = Core.class(Sprite)

function Victory:init()
  --first, start out with a light screen
  application:setBackgroundColor(COLOR_WHITE)
  --then add "You did it"
  self.words = TextField.new(FONT_XL, "You did it")
  self.words:setTextColor(COLOR_DKBLUE) 
  self.words:setPosition(APP_WIDTH/2 - self.words:getWidth()/2, APP_HEIGHT/2 - 200)
  self:addChild(self.words)
  --a is the setAlpha value for the words on this screen
  self.a = 0 
  --time is a timer to display text after consecutive touches 
  --self.time = 0
  self:addEventListener(Event.ENTER_FRAME, self.onEnterFrame, self)
  self:addEventListener(Event.MOUSE_UP, self.onMouseUp, self)
end

function Victory:onEnterFrame(event)
  self.a = self.words:getAlpha()
  if self.a < 1 then
    self.words:setAlpha(self.a + 0.01)
  else
    self.a = 0
  end
end

function Victory:onMouseUp(event)
  self.time = self.time + 1
  --if they touch the screen, make sure the previous words are full Alpha
  self.words:setAlpha(1)
  if self.time == 1 then
    --after the first touch, ask to try again
    self.words = TextField.new(FONT_LARGE, "touch to try again") 
    self.words:setTextColor(COLOR_BLACK) 
    self.words:setPosition(APP_WIDTH/2 - self.words:getWidth()/2, APP_HEIGHT/2 + 400)
    self.words:setAlpha(self.a)
    self:addChild(self.words)
  else 
    --after the final touch, exit the scene
    application:setBackgroundColor(COLOR_BLACK)
    sceneManager:changeScene("play", TRANSITION_TIME, TRANSITION)
  end
end

Victory:onEnterFrame() brightens the words on the screen.  The idea is to cycle through text after each touch.  So it brightens then sets them to alpha = 0 when they’re fully visible so the next set of words brightens on the screen.

Victory:onMouseUp() allows us to pause on this screen, waiting for a touch.  It displays a series of words until the last set is shown.  Then it restarts the game with a call to sceneManager:changeScene().

I use the same logic in the Death class as well.

death

Behind the scenes

Once you’ve written one scene, you can create a whole bunch.  A Start scene seems entirely appropriate for this game and every game.  Maybe a Start scene that goes to a MainMenu scene that gives you the option to Play or create a new PC?  The latter choice would then call a Character scene.  Maybe a add a button to the main screen that will take them to a Options scene?

In our game, I created a Start scene and a  Character scene file that I put in the scenes folder.  Start calls the Character scene after the player touches the screen.

function Start:onMouseUp()
  self.words = TextField.new(FONT_MEDIUM, "Good luck") 
  self.words:setTextColor(COLOR_RED)
  self.words:setPosition(APP_WIDTH/2 - self.words:getWidth()/2, APP_HEIGHT/2 + 400)
  self.words:setAlpha(1)
  self:addChild(self.words)
  sceneManager:changeScene("char", TRANSITION_TIME, TRANSITION)
end

One thing you’re going to want to do, when you start thinking about jumping around different scenes is storing data that can be shared across scenes.  That’s where dataSaver.lua comes in.  DataSaver is another ridiculously easy to use class that allows you to save and retrieve data.  Gideros Mobile has a couple options for saving data.  One of them is saving into a Gideros directory and that’s what dataSaver uses.

Here’s Character.lua.

Character = Core.class(Sprite)

function Character:init()
  -- create 
  local hero = Player.new() 
  -- and save 
  dataSaver.save("|D|hero", hero)
  -- then play
  Timer.delayedCall(2000, function() 
    sceneManager:changeScene("play", TRANSITION_TIME, TRANSITION) end)
 
end

Instead of creating the hero in Game:init(), we can now create it in a separate scene and save the variable information with a dataSaver.save() call.  When Game:init() is called, we can retrieve the hero variable with another dataSaver call.

function Game:init()
  self.hero = dataSaver.load("|D|hero")
  self.monsters = Monsters.new()
  self.world = WorldMap.new(self.hero, self.monsters)

Of course, every new scene that we add is kept track of with the table call we make in SceneManager.new() in main.lua. 

sceneManager = SceneManager.new({
  ["start"] = Start,     --first thing the player sees
  ["char"] = Character,  --the character creation scene
  ["play"] = Game,       --the main game scene
  ["death"] = Death,     --when the player dies
  ["victory"] = Victory, --when the player wins
})

I put five scenes in our game.  Feel free to add or subtract for your particular game.

A dimly heard din in the dungeon 

Sound is important in a game.  Back at Step 5, we talked about how color can really impact your game.  This is just as true for sound, but because human beings are such a visual species, sound is frequently neglected by game designers and their games lack feedback and immersion because of it.  How do we make sound happen in a game?  If you look up sound in your help menu/API documention, you see the following example under the entry for sound.

local sound = Sound.new("music.mp3")
local channel = sound:play()

So Sound is a class and one of its methods is play.  Pretty simple stuff.  It’s a little more complicated in that all the SoundChannel methods are automatically inherited when you create a Sound object.  But all that really means is we can easily control the volume and other things you would expect to be able to do with a sound:

sound:setVolume(0.5)
sound:stop()

So it’s easy to play sounds, but only if we have them.  Where do we find actual sounds?  The internet is a wonderful thing.  Are you looking for footsteps?  Battle sounds?  A constant drip of water?  We may not always find exactly what we want, but something close is out there.  Once we have a .mp3 or .wav file to work with, we can create a sounds directory and copy it into our program.  If we find it’s not quite what we want, check out the free sound editors out there such as Audacity.

With some sounds in mind, let’s create our own Sounds class in a Sounds.lua file.  All we need to do in Sounds:init() is create a self.sounds table with our sound files in a sounds/ directory.

Sounds = Core.class()

function Sounds:init()
  self.sounds = {hero-steps = "sounds/footsteps.wav"}
end

To play, we would call it like the example above

self.sounds = Sounds.new()
local sound = Sounds.new(self.sounds["hero_steps"))
local channel = sound:play()

Actually, let’s build on that a little bit.  Our goal is to create a big table of sounds that we can play at a particular volume.  The book Gideros Mobile Game Development creates a Sounds class that goes one further and adds the ability to play 1 of 3 sounds or 1 of 5 sounds when a particular sound is called for.  That way, our player isn’t annoyed that their hero’s footsteps are exactly the same every single time.

Sounds = Core.class(EventDispatcher)

function Sounds:init(scene)
  self.sounds = {}
  if scene == "game" then
    self:add("melee-hit", "sounds/melee_hit.wav")
    self:add("melee-hit", "sounds/melee_hit2.wav")
    self:add("melee-miss", "sounds/melee_miss.mp3")
    self:add("hero-steps", "sounds/footsteps-light.wav")
    self:add("hero-steps", "sounds/footsteps-03.wav")
    self:add("monster-steps", "sounds/footsteps-02.wav")
    self:add("monster-steps", "sounds/footsteps-one.wav")
  end
end

function Sounds:add(name, sound)
  --ties a name to a sound or a table of sounds.
  if self.sounds[name] == nil then
    self.sounds[name] = {}
  end
  self.sounds[name][#self.sounds[name]+1] = Sound.new(sound)
end

function Sounds:play(name, volume)
  --plays a named sound at a particular volume
  if self.sounds[name] then
    --play one of however many sounds there are for that named sound
    sound = self.sounds[name][math.random(1, #self.sounds[name])]:play()
    if volume then
      sound:setVolume(volume)
    end
  end
end

Sounds:add() creates an entry in the self.sounds table for every sound we want to make.  For particularly repetitive actions we can add a couple sounds using the same name.  When it comes time to play them, Sounds:play() will randomly select which one to play.

With the creation of our Sounds class, we now have a new gaming variable we can add to Game:init() in our Gamelogic scene

 --the major gaming variables
 self.hero = dataSaver.load("|D|hero", hero)
 self.monsters = Monsters.new()
 self.world = WorldMap.new(self.hero, self.monsters) 
 self.msg = Messages.new()
 self.sounds = Sounds.new("game")

Which allows us to integrate sound directly into our game.  For instance, in Game:checkMove(), when a valid move is about to be made:

 --everything else is a valid move
 self.sounds:play("hero-steps")
 self.world:moveHero(self.hero, dx, dy)
 self:turnOver()

Similarly, in Game:MonsterAI(), when a monster moves:

if monster.seesHero then
  --move towards the hero
  self.sounds:play("monster-steps")
  dx, dy = self.world:whichWay(monster, self.hero.x, self.hero.y)

As long as the sound is imported in Sounds:init(), it’s trivial to add sound wherever you want your game to make a sound.

Let those who have ears, cheer 

For simple sounds, we use Sounds:play() to make sound happen.  Music is slightly different.  Actually, Gideros Mobile doesn’t care if we call our sound ‘sound’ or call our sound ‘music’, but for both aesthetic and practical purposes, we care.  Let me show you what I mean.

When we play a sound in Sounds:play, we just play it.  Because it’s so short and simple, we don’t need to manipulate the sound except for adjusting the volume before we play.  For music though, we might want to take advantage of the other SoundChannel methods that sounds have.  Find a cool song or extended sound effect you want to use as your title music and add it to the Start scene.

function Sounds:init(scene)
  self.sounds = {}
  if scene == "game" then
    self:add("hero-steps", "sounds/footsteps-light.wav")
    self:add("hero-steps", "sounds/footsteps-03.wav")
  elseif scene == "title" then
    self:add("music-title", "sounds/music-title.mp3")
  end
end

If you add this to Start.lua

function Start:init()
  --first, start out with a dark screen
  application:setBackgroundColor(COLOR_BLACK)
  --play the music of the scene
  local sounds = Sounds.new("title")
  self.music = sounds:play("music-title")

You will now music play when you start your game.  Becauses playing the sound, we also assign the sound object to self.music which means we need Sounds:play() to return the sound object when we play.  Add the following to Sounds:play()

 if string.sub(name, 1, 5) == "music" then 
   return sound
 end

Why do we return the sound when we play it?  Because that allows us to refer to the sound later on, if we want to stop it.

function Start:onMouseUp()
  --this will be called when they're ready to play
  self.music:stop()
  self.words = TextField.new(FONT_MEDIUM, "Good luck") 
  self.words:setTextColor(COLOR_RED)
  self.words:setPosition(APP_WIDTH/2 - self.words:getWidth()/2, APP_HEIGHT/2 + 400)
  self.words:setAlpha(1)
  self:addChild(self.words)
  sceneManager:changeScene("char", TRANSITION_TIME, TRANSITION)
end

For purposes of our game, we’ll define sounds as music and return them when we play them so we can manipulate them while they’re playing.  Here we stopped the start scene music when the scene was done.  We could have used any of the SoundChannel methods such as self.music:setPaused(true) as well.

Summary

Remember when I promised a Roguelike written in Lua in 8 steps?  Well, it actually took us 7 steps!  At this point we have a complete game.  There are lights, sounds and action all happining on our stage.  Whether the hero wins or loses, if all the little bugs have been tracked down, our program should keep rolling along.

At this point we can just keep adding more features.  One that comes to mind for me is ranged combat.  Whether it’s a fireball launched across the room or a series of arrows fired from a bow, shooting monsters from a distance is a powerful attack most people expect their hero to do.  Along with ranged combat, we’ll add an animation of our arrow flying across the screen.

Next up:  An arrow in the dark