Wednesday, August 8, 2018

Game Control Inputs: Using On-Screen Controls /Part 1



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 – on-screen controls.
In our Game Scene class, define a private function loadControls:position: This function will be called in the GameScene didMoveToView:function, and will load the SKS file that contains our UI elements such as menu buttons and game controls. Make sure to call move(toParent:) on the node obtained from the SKS file so that it is moved into the GameScene’s node hierarchy. If you call addChile, an compiler error will be thrown because the node still belongs in the SKS file’s node hierarchy:

func loadControls(atPosition position: CGPoint){
        
        /** Load the control set **/

        guard let user_interface = SKScene(fileNamed: "user_interface") else {
            fatalError("Error: User Interface SKSCene file could not be found or failed to load")
        }
        
        guard let controlSet = user_interface.childNode(withName: "ControlSet_flatDark") else {
            fatalError("Error: Control Buttons from user_interface SKScene file either could not be found or failed to load")
        }
 
        controlSet.position = position
        controlSet.move(toParent: overlayNode)
 
       /** Load the control buttons **/
 
        guard let leftButton = controlSet.childNode(withName: "left") as? SKSpriteNode, let rightButton = controlSet.childNode(withName: "right") as? SKSpriteNode, let upButton = controlSet.childNode(withName: "up") as? SKSpriteNode, let downButton = controlSet.childNode(withName: "down") as? SKSpriteNode else {
            fatalError("Error: One of the control buttons either could not be found or failed to load")
        }
 
        self.leftButton = leftButton
        self.rightButton = rightButton
        self.upButton = upButton
        self.downButton = downButton
 
        buttonsAreLoaded = true
        print("buttons successfully loaded!")

    }

Now that the control buttons have been added to the GameScene node hierarchy, we can begin to implement their touch handlers in the GameScene’s touchesBegan function. The code snippet below is just a stub that we will finish by the end of the tutorial. In our touchesBegan:functionbelow, we loop through all the nodes in the overlayNode (which we added previously in the didMoveToViewfunction, and use the node names for each individual control button (as defined in the SKS file) to implement logic specific to that button:
override func touchesBegan(_ touches: Set, with event: UIEvent?) {
       
        guard let touch = touches.first else { return }
        
        let overlayNodeLocation = touch.location(in: overlayNode)
        
        if buttonsAreLoaded{

            for node in self.overlayNode.nodes(at: overlayNodeLocation){
          
                    if node.name == "up"{
   //Not yet implemented
                    }
                    
                    if node.name == "down"{
            ///Not yet implemented

                    }
                    
                    if node.name == "right"{
            ///Not yet implemented

                    }
                    
                    if node.name == "left"{
   ///Not yet implemented
                    }
 
                }
            }
            
      }
 

Create a nested type in the player class to represent the orientation of the player. In addition, the nested type will have convenience functions that allow us to get the orientation opposite of the player’s current orientation, as well as the clockwise and counterclockwise orientations immediately adjacent to the player. These functions will come in handy when we want to determine the shortest angle by which the player should rotate to align to the new orientation.

enum  Orientation{
        case up,down,left,right
        
        func getOppositeOrientation() -> Orientation{
            switch self {
            case .up:
                return .down
            case .down:
                return .up
            case .left:
                return .right
            case .right:
                return .left

            }
        }
        
        func getAdjacentCounterClockwiseOrientation() -> Orientation{
            switch self {
            case .up:
                return .left
            case .down:
                return .right
            case .left:
                return .down
            case .right:
                return .up
                
            }
            
        }
        
        func getAdjacentClockwiseOrientation() -> Orientation{
            swith self {
            case .up:
                return .right
            case .down:
                return .left
            case .left:
                return .up
            case .right:
                return .down
                
            }
            
        }
    }
Now that our Orientation nested type is already to go, define public variable of Orientation type on the Player class. This variable will also include a property observer, so that whenever a new orientation is set for the player, an animation is run that rotates the player the smallest angle necessary to align with the new orientation. This property observer will make use of the helper functions defined in the Orientation enum, so that we can avoid having to write switch statement cases for each of the player’s possible current orientations and nested switch statements for each of the possible previous orientations.
var currentOrientation: Orientation{
        didSet{
            
            guard oldValue != currentOrientation else { return }
            
            
            var angleOfRotaiton: Double = 0.00
            
            if (oldValue == currentOrientation.getOppositeOrientation()){
                angleOfRotaiton = Double.pi
            } else if (oldValue == currentOrientation.getAdjacentClockwiseOrientation()){
                angleOfRotaiton = Double.pi/2
            } else if (oldValue == currentOrientation.getAdjacentCounterClockwiseOrientation()){
                angleOfRotaiton = -Double.pi/2
            }
            
            run(SKAction.rotate(byAngle: CGFloat(angleOfRotaiton), duration: 0.30))
        }
     
     }
 
Remember to provide an initial value for the player orientation in the default initializer. We use .right to default initialize the player since the default texture for the player is right-facing:

override init(texture: SKTexture?, color: UIColor, size: CGSize) {
        
        ... 

 self.currentOrientation = .right
        
        ...
        
        super.init(texture: texture, color: color, size: size)
        
        ....
        
    }
In the player class, add a private variable for a computed property that will represent the unit vector corresponding to the player’s current orientation. This unit vector will make it easier to apply impulse and forces of varying magnitude, giving the user the option to increase and decrease the speed of player movement during gameplay.

  
private var appliedUnitVector: CGVector{
        
        switch currentOrientation {
        case .up:
            return CGVector(dx: 0.00, dy: 1.00)
        case .down:
            return CGVector(dx: 0.00, dy: -1.00)
        case .left:
            return CGVector(dx: -1.00, dy: 0.0)
        case .right:
            return CGVector(dx: 1.00, dy: 0.0)
        }
       
    }
 
Now, in the player class, add a convenience function that takes parameter for the magnitude of the force that we want to apply for player movements. This function will be called in our game scene when the user taps the control buttons corresponding to different player orientations. The force will be varied with a control separate from that of the directional controls.
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. For each different control button (i.e. left, right, up, and down), we’ll go ahead and set the orientation of the player appropriately, as well as call the applyMovementImpulse function so that the player will move in a direction consistent with his alignment. Also, make sure to set the linear damping of the player’s physics body to around 1.00 or more to give the sense that the player is walking forward in short bursts, otherwise the player will appear to be sliding like a pinball.

override func touchesBegan(_ touches: Set, with event: UIEvent?) {
       
        guard let touch = touches.first else { return }
        
        let overlayNodeLocation = touch.location(in: overlayNode)
        
        if buttonsAreLoaded{

            for node in self.overlayNode.nodes(at: overlayNodeLocation){
          
                    if node.name == "up"{
                     player.currentOrientation = .up
                     player.applyMovementImpulse(withMagnitudeOf: 1.5)

                    }
                    
                    if node.name == "down"{
                   player.currentOrientation = .down
                   player.applyMovementImpulse(withMagnitudeOf: 1.5)

                    }
                    
                    if node.name == "right"{
                    player.currentOrientation = .right
                    player.applyMovementImpulse(withMagnitudeOf: 1.5)

                    }
                    
                    if node.name == "left"{
                        player.currentOrientation = .left
                        player.applyMovementImpulse(withMagnitudeOf: 1.5)

                    }
 
                }
            }
            
      }
        
  
 
And voila! Our player should now be moving just the way we like when tested in the simulator:


No comments:

Post a Comment