• entries
455
639
• views
424569

# Lua Side Scrolling Shoot'em Up Version 1.0

269 views

So, huzzah! I've finally got down and finished Version 1.0 of the Lua based side scrolling shoot'em up. It was mostly done, I had to generate the fonts and then 'tweak' things a bit from the changes between the Java engine and the Lua engine it's now using.

The key point about this project is that the vast majority of this game is written in Lua; main loop, timing and even event responce is in Lua.

There is still a C++ backend but that deals with such nice things as
- drawing
- window setup
- event pumping to populate a Lua table of events

As the game is at the 1.0 mark I thought I'd take this time to talk about how it's all put together and how Lua and the C++ backend interact.

Startup

The old version of the engine which powered this game was a Java app, this was due to assignment constraints at the time. While the main game logic was handled in Lua still the main loop and event responce was all Java side.

Upon startup the Java code first registered an instance of the 'engine' class with Lua and then loaded the main lua game file (j3su_startup.lua) and executed it in order to load everything up and register 3 key functions;

- initalise
- update
- render

The java code then dropped into a classic main loop, letting the event handling built into Java to update the keyboard state when keys were pressed.

The new version chagnes things slightly; while there is still a 'startup.exe' to get things going this is infact optional; you could run the program via lua.exe as all the startup.exe does is initalise Lua and load 'startup.lua' to kick things into life. Once this file returns the application exits and everything is done.

As I already had a game setup I decided to use the 'startup.lua' to setup a sane enviroment for the game to run in, the idea being to try to minmise the changes required to the main game code.

To that end the first thing the lua file does is load the backend engine, initalise the graphics subsystem and create a window;
require "engine"screenWidth = 800screenHeight = 600print("Setting up graphics subsystem...")gfxengine = bonsai.createGraphics()gfxengine:createWindow({width = screenWidth, height = screenHeight, fullscreen=false, clearColor = {r = 0, g = 0, b = 0, a = 1}})gfxengine:setAlphaTest(0.0)gfxengine:enableAlphaTest(true)gfxengine:setFontDirectory("data")

The final function it performs is telling the gfx subsystem where to load font data from, this is needed due to changes in how fonts were loaded in Bonsai during this clean up (more on that later).

Another thing which was required for the game to work was a method of handling the keyboard; the old code simply queried a keyboard 'object' for the state of various keys and reacted as required, so with that in mind the following was constructed in Lua;
Keyboard = {upkeydown = false, downkeydown = false, firekeydown = false, anykeydown = false}function Keyboard:CheckAnyKey()	if self.anykeydown then 		self.anykeydown = false 		self.upkeydown = false		self.downkeydown = false		self.firekeydown = false		return true 	end		return falseendfunction Keyboard:CheckUpKey()	if self.upkeydown then return true end		return falseendfunction Keyboard:CheckDownKey()	if self.downkeydown then return true end		return falseendfunction Keyboard:CheckFireKey()	if self.firekeydown then return true end		return falseendfunction Keyboard:ProcessEvent(e)	if e.message == bonsai.Event.types.keydown	then		if e.param1 == bonsai.Event.keycodes["up"]		then			self.upkeydown = true		elseif e.param1 == bonsai.Event.keycodes["down"]		then 			self.downkeydown = true		elseif e.param1 == bonsai.Event.keycodes["ctrl"]		then			self.firekeydown = true		end		self.anykeydown = true	elseif e.message == bonsai.Event.types.keyup	then		if e.param1 == bonsai.Event.keycodes["up"]		then			self.upkeydown = false		elseif e.param1 == bonsai.Event.keycodes["down"]		then 			self.downkeydown = false		elseif e.param1 == bonsai.Event.keycodes["ctrl"]		then			self.firekeydown = false		end		self.anykeydown = false	end		endkeyboard = Keyboard

The first line tells Lua we want a table called 'Keyboard' and that is should contain a certain set of Boolean variables.
After that we declare some functions which will work on the keyboard object. The 'table:function' syntax is Lua's method of doing C++ like OOP; as you know when you call class.function() the first parameter pushed is the address of the class so that per-class data can be accessed. With Lua doing 'table:function' sets up the same system, where by the first parameter passed is a reference to an instance of the table, which in code is accessed via 'self'.

The final function reacts to the events which are processed in the main loop, setting the key state as required; atm this is hard coded to the up and down arrow keys and the ctrl key; I've also invented the 'any key' to make life easier in situations where you don't care what key is pressed.

After all this is setup we simply 'require "l3su_startup" in order to load the game in and have it set itself up just as before. Then a 'quit' flag is set, the game state is initialised and we enter into the main loop, which is fixed time based;

lastupdate = 0while (quit == false)do	events = bonsai.getEvents()	for i = 1, #events 	do		if events.message == bonsai.Event.types.quit 		then 			quit = true 		elseif events.message == bonsai.Event.types.keydown		and events.param1 == bonsai.Event.keycodes["esc"]		then			quit = true		end 		keyboard:ProcessEvent(events)	end		currentTime = bonsai.currentTimeMillis()	if lastupdate + 20 < currentTime	then		update(currentTime)		lastupdate = currentTime	end	gfxengine:startFrame()	render()	gfxengine:endFrame()	end

The structure is pretty easy to follow;
1) Any outstanding events are processed. bonsai.getEvents() returns a table containing tables which hold event details.
2) Check to see if it's time for an update; the game updates every 20ms (give or take, the timeperiod is set to 1ms on Windows machines)
3) Finally the frame is rendered

Once this loop exits the inital Lua call we made in 'startup.exe' completes and the application quits.

l3su_startup.lua

This file is where the "magic" happens, or so to speak.

The game is broken into a series of 'screens';
- instructions
- game
- game over

The code for each of these is loaded at the start of file and instances of each object is constructed, each one filling out an internal table indictating a function which should be used to;
- initalise the screen
- update the screen
- render the screen
- check the state of the screen

A progression table is then constructed, showing which state should follow the next. This allows the game to not really care about what screens need to be shown.

Finally, 3 functions are defined;
- one to initialise the game to a default state
- one to deal with updates and state swapping
- one to render

The middle of the 3 is the most intresting as it calls a function on the current state to see if it is time to swap to the next state; once its told it is moves to the next state, initalises it and then runs its update;
function update(time)	if progression[progression.current]:checkState()	then		progression.current = progression.current + 1		if progression.current > #progression		then			progression.current = 1		end		progression[progression.current]:init()	end	progression[progression.current]:update(time)end

For this game the progression is circular, in that when we go beyond the current avaliable states we wrap around (in Lua # is a length of table operator, so in this case it returns 4 as we have 4 screens or states).

The render function simply tells the current state to draw itself.

Drawing the main game

The biggest changes from the orignal Java version is how things are drawn; in the Java version we would loop over each alive sprite and make a draw call for each one. This meant that we were swapping between Java and Lua quite often, and while this wasn't the main bottleneck in that version (sucky Java2D usage was it seems) this still isn't a good idea.

I'd considered this when designing the API for Bonsai and instead of each draw call taking a table with details in it instead takes a table of tables, with each sub table containing all the details required to render a sprite.

This meant that the 'render' function for each in game object (player, enemy and bullets) no longer drew themselves; instead they returned a table with information in and this table was added to a drawing list and the whole lot was rendered in one go.
renderlist = {}		-- compile the render list	playersprite = self.player:render()	if playersprite ~= nil then 		table.insert(renderlist, playersprite)	end		for k,enemy in ipairs(self.aliveenemies) do		table.insert(renderlist, enemy:render())	end		for k,enemy in ipairs(self.dyingenemies) do		table.insert(renderlist, enemy:render())	end		for k,bullet in ipairs(self.playerbullets) do		table.insert(renderlist,bullet:render())	end	for k,bullet in ipairs(self.enemybullets) do		table.insert(renderlist,bullet:render())	end		gfxengine:renderSprites(renderlist)

Well, I say 'in one go' the Bonsai backend still uses glVertex et al to draw things and doesn't do any texture batching at all (although code exists to deal with redudant state changes). As yet I don't know how I'm going to replace this; a vertex array solution will happen when the backend gets ported to OpenGL3.0, however how it's handled depends on how things are changed in the sprite framework.

// Current Bonsai Sprite rendering codeint l_renderSprites(lua_State *L){	gfxEngineDetails * engine = CheckValidGraphicsObject(L);	// Basically, loop over each table and construct the information required to render a quad	// Table is at position -1	lua_pushnil(L);	// starting the iteration	int currentTexture = -1;	while(lua_next(L,-2))	// table is the 2nd arg pushed	{		// Check value type as position -1 is a table		if(lua_istable(L,-1))		{			float x = ::Utility::getNumberFromTable(L,"x");			float y = ::Utility::getNumberFromTable(L,"y");			float currentframe = ::Utility::getNumberFromTable(L,"currentFrame");			lua_pushstring(L,"sprite");			lua_gettable(L,-2);			float imgwidth = ::Utility::getNumberFromTable(L,"width");			float imgheight = ::Utility::getNumberFromTable(L,"height"); 			float framewidth = ::Utility::getNumberFromTable(L,"frameWidth");							int textureId = ::Utility::getIntegerFromTable(L,"textureId");			if(textureId != 0)			{				if(textureId != currentTexture)				{					glEnable(GL_TEXTURE_2D);					glBindTexture(GL_TEXTURE_2D, textureId);					glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE,GL_MODULATE);					currentTexture = textureId;				}			}			else			{				glDisable(GL_TEXTURE_2D);			}			float step = (framewidth/imgwidth);			float offset = step * currentframe;			glBegin(GL_QUADS);					glTexCoord2f(offset		, 0.0f);	glVertex2f(x		  , y			);	// top left			glTexCoord2f(offset		, 1.0f);	glVertex2f(x		  , y + imgheight	);	// bottom left			glTexCoord2f(offset + step	, 1.0f);	glVertex2f(x + framewidth , y + imgheight	);  // bottom right			glTexCoord2f(offset + step	, 0.0f);	glVertex2f(x + framewidth , y			);	// top right			glEnd();			lua_pop(L,1);	// clean the 'sprite' table from the stack		}		lua_pop(L,1);	// remove the value and leave key for next iteration	}	return 0;}

Font Rendering

As noted above, one of the things the system does on startup is tell the graphics subsystem where font data is found. The reason for this is simple; I wanted the font loader to automagically load fonts just based on a name.

As I've mentioned in another entry Bonsai uses bitmap fonts which are generated by the Bitmap Font Maker at Angel Code. Before this revision to load the font you would have to supply a 'base name' and then the extension of the file to be loaded with the graphics in. The meant function calls like this;

However, I considered it daft that I had to supply all that information, more so if I happened to swap how the bitmap data was being stored. A quick investigation of the generated 'fnt' files however yeilded a solution; it notes which images file goes with which page of characters. So, a quick change to the parser to extract this data and we now pull the filename from the fnt file. This means the function call above now becomes;

and it does the Right Thing(tm). However this still left the problem of directories, as I would have liked to keep the fonts in the 'data' directory along with everything else. I considered including it in the font name, so the call would be;

However that felt clunky and I'd have to extract the 'data\' portion for file loading. No, instead the setFontDirectory() function was born to allow it to work as before.

This is probably only a tempory fix however as I plan to intergrate PhysFS into Lua at some point which means its path searching will replace the above for finding files.

The Game in general

The rest of the game functions much like any other game loop;

The update loop first checks to see it its time to spawn a new enemy, if so it selects a random location along the top of the playing field and spawns a new enemy there, inserting it into the 'alive' list.

Then the update function does a check for all the players bullets vs all the enemies, it does this by first of all removing the bullet from the 'alive' list, checking against the enemies and reinserting it if it wasn't found. This is then repeated for enemy bullets vs the player.

After that the update functions are called for the player, each enemy and all the bullets. Finally any bullets which are off screen are culled. The final part of this might need some optimisation as it goes over the first list to see who is off screen, makes a note of their key in another list and then goes over that list to remove the bullets from the first. I believe when I wrote the first version I had a better solution but it relied on a Lua5.1 feature and I was suck on Lua5.0.

The enemies AI is pretty... well, lacking the 'I' part really; they simply bounce around an invisable box randomly firing a rocket based on a random number generator.

That's all folks

That's pretty much it for the general over view of how things work right now. There are still plenty of things to be done however;

- General polishing of things, as right now it looks a bit crappy [grin]
- More input options, such as my XBox360 joypad!
- Sound!
- A proper menu with options.
- Generally improve things

The 2nd and 3rd require additions to Bonsai, however now I've got a basic game to work with I can use it to test them out.

I also need to look at the package system for Lua a bit closer to work out how to get all of Bonsai into a Bonsai subdirectory and working correctly; I suspect part of it is going to be dll troubles.

Anyways, L3SU can be downloaded in it's current state from here. Yes, I know the title gfx still says "Java", I'll make a new shiney one at some point... honest [grin]

Controls;
Arrow up = move up
Arrow down = move down
Ctrl = fire
Esc = quit

If you want to play fullscreen then open up 'startup.lua' and change 'fullscreen=false' to 'fullscreen=true'.

One note on system requirements; You are going to need a card which can do OpenGL2.0 and handle, in some form, non-power-of-two textures. The game does the best not to drop to Software mode with them by disabling filtering if it can't find true non-power-of-two support, just GL2.0. It works fine on my X1900XT, so I suspect it'll work fine on any DX9 card.

At some point the whole sprite system will get reworked so instead of loading a texture per sprite things are in a sprite sheet, but that'll require some kind of storage system so I need to work on that.

Anyways, all feed back appriated as always [smile]

## 1 Comment

Very cool.

I am a lua convert too, its a great language. What I love most is ease of change. In your game for example, the bullets are a bit on the slow side. It only took a short search to find this magical constant (2) and ramp it up to a more action paced value. Of course, making it much faster made them impossible to dodge. Again, if I liked I could change the speed of the player and enemies too I suppose. Point made I hope [smile]. This is the reason why I use lua in my games (although I use it for less of the total game code than you).

It is a fun game though, tinkering aside!

## Create an account

Register a new account