The Basics
In Lua, a module is a code file (.lua) that contains functions and/or data and can be accessed from any other Lua file. Not only does using modules help to keep your project organized, it also promotes code reusability. Since a well-written module provides an interface, but keeps its inner workings hidden, it allows you to reap the benefits of encapsulation.
The most common way to create a module is to
create a table, place some functions and variables into it,
and return the table. The name we give to the table creates a namespace - a named container to keep the contents of the module in. This namespace table also keeps things from being
placed into the global environment, which is a good thing. This is exactly how other Lua modules like math and string work.
Let's create a simple module that contains a single function:
Let's create a simple module that contains a single function:
local mymodule = {}
function mymodule.MyFunction()
-- do stuff
end
return mymodule
If you save the above code as a standalone .lua file (mymodule.lua, for example), you will then be able to include its functionality in any other .lua file by using Lua's require() function. For example:
local mymodule = require("mymodule") -- load our module into namespace
"mymodule"
mymodule.MyFunction() -- and now the function can be called
Note that the module could be loaded under any name. The following works just as well:
l
ocal m = require("mymodule") -- load the module into namespace
"m"
m.MyFunction() -- its contents are the same
If all you want to do is create a collection of functions, the above is all you need to know. Simply add more functions to the file and they will all be available in the module by using the module.function() syntax when that module is loaded via a call to require().
C/C++ programmers may assume at first glance that the require() function is similar to the #include preprocessor directive, but really it isn't. It's a function, called at runtime just like any other, and simply runs the contents of the module file when it is called.
C/C++ programmers may assume at first glance that the require() function is similar to the #include preprocessor directive, but really it isn't. It's a function, called at runtime just like any other, and simply runs the contents of the module file when it is called.
Modeling an Object
For those with some Object Oriented Programming (OOP) background, a Lua module can in some ways be thought of as similar to a class: a
collection of functions (or methods) and variables (or properties), some private and some externally facing. Let's look at a slightly more interesting example:
Here we've created a simple module that models a "weapon" object for a video game. We use it by assigning a weapon type, then calling the Attack() function whenever the player attacks an enemy. Our calling code might look something like this:
We've assumed here that the weapon object is stored as a part of a "knight" object representing the player character. The end result is that when the troll is attacked, troll.life is decreased by whatever amount of damage the sword inflicts.
Notice that there is actually more to the weapon object than just its type variable and Attack() function, but the programmer using it doesn't need to know about the rest. We've kept the other properties (strength, price, etc.) local and therefore not visible to outside code - we have also kept them out of the weapon table the module returns. The same goes for the CalculateDamage() function; it's an internal part of the weapon that is used by the Attack() function, not usable by any code outside the object. We only put the parts of the module we want to expose to the rest of the program into the namespace table. This stops programmers (including ourselves!) from accidentally tampering with data they're not supposed to be touching.
Something important to note about modules is that the require() function caches loaded modules by default. Calling require() again on the same module will not create a second instance, but will simply return the same module table that was previously loaded. If you don't keep this in mind, you could end up with very unexpected results in your program!
Let's say we are working on our knight vs troll game. We have written one module to handle the knight's behaviour, and another module to handle the troll's behaviour. We want to give both the knight and the troll weapons, so each module calls upon our weapon module. Our modules might look like this:
Looks good. In our game, we can then assign weapons to the knight and troll, so they can get to fighting each other:
Suddenly, mid-game, our knight finds himself holding a crude wooden club for no apparent reason!
In some cases though, this is exactly the behaviour we want. If, instead of trying to model a "weapon object" class like we've done here, we decided to create a "weapon handler" class that would manage all weapons in the game, we would likely only want a single instance of the handler shared across all parts of our code. (If you're into OOP, this functionality is very similar to the Singleton design pattern and can easily be used as such.)
If we want to fix our above example, ideally we would like to create separate instances of our weapon object with a constructor like weapon.new(). That's beyond the scope of this simple article, but it's not too complicated. Here's an example.
Lua is a simple language. It doesn't have any OOP constructs like objects or classes, or inheritance. But that doesn't mean you can't create them yourself! With a little more work, it is possible to do real object oriented programming in Lua. It's up to you to decide whether or not it's needed for your particular project. Either way, modules are an essential part of almost any project in Lua.
local weapon = {} -- PRIVATE - cannot be accessed by code outside this module local strength, quality, speed, price local function CalculateDamage() -- complex calculations based on weapon type ... return amount end -- PUBLIC - usable by everyone weapon.type = "Sword" function weapon.Attack(victim) local damage = CalculateDamage() victim.life = victim.life - damage end return weapon
Here we've created a simple module that models a "weapon" object for a video game. We use it by assigning a weapon type, then calling the Attack() function whenever the player attacks an enemy. Our calling code might look something like this:
knight.weapon.type = "Sword" -- the knight picks up a sword ... knight.weapon.Attack(troll) -- the knight attacks an enemy troll with the sword
We've assumed here that the weapon object is stored as a part of a "knight" object representing the player character. The end result is that when the troll is attacked, troll.life is decreased by whatever amount of damage the sword inflicts.
Notice that there is actually more to the weapon object than just its type variable and Attack() function, but the programmer using it doesn't need to know about the rest. We've kept the other properties (strength, price, etc.) local and therefore not visible to outside code - we have also kept them out of the weapon table the module returns. The same goes for the CalculateDamage() function; it's an internal part of the weapon that is used by the Attack() function, not usable by any code outside the object. We only put the parts of the module we want to expose to the rest of the program into the namespace table. This stops programmers (including ourselves!) from accidentally tampering with data they're not supposed to be touching.
Single Instance
In a larger project, our modules would likely could call upon even more modules. Often, the same module will be used by multiple modules. For our "weapon" module example above, we'd probably refer to it in the code that handles the player character, the code for enemies, maybe the code for a weapon shop ... anywhere we'd like to use weapons in the game.
Something important to note about modules is that the require() function caches loaded modules by default. Calling require() again on the same module will not create a second instance, but will simply return the same module table that was previously loaded. If you don't keep this in mind, you could end up with very unexpected results in your program!
Let's say we are working on our knight vs troll game. We have written one module to handle the knight's behaviour, and another module to handle the troll's behaviour. We want to give both the knight and the troll weapons, so each module calls upon our weapon module. Our modules might look like this:
Looks good. In our game, we can then assign weapons to the knight and troll, so they can get to fighting each other:
knight.weapon.type = "Sword" troll.weapon.type = "Wooden Club" ... print(knight.weapon.type) -- this prints "Wooden Club" ... what gives??
Suddenly, mid-game, our knight finds himself holding a crude wooden club for no apparent reason!
Despite both modules storing the called upon weapon module as a local table, they are actually sharing the same instance of the table. There is only one weapon, and both characters are holding it at the same time! If our poor knight tries to remedy the situation by swapping weapons using the code knight.weapon.type = "Rocket Launcher", the troll will also be holding one.
So, our module diagram actually looks like this:
Obviously, this isn't the behaviour we wanted.
So what does this mean? Basically, the list of loaded modules in your Lua program is global (even though each module is still only accessible by the files that call it via require()). There will only ever be one instance of each.
So, our module diagram actually looks like this:
Obviously, this isn't the behaviour we wanted.
So what does this mean? Basically, the list of loaded modules in your Lua program is global (even though each module is still only accessible by the files that call it via require()). There will only ever be one instance of each.
In some cases though, this is exactly the behaviour we want. If, instead of trying to model a "weapon object" class like we've done here, we decided to create a "weapon handler" class that would manage all weapons in the game, we would likely only want a single instance of the handler shared across all parts of our code. (If you're into OOP, this functionality is very similar to the Singleton design pattern and can easily be used as such.)
If we want to fix our above example, ideally we would like to create separate instances of our weapon object with a constructor like weapon.new(). That's beyond the scope of this simple article, but it's not too complicated. Here's an example.
Lua is a simple language. It doesn't have any OOP constructs like objects or classes, or inheritance. But that doesn't mean you can't create them yourself! With a little more work, it is possible to do real object oriented programming in Lua. It's up to you to decide whether or not it's needed for your particular project. Either way, modules are an essential part of almost any project in Lua.
No comments:
Post a Comment