Making a Top-Down Shooter Game
Game Control Inputs: Using Direct Touch Control
In order to allow for direct control of our player, it will help to first define a supporting enum for CompassDirection (which is the approach taken from Apple's Demobots game, from which this code snippet is borrowed):
import Foundation
import CoreGraphics
enum CompassDirection: Int{
case east = 0, eastByNorthEast, northEast, northByNorthEast
case north, northByNorthWest, northWest, westByNorthWest
case west, westBySouthWest,southWest,southBySouthWest
case south, southBySouthEast,southEast,eastBySouthEast
static let allDirections: [CompassDirection] = [
.east, .eastByNorthEast, .northEast, .northByNorthEast,
.north, .northByNorthWest, .northWest, .westByNorthWest,
.west, .westBySouthWest, .southWest, .southBySouthWest,
.south, .eastBySouthEast, .southEast, .eastBySouthEast
]
var zRotation: CGFloat{
let stepSize = CGFloat((Double.pi*2))/CGFloat(CompassDirection.allDirections.count)
return CGFloat(self.rawValue)*stepSize
}
init(zRotation: CGFloat) {
let twoPi = Double.pi * 2
// Normalize the node's rotation.
let rotation = (Double(zRotation) + twoPi).truncatingRemainder(dividingBy: twoPi)
// Convert the rotation of the node to a percentage of a circle.
let orientation = rotation / twoPi
// Scale the percentage to a value between 0 and 15.
let rawFacingValue = round(orientation * 16.0).truncatingRemainder(dividingBy: 16.0)
// Select the appropriate `CompassDirection` based on its members' raw values, which also run from 0 to 15.
self = CompassDirection(rawValue: Int(rawFacingValue))!
}
init(string: String) {
switch string {
case "North":
self = .north
case "NorthEast":
self = .northEast
case "East":
self = .east
case "SouthEast":
self = .southEast
case "South":
self = .south
case "SouthWest":
self = .southWest
case "West":
self = .west
case "NorthWest":
self = .northWest
default:
fatalError("Unknown or unsupported string - \(string)")
}
}
}
In our Player class, we add a variable for compassDirection and define a property observer that calculates the shortest degree of rotation required for the player to rotate in order to align to the new compass orientation. At the same time, the player sprite runs a rotation animation to show the changed orientation.
var compassDirection: CompassDirection{
didSet{
guard oldValue != compassDirection else { return }
guard rotation = ((compassDirection.zRotation - oldValue.zRotation) <= CGFloat.pi) && (compassDirection.zRotation > oldValue.zRotation) ? (compassDirection.zRotation - oldValue.zRotation) : -(oldValue.zRotation - compassDirection.zRotation)
print("Old zRotation is \(oldValue)")
print("New zRotation is \(zRotation)")
run(SKAction.rotate(byAngle: CGFloat(rotation), duration: 0.10))
}
}
As with the On-Screen controls, we define a computed property for the appliedUnitVector, which will come in handy for applying forces and impulses with larger magnitudes whose direction is aligned with that of player's compassDirection. The computed property uses some basic trig to get the x and y unit vectors for the compass direction:
var appliedUnitVector: CGVector{
let xUnitVector = cos(compassDirection.zRotation)
let yUnitVector = sin(compassDirection.zRotation)
return CGVector(dx: xUnitVector, dy: yUnitVector)
}
The applyMovementImpulse function has the same implementation as before:
func applyMovementImpulse(withMagnitudeOf forceUnits: CGFloat){
let dx = self.appliedUnitVector.dx*forceUnits
let dy = self.appliedUnitVector.dy*forceUnits
self.physicsBody?.applyImpulse(CGVector(dx: dx, dy: dy))
}
Now, back in our GameScene class, return to the touchesBegan function. Unlike the previous case, where we had four buttons (up,left,right,down) for our player input contorols, the current case requires some more intensive trig calculations to get the player's new orientation (i.e. compassDirection). Once this calculation is completed, we set the compassDirection property of the player, which should trigger the property observer, which in turn should cause the player to rotate, and also apply an impulse to the player to simulate movement in the direction of the new compass orientation.
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
guard let touch = touches.first else { return }
print("You touched the screen")
let touchLocation = touch.location(in: self)
print("Screen touched at position x: \(touchLocation.x), y: \(touchLocation.y)")
let xDelta = (touchLocation.x - player.position.x)
let yDelta = (touchLocation.y - player.position.y)
let absDeltaX = abs(xDelta)
let absDeltaY = abs(yDelta)
var zRotation: CGFloat = 0.00
if(xDelta > 0){
if(yDelta > 0){
zRotation = atan(absDeltaY/absDeltaX)
} else {
zRotation = 2*CGFloat.pi - atan(absDeltaY/absDeltaX)
}
} else {
if(yDelta > 0){
zRotation = CGFloat.pi - atan(absDeltaY/absDeltaX)
} else {
zRotation = CGFloat.pi + atan(absDeltaY/absDeltaX)
}
}
if(zRotation <= CGFloat.pi*2){
player.compassDirection = CompassDirection(zRotation: zRotation)
player.applyMovementImpulse(withMagnitudeOf: 5.00)
}
}
And voila! Our player should now be moving just the way we like when tested in the simulator: