gamedir
|-base
|-scripts
|-<extra dir-1>
...
\-<extra dir-n>
|
|
The full source code of this tutorial is available in the demos/tutorial directory. |
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>
|
|
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:
run the config.lua script
initialize output surface
run base/init.lua
the init script will run scripts/main.lua (if one is using the base library)
|
|
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). |
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.
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.
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.
|
|
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.
|
|
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.
|
|
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. |
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.
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.
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
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")
|
|
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.
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.
|
|
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)
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.
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.
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.
|
|
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. |
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.