Skip to content

Arx 02 Splitting Code into Several Files

noooway edited this page Jan 15, 2017 · 1 revision

Right now the program is quite small. However, features pile up quickly, so that amount of code will grow sufficiently. It is much better to split the program into different files to simplify it's maintenance. A good idea is to make separate files for the ball, the brick, and the platform tables that were defined in the previous part.

Intro to Lua Modules

Say we have a greetings.lua file with some variables and function definitions:

hello_message = "Hello from Greetings module."

function say_hello()
  print( hello_message )
end

It is possible to load it into another Lua program with require function. When a file is required, it's contents are executed. The execution happens in the global environment of the program. For example, if we require greetings.lua in the Lua interpreter, the definitions from this file will be accessible in the global scope (note that ".lua" extension is omitted when the file is required):

> require "greetings"
true
> hello_message
Hello from Greetings module.
> say_hello
function: 0x100cb20
> say_hello()
Hello from Greetings module.

A good practice is to prohibit the code from external files to mess with the global environment of the main program. Therefore, it is necessary to isolate it from the rest of the program. This is done by creating a local environment inside the file and executing all of the code inside this environment. In Lua, this is accomplished by creating an empty table and setting it as the environment for the code in the file. As the first attempt let's try:

greetings = {} --(*1)

if setfenv then --(*2)
   setfenv(1, greetings) -- for 5.1
else
   _ENV = greetings -- for 5.2
end

hello_message = "Hello from Greetings module."

function say_hello()
  print( hello_message )
end

(*1): Empty table declaration. Actual name is not important and can be anything.
(*2): The code inside the if-else does environment switch. It's form is slightly different depending on the Lua version.

After the environment switch (*2), the greetings table acts as the global environment for the rest of the code in the file. Therefore, all subsequent variables and function definitions are stored inside it. On the other hand, the greetings table itself was declared before (*2) and therefore belongs to the global environment of the main program. For example, if we require such file from an interpreter, we would get greetings table in the global environment with all the necessary definitions inside it:

> require "greetings"
true
> greetings.hello_message
Hello from Greetings module.
> greetings.say_hello
function: 0x15b2a90

There are two problems, however. The first one is that greetings table leaks in the global environment, which is not good. The second problem is that the say_hello function doesn't work:

> greetings.say_hello()
./greetings.lua:12: attempt to call a nil value (global 'print')
stack traceback:
        ./greetings.lua:12: in function <./greetings.lua:11>
        (...tail calls...)
        [C]: in ?

The first problem is fixed by declaring the empty table local. This way, it's scope would be limited to the file where it is declared. With such declaration, it won't be defined in the global environment of the main program, and we would need to use return statement at the end of the module to actually return this table into the main code. The value of the return statement in the module will be the result of the require in the main program.

The problem with the say_hello function is because the print function is not available in the greetings environment. This is fixed by creating local variable print and assigning it the value of the print from the global environment. Since the scope of a local variable is the whole file, say_hello function now can get access to this local print. In a similar fashion it is necessary to load any global functions used by the module.

local print = print --(*1)

local greetings = {}  --(*2)

if setfenv then  --(*3)
   setfenv(1, greetings) -- for 5.1
else
   _ENV = greetings -- for 5.2
end

--(*4)  

hello_message = "Hello from Greetings module."  --(*5)
greetings.hi_message = "Hi from Greetings module." 

function say_hello() --(*6)
  print( hello_message )
end

function greetings.say_hi() 
  print( hi_message )
end

return greetings --(*7)

(*1): All the necessary functions and modules are loaded in the beginning of the file.
(*2): A declaration of the temporary table, used to store module contents. This table is returned at (*7) and will be a result of requiring this module from the main program.
(*3): By default, a scope in the module is global, that is, any nonlocal variable is visible in the whole program. Inside the if-else, the scope is changed such that any nonlocal variable is added to the module table instead of the global environment. This idiom is slightly different for Lua 5.1 and Lua 5.2, hence the if-else structure.
(*4): After the scope is changed, definitions of the module functions and variables starts.
(*5): Two module variables are declared. It is possible to prefix a variable with a module table name: hi_message is prefixed with the module temporaty name, while hello_message is not. Both forms are equivalent in the result they produce.
(*6): Two functions are declared inside the module. Again, one declaration is prefixed with the module table name and the result is equivalent to the form without the prefix.
(*7): The table with the module variables and functions is returned. This is going to be a result of require "greetings" expression.

This is a typical structure of the module. We then require it from the main program:

> greet = require "greetings"
> greet.say_hello()
Hello from Greetings module.
> greet.say_hi()
Hi from Greetings module.

Modules for The Ball, The Brick, and The Platform

Let's start from a module for the ball and place it into ball.lua file. According to what has been said, there is some boilerplate code, necessary to define the module:

local love = love

local ball = {}

if setfenv then
   setfenv(1, ball) -- for 5.1
else
   _ENV = ball -- for 5.2
end

.....

return ball

The first step is to move the ball properties from the previous example to the ball.lua:

.....
if setfenv then
   setfenv(1, ball) -- for 5.1
else
   _ENV = ball -- for 5.2
end

position_x = 300
position_y = 300
speed_x = 300
speed_y = 300
radius = 10
.....

Also we can notice that the previous love.update() and love.draw() functions can be split in noninteracting parts, regarding update and draw of the ball, the brick and platform independently.

Therefore, it is convenient to define update and draw function in each module. In the case of the ball:

.....
radius = 10

function update( dt )
   position_x = position_x + speed_x * dt
   position_y = position_y + speed_y * dt
end

function draw()
   local segments_in_circle = 16
   love.graphics.circle( 'line',
			 position_x,
			 position_y,
			 radius,
			 segments_in_circle )
end
.....

Because love.graphics.circle is used in the body of the draw, it is necessary to place local love = require love line in the beginning of the ball.lua.

In the main.lua we need to replace parts regarding the ball update and draw by calls to the update and draw from the ball module.

function love.update( dt )
   ball.update( dt )
   .....
end
 
function love.draw()
   ball.draw()
   .....
end

The brick.lua and the platform.lua are quite similar, so I won't stop on them. After they are done, it is necessary to load the created modules from the main.lua This is done by placing the following lines in the beginning of the file.

local platform = require "platform"
local ball = require "ball"
local brick = require "brick"

.....

    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