Hostingheaderbarlogoj
Join InMotion Hosting for $3.49/mo & get a year on Tuts+ FREE (worth $180). Start today.
Advertisement

Creating a Simple Basketball Game With Corona Game Edition

by
Gift

Want a free year on Tuts+ (worth $180)? Start an InMotion Hosting plan for $3.49/mo.

The physics engine that comes with Corona Game Edition is an incredibly powerful and easy to use tool. In this tutorial, we will cover the completion of a rudimentary basketball game using this exciting technology.

Step 1: Setting Up the Physics Engine

display.setStatusBar(display.HiddenStatusBar)

local physics = require "physics"
physics.start()
physics.setGravity(9.81, 0)  -- 9.81 m/s*s in the positive x direction
physics.setScale(80)  -- 80 pixels per meter
physics.setDrawMode("normal")

The first thing we do (as in many programs) is get rid of the status bar at the top of the screen. Next, we make the necessary require statement for using physics and store the result in the aptly named "physics" variable. Things become more interesting in the next few lines. In line five, we set the gravitational acceleration. Typically, gravity is set to 9.8 m/s*s in the positive y-direction, but in this instance we want to make gravity pull in the positive x-direction because the application will have a landscape orientation. Furthermore, we set the scale to 80 pixels per meter. This number can vary quite a bit based on the size of the objects in your application, and you may have to play around with it to give your game the correct feel. I chose 80 px/m because I want to fit about 15 feet of vertical space on the screen. Knowing that, it's just a simple matter of unit conversion to get a value.

Note: It's important to try and tie everything to real world objects in applications with physics. The more real life measurements you use, the less guesswork there will be and the more realistic your application will seem.

We round out these few lines by setting the draw mode to normal. This line makes it easier to change to debug mode later if we should have to fix some unintended behavior with collisions. Setting this to normal is the default behavior and draws the shapes as the user will see them in the final game.


Step 2: Creating the Arena

local background = display.newImage("CourtBackground.png")
local score = display.newText("Score: 0", 50, 300)
score:setTextColor(0, 0, 0)
score.rotation = -90
score.size = 36
local floor = display.newRect(320, 0, 1, 480)
local lWall = display.newRect(0, 480, 320, 1)
local rWall = display.newRect(0, -1, 320, 1)
local ceiling = display.newRect(-1, 0, 1, 480)

staticMaterial = {density=2, friction=.3, bounce=.4}
physics.addBody(floor, "static", staticMaterial)
physics.addBody(lWall, "static", staticMaterial)
physics.addBody(rWall, "static", staticMaterial)
physics.addBody(ceiling, "static", staticMaterial)

This block establishes the boundaries of the arena, and the properties of all the static objects in the application. We begin by adding a simple image to the background. Inside of the white rectangle in the background image, we position some text to display the current score. Because the application will be displayed in landscape mode, we also make necessary rotation adjustments here. The arena needs to trap the ball within the visible portion of the screen. We achieve this with four static rectangles (floor, lWall, rWall, ceiling) placed just out of view.

Next, we bring physics back into the equation. Instead of retyping the table for the physical properties of each object, we create a table name staticMaterial to be reused for each of the walls and the goal itself. I've chosen fairly standard values for these properties, though I encourage you to play around with them. There is one more step we must take, and that is to tell Corona that these objects should participate in physics calculations. We do this by calling the addBody function of the physics object. This function takes three arguments:

  1. The object
  2. An optional modifier
  3. A table of physical properties

We've already determined the properties and the objects, so all that remains is the optional modifier. We use "static" to prevent gravity, or any force for that matter, from displacing our walls!


Step 3: Adding a Ball and a Goal

-- Create the goal
local vertPost = display.newRect(110, 5, 210, 10)
vertPost:setFillColor(33, 33, 33)
local horizPost = display.newRect(110, 10, 10, 40)
horizPost:setFillColor(33, 33, 33)
local backboard = display.newRect(55, 50, 85, 5)
backboard:setFillColor(33, 33, 33)

physics.addBody(vertPost, "static", staticMaterial)
physics.addBody(horizPost, "static", staticMaterial)
physics.addBody(backboard, "static", staticMaterial)

--Create the Ball
local ball = display.newCircle(50, 200, 10)
ball:setFillColor(192, 99, 55)

physics.addBody(ball, {density=.8, friction=.3, bounce=.6, radius=10})

In one fell swoop, we create the rest of the visual elements of our app. This should all look very familiar. There are just two things that I would like to point out. First, some of the values for the positioning of the goal may seem off. This is to account for the landscape orientation. The goal will appear upright when the device is rotated on its side. Also, be sure to include the radius property in the properties table of the ball so it will behave correctly.


Step 4: Creating Drag Support for the Ball

local function drag( event )
	local ball = event.target
	
	local phase = event.phase
	if "began" == phase then
		display.getCurrentStage():setFocus( ball )

		-- Store initial position
		ball.x0 = event.x - ball.x
		ball.y0 = event.y - ball.y
		
		-- Avoid gravitational forces
		event.target.bodyType = "kinematic"
		
		-- Stop current motion, if any
		event.target:setLinearVelocity( 0, 0 )
		event.target.angularVelocity = 0

	else
		if "moved" == phase then
			ball.x = event.x - ball.x0
			ball.y = event.y - ball.y0
		elseif "ended" == phase or "cancelled" == phase then
			display.getCurrentStage():setFocus( nil )
			event.target.bodyType = "dynamic"
		end
	end

	return true
end
ball:addEventListener("touch", drag)

This function gives us very basic drag support. Some of the high points include setting the bodyType of the ball to kinematic so gravity won't pull the ball out of the user's hands (Note: be sure to set this back to dynamic after the touch has ended). The lines just after that are equally important. There we stop all of the ball's motion when it is touched to avoid the same problem we had with gravity.

If you run the app as it is now, you will probably notice that the ball loses all of its momentum as soon as you stop touching it. To remedy this, we need to create a function to track the speed of the ball, and then set the speed of the ball appropriately after the touch ends.

local speedX = 0
local speedY = 0
local prevTime = 0
local prevX = 0
local prevY = 0

function trackVelocity(event) 
	local timePassed = event.time - prevTime
	prevTime = prevTime + timePassed
	
	speedX = (ball.x - prevX)/(timePassed/1000)
	speedY = (ball.y - prevY)/(timePassed/1000)

	prevX = ball.x
	prevY = ball.y
end

Runtime:addEventListener("enterFrame", trackVelocity)

We create trackVelocity as a listener of the enterFrame event, so it is called everytime the screen is redrawn. What it does is find the change in speed over the change in time to find the velocity of the ball in pixels per second. There's really not much to it. Add the following line to the drag function to properly set the linear velocity of the ball.

ball:setLinearVelocity(speedX, speedY)

Step 5: Creating the Hoop and Scoring Mechanism

We begin with some more visual work, but by now you should be a pro at rectangles, so it should be painless. The following code creates the rim. Notice that the middle portion of the rim is not going to be part of the physical system because we want the ball to pass through freely.

local rimBack = display.newRect(110, 55, 5, 7)
rimBack:setFillColor(207, 67, 4)
local rimFront = display.newRect(110, 92, 5, 3)
rimFront:setFillColor(207, 67, 4)
local rimMiddle = display.newRect(110, 62, 5, 30)
rimMiddle:setFillColor(207, 67, 4)

physics.addBody(rimBack, "static", staticMaterial)
physics.addBody(rimFront, "static", staticMaterial)

Next we need a way to know when the ball has passed through the goal. The easiest way to accomplish this is by designating a small patch of the screen near the rim as a "score zone". Whenever the ball is in this zone we can increment the score. To prevent the score from miscounting when the ball lingers around the rim, we keep track of the time of the last goal, and ensure that there is adequate separation between each successive goal. A one second delay should work nicely.

scoreCtr = 0
local lastGoalTime = 1000

function monitorScore(event) 
	if event.time - lastGoalTime > 1000  then
		if ball.x > 103 and ball.x < 117 and ball.y > 62 and ball.y < 92 then
			scoreCtr = scoreCtr + 1
			print(score)
			lastGoalTime = event.time
			score.text = "Score: " .. scoreCtr
		end
	end
end
Runtime:addEventListener("enterFrame", monitorScore)

Conclusion

Even though this app could have been created with the standard version of the Corona SDK, it would have been mountains more work trying to keep track of collisions, friction, gravity, etc. Corona Game Edition takes care of the more difficult physics tasks, leaving you with more time to focus on the content and gameplay of your game.

Advertisement