Blog Archive

Tag Cloud


Moai Tutorials: Bug Squisher

On today’s tutorial, we’re going to make a game! Well, we’re going to make the base of a game at least. Today’s game is going to be “Bug Squish”. Several bugs are going to appear on the screen and it’s up to the player to squish them all. Download the image files here.

Initial Game Setup

The first step, and the easiest, is just creating our window and placing our first bug. If you’ve read our previous tutorials, this will all seem very simple.

-------------------------------
-- Bug Squishing Game
-------------------------------

-------------------------------
-- Initial Setup
-------------------------------

MOAISim.openWindow ( "Bug Squisher", 640, 960 )

viewport = MOAIViewport.new ()
viewport:setScale ( 640, 960 )
viewport:setSize ( 640, 960 )

layer = MOAILayer2D.new ()
layer:setViewport ( viewport )
MOAISim.pushRenderPass ( layer )

gameOver = false

-------------------------------
-- Bug Rig
-------------------------------

bugGfx = MOAIGfxQuad2D.new ()
bugGfx:setTexture ( "bug.png" )
bugGfx:setRect ( -64, -64, 64, 64 )
function spawnBug ()

	local bug = MOAIProp2D.new ()
	bug:setDeck ( bugGfx )
	layer:insertProp ( bug )
end

-------------------------------
-- Game Thread
-------------------------------

spawnBug ()

We created our window, viewport, layer, gameOver bool, our bug creation function, and then we’ve placed the bug into the game. Run it, and you’ll get:

Input

Now that we’ve got our bug, it’s time to squish him. We’ll need to make a few changes to our bug first:

 layer = MOAILayer2D.new ()
layer:setViewport ( viewport )
MOAISim.pushRenderPass ( layer )

partition = MOAIPartition.new ()
layer:setPartition ( partition )

-------------------------------
-- Bug Rig
-------------------------------

bugGfx = MOAIGfxQuad2D.new ()
bugGfx:setTexture ( "bug.png" )
bugGfx:setRect ( -64, -64, 64, 64 )

function spawnBug ()

	local bug = MOAIProp2D.new ()
	bug:setDeck ( bugGfx )
	partition:insertProp ( bug )

	function bug:destroy ()
	
		layer:removeProp ( self )
		self = nil	
	end
end

We’ve created a partition and placed it onto the layer, and then placed the bug onto the partition. This will help us in just a few seconds, when we put in our input. After the partition we create our destroy function for the bug. As soon as this is called, we’ll remove the bug from the layer. And how will we call that? With our input. If you remember from our user input tutorial, unless you’re using callbacks (we aren’t for this example), you’ll need to have a thread set up. So here’s our game thread with input.

 
-------------------------------
-- Game Thread
-------------------------------

spawnBug ()

mainThread = MOAIThread.new ()

mainThread:run (
	function ()

		while not gameOver do
			coroutine.yield ()
	
			if MOAIInputMgr.device.pointer then				
				mouseX, mouseY = layer:wndToWorld (  MOAIInputMgr.device.pointer:getLoc () )
			end
				
			if MOAIInputMgr.device.mouseLeft:down () then				
				local obj = partition:propForPoint ( mouseX, mouseY )
				
				if obj then					
					obj:destroy ()
				end
			end
		end
	end
)

The one thing that we’ve added here that we haven’t covered in any of our other tutorials is “propForPoint”, a function of MOAIPartition. This function will return a prop with the highest priority at the location given. Since we’re feeding it our X and Y coordinates, that will give us the prop that we’re clicking on. And with that prop, we will then destroy it. After running this, and clicking on the bug, it will disappear.

One bug is rather lonely, though, so we should spawn a bunch of them.

Spawning a swarm of bugs

We’ll need a way to count our starting bugs, and also a way to stop our bugs from spawning infinitely. We can do this by adding a startBugs variable and a maxBugs variable. Fifty sound like a good number for a swarm of bugs.

 gameOver = false
startBugs = 0
maxBugs = 50

And now, inside our spawnBug function, we’ll add a counter to increase the startBugs count, and also make it so the bugs spawn at a random location so they aren’t all at the center.

function spawnBug ()

	local bug = MOAIProp2D.new ()
	local randX = math.random ( -275, 275 )
	local randY = math.random ( -350, 300 )
	bug:setDeck ( bugGfx )
	bug:setLoc ( randX, randY )
	partition:insertProp ( bug )
	startBugs= startBugs+ 1

	function bug:destroy ()

		layer:removeProp ( self )
		self = nil	
	end
end

Now that we’ve got our counters all set, it’s time to throw them into the main game loop. We’ll go ahead and remove the “spawnBug ()” that’s outside the thread, since we’re going to be doing it multiple times.

 
-------------------------------
-- Game Thread
-------------------------------
mainThread = MOAIThread.new ()

mainThread:run (
	function ()
	
		while not gameOver do

			coroutine.yield ()		
					
			if MOAIInputMgr.device.pointer then				
				mouseX, mouseY = layer:wndToWorld (  MOAIInputMgr.device.pointer:getLoc () )
			end
				
			if MOAIInputMgr.device.mouseLeft:down () then				
				local obj = partition:propForPoint ( mouseX, mouseY )
				
				if obj then					
					obj:destroy ()
				end
			end
			
			if totalBugs < maxBugs then
				spawnBug ()
			end
		end
	end
)

We’ve set it up so that if our startBugs is less than our maxBugs, it will spawn more bugs. Pretty simple. Run this and you’ll get a screen similar to this:

Click away and squish the bugs. So we have the basis for a game, but there’s no challenge right now. The quickest way we can do this is by adding in a timer.

Timer

This step is going to be a little more involved. We’re going to use a rudimentary timer with a frame countdown system, and we’re also going to create a background layer to place textboxes onto. Going from to bottom, we’ll start by setting our game’s frame size to 60 frames per second. This is done with the MOAISim.setFrameSize function. We’ll just place this right under our openWindow.

MOAISim.openWindow ( "Bug Squisher", 640, 960 )
MOAISim.setFrameSize ( 1 / 60 )

setFrameSize will set the amount of time it takes, in seconds, for one frame to pass. After that, we will create a new layer, called bglayer, and render it before our original layer.

 bglayer = MOAILayer2D.new ()
bglayer:setViewport ( viewport )
MOAISim.pushRenderPass ( bglayer )

layer = MOAILayer2D.new ()
layer:setViewport ( viewport )
MOAISim.pushRenderPass ( layer )

And now, right below our gameOver, we’ll create a variable named “startTime” and set it to false.

gameOver = false
startTime = false
startBugs = 0

We’ll use this startTime to begin the timer after all the bugs have spawned in. Next up will be our timer set up.

---------------------------------
-- Timer
---------------------------------

timeBox = MOAITextBox.new ()
timeBox:setFont ( font )
timeBox:setTextSize ( font:getScale ())
timeBox:setRect ( -125, 50, 125, -50 )
timeBox:setLoc ( -50, -350 )
timeBox:setYFlip ( true )
timeBox:setString ( "Time left: " )

timeBoxTime = MOAITextBox.new ()
timeBoxTime:setFont ( font )
timeBoxTime:setTextSize ( font:getScale ())
timeBoxTime:setParent( timeBox )
timeBoxTime:setYFlip ( true )
timeBoxTime:setTraitMask ( MOAITraits.INHERIT_PARTITION + MOAITraits.INHERIT_LOC )
timeBoxTime:setRect ( -50, 50, 50, -50 )
timeBoxTime:setLoc ( 200, 0 )
timerFrames = 900

In the first chunk we set up our two timer textboxes; the first will say “Time left :” and the second will constantly refresh with the current time left. After that, we set our initial timer frames to be 900 ( 15 seconds ). Those out of the way, we then get to our timer.

 function timer ()
	
	local secondsRemain = math.floor ( timerFrames/60)

	if startTime == true then
		timerFrames = timerFrames - 1
		timeBoxTime:setString ( tostring ( secondsRemain ) )
		bglayer:insertProp ( timeBox )
		if timerFrames <= 0 then
			bglayer:removeProp ( timeBox )
			gameOver = true 
			gameOverTextbox:setString ( "Time Up!" )
			gameOverTextbox:setLoc ( 0, -350 )
		end
	end
end

First thing we do is create our remaining seconds variable. With math.floor, we divide the timerFrames by 60 (for frames per second). math.floor will give us a whole number, since we aren’t really interested in decimal places for this. After that, we’ll start our timer countdown and inserting our textboxes (onto the bglayer) once startTime has been set to true (we’ll set it in the main thread). We set our timerFrames to decrease by 1 every time the function is called, and set our timeBoxTime string as secondsRemain. Then the textboxes get inserted into the game. Once our timerFrames hits zero, we set gameOver to true and then set the gameOverTextbox string to “Time up!” Now we move to the main game thread.

 -------------------------------
-- Game Thread
-------------------------------

gameOverTextbox = MOAITextBox.new ()
gameOverTextbox:setFont ( font )
gameOverTextbox:setTextSize ( font:getScale ())
gameOverTextbox:setRect ( -250, 50, 250, -50)
gameOverTextbox:setLoc ( 0, 0 )
gameOverTextbox:setYFlip ( true )
gameOverTextbox:setString ( "Bugs Squished!" )

mainThread = MOAIThread.new ()

First, we’re just creating our gameOverTextbox. After that we just call our timer, and after gameOver is true, we remove the timer textboxes and insert our gameOverTextbox.

mainThread:run (

	function ()
	
		while not gameOver do
		
			timer ()

			coroutine.yield ()		
				
			if MOAIInputMgr.device.pointer then				
				mouseX, mouseY = layer:wndToWorld (  MOAIInputMgr.device.pointer:getLoc () )
			end
				
			if MOAIInputMgr.device.mouseLeft:down () then				
				local obj = partition:propForPoint ( mouseX, mouseY )
				
				if obj then					
					obj:destroy ()
				end
			end
			
			if startBugs < maxBugs then
				spawnBug ()
			elseif startBugs == maxBugs then
				startTime = true
			end
		end
		
		bglayer:insertProp ( gameOverTextbox )

	end
)

After all of that, we’re finally ready to play! Launch away and start clicking.

Now, after playing a few times, you may have noticed two things. One, the game can be difficult to click all the bugs in the short amount of time. Two, if you did manage to click all the bugs before the countdown finishes, you’ll notice that the countdown timer doesn’t stop counting down. This can be fixed pretty easily by adding a “totalBugs” variable in; when a bug gets spawned, we add to that, and when it gets destroyed, we’ll take away from it. If totalbugs is 0, then we’ll call gameOver. So the changes we have are:

 gameOver = false
startTime = false
startBugs = 0
maxBugs = 50
totalBugs = 0 
 function spawnBug ()

	local bug = MOAIProp2D.new ()
	local randX = math.random ( -275, 275 )
	local randY = math.random ( -350, 300 )
	bug:setDeck ( bugGfx )
	bug:setLoc ( randX, randY )
	partition:insertProp ( bug )
	startBugs = startBugs + 1
	totalBugs = totalBugs + 1
	
	function bug:destroy ()
	
		layer:removeProp ( self )
		self = nil
		totalBugs = totalBugs - 1
		
	end
end 
if startBugs < maxBugs then
				spawnBug ()
			elseif startBugs == maxBugs then
				startTime = true
			end
			
			if totalBugs == 0 then
				gameOver = true
			end

One last thing to do is take care of the first problem I mentioned up there ( too difficult ). This can be done a variety of ways, from increasing the amount of time, decreasing the amount of bugs, or my personal favorite, including a powerup.

Clock Powerup

The clock powerup is going to be handled pretty simply. We first create a timeOrbCounter to increase when we destroy a bug, then we’ll create the spawnTimeOrb function, and then when the timeOrb is destroyed, we’ll add 300 frames (five seconds) to the timer.

 	function bug:destroy ()
	
		layer:removeProp ( self )
		self = nil
		totalBugs = totalBugs - 1
		timeOrbCounter = timeOrbCounter + 1
		
	end

First, the timeOrb Counter is added to the bug:destroy function. Then we get into the time orb setup.

------------------------------
-- Time Orb 
------------------------------


timeOrbGfx = MOAIGfxQuad2D.new ()
timeOrbGfx:setTexture ( "clock.png")
timeOrbGfx:setRect ( -64, -64, 64, 64 )
timeOrbCounter = 0

function spawnTimeOrb ()

	local randX = math.random ( -275, 275 )
	local randY = math.random ( -325, 325 )
	local timeOrb = MOAIProp2D.new ()
	timeOrb:setDeck ( timeOrbGfx )
	timeOrb:setLoc ( randX, randY )
	partition:insertProp ( timeOrb )
	
			
	function timeOrb:destroy ()
	
		layer:removeProp ( self )
		self = nil
		timerFrames = timerFrames + 300
	end
end

We’ll first set the timeOrbCounter to 0. Then the spawn function is pretty much like the bug’s.

 			if startBugs < maxBugs then
				spawnBug ()
			elseif startBugs == maxBugs then
				startTime = true
			end
			
			if timeOrbCounter == 15 then			
				spawnTimeOrb ()
				timeOrbCounter = 0
			end

Once our timeOrbCounter hits 15, we’re going to spawn a timeOrb, and then set the counter back to 0. At this point, we’re almost done, there’s just one more thing I want to clean up. You may have noticed that if you destroy all the bugs while there’s still a clock on the screen, the clock won’t go away… To fix this, we’ll just do a very simple partition clear right before the gameOverTextbox is inserted.

		partition:clear ()
		bglayer:removeProp ( timeBox )
		bglayer:insertProp ( gameOverTextbox )

And there’s our bug squishing game! There’s quite a few ways that this can be added to; more bugs, different power ups, change the game so that the bugs spawn constantly and you go for a high score instead of just squishing every bug. But for now, we have a very solid base to grow upon. Here's the entirety of our code.

-------------------------------
-- Bug Squishing Game
-------------------------------

-------------------------------
-- Initial Setup
-------------------------------

SCREEN_WIDTH = 640
SCREEN_HEIGHT = 960

MOAISim.openWindow ( "Bug Squisher", 640, 960 )
MOAISim.setFrameSize ( 1 / 60 )

viewport = MOAIViewport.new ()
viewport:setScale ( 640, 960 )
viewport:setSize ( 640, 960 )

viewport = MOAIViewport.new ()
viewport:setScale ( 640, 960 )
viewport:setSize ( 640, 960 )

bglayer = MOAILayer2D.new ()
bglayer:setViewport ( viewport )
MOAISim.pushRenderPass ( bglayer )

layer = MOAILayer2D.new ()
layer:setViewport ( viewport )
MOAISim.pushRenderPass ( layer )

partition = MOAIPartition.new ()
layer:setPartition ( partition )

charcodes = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 .,:;!?()&/-'

font = MOAIFont.new ()
font:loadFromTTF ( 'arial-rounded.ttf', charcodes, 24, 163 )

gameOver = false
startTime = false
startBugs = 0
maxBugs = 50
totalBugs = 0

-------------------------------
-- Bug Rig
-------------------------------

bugGfx = MOAIGfxQuad2D.new ()
bugGfx:setTexture ( "bug.png" )
bugGfx:setRect ( -64, -64, 64, 64 )

function spawnBug ()

	local bug = MOAIProp2D.new ()
	local randX = math.random ( -275, 275 )
	local randY = math.random ( -350, 300 )
	bug:setDeck ( bugGfx )
	bug:setLoc ( randX, randY )
	partition:insertProp ( bug )
	startBugs = startBugs + 1
	totalBugs = totalBugs + 1
	
	function bug:destroy ()
	
		layer:removeProp ( self )
		self = nil
		totalBugs = totalBugs - 1
		timeOrbCounter = timeOrbCounter + 1
		
	end
end

------------------------------
-- Time Orb 
------------------------------


timeOrbGfx = MOAIGfxQuad2D.new ()
timeOrbGfx:setTexture ( "clock.png")
timeOrbGfx:setRect ( -64, -64, 64, 64 )
timeOrbCounter = 0

function spawnTimeOrb ()

	local randX = math.random ( -275, 275 )
	local randY = math.random ( -325, 325 )
	local timeOrb = MOAIProp2D.new ()
	timeOrb:setDeck ( timeOrbGfx )
	timeOrb:setLoc ( randX, randY )
	partition:insertProp ( timeOrb )
	
			
	function timeOrb:destroy ()
	
		layer:removeProp ( self )
		self = nil
		timerFrames = timerFrames + 300
	end
end

---------------------------------
-- Timer
---------------------------------

timeBox = MOAITextBox.new ()
timeBox:setFont ( font )
timeBox:setTextSize ( font:getScale ())
timeBox:setRect ( -125, 50, 125, -50 )
timeBox:setLoc ( 0, -350 )
timeBox:setYFlip ( true )
timeBox:setString ( "Time left: " )

timeBoxTime = MOAITextBox.new ()
timeBoxTime:setFont ( font )
timeBoxTime:setTextSize ( font:getScale ())
timeBoxTime:setParent( timeBox )
timeBoxTime:setYFlip ( true )
timeBoxTime:setTraitMask ( MOAITraits.INHERIT_PARTITION + MOAITraits.INHERIT_LOC )
timeBoxTime:setRect ( -50, 50, 50, -50 )
timeBoxTime:setLoc ( 200, 0 )

timerFrames = 900

function timer ()
	
	local secondsRemain = math.floor ( timerFrames/60)
	if startTime == true then
		timerFrames = timerFrames - 1
		timeBoxTime:setString ( tostring ( secondsRemain ) )
		bglayer:insertProp ( timeBox )
		if timerFrames <= 0 then
			gameOver = true 
			gameOverTextbox:setString ( "Time Up!" )
			gameOverTextbox:setLoc ( 0, -350 )
		end
	end
end

-------------------------------
-- Game Thread
-------------------------------
gameOverTextbox = MOAITextBox.new ()
gameOverTextbox:setFont ( font )
gameOverTextbox:setTextSize ( font:getScale ())
gameOverTextbox:setRect ( -250, 50, 250, -50 )
gameOverTextbox:setLoc ( 0, 0 )
gameOverTextbox:setYFlip ( true )
gameOverTextbox:setString ( "Bugs Squished!" )
gameOverTextbox:setLoc ( 0, 0 )


mainThread = MOAIThread.new ()

mainThread:run (

	function ()
	
		while not gameOver do
		
			timer ()

			coroutine.yield ()		
				
			if MOAIInputMgr.device.pointer then				
				mouseX, mouseY = layer:wndToWorld (  MOAIInputMgr.device.pointer:getLoc () )
			end
				
			if MOAIInputMgr.device.mouseLeft:down () then				
				local obj = partition:propForPoint ( mouseX, mouseY )
				
				if obj then					
					obj:destroy ()
				end
			end
			
			if startBugs < maxBugs then
				spawnBug ()
			elseif startBugs == maxBugs then
				startTime = true
			end
			
			if timeOrbCounter == 15 then			
				spawnTimeOrb ()
				timeOrbCounter = 0
			end
			
			if totalBugs == 0 then
				gameOver = true
			end
		end
		
		partition:clear ()
		bglayer:removeProp ( timeBox )
		bglayer:insertProp ( gameOverTextbox )
	end
)