Difference between revisions of "The Language"
m (Text replacement - "</source>" to "</syntaxhighlight>") |
|||
(2 intermediate revisions by 2 users not shown) | |||
Line 1: | Line 1: | ||
+ | == Basic Concepts == | ||
+ | |||
+ | Lua was created in 1993 by Roberto Ierusalimschy, Luiz Henrique de Figueiredo, and Waldemar Celes, members of the Computer Graphics Technology Group at PUC-Rio, the Pontifical University of Rio de Janeiro. Gideros Studio benefits from Lua, a powerful, fast, lightweight, embeddable scripting language. Lua’s power comes from its simplicity and speed. Lua is mentioned as the fastest language among all interpreted scripting languages. | ||
+ | |||
+ | Lua is used in commercial applications, including Adobe's Photoshop Lightroom, World of Warcraft, Far Cry, Garry's Mod, Supreme Commander and Sonic the Hedgehog. It’s widely used in many applications, where speed and convenience is sought. You also write your applications in Lua inside Gideros Studio, however can also extend your apps with other languages like Objective-C, C++, C or Java. | ||
+ | |||
+ | Despite the size of the Lua interpreter (about 150Kb in size), Lua is a very rich, extensive and expandable programming language. Applications written using Lua looks very clean and understandable, yet effective. | ||
+ | |||
+ | <syntaxhighlight lang="lua"> | ||
+ | print ("Hello, Gideros Studio") | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Here print() is a Lua function. We invoke a function using a paranthesis, and print takes the text between double quotes and writes to standard output. | ||
+ | |||
+ | We use "--" to start a comment, or use --[[ to start a mult-line comment. Consider this example: | ||
+ | |||
+ | <syntaxhighlight lang="lua> | ||
+ | --[[ This is a multi-line Lua comment | ||
+ | Where comment goes on and on... | ||
+ | And on... | ||
+ | ]] | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | == The Language == | ||
+ | |||
=== Assignments and Variables === | === Assignments and Variables === | ||
Lua is a dynamically-typed language, that is, variables don't have types, only the values. All values carry their own type. Consider the following as an example: | Lua is a dynamically-typed language, that is, variables don't have types, only the values. All values carry their own type. Consider the following as an example: | ||
− | < | + | <syntaxhighlight lang="lua"> |
MyTable = wide | MyTable = wide | ||
width = 3; height = 1.5 | width = 3; height = 1.5 | ||
area = width * height | area = width * height | ||
print (area) | print (area) | ||
− | </ | + | </syntaxhighlight> |
You can nest several lines using semicolons, but it’s optional and generally not a good programming habit as it decreases the readability of your code.Be careful not to use reserved words. Gideros Studio will complain when you use a reserved word in Lua. Consider this example: | You can nest several lines using semicolons, but it’s optional and generally not a good programming habit as it decreases the readability of your code.Be careful not to use reserved words. Gideros Studio will complain when you use a reserved word in Lua. Consider this example: | ||
− | < | + | <syntaxhighlight lang="lua"> |
local = 1 | local = 1 | ||
− | </ | + | </syntaxhighlight> |
Since Lua is a small language, it only has 8 basic data types: | Since Lua is a small language, it only has 8 basic data types: | ||
Line 26: | Line 51: | ||
# Threads | # Threads | ||
# Tables | # Tables | ||
+ | |||
Global variables in Lua do not need to be declared. If you want to declare a variable, just assign a value to it. Giving it a nil value removes the variable. In fact, when a variable is removed, and it’s not referenced by another variable, it’s ready to be cleared up from memory, thanks to the wonderful Lua garbage collector. | Global variables in Lua do not need to be declared. If you want to declare a variable, just assign a value to it. Giving it a nil value removes the variable. In fact, when a variable is removed, and it’s not referenced by another variable, it’s ready to be cleared up from memory, thanks to the wonderful Lua garbage collector. | ||
Line 31: | Line 57: | ||
Lua can support multiple assignments. Check the following assignment: | Lua can support multiple assignments. Check the following assignment: | ||
− | < | + | <syntaxhighlight lang="lua"> |
a, b, c = 1, 2, 3 | a, b, c = 1, 2, 3 | ||
Line 39: | Line 65: | ||
-- Function can return multiple values | -- Function can return multiple values | ||
n, m = calculate (phi, zeta) | n, m = calculate (phi, zeta) | ||
− | </ | + | </syntaxhighlight> |
=== Control Structures === | === Control Structures === | ||
Line 62: | Line 88: | ||
Arrays can have any index, e.g you can start with an array with -5, 0 or 1 - it’s all up to you. Consider the following example: | Arrays can have any index, e.g you can start with an array with -5, 0 or 1 - it’s all up to you. Consider the following example: | ||
− | < | + | <syntaxhighlight lang="lua"> |
-- creates an array with indices from -2 to 2 | -- creates an array with indices from -2 to 2 | ||
array = {} | array = {} | ||
Line 68: | Line 94: | ||
array[i] = "apples" | array[i] = "apples" | ||
end | end | ||
− | </ | + | </syntaxhighlight> |
Lua also supports multi-dimensional arrays, which we call "matrices". | Lua also supports multi-dimensional arrays, which we call "matrices". | ||
Line 74: | Line 100: | ||
Consider the following example. A similar array syntax can be used to create string lists. The statement below: | Consider the following example. A similar array syntax can be used to create string lists. The statement below: | ||
− | < | + | <syntaxhighlight lang="lua"> |
animals = {"giraffe", "boar", "wolf", "frog"} | animals = {"giraffe", "boar", "wolf", "frog"} | ||
− | </ | + | </syntaxhighlight> |
is equal to: | is equal to: | ||
− | < | + | <syntaxhighlight lang="lua"> |
animals = {} | animals = {} | ||
animals[1] = "giraffe"; animals[2] = "boar" | animals[1] = "giraffe"; animals[2] = "boar" | ||
animals[3] = "wolf"; animals[4] = "frog" | animals[3] = "wolf"; animals[4] = "frog" | ||
− | </ | + | </syntaxhighlight> |
=== Lua Tables (arrays) === | === Lua Tables (arrays) === | ||
In Lua, Tables and Arrays are terms so used interchangeably, However this is true to an extent. | In Lua, Tables and Arrays are terms so used interchangeably, However this is true to an extent. | ||
− | |||
− | |||
=== Basic Functions === | === Basic Functions === | ||
Line 96: | Line 120: | ||
Lua has the ability to call its own functions or C functions, and it handles functions as a "data type". Note that functions can return multiple values, and you can use multiple assignments to collect these functions. | Lua has the ability to call its own functions or C functions, and it handles functions as a "data type". Note that functions can return multiple values, and you can use multiple assignments to collect these functions. | ||
− | < | + | <syntaxhighlight lang="lua"> |
function calculate(w, d, h) | function calculate(w, d, h) | ||
if h > 4 then | if h > 4 then | ||
Line 108: | Line 132: | ||
-- Calculate the volume of the room | -- Calculate the volume of the room | ||
print (calculate (4,3,4)) | print (calculate (4,3,4)) | ||
− | </ | + | </syntaxhighlight> |
Try the function above, this time with values 4,5,5. | Try the function above, this time with values 4,5,5. | ||
+ | |||
+ | == Classes == | ||
+ | |||
+ | Lua does not support classes the way languages like C++, Java and ActionScript do. But Lua is a multi-paradigm language and have roots from prototype-based languages. In Lua, each object can define its own behaviour through metatables. Therefore, it is possible to emulate OO programming and classes in Lua. | ||
+ | |||
+ | '''Note:''' For the detailed discussion of object oriented programming in Lua, please refer to http://www.lua.org/pil/16.html | ||
+ | |||
+ | Gideros follows the same paradigm in its API design. Each instance created by Gideros API is a Lua table with a metatable attached. | ||
+ | |||
+ | === Creating Instances === | ||
+ | |||
+ | Instances in Gideros is created through new function. For example, to create a Sprite, Texture, Bitmap and a Timer instance: | ||
+ | |||
+ | <syntaxhighlight lang="lua"> | ||
+ | local sprite = Sprite.new() | ||
+ | local texture = Texture.new("image.png") | ||
+ | local bitmap = Bitmap.new(texture) | ||
+ | local timer = Timer.new(1000, 0) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | === Inheritance === | ||
+ | |||
+ | The Core.class function is used to create your own classes through inheritance (older versions of Gideros used the gideros.class function -- this function has now been deprecated). You can inherit from Gideros API's own classes (EventDispatcher, Sprite, etc.) or from your own classes. For example, you can create your own class that inherits from EventDispatcher class like so: | ||
+ | |||
+ | <syntaxhighlight lang="lua"> | ||
+ | MyEventDispatcher = Core.class(EventDispatcher) | ||
+ | </syntaxhighlight> | ||
+ | <br/> | ||
+ | |||
+ | You can also create a new class by using Core.class with no arguments: | ||
+ | |||
+ | <syntaxhighlight lang="lua"> | ||
+ | MyOwnClass = Core.class() | ||
+ | </syntaxhighlight> | ||
+ | <br/> | ||
+ | |||
+ | By using Inheritance, you can design and implement the visual elements of your game separately: | ||
+ | |||
+ | <syntaxhighlight lang="lua"> | ||
+ | -- create your own start button class | ||
+ | StartButton = Core.class(Sprite) | ||
+ | |||
+ | -- create your own menu class | ||
+ | Menu = Core.class(Sprite) | ||
+ | |||
+ | -- create your own player class | ||
+ | Player = Core.class(Sprite) | ||
+ | |||
+ | function Player:walk() | ||
+ | -- walk logic | ||
+ | end | ||
+ | |||
+ | function Player:jump() | ||
+ | -- jump logic | ||
+ | end | ||
+ | |||
+ | -- create and add a player instance to the stage | ||
+ | stage:addChild(Player.new()) | ||
+ | </syntaxhighlight> | ||
+ | <br/> | ||
+ | |||
+ | When an instance is created, init function is called to do the initialization: | ||
+ | |||
+ | <syntaxhighlight lang="lua"> | ||
+ | Player = Core.class(Sprite) | ||
+ | |||
+ | function Player:init() | ||
+ | -- do the initialization of Player instance | ||
+ | self.health = 100 | ||
+ | self.speed = 3 | ||
+ | end | ||
+ | |||
+ | -- after Player instance is created, init function is called | ||
+ | local player = Player.new() | ||
+ | </syntaxhighlight> | ||
+ | <br/> | ||
+ | |||
+ | Whether to use inheritance or not is related to your programming taste. It's possible to implement a whole game without creating custom classes. You can refer to "Jumping Ball" and "Jumping Balls" examples to see the difference between designing your code with classes or not. | ||
+ | |||
+ | Using classes makes it easier to reuse code. | ||
+ | |||
+ | === Inheriting From Classes With Constructor Arguments === | ||
+ | |||
+ | If you inherit from a class that takes a constructor argument, your new class has to pass in the arguments that the base class expects as its first arguments. You can then pass in any additional arguments: | ||
+ | |||
+ | <syntaxhighlight lang="lua"> | ||
+ | MyBitmap = Core.class(Bitmap) | ||
+ | |||
+ | function MyBitmap:init(texture, additional args) | ||
+ | .... | ||
+ | end | ||
+ | </syntaxhighlight> | ||
+ | <br/> | ||
+ | |||
+ | Here's a very simple example: | ||
+ | |||
+ | <syntaxhighlight lang="lua"> | ||
+ | A = Core.class() | ||
+ | |||
+ | function A:init(msg1) | ||
+ | print("A:init("..msg1..")") | ||
+ | end | ||
+ | |||
+ | B = Core.class(A) | ||
+ | |||
+ | function B:init(msg1, msg2) | ||
+ | print("B:init("..msg1..","..msg2..")") | ||
+ | end | ||
+ | |||
+ | B.new("hello","world") | ||
+ | </syntaxhighlight> | ||
+ | <br/> | ||
+ | |||
+ | This example produces: | ||
+ | <pre> | ||
+ | A:init(hello) | ||
+ | B:init(hello,world) | ||
+ | </pre> | ||
+ | <br/> | ||
+ | |||
+ | === Adding Explicit Dependencies for Inherited Classes === | ||
+ | |||
+ | You can put class definitions in different files, but if it doesn't work, it may be because the files are being loaded in the wrong order. You can specify file dependencies in the IDE. Select a file, right click, then select "Code Dependencies". | ||
+ | |||
+ | For example, if class A (in classA.lua) inherits from class B (in classB.lua) and you're getting a message like "attempt to index global 'B' (a nil value)" during the initialization process, select "classA.lua" and add a dependency on classB.lua. | ||
+ | |||
+ | === Accessing Overridden Functions === | ||
+ | |||
+ | If you inherit from a class and override one of its functions, you have to call the overridden function using syntax like "BaseClassName.function(self)". Here's an example: | ||
+ | <syntaxhighlight lang="lua"> | ||
+ | -- -------------------------------------- | ||
+ | |||
+ | A = Core.class() | ||
+ | |||
+ | function A:init(msg1) | ||
+ | self.text = "I'm an A" | ||
+ | end | ||
+ | |||
+ | function A:method() | ||
+ | print(self.text .. " in A:method()") | ||
+ | end | ||
+ | |||
+ | -- -------------------------------------- | ||
+ | |||
+ | B = Core.class(A) | ||
+ | |||
+ | function B:init(msg1, msg2) | ||
+ | self.text = "I'm a B" | ||
+ | end | ||
+ | |||
+ | function B:method(arg1) | ||
+ | print(self.text .. " in B:method()") | ||
+ | --A:method() <--- NOT THIS | ||
+ | A.method(self) | ||
+ | end | ||
+ | |||
+ | -- -------------------------------------- | ||
+ | |||
+ | b = B.new("hello","world") | ||
+ | b:method() | ||
+ | </syntaxhighlight> | ||
+ | <br/> | ||
+ | |||
+ | This will produce: | ||
+ | <pre> | ||
+ | I'm a B in B:method() | ||
+ | I'm a B in A:method() | ||
+ | </pre> | ||
+ | <br/> | ||
+ | |||
+ | If you try to use "A:method()" in "B:method()" you'll get an error about a nil value for the 'text' field. That's because you're using the "A" instance that was created in the "A = Core.class()" line (equivalent to "A.method(A)"). | ||
+ | |||
+ | If you try to use "self:method()", you'll be recursively calling "B:method()" | ||
+ | <br/> | ||
+ | |||
+ | == Standard Libraries == | ||
+ | |||
+ | === String Manipulation === | ||
+ | |||
+ | Lua has a powerful set of string manipulation functions, such as finding and extracting substrings. Unlike C, the first character of the string has "1" as the position index. You can also use negative indices in a string just like Python does - e.g the last character in a string is at position -1. You can use both methods, whichever you find convenient. | ||
+ | |||
+ | Note that the string library assumes one-byte character encodings. | ||
+ | |||
+ | The following table gives an overview of string manipulation commands. Have a look at them, and then study the examples below. | ||
+ | |||
+ | {| class="wikitable" | ||
+ | |- | ||
+ | ! String Command !! Meaning | ||
+ | |- | ||
+ | | string.byte (s [, i [, j]]) || Returns the internal numerical codes of the characters s[i], s[i+1], ···, s[j]. The default value for i is 1; the default value for j is i. | ||
+ | |- | ||
+ | | string.find (s, pattern [, init [, plain]]) || Looks for the first match of pattern in the string s. If it finds a match, then find returns the indices of s where this occurrence starts and ends; otherwise, it returns nil | ||
+ | |- | ||
+ | | string.format (formatstring, ···) || Returns a formatted version of its variable number of arguments following the description given in its first argument (which must be a string). | ||
+ | |- | ||
+ | | string.gmatch (s, pattern) || Returns an iterator function that, each time it is called, returns the next captures from pattern over string s. | ||
+ | |- | ||
+ | | string.len (s) || Returns the length of the string s. | ||
+ | |- | ||
+ | | string.lower (s) || Changes all characters of a string to lowercase. | ||
+ | |- | ||
+ | | string.upper (s) || Changes all characters of a string to uppercase. | ||
+ | |- | ||
+ | | string.reverse (s) || Returns a string which is a reverse of the string s. | ||
+ | |} | ||
+ | |||
+ | === File I/O === | ||
+ | |||
+ | In an app, there always comes a point where you might want to persist some data. Persisting data in simple terms means retaining the data even after the app is exited. Some examples would include the current level, a high score, the player‘s name and so on. There could be other more complex examples where one might have levels stored in files. For all of these there’s an API, the File I/O API. | ||
+ | |||
+ | There are two ways to reference these functions, the explicit and the implicit. Explicit is when the file to be worked on is specified, it is seen in the form of file: type commands where as implicit is by passing the fileHandle to the function for file operations and are generally seen starting with io. | ||
+ | |||
+ | Example: | ||
+ | <syntaxhighlight lang="lua"> | ||
+ | local playerName = "Awesome Player" | ||
+ | local file = io.open ( "settings.save" , "w+" ) | ||
+ | file:write( "Playername:" , playerName ) | ||
+ | file:close() | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | This has just saved a file called settings.save with the values "Playername: Awesome Player" | ||
+ | |||
+ | Now that we have some data saved in the file, let us try to read it from the file. | ||
+ | |||
+ | <syntaxhighlight lang="lua"> | ||
+ | local playerName = nil | ||
+ | local file = io.open ( “settings.save”, “r” ) | ||
+ | playerName = file:read() | ||
+ | file:close() | ||
+ | print ( “The playername saved is: ”, playerName) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | ==== Opening a File ==== | ||
+ | |||
+ | To open a file using lua, we would use | ||
+ | |||
+ | <syntaxhighlight lang="lua"> | ||
+ | file = io.open ( filename [, mode]) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | where the file called filename is opened and the file handle, a reference to this file is passed back in the variable file. The modes for opening the files can be one of the following. | ||
+ | |||
+ | * “r” read mode, this is the default mode | ||
+ | * “w” write mode | ||
+ | * “a” append mode | ||
+ | * “r+” update mode, all previous data is preserved | ||
+ | * “w+” update mode, all previous data is erased | ||
+ | * “a+” append update mode, previous data is preserved, writing is allowed only at the end of the file. | ||
+ | |||
+ | There is no explicit way to open a file, all files are opened using the io.open function. | ||
+ | |||
+ | ==== Closing a File ==== | ||
+ | |||
+ | To close a file, we would use | ||
+ | |||
+ | <syntaxhighlight lang="lua"> | ||
+ | io.close([file]) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | This closes the file handle passed to the function, if nothing is passed, the default output file is closed. | ||
+ | |||
+ | The explicit way to close a file is file:close() | ||
+ | |||
+ | ==== Getting Data/Input ==== | ||
+ | |||
+ | In a complex application, we need to get several kinds of data from files stored on the device. In order to get data from a file, we are given a couple of options, including lines and read. | ||
+ | |||
+ | When we use the lines function, it offers an iterator function, which returns a new line from the file each time it is called. Therefore this is used as | ||
+ | |||
+ | <syntaxhighlight lang="lua"> | ||
+ | for line in io.lines(filename) do … end | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Note that this function does not require the file to be opened or closed, all of that is managed by the function internally. | ||
+ | |||
+ | There is an explicit version of the same, which is used as file:lines(), the difference between this and the implicit function is that to get the filehandle, we have to open the file, there is no parameter passed to the lines function and when the end of file is reached, the file is not automatically closed, we have to close it. | ||
+ | |||
+ | The other way to get data from a file is the read function, which is used as | ||
+ | |||
+ | <syntaxhighlight lang="lua"> | ||
+ | data = io.read( format1, … ) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | The explicit equivalent is file:read( format1, … ) | ||
+ | |||
+ | Since the function does not have a filehandle passed to it, io.read reads from the standard input and hence if you want to read from a file, | ||
+ | |||
+ | The formats that can be used for the function are: | ||
+ | |||
+ | * “*n” this will read a number, this returns a numeric value than a string | ||
+ | * “*a” this will read all the data in the file, starting from the current position. | ||
+ | * “*l” this will read the next line, this is the default for this command | ||
+ | |||
+ | where nn if a number is used for format, it tells the function to read those many (nn) characters from the file. | ||
+ | |||
+ | ==== Temporary Files ==== | ||
+ | |||
+ | Sometimes there might be the need to create a temporary file either to transfer portions of data or to save a backup copy before being certain. The easiest way to create one than trying to name the files through the code and then keeping track of the file open and close, etc is to use the io.tmpfile() function. This returns a file handle to a temporary file opened in update mode and this file is automatically removed when the app terminates. | ||
+ | |||
+ | ==== Writing Data ==== | ||
+ | |||
+ | To write data back to the files, it has to be opened in a mode that supports writing data to the file. The one and only way to write data is to use the function write. | ||
+ | |||
+ | <syntaxhighlight lang="lua"> | ||
+ | io.write(value1, …) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | The data passed to the function is written to the file, however since there is no filehandle passed to this function, it writes to the standard output, so if you want to write to a file, the file:write(value1, ...) function should be used. | ||
+ | |||
+ | ==== Seeking Your Position in the File ==== | ||
+ | |||
+ | Updating data is one of the easiest and one of the most difficult task. An easy way to update data is overwrite the entire file with the new data, but sometimes that can be very time consuming so the easier way is to position the write head to the position and then write the data there. We can get the position and set the position by using the function seek, this is an explicit function only. | ||
+ | |||
+ | <syntaxhighlight lang="lua"> | ||
+ | position = file:seek( [mode] [,offset] ) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | By default, the mode is set to “cur” and the offset to 0. There are three modes which are | ||
+ | * “set” this is from the 0 position (start of file) and the head is moved to the position as specified by offset | ||
+ | * “cur” this is from the current position, so the offset is added to the current position | ||
+ | * “end” this is from the end of the file | ||
+ | |||
+ | so to get the size of the file, one can use | ||
+ | |||
+ | <syntaxhighlight lang="lua"> | ||
+ | filesize = file:seek(“end”) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | the set can be used to rewind the reading. | ||
+ | |||
+ | ==== Buffering ==== | ||
+ | |||
+ | The buffering for a file can be set by the file:setvbuf() command which is defined as | ||
+ | |||
+ | <syntaxhighlight lang="lua"> | ||
+ | file:setvbuf( mode [, size] ) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | there are three modes that are available for this command: | ||
+ | * “no”: There is no buffering, the results are instantaneous | ||
+ | * “full”: The output operations occur only when the buffer is full, or when you flush | ||
+ | * “line”: The output is buffered on a per line basis (till a newline character is received) | ||
+ | |||
+ | The size is the size of the buffer in bytes. | ||
+ | |||
+ | ==== Flushing Data ==== | ||
+ | |||
+ | If you are using a buffer for writes, you might want to flush i.e. commit the file’s output buffer and save all of that to the file. Buffering is not really required in modern systems but still can be helpful at times. So before closing a file, if buffering was on, it is a good practice to flush and ensure that all the data is saved from the buffers to the file. This is an explicit function so is defined as | ||
+ | |||
+ | <syntaxhighlight lang="lua"> | ||
+ | file:flush() | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | there are no parameters passed to this function. | ||
+ | |||
+ | ==== Validating the File Operations ==== | ||
+ | |||
+ | While working with files there are a lot of situations that could lead to errors, so it is very important to check for the return values, generally the return values are nil when the end of the file is reached or a file could not be opened, etc. | ||
+ | |||
+ | However to check the state of the file via the file handle can be with the type function as | ||
+ | |||
+ | <syntaxhighlight lang="lua"> | ||
+ | result = io.type(fileHandle) | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | the return value is either “file” if the file is open, “closed” if the file is closed or nil if the fileHandle is not a file handle. | ||
+ | |||
+ | |||
+ | END. |
Latest revision as of 14:33, 13 July 2023
Basic Concepts
Lua was created in 1993 by Roberto Ierusalimschy, Luiz Henrique de Figueiredo, and Waldemar Celes, members of the Computer Graphics Technology Group at PUC-Rio, the Pontifical University of Rio de Janeiro. Gideros Studio benefits from Lua, a powerful, fast, lightweight, embeddable scripting language. Lua’s power comes from its simplicity and speed. Lua is mentioned as the fastest language among all interpreted scripting languages.
Lua is used in commercial applications, including Adobe's Photoshop Lightroom, World of Warcraft, Far Cry, Garry's Mod, Supreme Commander and Sonic the Hedgehog. It’s widely used in many applications, where speed and convenience is sought. You also write your applications in Lua inside Gideros Studio, however can also extend your apps with other languages like Objective-C, C++, C or Java.
Despite the size of the Lua interpreter (about 150Kb in size), Lua is a very rich, extensive and expandable programming language. Applications written using Lua looks very clean and understandable, yet effective.
print ("Hello, Gideros Studio")
Here print() is a Lua function. We invoke a function using a paranthesis, and print takes the text between double quotes and writes to standard output.
We use "--" to start a comment, or use --[[ to start a mult-line comment. Consider this example:
--[[ This is a multi-line Lua comment
Where comment goes on and on...
And on...
]]
The Language
Assignments and Variables
Lua is a dynamically-typed language, that is, variables don't have types, only the values. All values carry their own type. Consider the following as an example:
MyTable = wide
width = 3; height = 1.5
area = width * height
print (area)
You can nest several lines using semicolons, but it’s optional and generally not a good programming habit as it decreases the readability of your code.Be careful not to use reserved words. Gideros Studio will complain when you use a reserved word in Lua. Consider this example:
local = 1
Since Lua is a small language, it only has 8 basic data types:
- nil (null)
- Booleans
- Numbers
- Strings
- Functions
- Userdata
- Threads
- Tables
Global variables in Lua do not need to be declared. If you want to declare a variable, just assign a value to it. Giving it a nil value removes the variable. In fact, when a variable is removed, and it’s not referenced by another variable, it’s ready to be cleared up from memory, thanks to the wonderful Lua garbage collector.
Lua can support multiple assignments. Check the following assignment:
a, b, c = 1, 2, 3
-- Easy method to swap two variables
x, y = y, z
-- Function can return multiple values
n, m = calculate (phi, zeta)
Control Structures
Lua provides a strong control mechanism between different variable types. As an example for logical decisions, consider the following:
Logical Decision | Meaning |
---|---|
A and B | If A is true, then return the result of expression B.
If A is false, then return A |
A or B | If A is true, then return the result of expression A
If A is false, then return B |
Arrays
Arrays that can be indexed not only with numbers, but with any value. Arrays do not have to be defined a size, and they can grow as needed. When an array is defined, and you try to reach a value exceeding the boundary, you get a nil (instead of zero).
Arrays can have any index, e.g you can start with an array with -5, 0 or 1 - it’s all up to you. Consider the following example:
-- creates an array with indices from -2 to 2
array = {}
for i=-2, 2 do
array[i] = "apples"
end
Lua also supports multi-dimensional arrays, which we call "matrices".
Consider the following example. A similar array syntax can be used to create string lists. The statement below:
animals = {"giraffe", "boar", "wolf", "frog"}
is equal to:
animals = {}
animals[1] = "giraffe"; animals[2] = "boar"
animals[3] = "wolf"; animals[4] = "frog"
Lua Tables (arrays)
In Lua, Tables and Arrays are terms so used interchangeably, However this is true to an extent.
Basic Functions
Lua has the ability to call its own functions or C functions, and it handles functions as a "data type". Note that functions can return multiple values, and you can use multiple assignments to collect these functions.
function calculate(w, d, h)
if h > 4 then
print ("height of the room cannot be more than 4")
return
end
volume = w * d * h
return volume
end
-- Calculate the volume of the room
print (calculate (4,3,4))
Try the function above, this time with values 4,5,5.
Classes
Lua does not support classes the way languages like C++, Java and ActionScript do. But Lua is a multi-paradigm language and have roots from prototype-based languages. In Lua, each object can define its own behaviour through metatables. Therefore, it is possible to emulate OO programming and classes in Lua.
Note: For the detailed discussion of object oriented programming in Lua, please refer to http://www.lua.org/pil/16.html
Gideros follows the same paradigm in its API design. Each instance created by Gideros API is a Lua table with a metatable attached.
Creating Instances
Instances in Gideros is created through new function. For example, to create a Sprite, Texture, Bitmap and a Timer instance:
local sprite = Sprite.new()
local texture = Texture.new("image.png")
local bitmap = Bitmap.new(texture)
local timer = Timer.new(1000, 0)
Inheritance
The Core.class function is used to create your own classes through inheritance (older versions of Gideros used the gideros.class function -- this function has now been deprecated). You can inherit from Gideros API's own classes (EventDispatcher, Sprite, etc.) or from your own classes. For example, you can create your own class that inherits from EventDispatcher class like so:
MyEventDispatcher = Core.class(EventDispatcher)
You can also create a new class by using Core.class with no arguments:
MyOwnClass = Core.class()
By using Inheritance, you can design and implement the visual elements of your game separately:
-- create your own start button class
StartButton = Core.class(Sprite)
-- create your own menu class
Menu = Core.class(Sprite)
-- create your own player class
Player = Core.class(Sprite)
function Player:walk()
-- walk logic
end
function Player:jump()
-- jump logic
end
-- create and add a player instance to the stage
stage:addChild(Player.new())
When an instance is created, init function is called to do the initialization:
Player = Core.class(Sprite)
function Player:init()
-- do the initialization of Player instance
self.health = 100
self.speed = 3
end
-- after Player instance is created, init function is called
local player = Player.new()
Whether to use inheritance or not is related to your programming taste. It's possible to implement a whole game without creating custom classes. You can refer to "Jumping Ball" and "Jumping Balls" examples to see the difference between designing your code with classes or not.
Using classes makes it easier to reuse code.
Inheriting From Classes With Constructor Arguments
If you inherit from a class that takes a constructor argument, your new class has to pass in the arguments that the base class expects as its first arguments. You can then pass in any additional arguments:
MyBitmap = Core.class(Bitmap)
function MyBitmap:init(texture, additional args)
....
end
Here's a very simple example:
A = Core.class()
function A:init(msg1)
print("A:init("..msg1..")")
end
B = Core.class(A)
function B:init(msg1, msg2)
print("B:init("..msg1..","..msg2..")")
end
B.new("hello","world")
This example produces:
A:init(hello) B:init(hello,world)
Adding Explicit Dependencies for Inherited Classes
You can put class definitions in different files, but if it doesn't work, it may be because the files are being loaded in the wrong order. You can specify file dependencies in the IDE. Select a file, right click, then select "Code Dependencies".
For example, if class A (in classA.lua) inherits from class B (in classB.lua) and you're getting a message like "attempt to index global 'B' (a nil value)" during the initialization process, select "classA.lua" and add a dependency on classB.lua.
Accessing Overridden Functions
If you inherit from a class and override one of its functions, you have to call the overridden function using syntax like "BaseClassName.function(self)". Here's an example:
-- --------------------------------------
A = Core.class()
function A:init(msg1)
self.text = "I'm an A"
end
function A:method()
print(self.text .. " in A:method()")
end
-- --------------------------------------
B = Core.class(A)
function B:init(msg1, msg2)
self.text = "I'm a B"
end
function B:method(arg1)
print(self.text .. " in B:method()")
--A:method() <--- NOT THIS
A.method(self)
end
-- --------------------------------------
b = B.new("hello","world")
b:method()
This will produce:
I'm a B in B:method() I'm a B in A:method()
If you try to use "A:method()" in "B:method()" you'll get an error about a nil value for the 'text' field. That's because you're using the "A" instance that was created in the "A = Core.class()" line (equivalent to "A.method(A)").
If you try to use "self:method()", you'll be recursively calling "B:method()"
Standard Libraries
String Manipulation
Lua has a powerful set of string manipulation functions, such as finding and extracting substrings. Unlike C, the first character of the string has "1" as the position index. You can also use negative indices in a string just like Python does - e.g the last character in a string is at position -1. You can use both methods, whichever you find convenient.
Note that the string library assumes one-byte character encodings.
The following table gives an overview of string manipulation commands. Have a look at them, and then study the examples below.
String Command | Meaning |
---|---|
string.byte (s [, i [, j]]) | Returns the internal numerical codes of the characters s[i], s[i+1], ···, s[j]. The default value for i is 1; the default value for j is i. |
string.find (s, pattern [, init [, plain]]) | Looks for the first match of pattern in the string s. If it finds a match, then find returns the indices of s where this occurrence starts and ends; otherwise, it returns nil |
string.format (formatstring, ···) | Returns a formatted version of its variable number of arguments following the description given in its first argument (which must be a string). |
string.gmatch (s, pattern) | Returns an iterator function that, each time it is called, returns the next captures from pattern over string s. |
string.len (s) | Returns the length of the string s. |
string.lower (s) | Changes all characters of a string to lowercase. |
string.upper (s) | Changes all characters of a string to uppercase. |
string.reverse (s) | Returns a string which is a reverse of the string s. |
File I/O
In an app, there always comes a point where you might want to persist some data. Persisting data in simple terms means retaining the data even after the app is exited. Some examples would include the current level, a high score, the player‘s name and so on. There could be other more complex examples where one might have levels stored in files. For all of these there’s an API, the File I/O API.
There are two ways to reference these functions, the explicit and the implicit. Explicit is when the file to be worked on is specified, it is seen in the form of file: type commands where as implicit is by passing the fileHandle to the function for file operations and are generally seen starting with io.
Example:
local playerName = "Awesome Player"
local file = io.open ( "settings.save" , "w+" )
file:write( "Playername:" , playerName )
file:close()
This has just saved a file called settings.save with the values "Playername: Awesome Player"
Now that we have some data saved in the file, let us try to read it from the file.
local playerName = nil
local file = io.open ( “settings.save”, “r” )
playerName = file:read()
file:close()
print ( “The playername saved is: ”, playerName)
Opening a File
To open a file using lua, we would use
file = io.open ( filename [, mode])
where the file called filename is opened and the file handle, a reference to this file is passed back in the variable file. The modes for opening the files can be one of the following.
- “r” read mode, this is the default mode
- “w” write mode
- “a” append mode
- “r+” update mode, all previous data is preserved
- “w+” update mode, all previous data is erased
- “a+” append update mode, previous data is preserved, writing is allowed only at the end of the file.
There is no explicit way to open a file, all files are opened using the io.open function.
Closing a File
To close a file, we would use
io.close([file])
This closes the file handle passed to the function, if nothing is passed, the default output file is closed.
The explicit way to close a file is file:close()
Getting Data/Input
In a complex application, we need to get several kinds of data from files stored on the device. In order to get data from a file, we are given a couple of options, including lines and read.
When we use the lines function, it offers an iterator function, which returns a new line from the file each time it is called. Therefore this is used as
for line in io.lines(filename) do … end
Note that this function does not require the file to be opened or closed, all of that is managed by the function internally.
There is an explicit version of the same, which is used as file:lines(), the difference between this and the implicit function is that to get the filehandle, we have to open the file, there is no parameter passed to the lines function and when the end of file is reached, the file is not automatically closed, we have to close it.
The other way to get data from a file is the read function, which is used as
data = io.read( format1, … )
The explicit equivalent is file:read( format1, … )
Since the function does not have a filehandle passed to it, io.read reads from the standard input and hence if you want to read from a file,
The formats that can be used for the function are:
- “*n” this will read a number, this returns a numeric value than a string
- “*a” this will read all the data in the file, starting from the current position.
- “*l” this will read the next line, this is the default for this command
where nn if a number is used for format, it tells the function to read those many (nn) characters from the file.
Temporary Files
Sometimes there might be the need to create a temporary file either to transfer portions of data or to save a backup copy before being certain. The easiest way to create one than trying to name the files through the code and then keeping track of the file open and close, etc is to use the io.tmpfile() function. This returns a file handle to a temporary file opened in update mode and this file is automatically removed when the app terminates.
Writing Data
To write data back to the files, it has to be opened in a mode that supports writing data to the file. The one and only way to write data is to use the function write.
io.write(value1, …)
The data passed to the function is written to the file, however since there is no filehandle passed to this function, it writes to the standard output, so if you want to write to a file, the file:write(value1, ...) function should be used.
Seeking Your Position in the File
Updating data is one of the easiest and one of the most difficult task. An easy way to update data is overwrite the entire file with the new data, but sometimes that can be very time consuming so the easier way is to position the write head to the position and then write the data there. We can get the position and set the position by using the function seek, this is an explicit function only.
position = file:seek( [mode] [,offset] )
By default, the mode is set to “cur” and the offset to 0. There are three modes which are
- “set” this is from the 0 position (start of file) and the head is moved to the position as specified by offset
- “cur” this is from the current position, so the offset is added to the current position
- “end” this is from the end of the file
so to get the size of the file, one can use
filesize = file:seek(“end”)
the set can be used to rewind the reading.
Buffering
The buffering for a file can be set by the file:setvbuf() command which is defined as
file:setvbuf( mode [, size] )
there are three modes that are available for this command:
- “no”: There is no buffering, the results are instantaneous
- “full”: The output operations occur only when the buffer is full, or when you flush
- “line”: The output is buffered on a per line basis (till a newline character is received)
The size is the size of the buffer in bytes.
Flushing Data
If you are using a buffer for writes, you might want to flush i.e. commit the file’s output buffer and save all of that to the file. Buffering is not really required in modern systems but still can be helpful at times. So before closing a file, if buffering was on, it is a good practice to flush and ensure that all the data is saved from the buffers to the file. This is an explicit function so is defined as
file:flush()
there are no parameters passed to this function.
Validating the File Operations
While working with files there are a lot of situations that could lead to errors, so it is very important to check for the return values, generally the return values are nil when the end of the file is reached or a file could not be opened, etc.
However to check the state of the file via the file handle can be with the type function as
result = io.type(fileHandle)
the return value is either “file” if the file is open, “closed” if the file is closed or nil if the fileHandle is not a file handle.
END.