How To Make a Simple Game with Moai

This is a tutorial for beginner Moak SDK developers, you’ll learn how to create a new animal-feeding game for iOS from scratch. With Moai, you don’t need to fear being locked in to one platform — you can let everyone enjoy the fruits of your labors! By .

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 6 of this article. Click here to view the first page.

Cats and Dogs and Bunnies, Oh My! — Adding Characters to the Game

In your game, a customer is an animal that waits to be served. The customer has a plate, and the player has to give the customer a specific food to earn points in the game. There’s a limit of three customers that can be active at a time. Customers are placed in a horizontal line at locations (-300, 200), (0, 200) and (300, 200).

As customers are served, they’ll disappear off the screen. If there is an empty spot for a customer, a new one should be spawned every 5 seconds. Aha! Another use for the newLoopingTimer() timer factory! It’s already proving useful! :]

Paste the following code at the bottom of main.lua:

local listOfCustomers = {} -- we'll keep list of active customers here
function horizontalPositionForCustomerNumber ( num )
	return 300 * num - 600
end
local function spawnCustomer ()
	if #listOfCustomers >= 3 then
		return
	end
	-- customerData is an array of arrays:
	-- each one has 3 elements: first is sprite name of that customer
	-- second one is another array: of foods this type of customer accepts
	-- third one is an audio file this customer can make
	local customerData = {
		{"cat.png", {"catfood.png", "2catcans.png"}, "cat.wav"},
		{"dog.png", {"bone.png", "dogfood.png"}, "dog.wav"},
		{"rabbit.png", {"carrot.png"}, "rabbit.wav"},
	}
	local customer = randomArrayElement ( customerData )
	local customerSprite = newSprite ( "gfx/"..customer[ 1 ] )
	customerSprite.soundName = customer [ 3 ]
	customerSprite.requestedFood = {}
	local customerIdx = #listOfCustomers + 1
	listOfCustomers[customerIdx] = customerSprite
	customerSprite:setLoc(horizontalPositionForCustomerNumber ( customerIdx ), 200 )
	layer:insertProp ( customerSprite )

	-- plate
	local plate = newSprite ( "gfx/plate.png" )
	plate:setParent ( customerSprite )
	-- plate should be positioned below the customer
	plate:setLoc ( 0, -140 ) 
	layer:insertProp ( plate )

	-- random 2 food pieces (from the accepted by the customer)
	for i=1,2 do
		local foodPiece = newSprite ( "gfx/" .. randomArrayElement ( customer [ 2] ))
		foodPiece:setParent ( plate )
		foodPiece:setLoc ( i*100 - 150, 0 )
		layer:insertProp ( foodPiece )
		foodPiece:setScl ( 0.5, 0.5 )
		customerSprite.requestedFood [ i ] = foodPiece
	end

	-- those will need to be nil'ed when removing the customer
	customerSprite.plate = plate
	plate.customerSprite = customerSprite
end
local customerSpawnTimer = newLoopingTimer ( 5.0, spawnCustomer, true)

This method creates a new customer (as long as there aren’t 3 or more customers already). It also creates two random food pieces that that customer will accept.

Just like randomly spawning food objects, customer objects are spawned and stored in an array of possible customers. The customerData table keeps track of the sprites, which foods each customer will accept, and some sound effects.

Note :setParent(); it allows you to position an element relative to its parent. When the parent moves, the “child” will be moved accordingly so that its relative position remains unchanged.

If you run this code, you should see a new customer show up every five seconds until all three spots are filled, just like in the screenshot below:

Gameplay – food moves and customers appear…

Feeding the Hordes — Adding Interaction to the Game

What’s a game without some player interaction? Things appear on-screen and move around, but you can’t actually feed the animals yet.

Time to add some input handlers!

Since Moai is meant to be cross-platform, there are very similar ways to handle both mouse and touch events from code. This is easy to do for the simple case — but of course there’s no way to get the mouse over event for a touch event, or a multi-touch event on a regular mouse! :]

Paste in the mouse/touch handling code at the bottom of main.lua:

----------------------------------------------------------------
-- Input (touches and mouse) handling
----------------------------------------------------------------
-- location of the mouse cursor (or touch point)
local mouseX, mouseY
-- this is to keep reference to what's being dragged
local currentlyTouchedProp 

local function dragObject ( object, x, y )
	print ("Dragging")
end
local function pointerCallback ( x, y )
	-- this function is called when the touch is registered (before clickCallback)
	-- or when the mouse cursor is moved
	mouseX, mouseY = layer:wndToWorld ( x, y )
	print ("mouse moved")
end

function clickCallback ( down )
	-- this function is called when touch/click 
	-- is registered
	print ("Click!")
end

These functions are generic and deal with (x,y) positions and some kind of “click”. It doesn’t really matter whether it’s a touch event or a mouse event — you only need one set of code to handle both! pointerCallback will keep track of the (x,y) position in general; clickCallback will handle touches and mouse clicks, and dragObject will be called as the player clicks and drags with either their mouse or their finger.

Now that your input handlers are in place, it’s time to register the callbacks.

Paste the following code at the bottom of main.lua:

-- Here we register callback functions for input - both mouse and touch
if MOAIInputMgr.device.pointer then
	-- mouse input
	MOAIInputMgr.device.pointer:setCallback ( pointerCallback )
	MOAIInputMgr.device.mouseLeft:setCallback ( clickCallback )
else
	-- touch input
	MOAIInputMgr.device.touch:setCallback ( 
		-- this is called on every touch event
		function ( eventType, idx, x, y, tapCount )
			pointerCallback ( x, y ) -- first set location of the touch
			if eventType == MOAITouchSensor.TOUCH_DOWN then
				clickCallback ( true )
			elseif eventType == MOAITouchSensor.TOUCH_UP then
				clickCallback ( false )
			end
		end
	)
end

This bit of code checks the device type. On mouse devices, it simply sets a callback that is called when the pointer is moved and when the left mouse button is clicked. On touch devices, it sets a callback function for a touch event, calls pointerCallback() — just as for mouse movement — and then finally it calls clickCallback().

Note: If you develop a real cross-platform game, it’s a good idea to implement the UI best practices for each particular platform. For example, when using a mouse, people expect that interactive elements will change their appearance when the cursor is over them to indicate that they are interactive.

In iOS, you can use platform-specific code to handle multitouch input — if it makes sense in your game — and figure out a different way to implement the same behaviour in your game using a mouse.

Just because an SDK allows you to write one set of code for multiple platforms doesn’t mean that you shouldn’t implement platform-specific behaviors. However, that is out of scope for this particular tutorial.

Run your app, play with the game, and check the Terminal to see if the print statements are being created on the console. You should see events like the following:

mouse moved
mouse moved
mouse moved
Click!
Click!
mouse moved
Click!
Click!
Click!
mouse moved
mouse moved

There’s something gratifying about seeing your app handle events — even if it’s just being dumped to Terminal! :]

Now you need to do something useful with your touch handlers, instead of just dumping them out to the console. The most important handler is the click function, as it handles all the touches and, depending on the context, further dispatches the event.

Replace all of clickCallback() in main.lua with the following code:

function clickCallback ( down )
	-- this function is called when touch/click 
	-- is registered
	local pick = partition:propForPoint ( mouseX, mouseY )
	local phase
	if down then
		phase = "down"
	else
		phase = "up"
	end
	
	-- if the touch/click is currently "locked" on some object
	-- this object should be treated as touched, 
	-- not something that's above it, for example
	if currentlyTouchedProp then
		pick = currentlyTouchedProp
	end

	event = {
		target = pick,
		x = mouseX,
		y = mouseY,
		phase = phase,
	}
	print ( phase, mouseX, mouseY )
	if down then
		if pick then
			currentlyTouchedProp = pick
			local x, y = currentlyTouchedProp:getLoc ()
			-- we store the position of initial touch inside the object
			-- so when it's dragged, it follows the finger/cursor smoothly
			currentlyTouchedProp.holdX = x - mouseX
			currentlyTouchedProp.holdY = y - mouseY
		end
		if pick and pick.onTouchDown then
			pick.onTouchDown ( event )
			return
		end
	else
		currentlyTouchedProp = nil
		if pick and pick.onTouchUp then
			pick.onTouchUp ( event )
			return
		end
	end	
end

Players should be able to drag the food items to plates, so the click handler here needs to keep track of when the mouse button click/touch begins (touchDown) and when the button/touch ends (touchUp).

The object that is clicked on is stored in currentlyTouchedProp so it can be referenced later to check if it’s the correct food for the customer. As well, this reference is used so that the object can follow the mouse cursor or the finger on the screen.

In order to handle the drag event and move the sprites on the screen, replace the contents of dragObject() in main.lua with the following:

local function dragObject ( object, x, y )
	object:setLoc ( x + object.holdX, y + object.holdY )
end

As an item is dragged to a new (x,y) position, you set its location to that same (x,y) position. holdX and holdY are just offsets, so it doesn’t matter which point you use to drag the object. For example, dragging the object having clicked on the center should work just as well as dragging by having clicked on the edge of the object.

Next, replace pointerCallback() in main.lua with the following code:

local function pointerCallback ( x, y )
	-- this function is called when the touch is registered (before clickCallback)
	-- or when the mouse cursor is moved
	mouseX, mouseY = layer:wndToWorld ( x, y )
	if currentlyTouchedProp and currentlyTouchedProp.isDragable then
		dragObject(currentlyTouchedProp, mouseX, mouseY)
	end
end

If there’s no dragging going on, you normally don’t care if the mouse moves around. currentlyTouchedProp will only need to be set if you’re in the middle of a drag event, at which point dragObject() will need to be called.

For a sprite to respond to touches and clicks, it needs an .isDragable = true attribute on the sprite. Food will be dragged to the animals, so you need to add .isDragable = true to the implementation of spawnFoodObject.

Update spawnFoodObject as below:

	-- this line should already be here:
	foodObject:setLoc(-520, -230) -- initial position, outside of the screen

	-- add this one line below!
	foodObject.isDragable = true

	-- this line should already be here as well:
	local anim = foodObject:moveLoc ( STAGE_WIDTH*1.2, 0, 12, MOAIEaseType.LINEAR )

Build and run to see how it looks so far. You can drag items, but after you drag them they continue to scroll:

Dragging problems

Dragging problems

Unfortunately, the food will continue to animate (conveyor belt style) even after you try to drag it! Hmm, the game is supposed to be challenging — but not that challenging! :]

Add the following lines to spawnFoodObject() in main.lua just before the final end that closes the function:

	foodObject.onTouchDown = function ( ev )
		anim:stop ()
	end

Run your app!

Try to drag some food items around. Ahh, that’s better — the horizontal animation stops playing whenever you drag the food item around the screen. Try dragging and dropping the food to make sure that those handlers are working correctly! Your screen should look similar to the following screenshot: