美文网首页
GameKit框架详细解析(三) —— iOS的Game Cen

GameKit框架详细解析(三) —— iOS的Game Cen

作者: 刀客传奇 | 来源:发表于2018-12-12 17:37 被阅读55次

    版本记录

    版本号 时间
    V1.0 2018.12.12 星期三

    前言

    GameKit框架创造经验,让玩家回到你的游戏。 添加排行榜,成就,匹配,挑战等等。接下来几篇我们就一起看一下这个框架。感兴趣的看下面几篇文章。
    1. GameKit框架详细解析(一) —— 基本概览(一)
    2. GameKit框架详细解析(二) —— iOS的Game Center:构建基于回合制的游戏(一)

    源码

    1. Swift

    首先看一下工程结构

    下面就是一起看一下代码

    1. GKTurnBasedMatch+Additions.swift
    
    import GameKit
    
    extension GKTurnBasedMatch {
      var isLocalPlayersTurn: Bool {
        return currentParticipant?.player == GKLocalPlayer.local
      }
      
      var others: [GKTurnBasedParticipant] {
        return participants.filter {
          return $0.player != GKLocalPlayer.local
        }
      }
    }
    
    2. SKTexture+Additions.swift
    
    import UIKit
    import SpriteKit
    
    extension SKTexture {
      class func recessedBackgroundTexture(of size: CGSize) -> SKTexture {
        return SKTexture(image: UIGraphicsImageRenderer(size: size).image { context in
          let fillColor = UIColor(white: 0, alpha: 0.2)
          let shadowColor = UIColor(white: 0, alpha: 0.3)
          
          let shadow = NSShadow()
          shadow.shadowColor = shadowColor
          shadow.shadowOffset = .zero
          shadow.shadowBlurRadius = 5
          
          let rectanglePath = UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 10)
          fillColor.setFill()
          rectanglePath.fill()
          
          let drawContext = context.cgContext
          
          drawContext.saveGState()
          drawContext.clip(to: rectanglePath.bounds)
          drawContext.setShadow(offset: .zero, blur: 0)
          drawContext.setAlpha((shadow.shadowColor as! UIColor).cgColor.alpha)
          drawContext.beginTransparencyLayer(auxiliaryInfo: nil)
          let rectangleOpaqueShadow = shadowColor.withAlphaComponent(1)
          drawContext.setShadow(offset: shadow.shadowOffset, blur: shadow.shadowBlurRadius, color: rectangleOpaqueShadow.cgColor)
          drawContext.setBlendMode(.sourceOut)
          drawContext.beginTransparencyLayer(auxiliaryInfo: nil)
          
          rectangleOpaqueShadow.setFill()
          rectanglePath.fill()
          
          drawContext.endTransparencyLayer()
          drawContext.endTransparencyLayer()
          drawContext.restoreGState()
        })
      }
      
      class func pillBackgroundTexture(of size: CGSize, color: UIColor?) -> SKTexture {
        return SKTexture(image: UIGraphicsImageRenderer(size: size).image { context in
          let fillColor = color ?? .white
          let shadowColor = UIColor(white: 0, alpha: 0.3)
          
          let shadow = NSShadow()
          shadow.shadowColor = shadowColor
          shadow.shadowOffset = CGSize(width: 0, height: 1)
          shadow.shadowBlurRadius = 5
          
          let drawContext = context.cgContext
          
          let pillRect = CGRect(origin: .zero, size: size).insetBy(dx: 3, dy: 4)
          let rectanglePath = UIBezierPath(roundedRect: pillRect, cornerRadius: size.height / 2)
          
          drawContext.setShadow(offset: shadow.shadowOffset, blur: shadow.shadowBlurRadius, color: shadowColor.cgColor)
          fillColor.setFill()
          rectanglePath.fill()
        })
      }
    }
    
    3. UIColor+Additions.swift
    
    import UIKit
    
    extension UIColor {
      static var background: UIColor {
        return UIColor(red: 26/255, green: 26/255, blue: 26/255, alpha: 1)
      }
      
      static var sky: UIColor {
        return UIColor(red: 112/255, green: 196/255, blue: 254/255, alpha: 1)
      }
    }
    
    4. GameCenterHelper.swift
    
    import GameKit
    
    final class GameCenterHelper: NSObject {
      typealias CompletionBlock = (Error?) -> Void
        
      static let helper = GameCenterHelper()
      
      static var isAuthenticated: Bool {
        return GKLocalPlayer.local.isAuthenticated
      }
    
      var viewController: UIViewController?
      var currentMatchmakerVC: GKTurnBasedMatchmakerViewController?
      
      enum GameCenterHelperError: Error {
        case matchNotFound
      }
    
      var currentMatch: GKTurnBasedMatch?
    
      var canTakeTurnForCurrentMatch: Bool {
        guard let match = currentMatch else {
          return true
        }
        
        return match.isLocalPlayersTurn
      }
      
      override init() {
        super.init()
        
        GKLocalPlayer.local.authenticateHandler = { gcAuthVC, error in
          NotificationCenter.default.post(name: .authenticationChanged, object: GKLocalPlayer.local.isAuthenticated)
    
          if GKLocalPlayer.local.isAuthenticated {
            GKLocalPlayer.local.register(self)
          } else if let vc = gcAuthVC {
            self.viewController?.present(vc, animated: true)
          }
          else {
            print("Error authentication to GameCenter: \(error?.localizedDescription ?? "none")")
          }
        }
      }
      
      func presentMatchmaker() {
        guard GKLocalPlayer.local.isAuthenticated else {
          return
        }
        
        let request = GKMatchRequest()
        
        request.minPlayers = 2
        request.maxPlayers = 2
        request.inviteMessage = "Would you like to play Nine Knights?"
        
        let vc = GKTurnBasedMatchmakerViewController(matchRequest: request)
        vc.turnBasedMatchmakerDelegate = self
        
        currentMatchmakerVC = vc
        viewController?.present(vc, animated: true)
      }
      
      func endTurn(_ model: GameModel, completion: @escaping CompletionBlock) {
        guard let match = currentMatch else {
          completion(GameCenterHelperError.matchNotFound)
          return
        }
        
        do {
          match.message = model.messageToDisplay
          
          match.endTurn(
            withNextParticipants: match.others,
            turnTimeout: GKExchangeTimeoutDefault,
            match: try JSONEncoder().encode(model),
            completionHandler: completion
          )
        } catch {
          completion(error)
        }
      }
    
      func win(completion: @escaping CompletionBlock) {
        guard let match = currentMatch else {
          completion(GameCenterHelperError.matchNotFound)
          return
        }
        
        match.currentParticipant?.matchOutcome = .won
        match.others.forEach { other in
          other.matchOutcome = .lost
        }
        
        match.endMatchInTurn(
          withMatch: match.matchData ?? Data(),
          completionHandler: completion
        )
      }
    }
    
    extension GameCenterHelper: GKTurnBasedMatchmakerViewControllerDelegate {
      func turnBasedMatchmakerViewControllerWasCancelled(_ viewController: GKTurnBasedMatchmakerViewController) {
        viewController.dismiss(animated: true)
      }
      
      func turnBasedMatchmakerViewController(_ viewController: GKTurnBasedMatchmakerViewController, didFailWithError error: Error) {
        print("Matchmaker vc did fail with error: \(error.localizedDescription).")
      }
    }
    
    extension GameCenterHelper: GKLocalPlayerListener {
      func player(_ player: GKPlayer, wantsToQuitMatch match: GKTurnBasedMatch) {
        let activeOthers = match.others.filter { other in
          return other.status == .active
        }
        
        match.currentParticipant?.matchOutcome = .lost
        activeOthers.forEach { participant in
          participant.matchOutcome = .won
        }
        
        match.endMatchInTurn(
          withMatch: match.matchData ?? Data()
        )
      }
      
      func player(_ player: GKPlayer, receivedTurnEventFor match: GKTurnBasedMatch, didBecomeActive: Bool) {
        if let vc = currentMatchmakerVC {
          currentMatchmakerVC = nil
          vc.dismiss(animated: true)
        }
        
        guard didBecomeActive else {
          return
        }
        
        NotificationCenter.default.post(name: .presentGame, object: match)
      }
    }
    
    extension Notification.Name {
      static let presentGame = Notification.Name(rawValue: "presentGame")
      static let authenticationChanged = Notification.Name(rawValue: "authenticationChanged")
    }
    
    5. GameModel.swift
    
    import GameKit
    
    struct GameModel: Codable {
      var turn: Int
      var state: State
      var lastMove: Move?
      var tokens: [Token]
      var winner: Player?
      var tokensPlaced: Int
      var millTokens: [Token]
      var removedToken: Token?
      var currentMill: [Token]?
      
      var currentPlayer: Player {
        return isKnightTurn ? .knight : .troll
      }
      
      var currentOpponent: Player {
        return isKnightTurn ? .troll : .knight
      }
      
      var messageToDisplay: String {
        let playerName = isKnightTurn ? "Knight" : "Troll"
        
        if isCapturingPiece {
          return "Take an opponent's piece!"
        }
        
        let stateAction: String
        switch state {
        case .placement:
          stateAction = "place"
          
        case .movement:
          if let winner = winner {
            return "\(winner == .knight ? "Knight" : "Troll")'s win!"
          } else {
            stateAction = "move"
          }
        }
        
        return "\(playerName)'s turn to \(stateAction)"
      }
      
      var isCapturingPiece: Bool {
        return currentMill != nil
      }
      
      var emptyCoordinates: [GridCoordinate] {
        let tokenCoords = tokens.map { $0.coord }
        
        return positions.filter { coord in
          return !tokenCoords.contains(coord)
        }
      }
      
      private(set) var isKnightTurn: Bool
      private let positions: [GridCoordinate]
      
      private let maxTokenCount = 18
      private let minPlayerTokenCount = 3
      
      init(isKnightTurn: Bool = true) {
        self.isKnightTurn = isKnightTurn
        
        turn = 0
        tokensPlaced = 0
        state = .placement
        tokens = [Token]()
        millTokens = [Token]()
        
        positions = [
          GridCoordinate(x: .min, y: .max, layer: .outer),
          GridCoordinate(x: .mid, y: .max, layer: .outer),
          GridCoordinate(x: .max, y: .max, layer: .outer),
          GridCoordinate(x: .max, y: .mid, layer: .outer),
          GridCoordinate(x: .max, y: .min, layer: .outer),
          GridCoordinate(x: .mid, y: .min, layer: .outer),
          GridCoordinate(x: .min, y: .min, layer: .outer),
          GridCoordinate(x: .min, y: .mid, layer: .outer),
          GridCoordinate(x: .min, y: .max, layer: .middle),
          GridCoordinate(x: .mid, y: .max, layer: .middle),
          GridCoordinate(x: .max, y: .max, layer: .middle),
          GridCoordinate(x: .max, y: .mid, layer: .middle),
          GridCoordinate(x: .max, y: .min, layer: .middle),
          GridCoordinate(x: .mid, y: .min, layer: .middle),
          GridCoordinate(x: .min, y: .min, layer: .middle),
          GridCoordinate(x: .min, y: .mid, layer: .middle),
          GridCoordinate(x: .min, y: .max, layer: .center),
          GridCoordinate(x: .mid, y: .max, layer: .center),
          GridCoordinate(x: .max, y: .max, layer: .center),
          GridCoordinate(x: .max, y: .mid, layer: .center),
          GridCoordinate(x: .max, y: .min, layer: .center),
          GridCoordinate(x: .mid, y: .min, layer: .center),
          GridCoordinate(x: .min, y: .min, layer: .center),
          GridCoordinate(x: .min, y: .mid, layer: .center),
        ]
      }
      
      func neighbors(at coord: GridCoordinate) -> [GridCoordinate] {
        var neighbors = [GridCoordinate]()
        
        switch coord.x {
        case .mid:
          neighbors.append(GridCoordinate(x: .min, y: coord.y, layer: coord.layer))
          neighbors.append(GridCoordinate(x: .max, y: coord.y, layer: coord.layer))
          
        case .min, .max:
          if coord.y == .mid {
            switch coord.layer {
            case .middle:
              neighbors.append(GridCoordinate(x: coord.x, y: coord.y, layer: .outer))
              neighbors.append(GridCoordinate(x: coord.x, y: coord.y, layer: .center))
            case .center, .outer:
              neighbors.append(GridCoordinate(x: coord.x, y: coord.y, layer: .middle))
            }
          } else {
            neighbors.append(GridCoordinate(x: .mid, y: coord.y, layer: coord.layer))
          }
        }
        
        switch coord.y {
        case .mid:
          neighbors.append(GridCoordinate(x: coord.x, y: .min, layer: coord.layer))
          neighbors.append(GridCoordinate(x: coord.x, y: .max, layer: coord.layer))
          
        case .min, .max:
          if coord.x == .mid {
            switch coord.layer {
            case .middle:
              neighbors.append(GridCoordinate(x: coord.x, y: coord.y, layer: .outer))
              neighbors.append(GridCoordinate(x: coord.x, y: coord.y, layer: .center))
            case .center, .outer:
              neighbors.append(GridCoordinate(x: coord.x, y: coord.y, layer: .middle))
            }
          } else {
            neighbors.append(GridCoordinate(x: coord.x, y: .mid, layer: coord.layer))
          }
        }
        
        return neighbors
      }
      
      func removableTokens(for player: Player) -> [Token] {
        let playerTokens = tokens.filter { token in
          return token.player == player
        }
        
        if playerTokens.count == 3, state == .movement {
          return playerTokens
        }
        
        return playerTokens.filter { token in
          return !millTokens.contains(token)
        }
      }
      
      func mill(containing token: Token) -> [Token]? {
        var coordsToCheck = [token.coord]
        
        var xPositionsToCheck: [GridPosition] = [.min, .mid, .max]
        xPositionsToCheck.remove(at: token.coord.x.rawValue)
        
        guard let firstXPosition = xPositionsToCheck.first, let lastXPosition = xPositionsToCheck.last else {
          return nil
        }
        
        var yPositionsToCheck: [GridPosition] = [.min, .mid, .max]
        yPositionsToCheck.remove(at: token.coord.y.rawValue)
        
        guard let firstYPosition = yPositionsToCheck.first, let lastYPosition = yPositionsToCheck.last else {
          return nil
        }
        
        var layersToCheck: [GridLayer] = [.outer, .middle, .center]
        layersToCheck.remove(at: token.coord.layer.rawValue)
        
        guard let firstLayer = layersToCheck.first, let lastLayer = layersToCheck.last else {
          return nil
        }
        
        switch token.coord.x {
        case .mid:
          coordsToCheck.append(GridCoordinate(x: token.coord.x, y: token.coord.y, layer: firstLayer))
          coordsToCheck.append(GridCoordinate(x: token.coord.x, y: token.coord.y, layer: lastLayer))
          
        case .min, .max:
          coordsToCheck.append(GridCoordinate(x: token.coord.x, y: firstYPosition, layer: token.coord.layer))
          coordsToCheck.append(GridCoordinate(x: token.coord.x, y: lastYPosition, layer: token.coord.layer))
        }
        
        let validHorizontalMillTokens = tokens.filter {
          return $0.player == token.player && coordsToCheck.contains($0.coord)
        }
        
        if validHorizontalMillTokens.count == 3 {
          return validHorizontalMillTokens
        }
        
        coordsToCheck = [token.coord]
        
        switch token.coord.y {
        case .mid:
          coordsToCheck.append(GridCoordinate(x: token.coord.x, y: token.coord.y, layer: firstLayer))
          coordsToCheck.append(GridCoordinate(x: token.coord.x, y: token.coord.y, layer: lastLayer))
          
        case .min, .max:
          coordsToCheck.append(GridCoordinate(x: firstXPosition, y: token.coord.y, layer: token.coord.layer))
          coordsToCheck.append(GridCoordinate(x: lastXPosition, y: token.coord.y, layer: token.coord.layer))
        }
        
        let validVerticalMillTokens = tokens.filter {
          return $0.player == token.player && coordsToCheck.contains($0.coord)
        }
        
        if validVerticalMillTokens.count == 3 {
          return validVerticalMillTokens
        }
        
        return nil
      }
      
      mutating func placeToken(at coord: GridCoordinate) {
        guard state == .placement else {
          return
        }
        
        let player = isKnightTurn ? Player.knight : Player.troll
        
        let newToken = Token(player: player, coord: coord)
        tokens.append(newToken)
        tokensPlaced += 1
        
        lastMove = Move(placed: coord)
        
        guard let newMill = mill(containing: newToken) else {
          advance()
          return
        }
        
        millTokens.append(contentsOf: newMill)
        currentMill = newMill
      }
      
      mutating func removeToken(at coord: GridCoordinate) -> Bool {
        guard isCapturingPiece else {
          return false
        }
        
        guard let index = tokens.firstIndex(where: { $0.coord == coord }) else {
          return false
        }
        
        let tokenToRemove = tokens[index]
        
        guard tokenCount(for: currentOpponent) == 3 || !millTokens.contains(tokenToRemove) else {
          return false
        }
        
        tokens.remove(at: index)
        lastMove = Move(removed: coord)
        advance()
        
        return true
      }
      
      mutating func move(from: GridCoordinate, to: GridCoordinate) {
        guard let index = tokens.firstIndex(where: { $0.coord == from }) else {
          return
        }
        
        let previousToken = tokens[index]
        let movedToken = Token(player: previousToken.player, coord: to)
        
        let millToRemove = mill(containing: previousToken) ?? []
        
        if !millToRemove.isEmpty {
          millToRemove.forEach { tokenToRemove in
            guard let index = millTokens.index(of: tokenToRemove) else {
              return
            }
            
            self.millTokens.remove(at: index)
          }
        }
        
        tokens[index] = movedToken
        lastMove = Move(start: from, end: to)
        
        if !millToRemove.isEmpty {
          for removedToken in millToRemove where removedToken != previousToken && mill(containing: removedToken) != nil {
            millTokens.append(removedToken)
          }
        }
        
        guard let newMill = mill(containing: movedToken) else {
          advance()
          return
        }
        
        millTokens.append(contentsOf: newMill)
        currentMill = newMill
      }
      
      mutating func advance() {
        if tokensPlaced == maxTokenCount && state == .placement {
          state = .movement
        }
        
        turn += 1
        currentMill = nil
        
        if state == .movement {
          if tokenCount(for: currentOpponent) == 2 || !canMove(currentOpponent) {
            winner = currentPlayer
          } else {
            isKnightTurn = !isKnightTurn
          }
        } else {
          isKnightTurn = !isKnightTurn
        }
      }
      
      func tokenCount(for player: Player) -> Int {
        return tokens.filter { token in
          return token.player == player
          }.count
      }
      
      func canMove(_ player: Player) -> Bool {
        let playerTokens = tokens.filter { token in
          return token.player == player
        }
        
        for token in playerTokens {
          let emptyNeighbors = neighbors(at: token.coord).filter({ emptyCoordinates.contains($0) })
          if !emptyNeighbors.isEmpty {
            return true
          }
        }
        
        return false
      }
    }
    
    // MARK: - Types
    
    extension GameModel {
      enum Player: String, Codable {
        case knight, troll
      }
      
      enum State: Int, Codable {
        case placement
        case movement
      }
      
      enum GridPosition: Int, Codable {
        case min, mid, max
      }
      
      enum GridLayer: Int, Codable {
        case outer, middle, center
      }
      
      struct GridCoordinate: Codable, Equatable {
        let x, y: GridPosition
        let layer: GridLayer
      }
      
      struct Token: Codable, Equatable {
        let player: Player
        let coord: GridCoordinate
      }
      
      struct Move: Codable, Equatable {
        var placed: GridCoordinate?
        var removed: GridCoordinate?
        var start: GridCoordinate?
        var end: GridCoordinate?
        
        init(placed: GridCoordinate?) {
          self.placed = placed
        }
        
        init(removed: GridCoordinate?) {
          self.removed = removed
        }
        
        init(start: GridCoordinate?, end: GridCoordinate?) {
          self.start = start
          self.end = end
        }
      }
    }
    
    6. BackgroundNode.swift
    
    import SpriteKit
    
    class BackgroundNode: SKSpriteNode {
      enum Kind {
        case pill
        case recessed
      }
      
      init(kind: Kind, size: CGSize, color: UIColor? = nil) {
        let texture: SKTexture
        
        switch kind {
        case .pill:
          texture = SKTexture.pillBackgroundTexture(of: size, color: color)
        default:
          texture = SKTexture.recessedBackgroundTexture(of: size)
        }
        
        super.init(texture: texture, color: .clear, size: size)
      }
      
      required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
      }
    }
    
    7. BoardNode.swift
    
    import SpriteKit
    
    final class BoardNode: SKNode {
      static let boardPointNodeName = "boardPoint"
      
      private enum NodeLayer: CGFloat {
        case background = 10
        case line = 20
        case point = 30
      }
      
      private let sideLength: CGFloat
      private let innerPadding: CGFloat
      
      init(sideLength: CGFloat, innerPadding: CGFloat = 100) {
        self.sideLength = sideLength
        self.innerPadding = innerPadding
        
        super.init()
        
        let size = CGSize(width: sideLength, height: sideLength)
        
        for index in 0...2 {
          let containerNode = SKSpriteNode(
            color: .clear,
            size: CGSize(
              width: size.width - (innerPadding * CGFloat(index)),
              height: size.height - (innerPadding * CGFloat(index))
            )
          )
          
          containerNode.zPosition = NodeLayer.background.rawValue + CGFloat(index)
          createBoardPoints(on: containerNode, shouldAddCenterLine: index < 2)
          
          addChild(containerNode)
        }
      }
      
      required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
      }
      
      func node(at gridCoordinate: GameModel.GridCoordinate, named nodeName: String) -> SKNode? {
        let layerPadding = innerPadding * CGFloat(gridCoordinate.layer.rawValue)
        let halfLayerSide = (sideLength - layerPadding) / 2
        let halfLayerPadding = layerPadding / 2
        let halfSide = sideLength / 2
        
        let adjustedXCoord = halfLayerPadding + (CGFloat(gridCoordinate.x.rawValue) * halfLayerSide)
        let adjustedYCoord = halfLayerPadding + (CGFloat(gridCoordinate.y.rawValue) * halfLayerSide)
        
        let relativeGridPoint = CGPoint(x: adjustedXCoord - halfSide, y: adjustedYCoord - halfSide)
        
        let node = atPoint(relativeGridPoint)
        return node.name == nodeName ? node : nil
      }
      
      func gridCoordinate(for node: SKNode) -> GameModel.GridCoordinate? {
        guard let parentZPosition = node.parent?.zPosition else {
          return nil
        }
        
        let adjustedParentZPosition = parentZPosition - NodeLayer.background.rawValue
        
        guard let layer = GameModel.GridLayer(rawValue: Int(adjustedParentZPosition)) else {
          return nil
        }
        
        let xGridPosition: GameModel.GridPosition
        if node.position.x == 0 {
          xGridPosition = .mid
        } else {
          xGridPosition = node.position.x > 0 ? .max : .min
        }
        
        let yGridPosition: GameModel.GridPosition
        if node.position.y == 0 {
          yGridPosition = .mid
        } else {
          yGridPosition = node.position.y > 0 ? .max : .min
        }
        
        return GameModel.GridCoordinate(x: xGridPosition, y: yGridPosition, layer: layer)
      }
      
      private func createBoardPoints(on node: SKSpriteNode, shouldAddCenterLine: Bool) {
        let lineWidth: CGFloat = 3
        let centerLineLength: CGFloat = 50
        let halfBoardWidth = node.size.width / 2
        let halfBoardHeight = node.size.height / 2
        let boardPointSize = CGSize(width: 24, height: 24)
        
        let relativeBoardPositions = [
          CGPoint(x: -halfBoardWidth, y: halfBoardHeight),
          CGPoint(x: 0, y: halfBoardHeight),
          CGPoint(x: halfBoardWidth, y: halfBoardHeight),
          CGPoint(x: halfBoardWidth, y: 0),
          CGPoint(x: halfBoardWidth, y: -halfBoardHeight),
          CGPoint(x: 0, y: -halfBoardHeight),
          CGPoint(x: -halfBoardWidth, y: -halfBoardHeight),
          CGPoint(x: -halfBoardWidth, y: 0),
        ]
        
        for (index, position) in relativeBoardPositions.enumerated() {
          let boardPointNode = SKShapeNode(ellipseOf: boardPointSize)
          
          boardPointNode.zPosition = NodeLayer.point.rawValue
          boardPointNode.name = BoardNode.boardPointNodeName
          boardPointNode.lineWidth = lineWidth
          boardPointNode.position = position
          boardPointNode.fillColor = .background
          boardPointNode.strokeColor = .white
          
          node.addChild(boardPointNode)
          
          if shouldAddCenterLine && (position.x == 0 || position.y == 0) {
            let path = CGMutablePath()
            path.move(to: position)
            
            let nextPosition: CGPoint
            if position.x == 0 {
              let factor = position.y > 0 ? -centerLineLength : centerLineLength
              nextPosition = CGPoint(x: 0, y: position.y + factor)
            } else {
              let factor = position.x > 0 ? -centerLineLength : centerLineLength
              nextPosition = CGPoint(x: position.x + factor, y: 0)
            }
            path.addLine(to: nextPosition)
            
            let lineNode = SKShapeNode(path: path, centered: true)
            lineNode.position = CGPoint(
              x: (position.x + nextPosition.x) / 2,
              y: (position.y + nextPosition.y) / 2
            )
            
            lineNode.strokeColor = boardPointNode.strokeColor
            lineNode.zPosition = NodeLayer.line.rawValue
            lineNode.lineWidth = lineWidth
            
            node.addChild(lineNode)
          }
          
          let lineIndex = index < relativeBoardPositions.count - 1 ? index + 1 : 0
          let nextPosition = relativeBoardPositions[lineIndex]
          
          let path = CGMutablePath()
          path.move(to: position)
          path.addLine(to: nextPosition)
          
          let lineNode = SKShapeNode(path: path, centered: true)
          lineNode.position = CGPoint(
            x: (position.x + nextPosition.x) / 2,
            y: (position.y + nextPosition.y) / 2
          )
          
          lineNode.strokeColor = boardPointNode.strokeColor
          lineNode.zPosition = NodeLayer.line.rawValue
          lineNode.lineWidth = lineWidth
          
          node.addChild(lineNode)
        }
      }
    }
    
    8. ButtonNode.swift
    
    import SpriteKit
    
    class ButtonNode: TouchNode {
      private let backgroundNode: BackgroundNode
      private let labelNode: SKLabelNode
      
      init(_ text: String, size: CGSize, actionBlock: ActionBlock?) {
        backgroundNode = BackgroundNode(kind: .recessed, size: size)
        backgroundNode.position = CGPoint(
          x: size.width / 2,
          y: size.height / 2
        )
        
        let buttonFont = UIFont.systemFont(ofSize: 24, weight: .semibold)
        
        labelNode = SKLabelNode(fontNamed: buttonFont.fontName)
        labelNode.fontSize = buttonFont.pointSize
        labelNode.fontColor = .white
        labelNode.text = text
        labelNode.position = CGPoint(
          x: size.width / 2,
          y: size.height / 2 - labelNode.frame.height / 2
        )
        
        let shadowNode = SKLabelNode(fontNamed: buttonFont.fontName)
        shadowNode.fontSize = buttonFont.pointSize
        shadowNode.fontColor = .black
        shadowNode.text = text
        shadowNode.alpha = 0.5
        shadowNode.position = CGPoint(
          x: labelNode.position.x + 2,
          y: labelNode.position.y - 2
        )
        
        super.init()
        
        addChild(backgroundNode)
        addChild(shadowNode)
        addChild(labelNode)
        
        self.actionBlock = actionBlock
      }
      
      required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
      }
      
      override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesBegan(touches, with: event)
        
        guard isEnabled else {
          return
        }
        
        labelNode.run(SKAction.fadeAlpha(to: 0.8, duration: 0.2))
      }
      
      override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesEnded(touches, with: event)
        
        guard isEnabled else {
          return
        }
        
        labelNode.run(SKAction.fadeAlpha(to: 1, duration: 0.2))
      }
    }
    
    9. InformationNode.swift
    
    import SpriteKit
    
    final class InformationNode: TouchNode {
      private let backgroundNode: BackgroundNode
      private let labelNode: SKLabelNode
      
      var text: String? {
        get {
          return labelNode.text
        }
        set {
          labelNode.text = newValue
        }
      }
      
      init(_ text: String, size: CGSize, actionBlock: ActionBlock? = nil) {
        backgroundNode = BackgroundNode(kind: .pill, size: size)
        backgroundNode.position = CGPoint(
          x: size.width / 2,
          y: size.height / 2
        )
        
        let font = UIFont.systemFont(ofSize: 18, weight: .semibold)
        
        labelNode = SKLabelNode(fontNamed: font.fontName)
        labelNode.fontSize = font.pointSize
        labelNode.fontColor = .black
        labelNode.text = text
        labelNode.position = CGPoint(
          x: size.width / 2,
          y: size.height / 2 - labelNode.frame.height / 2 + 2
        )
        
        super.init()
        
        addChild(backgroundNode)
        addChild(labelNode)
        
        self.actionBlock = actionBlock
      }
      
      required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
      }
    }
    
    10. TokenNode.swift
    
    import SpriteKit
    
    final class TokenNode: SKSpriteNode {
      static let tokenNodeName = "token"
      
      private let rotateActionKey = "rotate"
      
      var isIndicated: Bool = false {
        didSet {
          if isIndicated {
            run(SKAction.repeatForever(SKAction.rotate(byAngle: 1, duration: 0.5)), withKey: rotateActionKey)
          } else {
            removeAction(forKey: rotateActionKey)
            run(SKAction.rotate(toAngle: 0, duration: 0.15))
          }
        }
      }
      
      let type: GameModel.Player
      
      init(type: GameModel.Player) {
        self.type = type
        
        let textureName = "\(type.rawValue)-token"
        let texture = SKTexture(imageNamed: textureName)
        
        super.init(
          texture: texture,
          color: .clear,
          size: texture.size()
        )
        
        name = TokenNode.tokenNodeName
      }
      
      required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
      }
      
      func remove() {
        run(SKAction.sequence([SKAction.scale(to: 0, duration: 0.15), SKAction.removeFromParent()]))
      }
    }
    
    11. TouchNode.swift
    
    import SpriteKit
    
    class TouchNode: SKNode {
      typealias ActionBlock = (() -> Void)
      
      var actionBlock: ActionBlock?
      
      var isEnabled: Bool = true {
        didSet {
          alpha = isEnabled ? 1 : 0.5
        }
      }
      
      override var isUserInteractionEnabled: Bool {
        get {
          return true
        }
        set {
          // intentionally blank
        }
      }
      
      override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        if let block = actionBlock, isEnabled {
          block()
        }
      }
    }
    
    12. GameScene.swift
    
    import SpriteKit
    
    final class GameScene: SKScene {
      // MARK: - Enums
      
      private enum NodeLayer: CGFloat {
        case background = 100
        case board = 101
        case token = 102
        case ui = 1000
      }
      
      // MARK: - Properties
      
      private var model: GameModel
      
      private var boardNode: BoardNode!
      private var messageNode: InformationNode!
      private var selectedTokenNode: TokenNode?
      
      private var highlightedTokens = [SKNode]()
      private var removableNodes = [TokenNode]()
      
      private var isSendingTurn = false
      
      private let successGenerator = UINotificationFeedbackGenerator()
      private let feedbackGenerator = UIImpactFeedbackGenerator(style: .light)
      
      // MARK: Computed
      
      private var viewWidth: CGFloat {
        return view?.frame.size.width ?? 0
      }
      
      private var viewHeight: CGFloat {
        return view?.frame.size.height ?? 0
      }
      
      // MARK: - Init
      
      init(model: GameModel) {
        self.model = model
        
        super.init(size: .zero)
        
        scaleMode = .resizeFill
      }
      
      required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
      }
      
      override func didMove(to view: SKView) {
        super.didMove(to: view)
        
        successGenerator.prepare()
        feedbackGenerator.prepare()
        
        setUpScene(in: view)
      }
      
      override func didChangeSize(_ oldSize: CGSize) {
        removeAllChildren()
        setUpScene(in: view)
      }
      
      // MARK: - Setup
      
      private func setUpScene(in view: SKView?) {
        guard viewWidth > 0 else {
          return
        }
        
        backgroundColor = .background
        
        var runningYOffset: CGFloat = 0
        
        let sceneMargin: CGFloat = 40
        let safeAreaTopInset = view?.window?.safeAreaInsets.top ?? 0
        let safeAreaBottomInset = view?.window?.safeAreaInsets.bottom ?? 0
        
        let padding: CGFloat = 24
        let boardSideLength = min(viewWidth, viewHeight) - (padding * 2)
        boardNode = BoardNode(sideLength: boardSideLength)
        boardNode.zPosition = NodeLayer.board.rawValue
        runningYOffset += safeAreaBottomInset + sceneMargin + (boardSideLength / 2)
        boardNode.position = CGPoint(
          x: viewWidth / 2,
          y: runningYOffset
        )
        
        addChild(boardNode)
        
        let groundNode = SKSpriteNode(imageNamed: "ground")
        let aspectRatio = groundNode.size.width / groundNode.size.height
        let adjustedGroundWidth = view?.bounds.width ?? 0
        groundNode.size = CGSize(
          width: adjustedGroundWidth,
          height: adjustedGroundWidth / aspectRatio
        )
        groundNode.zPosition = NodeLayer.background.rawValue
        runningYOffset += sceneMargin + (boardSideLength / 2) + (groundNode.size.height / 2)
        groundNode.position = CGPoint(
          x: viewWidth / 2,
          y: runningYOffset
        )
        addChild(groundNode)
        
        messageNode = InformationNode(model.messageToDisplay, size: CGSize(width: viewWidth - (sceneMargin * 2), height: 40))
        messageNode.zPosition = NodeLayer.ui.rawValue
        messageNode.position = CGPoint(
          x: sceneMargin,
          y: runningYOffset - (sceneMargin * 1.25)
        )
        
        addChild(messageNode)
        
        let skySize = CGSize(width: viewWidth, height: viewHeight - groundNode.position.y)
        let skyNode = SKSpriteNode(color: .sky, size: skySize)
        skyNode.zPosition = NodeLayer.background.rawValue - 1
        runningYOffset -= skyNode.size.height / 2
        skyNode.position = CGPoint(
          x: viewWidth / 2,
          y: viewHeight - (skySize.height / 2)
        )
        addChild(skyNode)
        
        let buttonSize = CGSize(width: 125, height: 50)
        let menuButton = ButtonNode("Menu", size: buttonSize) {
          self.returnToMenu()
        }
        menuButton.position = CGPoint(
          x: (viewWidth - buttonSize.width) / 2,
          y: viewHeight - safeAreaTopInset - (sceneMargin * 2)
        )
        menuButton.zPosition = NodeLayer.ui.rawValue
        addChild(menuButton)
        
        loadTokens()
      }
      
      // MARK: - Touches
      
      override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        touches.forEach { touch in
          handleTouch(touch)
        }
      }
      
      private func handleTouch(_ touch: UITouch) {
        guard !isSendingTurn && GameCenterHelper.helper.canTakeTurnForCurrentMatch else {
          return
        }
        
        guard model.winner == nil else {
          return
        }
        
        let location = touch.location(in: self)
        
        if model.isCapturingPiece {
          handleRemoval(at: location)
          return
        }
        
        switch model.state {
        case .placement:
          handlePlacement(at: location)
          
        case .movement:
          handleMovement(at: location)
        }
      }
      
      // MARK: - Spawning
      
      private func loadTokens() {
        for token in model.tokens {
          guard let boardPointNode = boardNode.node(at: token.coord, named: BoardNode.boardPointNodeName) else {
            return
          }
          
          spawnToken(at: boardPointNode.position, for: token.player)
        }
      }
      
      private func spawnToken(at point: CGPoint, for player: GameModel.Player) {
        let tokenNode = TokenNode(type: player)
        
        tokenNode.zPosition = NodeLayer.token.rawValue
        tokenNode.position = point
        
        boardNode.addChild(tokenNode)
      }
      
      // MARK: - Helpers
      
      private func returnToMenu() {
        view?.presentScene(MenuScene(), transition: SKTransition.push(with: .down, duration: 0.3))
      }
      
      private func handlePlacement(at location: CGPoint) {
        let node = atPoint(location)
        
        guard node.name == BoardNode.boardPointNodeName else {
          return
        }
        
        guard let coord = boardNode.gridCoordinate(for: node) else {
          return
        }
        
        spawnToken(at: node.position, for: model.currentPlayer)
        model.placeToken(at: coord)
        
        processGameUpdate()
      }
      
      private func handleMovement(at location: CGPoint) {
        let node = atPoint(location)
        
        if let selected = selectedTokenNode {
          if highlightedTokens.contains(node) {
            let selectedSceneLocation = convert(selected.position, from: boardNode)
            
            guard let fromCoord = gridCoordinate(at: selectedSceneLocation), let toCoord = boardNode.gridCoordinate(for: node) else {
              return
            }
            
            model.move(from: fromCoord, to: toCoord)
            processGameUpdate()
            
            selected.run(SKAction.move(to: node.position, duration: 0.175))
          }
          
          deselectCurrentToken()
        } else {
          guard let token = node as? TokenNode, token.type == model.currentPlayer else {
            return
          }
          
          selectedTokenNode = token
          
          if model.tokenCount(for: model.currentPlayer) == 3 {
            highlightTokens(at: model.emptyCoordinates)
            return
          }
          
          guard let coord = gridCoordinate(at: location) else {
            return
          }
          
          highlightTokens(at: model.neighbors(at: coord))
        }
      }
      
      private func handleRemoval(at location: CGPoint) {
        let node = atPoint(location)
        
        guard let tokenNode = node as? TokenNode, tokenNode.type == model.currentOpponent else {
          return
        }
        
        guard let coord = gridCoordinate(at: location) else {
          return
        }
        
        guard model.removeToken(at: coord) else {
          return
        }
        
        tokenNode.remove()
        removableNodes.forEach { node in
          node.isIndicated = false
        }
        
        processGameUpdate()
      }
      
      private func gridCoordinate(at location: CGPoint) -> GameModel.GridCoordinate? {
        guard let boardPointNode = nodes(at: location).first(where: { $0.name == BoardNode.boardPointNodeName }) else {
          return nil
        }
        
        return boardNode.gridCoordinate(for: boardPointNode)
      }
      
      private func highlightTokens(at coords: [GameModel.GridCoordinate]) {
        let tokensFromCoords = coords.compactMap { coord in
          return self.boardNode.node(at: coord, named: BoardNode.boardPointNodeName)
        }
        
        highlightedTokens = tokensFromCoords
        
        for neighborNode in highlightedTokens {
          neighborNode.run(SKAction.scale(to: 1.25, duration: 0.15))
        }
      }
      
      private func deselectCurrentToken() {
        selectedTokenNode = nil
        
        guard !highlightedTokens.isEmpty else {
          return
        }
        
        highlightedTokens.forEach { node in
          node.run(SKAction.scale(to: 1, duration: 0.15))
        }
        
        highlightedTokens.removeAll()
      }
      
      private func processGameUpdate() {
        messageNode.text = model.messageToDisplay
        
        if model.isCapturingPiece {
          successGenerator.notificationOccurred(.success)
          successGenerator.prepare()
          
          let tokens = model.removableTokens(for: model.currentOpponent)
          
          if tokens.isEmpty {
            model.advance()
            processGameUpdate()
            return
          }
          
          let nodes = tokens.compactMap { token in
            boardNode.node(at: token.coord, named: TokenNode.tokenNodeName) as? TokenNode
          }
          
          removableNodes = nodes
          
          nodes.forEach { node in
            node.isIndicated = true
          }
        } else {
          feedbackGenerator.impactOccurred()
          feedbackGenerator.prepare()
          
          isSendingTurn = true
    
          if model.winner != nil {
            GameCenterHelper.helper.win { error in
              defer {
                self.isSendingTurn = false
              }
              
              if let e = error {
                print("Error winning match: \(e.localizedDescription)")
                return
              }
              
              self.returnToMenu()
            }
          } else {
            GameCenterHelper.helper.endTurn(model) { error in
              defer {
                self.isSendingTurn = false
              }
    
              if let e = error {
                print("Error ending turn: \(e.localizedDescription)")
                return
              }
              
              self.returnToMenu()
            }
          }
        }
      }
    }
    
    13. MenuScene.swift
    
    import GameKit
    import SpriteKit
    
    final class MenuScene: SKScene {
      private let transition = SKTransition.push(with: .up, duration: 0.3)
      private let feedbackGenerator = UIImpactFeedbackGenerator(style: .light)
      
      private var viewWidth: CGFloat {
        return view?.frame.size.width ?? 0
      }
      
      private var viewHeight: CGFloat {
        return view?.frame.size.height ?? 0
      }
      
      private var localButton: ButtonNode!
      private var onlineButton: ButtonNode!
      
      // MARK: - Init
      
      override init() {
        super.init(size: .zero)
        
        scaleMode = .resizeFill
      }
      
      required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
      }
      
      override func didMove(to view: SKView) {
        super.didMove(to: view)
        
        feedbackGenerator.prepare()
        GameCenterHelper.helper.currentMatch = nil
        
        NotificationCenter.default.addObserver(
          self,
          selector: #selector(authenticationChanged(_:)),
          name: .authenticationChanged,
          object: nil
        )
        
        NotificationCenter.default.addObserver(
          self,
          selector: #selector(presentGame(_:)),
          name: .presentGame,
          object: nil
        )
        
        setUpScene(in: view)
      }
      
      override func didChangeSize(_ oldSize: CGSize) {
        removeAllChildren()
        setUpScene(in: view)
      }
      
      private func setUpScene(in view: SKView?) {
        guard viewWidth > 0 else {
          return
        }
        
        backgroundColor = .sky
        
        var runningYOffset = viewHeight
        
        let sceneMargin: CGFloat = 40
        let buttonWidth: CGFloat = viewWidth - (sceneMargin * 2)
        let safeAreaTopInset = view?.window?.safeAreaInsets.top ?? 0
        let buttonSize = CGSize(width: buttonWidth, height: buttonWidth * 3 / 11)
        
        runningYOffset -= safeAreaTopInset + (sceneMargin * 3)
        
        let logoNode = SKSpriteNode(imageNamed: "title-logo")
        logoNode.position = CGPoint(
          x: viewWidth / 2,
          y: runningYOffset
        )
        addChild(logoNode)
        
        let groundNode = SKSpriteNode(imageNamed: "ground")
        let aspectRatio = groundNode.size.width / groundNode.size.height
        let adjustedGroundWidth = view?.bounds.width ?? 0
        groundNode.size = CGSize(
            width: adjustedGroundWidth,
            height: adjustedGroundWidth / aspectRatio
        )
        groundNode.position = CGPoint(
          x: viewWidth / 2,
          y: (groundNode.size.height / 2) - (sceneMargin * 1.375)
        )
        addChild(groundNode)
        
        let sunNode = SKSpriteNode(imageNamed: "sun")
        sunNode.position = CGPoint(
          x: viewWidth - (sceneMargin * 1.3),
          y: viewHeight - safeAreaTopInset - (sceneMargin * 1.25)
        )
        addChild(sunNode)
        
        localButton = ButtonNode("Local Game", size: buttonSize) {
          self.view?.presentScene(GameScene(model: GameModel()), transition: self.transition)
        }
        
        runningYOffset -= sceneMargin + logoNode.size.height
        localButton.position = CGPoint(x: sceneMargin, y: runningYOffset)
        addChild(localButton)
        
        onlineButton = ButtonNode("Online Game", size: buttonSize) {
          GameCenterHelper.helper.presentMatchmaker()
        }
        onlineButton.isEnabled = GameCenterHelper.isAuthenticated
        runningYOffset -= sceneMargin + buttonSize.height
        onlineButton.position = CGPoint(x: sceneMargin, y: runningYOffset)
        addChild(onlineButton)
      }
      
      // MARK: - Notifications
    
      @objc private func authenticationChanged(_ notification: Notification) {
        onlineButton.isEnabled = notification.object as? Bool ?? false
      }
      
      @objc private func presentGame(_ notification: Notification) {
        // 1
        guard let match = notification.object as? GKTurnBasedMatch else {
          return
        }
        
        loadAndDisplay(match: match)
      }
    
      // MARK: - Helpers
    
      private func loadAndDisplay(match: GKTurnBasedMatch) {
        // 2
        match.loadMatchData { data, error in
          let model: GameModel
          
          if let data = data {
            do {
              // 3
              model = try JSONDecoder().decode(GameModel.self, from: data)
            } catch {
              model = GameModel()
            }
          } else {
            model = GameModel()
          }
          
          GameCenterHelper.helper.currentMatch = match
          // 4
          self.view?.presentScene(GameScene(model: model), transition: self.transition)
        }
      }
    }
    
    14. AppDelegate.swift
    
    import UIKit
    
    @UIApplicationMain
    class AppDelegate: UIResponder, UIApplicationDelegate {
      var window: UIWindow?
      
      func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        window = UIWindow(frame: UIScreen.main.bounds)
        
        window?.rootViewController = GameViewController()
        window?.makeKeyAndVisible()
        
        return true
      }
    }
    
    15. GameViewController.swift
    
    import UIKit
    import SpriteKit
    
    final class GameViewController: UIViewController {
      private var skView: SKView {
        return view as! SKView
      }
      
      override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return .portrait
      }
      
      override var shouldAutorotate: Bool {
        return false
      }
      
      override func loadView() {
        view = SKView()
      }
      
      override func viewDidLoad() {
        super.viewDidLoad()
        
        skView.presentScene(MenuScene())
        
        GameCenterHelper.helper.viewController = self
      }
    }
    

    后记

    本篇主要讲述了iOS的Game Center:构建基于回合制的游戏,感兴趣的给个赞或者关注~~~

    相关文章

      网友评论

          本文标题:GameKit框架详细解析(三) —— iOS的Game Cen

          本文链接:https://www.haomeiwen.com/subject/xkcphqtx.html