Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Scenes

When making a game, there are all sorts of scenes that exist, from the main menu to the actual gameplay to the pause menu. Often these scenes have different interactions than the main game. Instead of moving your character around the world you instead move a cursor up and down menu options. In this chapter, we’ll refactor our main gameplay and game over state to be a bit easier to work with and then introduce a title scene that’s used to start the game.

The concept of a “scene” isn’t specific to DragonRuby Game Toolkit or any given game engine. It’s just a generic concept that we’re introducing to make our game code easier to work with. Much like a movie scene, it’s clear when one ends and when one begins.

When we introduced the game over functionality, we introduced a scene separate from gameplay. Kinda neat! But it wasn’t the time to think about them in terms of scenes and reckon with how we add more.

Before we get into scenes, though, there’s one structural change we should apply.

Wrapping It in module Main

You may have noticed when you opened the default main.rb back in Chapter 1 that everything was wrapped in module Main ... end. We dropped that wrapper in our earlier chapters to keep the code small while we were learning. Now that we have a real game, let’s bring our code in line with what DragonRuby’s docs and sample apps use.

The change is mechanical: indent everything in mygame/app/main.rb two spaces, then wrap it in a module Main block. The trailing DR.reset stays at the top level (we want it to run when the file is loaded, not when tick is called):

module Main
  FPS = 60

  def spawn_target(args)
    # ...
  end

  # ...all the other methods, indented...

  def tick args
    # ...
  end
end

DR.reset

That’s it. module Main is a Ruby module, a namespace that groups related code together. DragonRuby looks for a module called Main and calls its tick method 60 times a second, just like before. Wrapping our methods inside it keeps them from spilling into Ruby’s global scope, which is a healthier habit as your projects grow.

From this point on, the code samples in the rest of the book are shown with the module Main wrapper applied. The chapter 11 and 12 sample files (code/chapter_11/01_refactor, code/chapter_11/02_title, code/chapter_12/01_release) all use this form.

Refactor

So we’ve got two scenes right now: gameplay (where we shoot at targets) and game over (where we display the score and allow the player to restart). Let’s refactor the code to put our scenes in different methods and allow the game to switch between them given certain conditions being met.

Containing our scenes in methods will make it much easier to change one scene without impacting the others. It sets clear boundaries and will make our code easier to maintain.

We’ve already got #game_over_tick, so we can make use of that to contain the game over behavior.

Let’s introduce a #gameplay_tick method that will contain the logic for our gameplay. And then we’ll use args.state.scene to keep track of the current scene. Then if we change that value, it’ll change what scene our game uses.

Not much of the code changes, but we do shuffle things around a bit. None of the methods above #game_over_tick change, so they’re excluded:

  HIGH_SCORE_FILE = "high-score.txt"
  def game_over_tick(args)
    args.state.high_score ||= DR.read_file(HIGH_SCORE_FILE).to_i

    args.state.timer -= 1

    if !args.state.saved_high_score && args.state.score > args.state.high_score
      DR.write_file(HIGH_SCORE_FILE, args.state.score.to_s)
      args.state.saved_high_score = true
    end

    labels = []
    labels << {
      x: 40,
      y: args.grid.h - 40,
      text: "Game Over!",
      size_px: 42,
    }
    labels << {
      x: 40,
      y: args.grid.h - 90,
      text: "Score: #{args.state.score}",
      size_px: 30,
    }

    if args.state.score > args.state.high_score
      labels << {
        x: 260,
        y: args.grid.h - 90,
        text: "New high-score!",
        size_px: 28,
      }
    else
      labels << {
        x: 260,
        y: args.grid.h - 90,
        text: "Score to beat: #{args.state.high_score}",
        size_px: 28,
      }
    end

    labels << {
      x: 40,
      y: args.grid.h - 132,
      text: "Fire to restart",
      size_px: 26,
    }
    args.outputs.labels << labels

    if args.state.timer < -30 && fire_input?(args)
      DR.reset
    end
  end

  def gameplay_tick(args)
    args.outputs.solids << {
      x: 0,
      y: 0,
      w: args.grid.w,
      h: args.grid.h,
      r: 92,
      g: 120,
      b: 230,
    }

    args.state.player ||= {
      x: 120,
      y: 280,
      w: 100,
      h: 80,
      speed: 12,
    }

    player_sprite_index = 0.frame_index(count: 6, hold_for: 8, repeat: true)
    args.state.player.path = "sprites/misc/dragon-#{player_sprite_index}.png"

    args.state.fireballs ||= []
    args.state.targets ||= [
      spawn_target(args), spawn_target(args), spawn_target(args)
    ]
    args.state.score ||= 0
    args.state.timer ||= 30 * FPS

    args.state.timer -= 1

    if args.state.timer == 0
      args.audio[:music].paused = true
      args.outputs.sounds << "sounds/game-over.wav"
      args.state.scene = "game_over"
      return
    end

    handle_player_movement(args)

    if fire_input?(args)
      args.outputs.sounds << "sounds/fireball.wav"
      args.state.fireballs << {
        x: args.state.player.x + args.state.player.w - 12,
        y: args.state.player.y + 10,
        w: 32,
        h: 32,
        path: 'sprites/fireball.png',
      }
    end

    args.state.fireballs.each do |fireball|
      fireball.x += args.state.player.speed + 2

      if fireball.x > args.grid.w
        fireball.dead = true
        next
      end

      args.state.targets.each do |target|
        if args.geometry.intersect_rect?(target, fireball)
          args.outputs.sounds << "sounds/target.wav"
          target.dead = true
          fireball.dead = true
          args.state.score += 1
          args.state.targets << spawn_target(args)
        end
      end
    end

    args.state.targets.reject! { |t| t.dead }
    args.state.fireballs.reject! { |f| f.dead }

    args.outputs.sprites << [args.state.player, args.state.fireballs, args.state.targets]

    labels = []
    labels << {
      x: 40,
      y: args.grid.h - 40,
      text: "Score: #{args.state.score}",
      size_px: 30,
    }
    labels << {
      x: args.grid.w - 40,
      y: args.grid.h - 40,
      text: "Time Left: #{(args.state.timer / FPS).round}",
      size_px: 26,
      anchor_x: 1,
    }
    args.outputs.labels << labels
  end

  def tick args
    if Kernel.tick_count == 1
      args.audio[:music] = { input: "sounds/flight.ogg", looping: true }
    end

    args.state.scene ||= "gameplay"

    send("#{args.state.scene}_tick", args)
  end

#game_over_tick is the same except for the addition of:

args.state.timer -= 1

We need to continue to subtract from the timer in each tick since we rely upon it when we accept input to restart the game.

We introduce #gameplay_tick which contains all of our logic for when we’re actually playing the game. We set the background to the blue solid rectangle and initialize our player and animate the sprite. That’s all the same.

But when the timer is 0, after we pause the music and play the game over sound, we set args.state.scene to "game_over" and return early. This effectively changes the scene.

We continue to handle input and display the gameplay sprites and labels in #gameplay_tick.

Then, finally, #tick has been drastically simplified. It no longer needs to be responsible for so much. It can instead just handle three things:

  1. Starting the music for the game
  2. Lazily initializing the scene to start with (in our case, "gameplay")
  3. Calling the proper scene tick method and passing in args

The third item there is the new part of this chapter. #send is a method in Ruby that allows a method to be called by passing in the name of the method as the first parameter. This is really powerful! We use string interpolation to take the value set in args.state.scene and append it with _tick. Our game then calls that method and passes in args as the first parameter to the called method. So if args.state.scene is set to "gameplay", the #gameplay_tick method gets called. If it’s set to "game_over", then #game_over_tick gets called. If it’s set to "credits", then #credits_tick would get called.

The various scene tick methods need to exist in order for changing args.state.scene to work. But that’s quite simple to do, we just introduce a new method and set args.state.scene to change between scenes.

Title Scene

When players launch our game, they get dropped right into the gameplay. This can be a bit jarring, so let’s introduce a new scene that displays the title of the game, controls, and lets them press a button to start.

In our game code, let’s introduce #title_tick that takes args as its only parameter, just like our other *_tick methods for our scenes. In #title_tick, we’ll render some labels and look for input to start our game. If the fireball input is pressed, we’ll play a sound effect, change the scene, and return early so we can move on to the next scene.

  def title_tick args
    if fire_input?(args)
      args.outputs.sounds << "sounds/game-over.wav"
      args.state.scene = "gameplay"
      return
    end

    labels = []
    labels << {
      x: 40,
      y: args.grid.h - 40,
      text: "Target Practice",
      size_px: 34,
    }
    labels << {
      x: 40,
      y: args.grid.h - 88,
      text: "Hit the targets!",
    }
    labels << {
      x: 40,
      y: args.grid.h - 120,
      text: "by YOU",
    }
    labels << {
      x: 40,
      y: 120,
      text: "Arrows or WASD to move | Z or J to fire | gamepad works too",
    }
    labels << {
      x: 40,
      y: 80,
      text: "Fire to start",
      size_px: 26,
    }
    args.outputs.labels << labels
  end

Replace YOU with your name since you made it. It’s important to take credit for your work.

Then in #tick lazily initialize args.state.scene to now be "title":

  def tick args
    if Kernel.tick_count == 1
      args.audio[:music] = { input: "sounds/flight.ogg", looping: true }
    end

    args.state.scene ||= "title"

    send("#{args.state.scene}_tick", args)
  end

Now when you start the game, the title scene will be displayed:

black text on a gray background that reads ‘Target Practice’ with instructions on how to play

When you press the fire button, the game will start. And when you restart after the game is over, you’ll end up back on the title scene.

Extra Credit

  • Display a label with the current high score in the title scene so players know what to aim for.
  • Display the dragon sprite in the title scene to give a player a taste of what they can expect.
  • How could you make it so that the music doesn’t start until gameplay starts? Or play different music during the title scene?
  • Restarting the game back on the title scene may not be ideal. How would you change it so that restarting the game automatically goes to the gameplay scene?
  • Allow players to pause the game while in the middle of the gameplay scene.

Summary

We’ve now got three scenes in our game and can easily switch between them. The code for each scene is contained within its own method, making it easier to change a given scene without accidentally changing the others. Adding another scene to our game, like a pause menu, wouldn’t be very complicated.

What’s Next

For all intents and purposes, our game is done! You can start it, play it, and replay it. All that’s left to do is release it so that the world (or at the very least our friends) can play it.