-
Notifications
You must be signed in to change notification settings - Fork 17
Arx 02 Splitting Code into Several Files
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.
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 require
d, 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.
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"
.....
Feedback is crucial to improve the tutorial!
Let me know if you have any questions, critique, suggestions or just any other ideas.
Chapter 1: Prototype
- The Ball, The Brick, The Platform
- Game Objects as Lua Tables
- Bricks and Walls
- Detecting Collisions
- Resolving Collisions
- Levels
Appendix A: Storing Levels as Strings
Appendix B: Optimized Collision Detection (draft)
Chapter 2: General Code Structure
- Splitting Code into Several Files
- Loading Levels from Files
- Straightforward Gamestates
- Advanced Gamestates
- Basic Tiles
- Different Brick Types
- Basic Sound
- 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
- Improved Ball Rebounds
- Ball Launch From Platform (Two Objects Moving Together)
- Mouse Controls
- Spawning Bonuses
- Bonus Effects
- Glue Bonus
- Add New Ball Bonus
- Life and Next Level Bonuses
- Random Bonuses
- Menu Buttons
- Wall Tiles
- Side Panel
- Score
- Fonts
- More Sounds
- Final Screen
- Packaging
Appendix D: GUI Layouts
Appendix E: Love-release and Love.js
Beyond Programming: