Note The full source code of this tutorial is available in the demos/tutorial directory.

1. How LuaGame Works (sorta)

LuaGame is an execution environment for games. It exposes a good amount of the functionality of SDL, and extends it with a standard library. LuaGame is not a game engine. It provides enough features to easily build one, but it is much more general. This tutorial should give one a good enough understanding of the basics of writing games in LuaGame. It's recommended that one learn the basics of Lua before reading the rest of this tutorial, although it's not required.

Games in LuaGame must contain at least two files, scripts/config.lua and base/init.lua. Ideally, one will use the base library, in which case the file scripts/main.lua is also required.

The basic directory structure of a game is as follows:

 gamedir
 |-base
 |-scripts
 |-<extra dir-1>
    ...
 \-<extra dir-n>
Note
Paths

The gamedir directory is the base path of the game. All resources (scripts, images, sounds, etc…) must be referenced relative to that.

Ex: script somescript.lua is in scripts/somescript.lua

When LuaGame is executed in the gamedir directory or with the path to gamedir as an argument, it will do the following:

  1. run the config.lua script

  2. initialize output surface

  3. run base/init.lua

  4. the init script will run scripts/main.lua (if one is using the base library)

Tip
Why We Copy the base Directory

We copy the base directory (instead of installing it globally in perhaps $PREFIX/share) because it allows one to change any part of it that they feel is necessary. For example, one can extend the library or change the API and by having it separate, it keeps it from breaking compatibility with any other LuaGame-based games. Also, it's pretty small, and won't add significant weight to distributables.

Installing it globally also has the problem of making it more difficult to use LuaGame on Windows and Mac OSX (for both developer and end-user).

2. Getting Started

2.1. Initialize the Game Directory

Create a new directory and copy the base directory into it (if you want to use the base library, and you do).

Now, create the scripts directory and create a file called config.lua in it. Into that file place the following lines:

--variables that define the
--screen properties
s_width = 640
s_height = 480
s_depth = 32
s_fullscreen = 0

--the name of the game. Appears in titlebar
s_gamename = "my game"

The variable names should be self-explanatory. I'd recommend leaving s_depth at 32 because it makes life a lot easier.

Also, in scripts, create a blank file named main.lua. The directory is now set up and when run with LuaGame, it will produce a 640x480 window that will instantly disappear.

2.2. Keeping the Window Open

To keep the window open, one needs to add the main game loop to the main.lua file.

So, just add this code to main.lua:

--controls main game loop
done = false

--main game loop
while done ~= true do

end

If one runs this right now, it will produce a window that can't be closed except by interrupting (Ctrl-C) LuaGame. We're going to fix that in the next section, but we're going to reduce the CPU usage of the game first using an FPSManager.

The FPSManager is a class to handle proper delays and will attempt to run the main game loop at the chosen FPS. Add it to the code like so:

--controls main game loop
done = false

fps = FPSManager:new() -- create the FPSManager object
fps:set_fps(30) -- set the FPS to 30

--main game loop
while done ~= true do
  fps:update() -- delay to ensure proper FPS
end

The fps:update() call should/must come at the end of the main game loop. This tutorial covers frame-based games. For smoother animation, one should use deltas. LuaGames supports this, but using it is beyond the scope of this tutorial.

2.3. Introduction to Events

It's now time to add an EventManager to deal with the mouse, keyboard, and joystick events that your game will encounter. The EventManager class is designed such that one can create an arbitrary number of them and then just use them at the appropriate time.

Tip

For any real game, it's recommended that one put all declarations of EventManagers into a separate file for clarity (they can get pretty big).

Adding an EventManager is easy. Just add the following code to your main.lua above the main game loop.

evman = EventManager:new()
evman.quit = function () done = true end
evman.keyboard.pressed[Keys.ESCAPE] = function() done = true end

This creates an event manager and adds two events to it. The first event is the event fired when a user tries to close the window. The second is the event that occurs when one presses the escape key. When either event occurs, it causes the main game loop to exit. Notice, the events are assigned functions (in this case they're anonymous, but they just need to be functions). These functions are run when the events occur.

Tip

Functions are first-class values in Lua, that is, they can be passed and assigned just like any other variable. In the above example, anonymous functions are used. However, it's just as easy to use previously declared functions. See the documentation for the EventManager class for an example of using events and object methods together (HINT: Write a wrapper function.)

However, nothing's going to happen with just the declarations. We need to add a method call to the game loop to actually process any events.

So, at the beginning of the main game loop, add this line:

evman:gather_events()

That's pretty much all there is to event managers. Mouse and especially joystick handling is a little harder, but that's for another tutorial.

Tip

As is probably obvious now, making multiple event managers is an easy way to have multiple input contexts. By naming functions, common events don't need to have redundant code, and overall, it reduces the complexity of handling events.

3. Objects

3.1. Explanation of Objects

Objects in LuaGame are what one should be extending to create the actors in their games. The Object class is very much like a sprite class. It has a draw() method, an update() method, and an empty collide() method.

It is expected that all Objects (pure and inherited) have the three methods mentioned above. This comes into effect later with the ObjectList's convenience methods.

Besides those three methods, Objects have a position, heading, speed, rotation, and image.

3.2. Extending the Object

For the most part, one doesn't want to use the Object class directly. Thus, it is imperative to extend it to fit one's needs. Object Orientation in Lua is pretty simple. It's beyond the scope of this tutorial to cover the how or why it works.

Inheritance starts by making the new class be an instance of the base class, in this case Object. The type property needs to be set to the name of the new class for the sake of collision detection and just to be able to identify them since classes are just tables.

MyObj = Object:new()

MyObj.type = "MyObj"
MyObj.rects = ObjectList:new() -- we'll come back to this

The Object:new() method is what makes the object orientation work. However, we need to create a new() method for the new class to make the inheritance work.

function MyObj:new(o)
  o = o or {}
  setmetatable(o, self)
  self.__index = self
  return o
end

Due to the fact that MyObj is also an Object, the methods of Object can be run on an instance of MyObj. But, for the sake of clarity (at least for now), we'll add a MyObj:update() method.

function MyObj:update(delta)
  Object.update(self, delta)
end

For the most part, this is entirely redundant code. But it showcases two things. Number one, the update method should accept a single variable that allows for the use of time deltas (required for time-based games). Number two, accessing a function in a table using the ":" operator instead of "." passes a hidden first argument self which is the table that the function is being called on.

However, by defining the update() method we can extend it to do more than the base Object does, or we can exclude the call to Object.update(self,delta) entirely and write our own code.

3.3. Making MyObj Do More

At the moment, MyObj is basically just an Object with a different name. That's pretty boring, so we're going to make it do something a little different.

Say we want to take this lovely image images/wikilogo.png and make it move around on the screen. We need to set the MyObj.image property to be that image. So, go ahead and download that image and place it in gamedir, perhaps in data/images.

To actually load this image into MyObj, add the following code somewhere underneath the initial declaration.

MyObj.image, MyObj.w, MyObj.h = get_image("data/images/wikilogo.png")
Tip

The get_image() function loads up an image into the global image store. If the image has already been loaded, then it just returns it. To free an image from the global store, use release_image(). Note that the image is specified by its path relative to gamedir. To prevent a new copy from being loaded the exact same path must be used for all references to the image.

Now that the image is loaded, all that's left to do is make it move around in an interesting way. The easiest interesting thing to do is make it bounce around, so we're going to do just that. It will bounce off the edges of the screen from the center. The MyObj:update() method is now going to look like this:

function MyObj:reflect_x()
  self.heading = (-1 * self.heading) + 180
  self.heading = self.heading % 360
end

function MyObj:reflect_y()
  self.heading = -1 * self.heading
  self.heading = self.heading % 360
end

function MyObj:update(delta)
  Object.update(self, delta)

  --this code is what makes it bounce
  local cx, cy = Object.get_center(self)

  if cx <= 0 then
    self.x = 0 - math.floor(self.w/2)
    self:reflect_x()
  end

  if cx >= s_width then
    self.x = s_width - math.floor(self.w/2)
    self:reflect_x()
  end

  if cy <= 0 then
    self.y = 0 - math.floor(self.h/2)
    self:reflect_y()
  end

  if cy >= s_height then
    self.y = s_height - math.floor(self.h/2)
    self:reflect_y()
  end
end

For the purpose of reducing clutter, we've also added two methods to handle angle of reflection.

For now, we're done with MyObj. We'll be coming back to it shortly.

4. Putting it Together

4.1. Making a MyObj

Now that we've got a basic object, we're going to actually do something with it. So, we need to create one and add it to the main game loop. Place the following code somewhere above the loop.

--create a MyObj
mo = MyObj:new()
math.randomseed(os.time())
mo.heading = math.random(15,75) -- prevent shallow angles
mo.speed = 3
mo.angular_velocity = 8

What this does, is create a new object, assign a random heading to it, give it a speed of 3 pixels per frame, and an angular velocity of 8 degrees per frame. Angular velocity is how fast the image will rotate.

Note
About Rotation

When rotating an image, if it is a transparent PNG, it needs to be 32-bit, otherwise there will be an ugly background that rotates along with it.

If you were to run the game now, nothing would happen. That's because the object isn't being drawn to the screen. By adding three lines in the main game loop, we can remedy this.

  mo:update()
  mo:draw()
  update_screen()

The first line updates the object, the second draws it, and the third updates the screen so that everything that was drawn is visible. If we didn't update the screen then it would stay black and that's not very fun at all.

Running the code now should produce something that vaguely resembles modern art. This isn't really what we wanted. We wanted just the object, not the trail it leaves (however pretty). So, we need to add a line to the game loop before we draw the object to clear the screen to black.

  fill_screen(0,0,0)

4.2. Making Many MyObj's

Now that we've made one, why not make a few more? The main.lua should now look like the code below.

evman.quit = function () done = true end
evman.keyboard.pressed[Keys.ESCAPE] = function() done = true end

--controls main game loop
done = false

fps = FPSManager:new() -- create the FPSManager object
fps:set_fps(30) -- set the FPS to 30

--create a MyObj
mo = MyObj:new()
math.randomseed(os.time())
mo.heading = math.random(15,80) -- prevent shallow angles
mo.speed = 3
mo.angular_velocity = 5

--create another MyObj
mo1 = MyObj:new()
mo1.heading = math.random(15,80)
mo1.speed = 3
mo1.angular_velocity = 5

--create yet another MyObj
mo2 = MyObj:new()
mo2.heading = math.random(15,80)
mo2.speed = 3
mo2.angular_velocity = 5

--main game loop
while done ~= true do
  evman:gather_events()

  mo:update()
  mo1:update()
  mo2:update()

  fill_screen(0,0,0)

  mo:draw()
  mo1:draw()
  mo2:draw()

  update_screen()
  fps:update() -- delay to ensure proper FPS
end

When the game is run, three MyObj's will be bouncing around the screen. This is certainly not a game (yet) and for the purposes of this tutorial, it won't be. Yeah, sorry about that. This tutorial's just to familiarize one with the basics of LuaGame.

4.3. Organization Through ObjectList

We have three MyObj's now, and if we wanted to add more, the main loop would become cluttered pretty quickly. So, there's a very useful and flexible class called the ObjectList. It is basically a list that stores anything, but it has convenience methods for Object's for updating and drawing. It also makes doing collision detection between a large number of objects much easier.

So, we need to create an ObjectList and add the three objects to it. Add this code after the declaration of the three objects.

--create an ObjectList
olist = ObjectList:new()
olist:push_back(mo)
olist:push_back(mo1)
olist:push_back(mo2)

Now, replace the three update calls with:

  olist:update_all()

And replace the draw calls with:

  olist:draw_all()

With that, one now knows the basics of the ObjectList. If one has ever used a C++ STL list, the methods are basically the same.

4.4. Silly Collision Detection

This is the last part of the tutorial, and it deals lightly with collision detection in a very silly example. Since we actually want to have something happen when two MyObj's collide, we need to add a method to the definition.

--silly
function MyObj:collide(ids, object)
  print("Ouch! You hit me, "..object.type.." at "..os.time().."!")
end

This will print out a silly message when two objects collide. However, nothing's going to happen yet, at least not until we add a collision check to the main loop. Add it right before the update call.

  check_collisions_lists(olist, olist)

Remember way back when we defined the MyObj class? We defined the rects property to be an empty ObjectList. This is because we only want to deal with the overall bounding box, and MyObj doesn't have any bounding sub-rectangles.

Tip
Less Silly Collisions

As an exercise to the reader, implement the objects bouncing off each other. This will require looking at more than the type of the object that has collided. Also, for the sake of having professional looking bounces, one can do a great deal of code to handle objects that are intersecting, or just move their starting positions so that they don't intersect.

5. End

That's it. One should now have a good enough understanding to be able to make some small games, and with a little more exploration, it should be easy to make much larger things. Oh, and now that one knows how to do the basic setup of a game, the livecode template in the demos directory provides a nice template for a simple game.