Making a Top-Down Shooter Game

Game Control Inputs: Using Direct Touch Control

There are many ways of implementing control inputs (i.e. Core Motion, gesture recognizers, touch handling, etc.). In this tutorial, we explore one way of implementing control inputs - direct on-screen touch.

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:

Send any comments for feedback to 2081439477@qq.com.