版本记录
版本号 | 时间 |
---|---|
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:构建基于回合制的游戏,感兴趣的给个赞或者关注~~~