Skip to content

Arx 10 More Gamestates: Gamepaused and Gamefinished

noooway edited this page Jan 25, 2017 · 2 revisions

Apart from the "menu" and the "game" gamestates it is also common to have "gamepaused" and "gamefinished" states. The first one is activated, when, say, an "Esc" key is pressed, and the second one displays a congratulations screen at the end of the game. This chapter shows how to implement them. Besides, I also want introduce certain modifications to the already defined "game" and "menu" states.

The "gamefinished" state is mostly similar to the current "menu" except for some changes in the love.graphics.print text:

.....

local gamefinished = gamefinished or {}

.....

state_name = "gamefinished"

function gamefinished:enter()
end

function gamefinished:update( dt )
end

function gamefinished:draw()
   love.graphics.print(
      "Congratulations, you have finished the game.\n" ..
      "Press Enter to restart or Esc to quit",              --(*1)
      10, 10)
end

.....

(*1): I'll comment on how to implement reaction on "Esc" or "Enter" keys later.

Transition to the "gamefinished" stated happens from the "game" state, when there are no more levels. In previous parts, love.event.quit() was called in such cases. Now we need to change it to the transition to "gamefinished":

function game:enter()
   .....
   if level_counter > #level_sequence.sequence then
      Gamestate.switch( gamefinished )
   end
   .....
end


function switch_to_next_level()
   .....
   if level_counter > #level_sequence.sequence then
      Gamestate.switch( gamefinished )
   else
   .....
end

For the transition to the "gamepaused" state, we need to define keyreleased callback in the "game" state. After resuming from the pause, we want to restore the state of the game objects -- bricks in the bricks_container, the position and the speed of the ball, and the platform. If we simply use Gamestate.switch( gamepaused ) call, the game objects will be deleted on leaving the "game" state and then reconstructed on entering, effectively restarting the level. The solution is to pass all the objects, which we want to save, to the "gamepaused" state, and then pass them back to "game" state. The Gamestate.switch is defined in such a way, that it accepts an arbitrary number of arguments. We can pack all the necessary objects into a table, and pass it as an argument.

function game:keyreleased( key, code )      --(*1)
   if key == 'escape' then                  --(*2)
          Gamestate.switch( gamepaused,     --(*3) 
                        { level_sequence = level_sequence,  --(*4)
                          level_counter = level_counter,
                          level = level,
                          collider = collider,
                          ball = ball,
                          platform = platform,
                          walls_container = walls_container,
                          bricks_container = bricks_container } )
   end
end

(*1): In the "game" gamestate, we define keyreleased callback.
(*2): When "Esc" is pressed( released, actually ), we pause the game.
(*3): The first argument to the Gamestate.switch is the gamepaused state.
(*4): The second argument is a table packed with gameobjects, which state we don't want to loose on transition to another gamestate.

In the pause regime, the game objects are in the frozen state, that is, they are not updated. However, it is possible to display them in the gamepaused:draw callback. Apart from that, the gamepaused module is similar to the menu.

.....

function gamepaused:enter( previous_state, ... )  
   game_objects = ...                            --(*1)
end

function gamepaused:update( dt ) --(*2)
end

function gamepaused:draw()  
   for _,obj in pairs( game_objects ) do
      if type(obj) == "table" and obj.draw then
         obj:draw()  --(*3)
      end
   end
   self:cast_shadow() --(*4) 
   love.graphics.print(
      "Game is paused. Press Enter to continue or Esc to quit",
      10, 10)
end

function gamepaused:keyreleased( key, code )
   if key == 'return' then      
      Gamestate.switch( game, game_objects ) --(*5)
   elseif  key == 'escape' then
      love.event.quit()
   end    
end

function gamepaused:leave()
   game_objects = nil
end

function gamepaused:cast_shadow()  --(*4)
   local r, g, b, a = love.graphics.getColor( )
   love.graphics.setColor( 10, 10, 10, 100 )
   love.graphics.rectangle("fill",
                           0,
                           0,
                           love.window.getWidth(),
                           love.window.getHeight() )
   love.graphics.setColor( r, g, b, a )
end


return gamepaused

(*1) The received game objects are stored in the game_objects table. In Lua, ... is called a vararg expression. In the argument list of a function, it indicates, that the function accepts arbitrary number of arguments. In the body of the function, ... acts like a multivalued function, returning all the collected arguments.
(*2) We do not need to update those objects.
(*3) However, we need to draw them. We draw each object of a table type that has a draw method.
(*4) There is also some darkening effect.
(*5) Finally, we need to return the received objects back to the 'game' state.

On leaving the "gamepaused" state and entering the "game" state, it is necessary to restore the game objects. This requires changes in the game:enter function:

function game:enter( previous_state, ... )
   args = ...

   level_sequence = args.level_sequence or require "levels/sequence"
   level_counter = args.level_counter or 1
   if level_counter > #level_sequence.sequence then
      Gamestate.switch( gamefinished )
   end
   level_filename = "levels/" .. level_sequence.sequence[ level_counter ]
   level = args.level or require( level_filename )
   collider = args.collider or HC.new()
   ball = args.ball or Ball:new( { collider = collider } )
   platform = args.platform or Platform:new( { collider = collider } )
   bricks_container = args.bricks_container or
      BricksContainer:new( { level = level,
                             collider = collider } )
   walls_container = args.walls_container or
      WallsContainer:new( { collider = collider } )
end

Gamestates are convenient for managing level switches. In the switch_to_next_level function we need to insert the Gamestate.switch( game, ..... ) call with the appropriate arguments.

function switch_to_next_level()
   if bricks_container.no_more_bricks then
      level_counter = level_counter + 1
      ball:destroy()
      platform:destroy()
      bricks_container:destroy()
      walls_container:destroy()
      if level_counter > #level_sequence.sequence then
         Gamestate.switch( gamefinished )
      else
         Gamestate.switch( game, { level_sequence = level_sequence,
                                   level_counter = level_counter } )
      end
   end
end

To start the game from the first level, it is necessary to update the Gamestate.switch( game ) in the "menu" and "gamefinished", manually setting level_counter argument to 1.

function menu:keyreleased( key, code )
   if key == 'return' then      
      Gamestate.switch( game, { level_counter = 1 } )
   elseif  key == 'escape' then
      love.event.quit()
   end    
end

function gamefinished:keyreleased( key, code )
   if key == 'return' then      
      Gamestate.switch( game, { level_counter = 1 } )
   elseif  key == 'escape' then
      love.event.quit()
   end    
end

Finally, we need to require the new gamestates in the main.lua. A non-obvious thing is that we have to declare and preferably load tables for all gamestates in advance, since they can circularly depend on one another.

local Gamestate = require "gamestate"
menu = {}
game = {}
gamepaused = {}
gamefinished = {}
local menu = require "menu"
local game = require "game"
local gamepaused = require "gamepaused"
local gamefinished = require "gamefinished"

function love.load()	
   Gamestate.registerEvents()
   Gamestate.switch( menu )
end

.....

This is the reason for local gamepaused = gamepaused or require "gamepaused" line in the game.lua. We either use preloaded 'gamepaused' module or if it has not been loaded for some reason we load it manually.

local Gamestate = require "gamestate"
local gamepaused = gamepaused or require "gamepaused"
local gamefinished = gamefinished or require "gamefinished"
local Platform = require "Platform"
.....

    Home
    Acknowledgements
    Todo

Chapter 1: Prototype

  1. The Ball, The Brick, The Platform
  2. Game Objects as Lua Tables
  3. Bricks and Walls
  4. Detecting Collisions
  5. Resolving Collisions
  6. Levels

    Appendix A: Storing Levels as Strings
    Appendix B: Optimized Collision Detection (draft)

Chapter 2: General Code Structure

  1. Splitting Code into Several Files
  2. Loading Levels from Files
  3. Straightforward Gamestates
  4. Advanced Gamestates
  5. Basic Tiles
  6. Different Brick Types
  7. Basic Sound
  8. Game Over

    Appendix C: Stricter Modules (draft)
    Appendix D-1: Intro to Classes (draft)
    Appendix D-2: Chapter 2 Using Classes.

Chapter 3 (deprecated): Details

  1. Improved Ball Rebounds
  2. Ball Launch From Platform (Two Objects Moving Together)
  3. Mouse Controls
  4. Spawning Bonuses
  5. Bonus Effects
  6. Glue Bonus
  7. Add New Ball Bonus
  8. Life and Next Level Bonuses
  9. Random Bonuses
  10. Menu Buttons
  11. Wall Tiles
  12. Side Panel
  13. Score
  14. Fonts
  15. More Sounds
  16. Final Screen
  17. Packaging

    Appendix D: GUI Layouts
    Appendix E: Love-release and Love.js

Beyond Programming:

  1. Game Design
  2. Minimal Marketing (draft)
  3. Finding a Team (draft)

Archive

Clone this wiki locally