lunes, 29 de febrero de 2016

Breaking Fast: Menu Navigation

Hi all!

Breaking Fast is moving forward. This last week we worked on the menu navigation part, which includes the menu screens through which players navigate until the race begins. As for the technical side, I want to discuss the two main issues you have to consider when implementing this aspect of a game: how to manage the flow between screens, and how to pass information among screens. Let's get started!

Flow Management Schemes

There are two main "schemes" (I can't think of a better word) during menu navigation. In the first scheme, each screen is actually a different screen. This implies that, upon changing among screens, we swap the contents of one screen with the contents of the other screen. This can be implemented as a function in the screen manager:

 function changeScreen(newScreen)  
  currentScreen.unload()  
  currentScreen = newScreen  
  currentScreen.load()  
 end  

In order to manage transitions among screens (i.e. the implementation of the screen manager), it is useful to think of Finite State Machines. You can watch a series of two videos that I prepared for gaining insight on the matter.



 
In games it is also very common to use another scheme, in which the contents of different screens are all drawn, but only one of them has the focus at any moment. Think for example of a racing game in which you have to choose a scenario and then, the time of the race. You can implement this by using the aforementioned scheme, but it feels kind of overkill to unload one screen and to load another one just for the purpose of selecting the time. It seems more natural that the same screen presents both options, but that the player can only change one of them at any time. Actually, we can stop thinking of different screens in this situation. This "move the focus" approach can be implemented by means of screen phases or states

In particular, in Lua, we can hold a table of states, and we iterate over this table in order to draw and update the screen. We also have a variable that holds the value of the current state (the current focus), and we delegate the interaction of the player (be it with the keyboard or a gamepad) to the actual focused state. Some self-explanatory code is shown next:

 function load()  
  table.insert(states, state1)  
  table.insert(states, state2)  
  currentStateFocus = state1  
 end  
 function draw()  
  for _, v in ipairs(states) do   
   v.draw()  
  end  
 end  
 function update(dt)  
  for _, v in ipairs(states) do   
   v.update(dt)  
  end  
 end  
 function onKeyPressed(key)  
  currentStateFocus.onKeyPressed(key)  
 end  

Communication between Screens and States

Passing information among screens is essential. In particular for Breaking Fast, consider the following flow of actions:
  1. Players select the characters that will participate in the race.
  2. Players select the scenario in which the race will take place.
  3. Players select the duration of the race.

Consider the figure above, in which we mix the two menu navigation schemes explained before. Each transition among screens and states conveys both new information and the information carried by older transitions. In the end, when we prepare the race screen, we need to know all the options that have been chosen by the players through the different screens. Fortunately, implementing this in Lua is very simple thanks to a powerful feature of the language: function overriding of imported modules. Consider the following module, which models the Time Selection State:

 local timeSelectionState = {}  
   
 -- other functions  
   
 function timeSelectionState.toScenarioSelection()  
 end  
   
 function timeSelectionState.toGame(timeChosen)  
 end  
   
 return timeSelectionState  

As you can see, we are defining two empty functions. Any module that imports this module can re-define (i.e. override) these functions. So for example, consider the Scenario Selection Screen module, which contains the Time Selection State:

 local scenarioSelectionScreen = {}  
   
 -- other functions  
   
 function timeSelectionState.toScenarioSelection()  
      currentStateFocus = scenarioSelectionState  
 end  
   
 function timeSelectionState.toGame(timeChosen)  
      for _, v in ipairs(states) do  
           v.unloadResources()  
      end  
      scenarioSelectionScreen.toGame(numberOfPlayers, playersInfo, scenarioChosen, timeChosen)  
 end  
   
 function scenarioSelectionScreen.toGame(numberOfPlayers, playersInfo, scenarioChosen, timeChosen)  
 end  
   
 return scenarioSelectionScreen  

As you can observe, this module re-defines toScenarioSelction() and toGame() functions. In particular, toGame() unloads all the states of this screen, forwards the racing time that has been chosen by the player, and pads it to more information accumulated during previous phases. In turn, scenarioSelectionScreen defines an empty toGame() function that cam be re-defined by the module importing it. This latter module could therefore add some more information and pass it to the Race Screen.

And this is it! As always, I hope you enjoyed this tutorial, and if you have any question, don't hesitate to ask! Any feedback is also well appreciated.

See you!
FM

domingo, 21 de febrero de 2016

Breaking Fast: From Ad-Hoc to Scalable Local Multiplayer System

Hi!

This week we have been working on the local multiplayer of Breaking Fast. The first version of Breaking Fast, which was playtested several months ago, had an ad-hoc art design for only two players, and therefore neither the art nor the code were scalable for moving easily from 1 to 4 players. Therefore, the main work carried out during this week has consisted of refactoring the code so that it accepts a unique set of assets, and that it scales this set according to the number of players. Also, different viewports are created according to the number of players, as shown later.

As every programmer that is reading this post can figure, even for a medium-sized codebase the aformentioned task entails lots of work, but going step by step is the only way to go. In the following sections, I briefly explain the most important code changes that were performed:

1) Substituting scalar values by arrays of values: in the first version of the post, we had something like:

 local Camera = require "Camera"  
 local camera1 = Camera()  
 local camera2 = Camera()  
 local Player = require "Player"  
 local player1 = Player.new(...)   
 local player2 = Player.new(...)   

Of course, if we want to achieve scalability, this is not the way to go. Therefore, we changed the aforementioned scalar variables into arrays, as follows:

 local Camera = require "Camera"  
 for i = 1, numPlayers do  
   cameras[i] = Camera()  
 end  
 local Player = require "Player"  
 for i = 1, numPlayers do  
   players[i] = Player.new(...)  
 end  

This applies of course to every element that needs to be replicated according to the number of players.

2) Re-designing assets for one player scenario: in the single player mode, there are no viewports (there are no any other player), and therefore the assets need to be as large as they can be. Under any other circumstances (more than one player), the assets need to be scaled down proportionally according to the resolution and aspect ratio for which the game is being developed, 1920x1080 and 16:9, respectively.

In the first prototype of the game, the assets were designed ad-hoc for two players. For example, the background was designed with a resolution of 1920 x 540, and I would place two different backgrounds in different positions:

 background1 = love.graphics.newImage("background.png")  
 love.graphics.draw(background1, 0, 0)  
 background2 = love.graphics.newImage("background.png")  
 love.graphics.draw(background2, 0, 540)  

(This is an oversimplification for several reasons; first, we actually add the background to a layer of a camera, because we want it to scroll a little bit. Read this series of posts to get more information on parallax scrolling. Also, what we actually draw is not the background itself, but a batch of backgrounds that we paste once after another in order minimize draw calls and provide an illusion of infinite scenario).

After the changes, we have only one background with a resolution of 1920 x 1080*, and therefore we don't need to place background for different players in different positions; the viewports will take care of this according to the number of players.

*(Again, although this would be technically possible, we use a larger resolution to support scrolling in the vertical axis and for fixing some problems with different aspect ratios, as discussed later).

3) Designing viewports: if we want that more than one player can play on the same machine, we need to provide each player with a fragment of the screen. Each fragment is called a viewport. In the first prototype, we didn't need to worry about viewports, because everything was drawn ad-hoc for two players. However, now we have a unique set of assets, and depending on the number of players, these assets must be replicated for the different players and must be scaled down appropriately.

For example, for two players, we want that the first player is allocated the upper half of the screen, whereas the second player should be provided with the lower half. In the case of four players, each player should have a quarter of the screen.

In order to implement viewports in Löve, I saw two alternatives: using scissors or canvases. I didn't get to really grasp how to use scissors for this purpose in my first attempts (see video below), so I switched my attention quickly to the second option, which turned out to work great.

video

Fail with using scissor. The second player viewport was rendered onto the first player one.

Canvases represent an off-screen rendering target. Internally, it creates an OpenGL framebuffer object to which the contents are drawn, instead of drawing the contents to the screen. The process from a high-level perspective is as follows:
  1. Create as many canvases as the number of players (one canvas per camera).
  2. Depending on the number of players, scale the assets that will be drawn to the canvases.
  3. Place each canvas in its correct position according to the number of players.
  4. Draw all the stuff that we used to draw on the screen on the canvases instead, and then, draw the the canvases.
Some simplified code for the three players case is shown next:

 if numPlayers == 3 then   
   cameras[1]:setScale(2, 2)  
   cameras[1]:createCanvas(0.5 * intendedWidth, 0.5 * intentedHeight, 0, 0)  
   cameras[2]:setScale(2, 2)  
   cameras[2]:createCanvas(0.5 * intendedWidth, 0.5 * intentedHeight, 0.5 * intendedWidth, 0)  
   cameras[3]:setScale(2, 2)  
   cameras[3]:createCanvas(0.5 * intendedWidth, 0.5 * intentedHeight, 0.25 * intendedWidth, 0.5 * intentedHeight)  
 ...  

The createCanvas() function, which is added to my camera module, takes the width and height of the canvas, and its top left position. This would yield the following layout for the screen:


As part of this viewport design, we also needed to change the camera code for drawing. However, modifying this code to support drawing on canvases was surprisingly easy.

camera.lua

 function camera:draw()  
  love.graphics.setCanvas(self.canvas)  
  love.graphics.clear()  
  local bx, by = self.x, self.y  
  for _, v in ipairs(self.layers) do  
   self.x = bx * v.scale  
   self.y = by * v.scale  
   self:set()  
   v.draw()  
   self:unset()  
  end  
  self.x, self.y = bx, by  
  love.graphics.setCanvas()  
  love.graphics.draw(self.canvas, self.canvasLeft, self.canvasTop)  
 end  

The lines in bold text are the only additions we needed to add in order to make the function work with canvases.

You can watch the result of our work in the following video. Challenge 1: By the way, do you recognize the musical masterpiece that goes with the video? :P

video

4) Managing aspect ratios: ensuring that Breaking Fast can be played on most aspect ratios is vital, but it is also tricky. The solution requires twofold work from the artistic and code perspectives.

First, let's see what happens when we execute the code for two players for a 16:9 aspect ratio (the aspect ratio for which the game is being developed):


Now, consider the same code but for a resolution of 1280 x 800, which is a 16:10 aspect ratio and very common for a Macbook Pro, for example:


As you can see, the assets within the canvases are not properly scaled. Well, actually, they are properly adjusted to the width (note that the countdown on the top right corner is properly placed), but not to the height, because some part of the scenario is lost, like the legs of the characteres.

In order to solve this, the trick is to scale all the assets down to adjust them to the height of the screen, and given that the assets (e.g. the background) are bigger than the screen, the player will have the impression that the adjustement has been perfectly done in both dimensions, as shown next:


There is only one inconvenience. As we are losing the perfect adjustment to the width, the HUD elements like the energy bar or the countdown are not perfectly in the middle or at the end of the screen, respectively, but they have a small offset to the left.

Assuming that we have a perfect adjustment to the width, the middle of the screen is calculated as:

screenMiddleX = 0.5 * intendedWidth

, where intendedWidth is the width of the design resolution (1920). If we want to correct the position, we need to multiply this value by the following proportion:

screenMiddleX = 0.5 * intendedWidth * (scaleForAdjustToHeight / scaleForAdjustToWidth)

The result would be as follows:


Note that now the HUD elements are now properly placed.  The result for many different aspect ratios is shown in the next video. Challenge 2: again, do you recognize the composition that accompanies the video?

video

And that's all for now. We'll keep you  posted on the advances of Breaking Fast, so stay tuned!

See you!
FM