Gravity Maze

Updated on June 26th, 2021 at 1:11 am

What is it?

This was a game that I created in my mobile development class at my Junior College. The app was made with the Corona SDK and programmed in Lua. The concept of the game is similar to that of a marble labyrinth. There are two options on the bottom of the screen to place either blocks or balls. The blocks act as barriers or walls for the balls and do not move when you tilt the screen of the device. The balls, however, are reactive to the orientation of the device. Basically, the game is a marble labyrinth where you able place the walls of the maze.

screenshot of the gravity maze game
A screenshot of the Gravity Maze game

What does it do?

This app was designed to be a 2k mobile game. It was created with the Corona SDK and as such, can be built for iOS and Android devices. It may be possible to build the game for macOS and Windows as well.

How does it work?

Games made with the Corona SDK are primarily made of Lua Scripts. Each script would generally equate to a single scene or level within the game. Since this game only has a single scene, there is only one script. The rest of the files in the project are made up of images, icons, backgrounds, and sprites.

main.lua

-----------------------------------------------------------------------------------------
-- Gravity Maze Lite
-- By Ryan Bains-Jordan
--
-- main.lua
-----------------------------------------------------------------------------------------


-- Load the Requisites
local physics = require("physics")
local widget = require( "widget" )
local json = require( "json" )

-- Get screen size
WIDTH = display.actualContentWidth
HEIGHT = display.actualContentHeight
xMin = display.screenOriginX
yMin = display.screenOriginY
xMax = xMin + WIDTH
yMax = yMin + HEIGHT
xCenter = (xMin + xMax) / 2
yCenter = (yMin + yMax) / 2

-- Game objects
local blocks = {}
local balls = {}
local goals = {}
local bombs = {}
local gameOver = {}
local options = {}
local button = {}
local text = {}

-----------------------------------------------------------------------------------------
-- File Functions
-----------------------------------------------------------------------------------------

-- Save the block locations
local function saveData()
	local path = system.pathForFile( "myData.txt", system.DocumentsDirectory )

	-- Make the dataTable
	local dataTable = {}
	for i = 1, #blocks do
		local block = blocks[i]
		dataTable[i] = { x = block.x, y = block.y }
	end

	-- Open the file for writing
	local file = io.open( path, "w" )

	-- Write the Data
	local s = json.encode( dataTable )
	print(s)
	file:write(s)

	-- Close the file
	io.close(file)
end

-- Load the block locations
local function loadData()
	local path = system.pathForFile( "myData.txt", system.DocumentsDirectory )
	print( path )

	local file = io.open( path, "r" )

	-- Does the file exist?
	if file then
		-- Read our data from a file
		local s = file:read( "*a" )
		dataTable = json.decode(s)

		-- Turn dataTable contents into real rectangles
		for i = 1, #dataTable do
			local xyData = dataTable[i]
			local block = display.newRect( xyData.x, xyData.y, 20, 20 )
			block.name = "block"
			physics.addBody(block, "static", { density = 1.0, friction = 0.3, bounce = 0.2 })
			blocks[#blocks + 1] = block  -- add block to end of blocks array
		end

		-- Close the file (note: only do this if file is not nil)
		io.close( file)
	end
end

-----------------------------------------------------------------------------------------
-- Listener Functions
-----------------------------------------------------------------------------------------

local function onSystemEvent( event )
	-- Should save data in response to either suspend or exit
    if event.type == "applicationSuspend" or 
    	event.type == "applicationExit" then

    	physics.pause( )

    	button.resume.isVisible = true
    end
end

local function onOrientationChange( event )
	print( event.type )

	if event.type == "portrait" then
		--button.options.rotation = 0
		button.resume.rotation = 0
		button.reset.rotation = 0

		--text.portrait.isVisible = true
		--text.landscape.isVisible = false
	end
	if event.type == "landscapeLeft" then
		--button.options.rotation = -90
		button.resume.rotation = -90
		button.reset.rotation = -90

		--text.landscape.isVisible = true
		--text.portrait.isVisible = false
	end
	if event.type == "landscapeRight" then
		--button.options.rotation = 90
		button.resume.rotation = 90
		button.reset.rotation = 90

		--text.landscape.isVisible = true
		--text.portrait.isVisible = false
	end
end

local function resetButtonListener()

	--optionsGroup.isVisible = false

	for i = #blocks, 1, -1 do
		blocks[i]:removeSelf( )
		blocks[i] = nil
	end

	for i = #balls, 1, -1 do
		balls[i]:removeSelf( )
		balls[i] = nil
	end

	-- for i = #goals, 1, -1 do
	-- 	goals[i]:removeSelf( )
	-- 	goals[i] = nil
	-- end

	-- for i = #bombs, 1, -1 do
	-- 	bombs[i]:removeSelf( )
	-- 	bombs[i] = nil
	-- end

	text.blocksNum.text = 0
	text.ballsNum.text = 0
	-- text.goalsNum.text = 0
	-- text.bombsNum.text = 0

	physics.start( )

	saveData()
	return true
end

-- Listener for the Options Button ( Currently Disabled )
local function optionsButtonListener()

	physics.pause( )

	optionsGroup.isVisible = true
	return true
end

-- Listener for the Segments on the Options Screen ( Currently Disabled )
local function optionsSegmentListener( event )
	local target = event.target
	
	if target.segmentNumber == 1 then
		options.square.isVisible = true
		options.circle.isVisible = false
	elseif target.segmentNumber == 2 then
		options.square.isVisible = false
		options.circle.isVisible = true
	end			
end

-- Listener for the done button on the Options Screen ( Currently Disabled )
local function doneButtonListener()

	physics.start( )

	optionsGroup.isVisible = false
	return true
end

-- Listener for the resume button that appears when game is suspended
local function resumeButtonListener()
	physics.start( )
	button.resume.isVisible = false
	return true
end

-- Listener for the Segments on the main screen
local function segmentButtons( event )
local target = event.target
	if target.segmentNumber == 1 then
		button.segment.value = "block"
	elseif target.segmentNumber == 2 then
		button.segment.value = "ball"
	elseif target.segmentNumber == 3 then
		button.segment.value = "goal"
	elseif target.segmentNumber == 4 then
		button.segment.value = "bomb"
	end
end

--[[ -- Listener for collisions collisions between the ball and other objects ( Currently Disabled ) 
local function onLocalCollision( event )
	if event.phase == "began" then
		if event.target.name ~= nil then
			print( "collision by " .. event.target.name )
		end
		if event.other.name ~= nil then
			print( "Ball hit " .. event.other.name)
		end
	end
end --]]

-----------------------------------------------------------------------------------------
-- Touch Function
-----------------------------------------------------------------------------------------

-- Touching the screen places a block
local block = nil
local ball = nil
-- local goal = nil
-- local bomb = nil

-- What happens when the screen is tocuched above settings
local function touch(event)
	if event.phase == "began" and event.y < yMax - 125 then
		-- Place block at initial touch position
		if button.segment.value == "block" then
			block = display.newRect(event.x, event.y, 20, 20)
			block.name = "block"
			block:toBack( )
		elseif button.segment.value == "ball" then
			ball = display.newCircle(event.x, event.y, 15)
			ball.name = "ball"
			ball:setFillColor(1, 0, 0)
			ball:toBack()
		-- elseif button.segment.value == "goal" then
		-- 	goal = display.newCircle(event.x, event.y, 20)
		-- 	goal.name = "goal"
		-- 	goal:setFillColor(1, 1, 0.5, 0.6)
		-- 	goal:toBack()
		-- elseif button.segment.value == "bomb" then
		-- 	bomb = display.newImageRect( "bomb.png", 20, 20 )
		-- 	bomb.name = "bomb"
		-- 	bomb.x = event.x
		-- 	bomb.y = event.y
		-- 	bomb:toBack()
		end
	elseif event.phase == "moved" and event.y < yMax - 125 then
		-- Adjust block position while user drags
		if button.segment.value == "block" then
			block.x = event.x
			block.y = event.y
			block:toBack( )
		elseif button.segment.value == "ball" then
			ball.x = event.x
			ball.y = event.y
			ball:toBack( )
		-- elseif button.segment.value == "goal" then
		-- 	goal.x = event.x
		-- 	goal.y = event.y
		-- 	goal:toBack( )
		-- elseif button.segment.value == "bomb" then
		-- 	bomb.x = event.x
		-- 	bomb.y = event.y
		-- 	bomb:toBack( )		
		end
	elseif block then
		-- Make the block active in the final position and store it in the array
		physics.addBody(block, "static", { density = 1.0, friction = 0.3, bounce = 0.2 })
		blocks[#blocks + 1] = block  -- add block to end of blocks array
		print(#blocks .. " blocks")
		text.blocksNum.text = #blocks
		block = nil
		saveData()
	elseif ball then
		physics.addBody(ball, { density = 1.0, friction = 0.3, bounce = 0.7, radius = 15 })
		ball.collision = onLocalCollision
		--ball:addEventListener( "collision", onLocalCollision )
		ball.isSleepingAllowed = false
		balls[#balls + 1] = ball
		text.ballsNum.text = #balls
		ball = nil
	-- elseif goal then
	-- 	goals[#goals + 1] = goal
	-- 	text.goalsNum.text = #goals
	-- 	goal = nil
	-- elseif bomb then
	-- 	bombs[#bombs + 1] = bomb
	-- 	text.bombsNum.text = #bombs
	--	bomb = nil
	end
	return true
end

-----------------------------------------------------------------------------------------
-- Convenience Functions
-----------------------------------------------------------------------------------------

-- Convenience Function for creating Buttons
function createButton( label, x, y, w, h, listener )
	local b = widget.newButton {
	    label = label,
	    x = x, y = y, width = w, height = h,
	    shape="roundedRect",
	    cornerRadius = 5,
	    fillColor = { default={ 0, 0.5, 1 }, over={ 0, 0.3, 1 } },
	    labelColor = { default={ 1, 1, 1 } },
	    onEvent = listener
	}
	return b
end

-- Convenience Function for creating Segments
local function createSegment( x, y, segments, listener )
	local s = widget.newSegmentedControl {
		x = x, y = y,
		segments = segments,
		segmentWidth = 75,
		defaultSegment = 1,
		onPress = listener
	}
	return s
end

-- Convenience Function for creating Sliders
local function createSlider( x, y, width, listener )
	local s = widget.newSlider {
		x = x,
		y = y,
		width = width,
		value = 0,
		listener = listener
	}
	return s
end

-- Convenience Function for creating Text
local function createText( group, text, x, y, anchor )
	local t = display.newText {
		parent = group,
		text = text, 
		x = x, y = y,
		font = native.systemFont, 
		fontSize = 20
	}
	if anchor then
		t.anchorX = anchor
	end
	return t
end

-----------------------------------------------------------------------------------------
-- Init Game
-----------------------------------------------------------------------------------------

function initGame()
	-- Start physics engine
	physics.start()
	physics.setGravity( 0, 0 )

	-- Game Over Screen
	-- gameOver.pane = display.newRect( xCenter, yCenter, WIDTH - 50, HEIGHT - 50 )
	-- gameOver.pane:setFillColor( 0.6, 0.6, 0.6 )
	-- gameOver.pane.isVisible = false

	-- Buttons
	-- button.options = createButton( "Options", xMin + 45, yMax - 73, 70, 70, optionsButtonListener )

	button.reset = createButton( "Reset", xMin + 45, yMax - 73, 70, 70, resetButtonListener ) -- Remove to see options button

	button.resume = createButton( "Resume", xMin + 45, yMax - 73, 70, 70, resumeButtonListener )
	button.resume.isVisible = false

	--button.segment = createSegment( xCenter, yMax - 20, { "Block", "Ball", "Goal", "Bomb" }, segmentButtons )
	button.segment = createSegment( xCenter, yMax - 20, { "Block", "Ball" }, segmentButtons )

--------------------
-- Object Numbers
--------------------

	text.portrait = display.newGroup( )

	text.blocks = createText( text.portrait, "Blocks:", xMax - 230, yMax - 90, 0 )
	text.blocksNum = createText( text.portrait, 0, xMax - 160, yMax - 90, 0 )
	text.balls = createText( text.portrait, "Balls:", xMax - 230, yMax - 60, 0 )
	text.ballsNum = createText( text.portrait, 0, xMax - 160, yMax - 60, 0 )
	-- text.goals = createText( text.portrait, "Goals:", xMax - 120, yMax - 90, 0 )
	-- text.goalsNum = createText( text.portrait, 0, xMax - 40, yMax - 90, 0 )
	-- text.bombs = createText( text.portrait, "Bombs:", xMax - 120, yMax - 60, 0 )
	-- text.bombsNum = createText( text.portrait, 0, xMax - 40, yMax - 60, 0 )

--[[
	text.landscape = display.newGroup( )

	text.blocks = createText( text.landscape, "Blocks:", xMax - 30, yMax - 110 )
	--text.blocks.rotation = 90
	text.blocksNum = createText( text.landscape, 0, xMax - 50, yMax - 85 )
	--text.blocksNum.rotation = 90

	text.landscape.rotation = 10
	text.landscape.isVisible = false
--]]

	loadData()

--------------------
-- Walls
--------------------

	-- Make walls around the borders of the screen
	local thickness = 4
	local wallPhysicsOptions = { density = 1.0, friction = 0.3, bounce = 0.2 }
	local wall = display.newRect(xCenter, yMin, WIDTH, thickness)  -- top
	physics.addBody(wall, "static", wallPhysicsOptions)
	wall = display.newRect(xCenter, yMax - 115, WIDTH, thickness)   -- bottom
	physics.addBody(wall, "static", wallPhysicsOptions)
	wall = display.newRect(xMin, yCenter, thickness, HEIGHT)      -- left
	physics.addBody(wall, "static", wallPhysicsOptions)
	wall = display.newRect(WIDTH, yCenter, thickness, HEIGHT)  -- right
	physics.addBody(wall, "static", wallPhysicsOptions)
	wall.name = "wall"

--------------------
-- Options Screen
--------------------
--[[
	optionsGroup = display.newGroup( )

	-- Options Window
	options.pane = display.newRect( optionsGroup, xCenter, yCenter, WIDTH, HEIGHT )
	options.pane:setFillColor( 0, 0, 0 )

	-- Done Button
	options.done = createButton( "Done", xMin + 50, yMin + 50 , 75, 45, doneButtonListener )

	-- Segment
	options.segment = createSegment( xCenter, yCenter - 125, { "Block", "Ball" }, optionsSegmentListener)

	-- Sliders
	options.redSlider = createSlider( xCenter + 25, yCenter - 75, 150, redSliderListener )
	options.greenSlider = createSlider( xCenter + 25, yCenter - 25, 150, greenSliderListener )
	options.blueSlider = createSlider( xCenter + 25, yCenter + 25, 150, blueSliderListener  )

	-- Text
	options.redSliderText = display.newText( optionsGroup, "Red", xCenter - 100, yCenter - 75, native.systemFontBold, 20 )
	options.greenSliderText = display.newText( optionsGroup, "Green", xCenter - 100, yCenter - 25, native.systemFontBold, 20 )
	options.blueSliderText = display.newText( optionsGroup, "Blue", xCenter - 100, yCenter + 25, native.systemFontBold, 20 )
	options.redSliderText:setFillColor( 1, 0, 0 )
	options.greenSliderText:setFillColor( 0, 1, 0 )
	options.blueSliderText:setFillColor( 0, 0, 1 )

	-- Square and Circle
	options.square = display.newRect( optionsGroup, xCenter, yCenter + 100, 30, 30)
	options.circle = display.newCircle( optionsGroup, xCenter, yCenter + 100, 15 )

	-- Reset Button
	options.reset = createButton( "Reset", xCenter, yMax - 50, 100, 60, resetButtonListener )

	optionsGroup:insert( options.done )
	optionsGroup:insert( options.segment )
	optionsGroup:insert( options.redSlider )
	optionsGroup:insert( options.greenSlider )
	optionsGroup:insert( options.blueSlider )
	optionsGroup:insert( options.reset )

	optionsGroup.isVisible = false
--]]


end

-----------------------------------------------------------------------------------------
-- Other Functions and Listeners
-----------------------------------------------------------------------------------------

-- Handle accelerometer events. Simulate gravity in direction of device tilt.
function accelEvent(event)
	print(event.xRaw, event.yRaw)
	physics.setGravity( event.xRaw * 10, -event.yRaw * 10 )
end

-- Init the game, then add the event listeners
initGame()
Runtime:addEventListener( "accelerometer", accelEvent )
Runtime:addEventListener( "orientation", onOrientationChange )
Runtime:addEventListener( "touch", touch )
Runtime:addEventListener( "system", saveData )
Runtime:addEventListener( "system", onSystemEvent )


--Runtime:addEventListener( "system", onSystemEvent )

-- Load and show the joystick control if running on a simulator
if system.getInfo("environment") == "simulator" then
	local joystick = require("joystick")
	joystick:show(WIDTH - 50, yMin + 50)
end