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.

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 ||= args.gtk.read_file(HIGH_SCORE_FILE).to_i

  args.state.timer -= 1

  if !args.state.saved_high_score && args.state.score > args.state.high_score
    args.gtk.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_enum: 10,
  }
  labels << {
    x: 40,
    y: args.grid.h - 90,
    text: "Score: #{args.state.score}",
    size_enum: 4,
  }

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

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

  if args.state.timer < -30 && fire_input?(args)
    $gtk.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_enum: 4,
  }
  labels << {
    x: args.grid.w - 40,
    y: args.grid.h - 40,
    text: "Time Left: #{(args.state.timer / FPS).round}",
    size_enum: 2,
    alignment_enum: 2,
  }
  args.outputs.labels << labels
end

def tick args
  if args.state.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_enum: 6,
  }
  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_enum: 2,
  }
  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 args.state.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.