This task builds a simple implementation of Air Hockey for 2 players (for iPad) The first part of the task involves creating a class for a mallet – the mallet will be used to hit the puck, and it needs to be able to send information to the scene to say how hard it has hit the puck.
import SpriteKit
class Mallet: SKShapeNode {
}
protocol MalletDelegate {
func force(force: CGVector, fromMallet mallet: Mallet)
}
//keep track of previous touch time (will use to calculate vector)
var lastTouchTimeStamp: Double?
//delegate will refer to class which will act on mallet force
var delegate:MalletDelegate?
//this will determine the allowable area for the mallet
let activeArea:CGRect
//define mallet size
let radius:CGFloat = 40.0
//when we instantiate the class we will set the active area
init(activeArea: CGRect) {
//set the active area variable this class with the variable passed in
self.activeArea = activeArea
//ensure we pass the init call to the base class
super.init()
//allow the mallet to handle touch events
userInteractionEnabled = true
}
init(activeArea:)
method which will setup the mallet:
//create a mutable path (later configured as a circle)
let circularPath = CGMutablePath()
//define pi as CGFloat (type π using alt-p)
let π = CGFloat.pi
//create the circle shape
circularPath.addArc(center: CGPoint(x: 0, y:0), radius: radius, startAngle: 0, endAngle: 2*π, clockwise: true)
//assign the path to this SKShapeNode's path property
path = circularPath
lineWidth = 0;
fillColor = .red
//set physics properties (note physicsBody is an optional)
physicsBody = SKPhysicsBody(circleOfRadius: radius)
physicsBody!.mass = 500;
physicsBody!.affectedByGravity = false;
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
The code is now in place to create a pre-made mallet with the correct shape, size, colour and physics properties which we can use in a scene.
The next stage is to enable the mallet to calculate a vector representing the direction and speed of the mallet as it is moved by the player. Implement the touchesMoved(_ touches: with event:)
method as shown below:
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
var relevantTouch:UITouch!
//get array of touches so we can loop through them
let orderedTouches = Array(touches)
for touch in orderedTouches{
//if we've not yet found a relevant touch
if relevantTouch == nil{
//look for a touch that is in the activeArea (Avoid touches by opponent)
if CGRectContainsPoint(activeArea, touch.locationInNode(parent!)){
relevantTouch = touch
}
}
}
if (relevantTouch != nil){
//get touch position and relocate mallet
let location = relevantTouch!.location(in: parent!)
position = location
//find old location and use Pythagoras to determine length between both points
let oldLocation = relevantTouch!.previousLocation(in: parent!)
let xOffset = location.x - oldLocation.x
let yOffset = location.y - oldLocation.y
let vectorLength = sqrt(xOffset * xOffset + yOffset * yOffset)
//get elapsed time and use to calculate speed
if lastTouchTimeStamp != nil {
let seconds = relevantTouch.timestamp - lastTouchTimeStamp!
let velocity = 0.01 * Double(vectorLength) / seconds
//to calculate the vector, the velcity needs to be converted to a CGFloat
let velocityCGFloat = CGFloat(velocity)
//calculate the impulse
let directionVector = CGVector(dx: velocityCGFloat * xOffset / vectorLength, dy: velocityCGFloat * yOffset / vectorLength)
//pass the vector to the scene (so it can apply an impulse to the puck)
delegate?.force(directionVector, fromMallet: self)
}
//update latest touch time for next calculation
lastTouchTimeStamp = relevantTouch.timestamp
}
}
supportedInterfaceOrientations()
method to appear as shown below:
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.Portrait
}
viewDidLoad()
method, before the final line, add this code to ensure the scenes size matches that of the portrait view:scene.size = skView.frame.size
class GameScene: SKScene, MalletDelegate {
func force(force: CGVector, fromMallet mallet: Mallet) {
}
We will return to this method later
var puck : SKShapeNode?
var southMallet : Mallet?
var northMallet : Mallet?
didMove(to view:)
method and replace it with view.isMultipleTouchEnabled = true;
touchesBegan(_ touches: with event:)
functions, as we do not need it.func drawCenterLine(){
let centerLine = SKSpriteNode(color: UIColor.white, size: CGSize(width:size.width, height:10))
centerLine.position = CGPoint(x: size.width/2, y: size.height/2)
centerLine.colorBlendFactor = 0.5;
addChild(centerLine)
}
func malletAt(position: CGPoint, withBoundary boundary:CGRect) -> Mallet{
let mallet = Mallet(activeArea: boundary)
mallet.position = position
mallet.delegate = self
addChild(mallet)
return mallet;
}
func createMallets(){
let southMalletArea = CGRect(x: 0, y: 0, width: size.width, height: size.height/2)
let southMalletStartPoint = CGPoint(x: frame.midX, size.height/4)
let northMalletArea = CGRect(x: 0, y: size.height/2, width: size.width, height: size.height)
let northMalletStartPoint = CGPoint(x: frame.midX, y: size.height * 0.75)
southMallet = malletAt(position: southMalletStartPoint, withBoundary: southMalletArea)
northMallet = malletAt(position: northMalletStartPoint, withBoundary: northMalletArea)
}
didMoveToView()
function which will call the drawCenterLine()
and createMallets()
functions:
drawCenterLine()
createMallets()
func resetPuck(){
if puck == nil{
//create puck object
puck = SKShapeNode()
//draw puck
let radius : CGFloat = 30.0
let puckPath = CGMutablePath()
let π = CGFloat.pi
puckPath.addArc(center: CGPoint(x: 0, y:0), radius: radius, startAngle: 0, endAngle: 2*π, clockwise: true)
puck!.path = puckPath
puck!.lineWidth = 0
puck!.fillColor = UIColor.blue
//set puck physics properties
puck!.physicsBody = SKPhysicsBody(circleOfRadius: radius)
//how heavy it is
puck!.physicsBody!.mass = 0.02
puck!.physicsBody!.affectedByGravity = false
//how much momentum is maintained after it hits somthing
puck!.physicsBody!.restitution = 0.85
//how much friction affects it
puck!.physicsBody!.linearDamping = 0.4
}
//set puck position at centre of screen
puck!.position = CGPoint(size.width/2, size.height/2)
puck!.physicsBody!.velocity = CGVector(dy: 0, dy: 0)
//if not alreay in scene, add to scene
if puck!.parent == nil{
addChild(puck!)
}
}
didMoveToView()
method:
resetPuck()
force(_ force: fromMallet:)
method, which responds to movement from a mallet, and if it is touching the puck, applies the appropriate force to it:
//Be aware this is not the best way of checking the collision between two circles
if mallet.frame.intersects(puck!.frame){
puck!.physicsBody!.applyImpulse(force)
}
You should now be able to use a mallet to hit the puck, and it should behave as expected. The only problem is, once it goes of screen there's no retrieving it.
func createEdges(){
let bumperDepth = CGFloat(20.0)
let leftEdge = SKSpriteNode(color: UIColor.blue, size: CGSize(width: bumperDepth, height: size.height))
leftEdge.position = CGPoint(x: bumperDepth/2, y: frame.height/2)
//setup physics for this edge
leftEdge.physicsBody = SKPhysicsBody(rectangleOfSize: leftEdge.size)
leftEdge.physicsBody!.dynamic = false
addChild(leftEdge)
//copy the left edge and position it as the right edge
let rightEdge = leftEdge.copy() as! SKSpriteNode
rightEdge.position = CGPoint(x: size.width - bumperDepth/2, y: frame.height/2)
addChild(rightEdge)
//calculate some values for the end bumpers (four needed to allow for goals)
let endBumperWidth = (size.width / 2) - 150
let endBumperSize = CGSize(width: endBumperWidth, height: bumperDepth)
let endBumperPhysics = SKPhysicsBody(rectangleOfSize: endBumperSize)
endBumperPhysics.dynamic = false;
//create a bottom edge
let bottomLeftEdge = SKSpriteNode(color: UIColor.blueColor(), size: endBumperSize)
bottomLeftEdge.position = CGPoint(x: endBumperWidth/2, y: bumperDepth/2)
bottomLeftEdge.physicsBody = endBumperPhysics
addChild(bottomLeftEdge)
//copy edge to other three locations
let bottomRightEdge = bottomLeftEdge.copy() as! SKSpriteNode
bottomRightEdge.position = CGPoint(x: size.width - endBumperWidth/2, y: bumperDepth/2)
addChild(bottomRightEdge)
let topLeftEdge = bottomLeftEdge.copy() as! SKSpriteNode
topLeftEdge.position = CGPoint(x: endBumperWidth/2, y: size.height - bumperDepth/2)
addChild(topLeftEdge)
let topRightEdge = bottomRightEdge.copy() as! SKSpriteNode
topRightEdge.position = CGPoint(x: size.width - endBumperWidth/2, y: size.height - bumperDepth/2 )
addChild(topRightEdge)
}
createEdges()
function in the didMoveToView()
method:
createEdges()
func isOffScreen(node: SKShapeNode) -> Bool{
return !frame.contains(node.position)
}
update(currentTime:)
loop function to check if the puck has gone through a goal:
if isOffScreen(node: puck!){
resetPuck()
}
You should now be able to play the game
A completed version of the game can be downloaded from GitHub
Write your own code (which you will likely need to research first), to add the following features:
Make the game a single player game, writing some artificial intelligence to move the opponent’s mallet. Ensure that it is still possible, but not too easy, to win.