The Battlejack game is fully functional, but there are still convenience functions to add, including saving and loading game progress, and switching between fullscreen and windowed mode.
Realistically, Battlejack doesn’t exactly demand a fullscreen mode, since it’s a relatively simple game. But for more complex games with a complex story, you might want to encourage immersion, and one way to help the player focus on the game and only the game is to give them the option for the game to take over their entire screen.
The problem with switching between fullscreen and windowed mode is that the graphic sizes must be recalculated. You may even see, in some games, that changing the resolution of the game requires the game to be relaunched before it can redraw. LÖVE can change dimension dynamically, as long as your code adjusts for the change.
Fullscreen
In its current state, your Battlejack code requires some changes to account for a sudden change in game window size. If you’re not a fan of math, many of the changes will seem a little mysterious, but the most important concept is the use of relative measurements. You must base the size of game elements on the width and height of the user’s current window size, but in order to do that you must ask the user’s system what that size is. In practice, this often means that you have to adjust the location of elements to account for a potential change in empty (or “negative”) space.
Forces the window to update its mode settings.
With these functions, you can always determine whether your game screen is fullscreen or windowed, and extract the dimensions of the space you have available.
The fullscreen toggle selection in menu.lua is option 3 (it’s the third selection in the menu entries array). Currently, there’s a placeholder there: it returns true, which in this context is Lua shorthand for not an error.
At the end of this code, a new variable called fsupdated is created and set to 1. This variable serves as a flag signaling that the window mode has been changed, which in turn lets you write code in game.lua that only runs when a window mode change is made.
For the game to resize, several adjustments are required. Some game elements in Battlejack have been purposefully misconfigured in earlier chapters so that you can see the difference between a reasonable first attempt and, ultimately, the correct code. The important thing to understand is that when scaling a game, you must think about many different factors, and these factors may be unique to your game, depending on the assets you use. The examples in this section are specific to Battlejack, but the principles of using variables for calculations, detecting changes in screen settings, and knowing what can and cannot scale, are broadly applicable no matter what your game is like.
Open game.lua for editing. The various states of the game are defined by the variable STATE. Any time a user enters game mode, it is by triggering the game.activate function , whether by selecting New game or pressing Esc. That means that any time a user is in the menu screen, the game.activate function is the entry point to the game, and the ideal place to detect whether the screen size has changed.
If the fsupdated variable is 1, this function now your game.scaler function to recalculate a new scale for each card relative to the size of the screen.
Additionally, the function cycles through each of the hand, horde, back, and grab decks and applies the new size to the cards in play.
Finally, it sets the fsupdated to 0 so that no further size updates are triggered until the user changes the screen settings again.
Launch the game and change the screen setting now. Take note of what scales properly and what does not. Spend some time thinking about the problems you see, and try to predict what function contains the problem code.
Usability
One point of confusion you might detect after testing is that there’s no explicit way to return to the game from the menu screen. On one hand, it’s reasonable to expect that since the user had to press Esc to get to the menu screen in the first place, the user can probably guess that pressing Esc again returns to the game. On the other hand, making the user think too hard about an interface can be frustrating, so there’s no reason to leave it up to chance.
Add a menu option to return to a game already in progress. Phrases like “Exit menu” and “Resume game” can be taken many different ways: exiting the menu could also mean exiting the application, and resuming a game could mean loading a previously saved game. A good interface uses the clearest possible language, which in this case is simply “Return to game”.
Scaling Adjustments
Generally, measurements that need to change dynamically are more predictable when they are based on a single point of authority. All the scaling problems you are witnessing in your test are bugs in the game.draw function, where there is heavy reliance on card.x and card.y values. Since there are several arrays defining cards, the values stored in various card values are unpredictable. There is also some dependence on the pad value, which defines a margin around the edges of the screen, but it changes depending on the screen size. And finally, while the slot variable provides the width of the cards, there’s no value at all for the height of cards in relation to the screen size.
In summary, the layout of a dynamically resizable game must be based on the size of its parent window.
Notice that the new method of placing the glow and the deck relies exclusively on values derived from the screen size, and not at all on card-specific variables. As usual in programming, there are actually several different ways to arrive at the same solution, but this is the most efficient; an alternative is to leave the code unchanged, and use screen size calculations to update the important card variables, but that requires updating values in several places, whereas the screen size calculations are made once and then can be used many times. Any time you have the opportunity to require the computer to do less work, you are optimizing your code, and that’s always a good thing.
The 72 in each line is the font size of the onscreen text. By now, you’re hopefully suspicious of any hard coded value, so you’re probably wondering whether even this value ought to become relative. In fact, you could make the font size more dynamic by either using some calculation to determine the optimal size depending on screen size, or you could create an array of font sizes for specific ranges of screen sizes. As screen sizes vary wildly in sizes, from mobile phones to 4k monitors, this is a worthwhile exercise, but one left for you to manage on your own.
All of these changes have affected the grab code. When a player selects a card, it’s added to the grab array, but it’s also moved up the screen and given a particle effect, very precisely placed just under its top border.
Notice that the particle effect line uses 32/2 instead of the old pad/3 calculation to position the particle image below the card’s top border. This is dependent on the particle image, so if you have changed or plan on changing the particle seed image, you must adjust the value 32 to match the height of your custom image. To make this kind of change even easier, you could create a variable at the top of the file to define the height of the particle image, and then use that variable in this code. That way, all the values you need to change are easily found at the very top of your file, saving you from having to search through your code for all the hard coded values requiring updates. Again, this exercise is left to you to do on your own time.
Try playing the game again to experience dynamic switching of screen sizes. Make adjustments and changes as required, and remember to commit your changes to Git once satisfied.
Save States
Save states for games are a nice convenience for users, and generally expected in modern gaming. Luckily, there’s a wide range in what users accept for save states. Some games save every last detail of a game so that when you resume a saved game, it’s as if you had only paused the game. Others save less information, placing you back at a waypoint or checkpoint. Still others only save your level and nothing else.
Considering that Battlejack falls within the puzzle or card game genre, saving just the player’s level would probably be acceptable by most users. However, Lua’s heavy use of tables makes saving and restoring complex data easy, so Battlejack can have full save state support.
There are two bundles of information that need to be saved: there’s the player information and the game state. Player information is anything that remains true regardless of a game round, such as whether or not the game is in fullscreen mode, and what level the player has reached. Game information is anything specific to the current round, such as the contents of the draw decks and the player’s hands.
You will use two different methods of saving this information, and the information is saved to two separate locations on the user’s drive. Player information is generally referred to as the user configuration and the game information is game data or just data.
One of the most common errors when saving and loading data in an application’s code are missing files or directories. For instance, if you write Lua code to save a the file bar.conf in a directory called foo, if Lua can’t find the foo directory, then Lua crashes.
Lua has no way of knowing whether a file or a directory exists, so you must write a function for this. This is a low level operation that is usually the domain of the operating system, and as such Lua’s os function has some tools that can help solve this puzzle. Lua functions are documented on the Lua website ( www.lua.org/manual ) and also in the book Programming in Lua by Roberto Ierusalimschy (Lua.org, 2016), the official Lua guide from the language creators.
While there is no function to determine whether a file or directory exists, the os.rename function requires that a file or directory exists in order to successfully rename it. It’s a little bit of a hack, but by invoking os.rename on a file path, you can parse its output to determine whether it was able to find a file or not.
User Configuration
This default location is not arbitrary. In the computer world, everyone benefits from standards. Standards are conventions that programmers mutually agree to follow in order to ensure compatibility. Most modern low-level computing standards are defined by the open source POSIX specification, while user-level specifications for Linux are defined by freedesktop.org. Operating systems that are not open source often follow a combination of open standards and their own standards. You have the option of following one or the other, but since the open standards are available to all, this book follows those.
Freedesktop.org defines two hidden directories in a user’s home for configuration and application data. The .config directory contains configuration data and .local/share contains application-specific data files.
The if statement that invokes saver.exists is a subroutine that ensures the destination for the save file exists. If it does not exist, the os.execute function runs the mkdir command on the operating system to create the directory. The mkdir command works on Linux, MacOS, BSD, and Windows.
The last line of the code block uses the inifile library to save the conf table to a file. It uses the io Lua module to create the file.
Game Data
Saving the game data is theoretically a simple matter of taking all the tables that contain game information and dumping their contents into a file. That’s a lot of code to write, and also potentially prone to error if a table is very complex. It’s also a common task, however, so a user on the lua-users.org website created a handy script to save and load tables. This is the sort of script you might usually find with Luarocks, but it just happens that this script has never been entered into the Luarocks repository, so it just exists on the Internet.
Download the script from lua-users.org/wiki/SaveTableToFile or from the code included with this book. However you obtain it, save it in your project directory, in the local/share/lua/5.3/ folder, as table_save.lua.
Caution
You must rename the file from its default table.save-1.0.lua to table_save.lua to avoid Lua from interpreting the file name as a table.
Of course, if you only provide a filename, Lua would do exactly as it is told and save the files into your current directory , so you must interpret filename broadly to mean the path to the file you want to create.
Launch the game and start a round. Once you have drawn one or two cards, go to the menu screen. Save the game and then quit.
Loading a Save File
The inverse of saving a game is loading a game. This is a little more complex than the save process, because there is so much setup when starting a new game, which is “clobbered” by loading in existing data. Because loading a game replaces the game.new and game.setup functions when invoked, the loading process happens in the game.lua file.
First of all, you want to ensure that you’re facing a clean workspace when loading a saved game. Currently, the slate is cleaned by game.new, but if you move the cleanup code to its own function, then you can use it again in your loading code.
The next logical step would seem to be loading the game data, but first there’s another bit of code that usually only happens in game.new: generating a background.
Once the load process is finished, it calls game.activate to put the user back in the game.
Launch the game and load your saved game. Try this process a few times, and play through a few games. Take note of any errors or glitches you encounter.
Homework
The next chapter makes no further changes to the game code aside from adding sounds to the experience. That means the game is basically feature-complete, so any glitches or unoptimized code you find are permanent unless you change it.
Only one save is supported. Program a method to permit different save slots.
If a user forgets to save their game before quitting, they lose their progress. Devise a system to avoid this.
The text of the message screen isn’t always positioned correctly for all screen sized. Change the code of msg.lua to fix this problem.
Remember to commit your changes to Git to preserve snapshots of your progress.