DemoBots/TouchControlInputNode.swift
/* |
Copyright (C) 2016 Apple Inc. All Rights Reserved. |
See LICENSE.txt for this sample’s licensing information |
Abstract: |
An implementation of the `ControlInputSourceType` protocol that enables support for touch-based thumbsticks on iOS. |
*/ |
import SpriteKit |
class TouchControlInputNode: SKSpriteNode, ThumbStickNodeDelegate, ControlInputSourceType { |
// MARK: Properties |
/// `ControlInputSourceType` delegates. |
weak var delegate: ControlInputSourceDelegate? |
weak var gameStateDelegate: ControlInputSourceGameStateDelegate? |
let allowsStrafing = true |
/// Analog thumb stick controls for the left and right half of the screen. |
let leftThumbStickNode: ThumbStickNode |
let rightThumbStickNode: ThumbStickNode |
/// Node representing the touch area for the pause button. |
let pauseButton: SKSpriteNode |
/// Sets used to keep track of touches, and their relevant controls. |
var leftControlTouches = Set<UITouch>() |
var rightControlTouches = Set<UITouch>() |
/// The width of the zone in the center of the screen where the touch controls cannot be placed. |
let centerDividerWidth: CGFloat |
var hideThumbStickNodes: Bool = false { |
didSet { |
leftThumbStickNode.isHidden = hideThumbStickNodes |
rightThumbStickNode.isHidden = hideThumbStickNodes |
} |
} |
// MARK: Initialization |
/* |
`TouchControlInputNode` is intended as an overlay for the entire screen, |
therefore the `frame` is usually the scene's bounds or something equivalent. |
*/ |
init(frame: CGRect, thumbStickNodeSize: CGSize) { |
// An approximate width appropriate for different scene sizes. |
centerDividerWidth = frame.width / 4.5 |
// Setup the thumbStickNodes. |
let initialVerticalOffset = -thumbStickNodeSize.height |
let initialHorizontalOffset = frame.width / 2 - thumbStickNodeSize.width |
leftThumbStickNode = ThumbStickNode(size: thumbStickNodeSize) |
leftThumbStickNode.position = CGPoint(x: -initialHorizontalOffset, y: initialVerticalOffset) |
rightThumbStickNode = ThumbStickNode(size: thumbStickNodeSize) |
rightThumbStickNode.position = CGPoint(x: initialHorizontalOffset, y: initialVerticalOffset) |
// Setup pause button. |
let buttonSize = CGSize(width: frame.height / 4, height: frame.height / 4) |
pauseButton = SKSpriteNode(texture: nil, color: UIColor.clear, size: buttonSize) |
pauseButton.position = CGPoint(x: 0, y: frame.height / 2) |
super.init(texture: nil, color: UIColor.clear, size: frame.size) |
rightThumbStickNode.delegate = self |
leftThumbStickNode.delegate = self |
addChild(leftThumbStickNode) |
addChild(rightThumbStickNode) |
addChild(pauseButton) |
/* |
A `TouchControlInputNode` is designed to receive all user interaction |
and forwards it along to the child nodes. |
*/ |
isUserInteractionEnabled = true |
} |
required init?(coder aDecoder: NSCoder) { |
fatalError("init(coder:) has not been implemented") |
} |
// MARK: ThumbStickNodeDelegate |
func thumbStickNode(thumbStickNode: ThumbStickNode, didUpdateXValue xValue: Float, yValue: Float) { |
// Determine which control this update is relevant to by comparing it to the references. |
if thumbStickNode === leftThumbStickNode { |
let displacement = float2(x: xValue, y: yValue) |
delegate?.controlInputSource(self, didUpdateDisplacement: displacement) |
} |
else if thumbStickNode === rightThumbStickNode { |
let displacement = float2(x: xValue, y: yValue) |
// Rotate the character only if the `thumbStickNode` is sufficiently displaced. |
if length(displacement) >= GameplayConfiguration.TouchControl.minimumRequiredThumbstickDisplacement { |
delegate?.controlInputSource(self, didUpdateAngularDisplacement: displacement) |
} |
else { |
delegate?.controlInputSource(self, didUpdateAngularDisplacement: float2()) |
} |
} |
} |
func thumbStickNode(thumbStickNode: ThumbStickNode, isPressed: Bool) { |
if thumbStickNode === rightThumbStickNode { |
if isPressed { |
delegate?.controlInputSourceDidBeginAttacking(self) |
} |
else { |
delegate?.controlInputSourceDidFinishAttacking(self) |
} |
} |
} |
// MARK: ControlInputSourceType |
func resetControlState() { |
// Nothing to do here. |
} |
// MARK: UIResponder |
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { |
super.touchesBegan(touches, with: event) |
for touch in touches { |
let touchPoint = touch.location(in: self) |
/* |
Ignore touches if the thumb stick controls are hidden, or if |
the touch is in the center of the screen. |
*/ |
let touchIsInCenter = touchPoint.x < centerDividerWidth / 2 && touchPoint.x > -centerDividerWidth / 2 |
if hideThumbStickNodes || touchIsInCenter { |
continue |
} |
if touchPoint.x < 0 { |
leftControlTouches.formUnion([touch]) |
leftThumbStickNode.position = pointByCheckingControlOffset(suggestedPoint: touchPoint) |
leftThumbStickNode.touchesBegan([touch], with: event) |
} |
else { |
rightControlTouches.formUnion([touch]) |
rightThumbStickNode.position = pointByCheckingControlOffset(suggestedPoint: touchPoint) |
rightThumbStickNode.touchesBegan([touch], with: event) |
} |
} |
} |
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { |
super.touchesMoved(touches, with: event) |
/* |
If the touch pertains to a `thumbStickNode`, pass the |
touch along to be handled. |
Holding onto individual touches allows the user to drag |
a touch that initially started on the `leftThumbStickNode` |
over the the `rightThumbStickNode`s zone or vice versa, |
while ensuring it is handled by the correct thumb stick. |
*/ |
let movedLeftTouches = touches.intersection(leftControlTouches) |
leftThumbStickNode.touchesMoved(movedLeftTouches, with: event) |
let movedRightTouches = touches.intersection(rightControlTouches) |
rightThumbStickNode.touchesMoved(movedRightTouches, with: event) |
} |
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { |
super.touchesEnded(touches, with: event) |
for touch in touches { |
let touchPoint = touch.location(in: self) |
/// Toggle pause when touching in the pause node. |
if pauseButton === atPoint(touchPoint) { |
gameStateDelegate?.controlInputSourceDidTogglePauseState(self) |
break |
} |
} |
let endedLeftTouches = touches.intersection(leftControlTouches) |
leftThumbStickNode.touchesEnded(endedLeftTouches, with: event) |
leftControlTouches.subtract(endedLeftTouches) |
let endedRightTouches = touches.intersection(rightControlTouches) |
rightThumbStickNode.touchesEnded(endedRightTouches, with: event) |
rightControlTouches.subtract(endedRightTouches) |
} |
override func touchesCancelled(_ touches: Set<UITouch>?, with event: UIEvent?) { |
super.touchesCancelled(touches!, with: event) |
leftThumbStickNode.resetTouchPad() |
rightThumbStickNode.resetTouchPad() |
// Keep the set's capacity, because roughly the same number of touch events are being received. |
leftControlTouches.removeAll(keepingCapacity: true) |
rightControlTouches.removeAll(keepingCapacity: true) |
} |
// MARK: Convenience Methods |
/// Calculates a point that keeps the `thumbStickNode` completely on screen. |
func pointByCheckingControlOffset(suggestedPoint: CGPoint) -> CGPoint { |
// `leftThumbStickNode` is an arbitrary choice - both are the same size. |
let controlSize = leftThumbStickNode.size |
let sceneSize = scene!.size |
/* |
The origin of `SKNode`'s coordinate system is at the center of the screen. |
Points to the left and below the origin are negative; |
points above and to the right are positive. |
Offset by 2/3 times the size of the control to maintain some padding |
around the edge of the view. |
*/ |
let minX = -sceneSize.width / 2 + controlSize.width / 1.5 |
let maxX = sceneSize.width / 2 - controlSize.width / 1.5 |
let minY = -sceneSize.height / 2 + controlSize.height / 1.5 |
let maxY = sceneSize.height / 2 - controlSize.height / 1.5 |
let boundX = max(min(suggestedPoint.x, maxX), minX) |
let boundY = max(min(suggestedPoint.y, maxY), minY) |
return CGPoint(x: boundX, y: boundY) |
} |
} |
Copyright © 2016 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2016-09-13