Bubble Fish

Updated on June 26th, 2021 at 1:09 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 game can be described as a target shooter. You tap somewhere on the bottom of that screen to blow a bubble at that location. When a bubble is blown, it will float up to the top of the water. Meanwhile, fish will be swimming from either side of the screen at random speeds. The goal of the game is to blow bubbles at the right time and at the right place so that the bubbles will intercept the fish that are swimming by. If a fish and a bubble collide, the fish will be captured by the bubble and gently float up to the surface.

A basic counter will keep track of how many bubbles you have blown as well as how many fish you have captured. Your score is based on the average bubble to captured fish ratio. The better your average, the better your score.

a screenshot of the bubble fish game
A screenshot of the Bubble Fish 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

-----------------------------------------------------------------------------------------
-- Target Shooter
-- Ryan Bains-Jordan
--
-- Main
-----------------------------------------------------------------------------------------

display.setStatusBar( display.HiddenStatusBar )

-- Get the screen metrics (use the entire device screen area)
local WIDTH = display.actualContentWidth
local HEIGHT = display.actualContentHeight
local xMin = display.screenOriginX
local yMin = display.screenOriginY
local xMax = xMin + WIDTH
local yMax = yMin + HEIGHT
local xCenter = (xMin + xMax) / 2
local yCenter = (yMin + yMax) / 2

--------------------
--Objects
--------------------

local blowBubble
local popBubble
local trapFish
local bubbleDone
local newFrame
local spawnFish
local hitTest
local hasCollidedCircle

local hits = {}
local misses = {}
local percent = {}
local percentUpdate

--------------------
--Display Groups
--------------------
local background = display.newGroup( )
local bubbles = display.newGroup( )
local fishes = display.newGroup( )
local traps = display.newGroup( )


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

local function initGame()

	--Background
	local bg = display.newImage( background, "background.png", 380, 570 )
	bg.x, bg.y = xCenter, yCenter

	local water = display.newImageRect( background, "waves.png", 760, 130 )
	water.anchorX = 0
	water.y = 50
	local function waterAnimation()
		water.x = -380
		transition.to( water, { x = 0, time = 5000, onComplete = waterAnimation, onCancel = waterAnimation } )
	end
	background:toBack( )
	waterAnimation()

	-- Text Color
	local color = 
	{
		hightlight = { r=0.2, g=0.2, b=0.2 },
		shadow = { r=0.5, g=0.5, b=0.5 }
	}

	-- Hit Score
	hits.number = 0

	hits.image = display.newImage( "trapped_fish.png" )
	hits.image:scale( .5, .5 )
	hits.image.x = xMin + 30
	hits.image.y = yMin + 30

	hits.text = display.newEmbossedText( "'s = 0", xMin + 60, yMin + 30, native.systemFont, 25 )
	hits.text:setFillColor( 0.3, 0.4, 0.5 )
	hits.text:setEmbossColor( color )
	hits.text.anchorX = 0

	--Miss text
	misses.number = 0

	misses.image = display.newImage( "bubble.png" )
	misses.image:scale( .6, .6 )
	misses.image.x = xMin + 30
	misses.image.y = yMin + 80

	misses.text = display.newEmbossedText( "'s = 0", xMin + 60, yMin + 80, native.systemFont, 25 )
	misses.text:setFillColor( 0.3, 0.4, 0.5 )
	misses.text:setEmbossColor( color )
	misses.text.anchorX = 0


	--Percentage text
	percent.number = 0
	percent.text = display.newEmbossedText( string.format( "%3.f", percent.number ) .. "%", xMax-50, yMin+30, native.systemFontBold, 25 )
	percent.text:setFillColor( 0.3, 0.4, 0.5 )
	percent.text:setEmbossColor( color )

	--------------------
	--Listeners
	--------------------
	bg:addEventListener( "touch", blowBubble )
	Runtime:addEventListener( "enterFrame", newFrame )
end

-----------------------------------------------------------------------------------------
-- Main Functions
-----------------------------------------------------------------------------------------

-- Main Loop
function newFrame()
	spawnFish()
	hitTest()
	percentUpdate()
end

-- Spawn the bubble when sand is touched
function blowBubble( event )
	if event.phase == "began" and event.y > 430 then
		local bubble = display.newImage( bubbles, "bubble.png", true )
		bubble.x = event.x
		bubble.y = event.y
		transition.to( bubble, { time = 3000, y = yMin - 40, onComplete = popBubble, onCancel = bubbleDone } )

		return true
	end
end

-- Pop the bubble when it reaches the surface
function popBubble( object )
	object:removeSelf( )
	misses.number = misses.number + 1
	misses["text"].text = "'s = " .. misses.number
end

-- Spawns the fish
function spawnFish()
	if math.random( 1, 50 ) == 1 then
		fish = display.newImage(fishes, "fish.png", true)
		fish.x = xMin - 40
		fish.y = math.random( yMin + 150, 360 )
		transition.to( fish, { time = math.random( 2000, 5000 ), x = xMax + 40, onComplete = bubbleDone } )
	end
end

-- Trap the fish if the bubble hits it
function trapFish( object )
	object:removeSelf( )
	bubbleTrap = display.newImage( traps, "trapped_fish.png", true )
	bubbleTrap.x = object.x
	bubbleTrap.y = object.y
	transition.to( bubbleTrap, { y = 20 , time = 2000, onComplete = blowUp } )
	hits.number = hits.number + 1
	hits["text"].text = "'s = " .. hits.number
end

-- Blow up the bubble with the fish when it reaches the surface
function blowUp( object )
	transition.to( object, { xScale = 2, yScale = 2, alpha = 0, time = 250, onComplete = bubbleDone } )
end

-- Remove the bubble object when completed
function bubbleDone( object )
	object:removeSelf( )
end

-- Check for hits every frame
function hitTest()
	for b = 1, bubbles.numChildren do
		for f = 1, fishes.numChildren do
			if hasCollidedCircle( bubbles[b], fishes[f] ) then
				transition.cancel( bubbles[b] )
				transition.cancel( fishes[f] )
				trapFish(fishes[f])
			end
		end
	end
end


-- Circle-based collision detection (From Corona Docs)
function hasCollidedCircle( obj1, obj2 )
    if ( obj1 == nil ) then  -- Make sure the first object exists
        return false
    end
    if ( obj2 == nil ) then  -- Make sure the other object exists
        return false
    end

    local dx = obj1.x - obj2.x
    local dy = obj1.y - obj2.y

    local distance = math.sqrt( dx*dx + dy*dy )
    local objectSize = (obj2.contentWidth/2) + (obj1.contentWidth/2)

    if ( distance < objectSize ) then
        return true
    end
    return false
end

-- Update the percentage each frame
function percentUpdate()
	if hits.number + misses.number == 0 then
		percent.number = 100
	else
		percent.number = ( hits.number / ( hits.number + misses.number ) ) * 100
	end
	percent["text"].text = string.format( "%3.f", percent.number ) .. "%"
end

-----------------------------------------------------------------------------------------
-- InitGame Call
-----------------------------------------------------------------------------------------
initGame()