这是本书学习Swift的最后一章。希望你喜欢这次的学习!
到现在为止,你应该对Swift的编程语言已经相当熟悉了,是不是忍不住的想要操作练习下?相比有些怪异的Objective-C,我想你也相信强大且简洁的Swift语言一定能取代他了。
在最后一章,你将新建一个应用,这个应用里会涉及很多到目前你所学到的Swift内容,你将创建一个流行的黑白棋游戏,让你的用户可以和电脑进行比赛。
黑白棋应用已经用Objective-C 实现且发布了,在raywenderlich.com上下两部分文章中有:
• http://www.raywenderlich.com/29228/how-to-develop-an-ipad-board-game-app-part-12
• http://www.raywenderlich.com/29248/how-to-develop-an-ipad-board-game-app-part-22
本章将用和Objective-C同样的结构与开发步骤来用Swift编写,让你可以同步比较下Swift和Objective-C在实现上的不同。
Getting started - 开始
在准备的开始项目中已经包含了图片等资源。运行项目,会看到如下界面:
花上一些时间去熟悉下项目代码的结构。项目代码里有一个在Main.storyboard中布局的控制器以及一个对应的ViewController.swif代码。控制器有几个连接属性outlets,稍后的教程里会介绍。
Modeling the playing board - 建立model棋盘模型
在黑白棋中,玩家不断的在一个8*8的网状格子中替换黑棋和白棋。游戏的规则非常的简单,如果你不熟悉的话可以自行百度。
你的第一个任务是创建一个模型用于表示当前Board棋盘的状态。64个格子中棋子cells可能是黑色,白色或者是空。听起来用Swift的枚举是个不错的选择。
新建一个Swift文件取名叫BoardCellState.swift,并添加到Model文件夹中。添加diam如下:
import Foundation
enum BoardCellState {
case Empty, Black, White
}
上面的枚举定义了棋盘上每个格子上必有的三种状态。
接下来的一系列步骤用于处理棋盘上的逻辑。你将用一对整数:行和列来表示每个方形的格子。将逻辑写入每一个这样的格子中。
在Model文件夹中添加Swift文件BoardLocation.swift。添加内容如下:
import Foundation
struct BoardLocation {
let row: Int, column: Int
init(row: Int, column: Int) {
self.row = row
self.column = column
}
}
上面用了一个简单的结构BoardLocation,在其中定义结合了整型的行和列。你可能会想用元组来实现同样的目的,例如let postion:(Int,Int),但是你会发现结构描述的更加清晰些。而且,结构可以实现协议和方法,在本章后续内容中你会使用到这些特征。
现在该处理棋盘board了。在Model文件夹中添加Board.swift,添加内容如下:
import Foundation
class Board {
private var cells: [BoardCellState]
let boardSize = 8
init () {
cells = Array(count: boardSize * boardSize, repeatedValue: BoardCellState.Empty)
}
subscript(location: BoardLocation) -> BoardCellState {
get {
return cells[location.row * boardSize + location.column] }
set {
cells[location.row * boardSize + location.column] = newValue
}
}
}
这定义了一个Board类,包含一个常数数组cells用于表示格子,以及一个常数boardSize表示棋盘大小。初始化创建了个cell数组,里面每个元素都用BoardCellState.Empty进行填充。
subscript提供了一个简明的方法去获取以及设置每个cell的值,比如如下:
var board = Board()
board[BoardLocation(row: 4, column: 5)] = BoardCellState.White
然而,还有更加简洁的格式,在Board类中添加如下subscript
subscript(row: Int, column: Int) -> BoardCellState {
get {
return self[BoardLocation(row: row, column: column)] }
set {
self[BoardLocation(row: row, column: column)] = newValue
}
}
上面的subscript你可以如下使用
var board = Board()
board[4, 5] = BoardCellState.White
Subscripts允许你为你的Swift类创建非常灵活的API。你可以定义更多的subscript,让你能以更多的方式来访问在这类中的数据。
为了测试你刚添加的代码,你需要利用这个model创建一个UI。然而,还有一个更直接立即可用的机制,单元测试!
打开SwiftReversiTests.swift并将以下方法添加到SwiftReversiTests.swift类中。
func test_subscript_setWithValidCoords_cellStateIsChanged() {
let board = Board()
// set the state of one of the cells
board[4, 5] = BoardCellState.White
// verify
let retrievedState = board[4, 5];
XCTAssertEqual(BoardCellState.White, retrievedState, "The cell should have been white!");
}
上面的代码用了个简单的单元测试,实现设置和获取一个board中cell内容。这实现的实际是两个subscript方法。因为第二个获取方法会调用对应的subscript。
在你编译项目前,确保你希望测试的Swift文件在正确的target membership中。你可以打开File Inspector。确保BoardCellState.swift,BoardLocation.swif和Board.swift都同时选中了SwiftReversi和SwiftReversiTests,否则测试代码不会编译。
在xcode菜单栏选择Product\Test 编译并运行tests.会有个清晰的提示告诉你测试代码正在执行
你在控制台内还将看到测试的进展报告以及绿色的小点标记每个测试用例。
注意:本教程的其他部分均没有使用单元测试。继续本教程的学习,你可以使用单元测试来练习检查。
在Board类中将每格的坐标用subscript暴露出来十分高明,但也有个小缺点。如果你提供的行或列的值超出了函数的边界值,便会有些意外出现,现在就来纠正防止这个问题。
在Board的类中,添加如下的方法:
func isWithinBounds(location: BoardLocation) -> Bool {
return location.row >= 0 && location.row < boardSize && location.column >= 0 && location.column < boardSize
}
上面这个函数简单的确保了BoardLocation的行和列的属性值是有效的。
更新subscript的实现用于检测属性值如下:
subscript(location: BoardLocation) -> BoardCellState {
get {
assert(isWithinBounds(location),
"row or column index out of bounds")
return cells[location.row * boardSize + location.column]
}
set {
assert(isWithinBounds(location), "row or column index out of bounds")
cells[location.row * boardSize + location.column] = newValue
}
}
上面的代码是在subscript的设置和获取的地方设置了断言assertion。当row或column超出边界时,应用将会闪退,如果你在调试模式下的话能捕获到bug的相关信息。
Swift vs. Objective-C
是时候暂停一会儿来比较下Swift和Objective-C在实现黑白棋上不同的特点了。
相对于两个实现的语法有不同之外,还有一些需要注意到的差异。
相对于Objective-C,Swift的subscript提供了一个非常大的进步,当设置cell值的时候需要一个非常长的方法调用
[board setCellState:BoardCellStateWhitePiece forColumn:4 andRow:5];
但Swift的则简单的多:
board[4, 5] = .White
当需要初始化cell数组时,Objective-C的实现需要用到memset:
memset(_board, 0, sizeof(NSUInteger) * 8 * 8);
你在Swift中真的不应该继续再用c的api。因此,Swift的实现需要另外一种方法,当在初始化数组时过滤值。
cells = Array(count: boardSize * boardSize, repeatedValue: BoardCellState.Empty)
这个函数的代码显然更易读且十分的清晰。
最后,Swift应用只用了个一维数组来保存cells信息,然而Objective-C的实现用了个二维数组。有理由相信这样的性能更好些。在本章的后面会继续了解,和memset相关的内容。
最后一步,你需要添加逻辑去复制游戏棋盘board。Objective-C的实现需要用到c的memcpy函数。你可以很容易的用个2维数组,而Swift的实现只是简单的复制下。幸运的是,在这个应用中,下标subscript会隐藏掉来自于其他类或者子类的实现细节。
Additional game state - 附加游戏的状态
当前的Board类是个通用的,你可以用其来实现游戏的其他扩展。接下来,你需要添加一个Board的子类来实现黑白棋的特殊逻辑。
在Model文件夹中添加Swift文件,并命名为ReversiBoard.swift.添加代码如下:
import Foundation
class ReversiBoard: Board {
private (set) var blackScore = 0, whiteScore = 0
func setInitialState() {
super[3,3] = .White
super[4,4] = .White
super[3,4] = .Black
super[4,3] = .Black
blackScore = 2
whiteScore = 2
}
}
ReversiBoard是Board的子类,添加一个简单的setInitialState 用于初始化棋盘在游戏开始时的状态。
打开Board.swift并添加游戏逻辑如下:
func cellVisitor(fn: (BoardLocation) ->()) {
for column in 0..<boardSize {
for row in 0..<boardSize {
let location = BoardLocation(row: row, column: column)
fn(location)
}
}
}
上述函数遍历了每一个cell格子,为每一个格子都提供了这个函数。理解这种方法的最好方式就是看他的实际应用。
在同一个文件中,添加一个clearBoard的方法:
func clearBoard() {
cellVisitor { self[$0] = .Empty }
}
上面的代码原型为:
func clearBoard() -> () {
cellVisitor {
(location:BoardLocation) -> () in
self[location] = .Empty
}
}
上面的调用了cellVisitor,利用闭包函数来当做fn参数使用。cellVisitor为每一个单元格都调用此函数,$0参数是BoardLocation为每个迭代,clearBoard的结果是让每个cell都设置为Empty。
最后,回到ReversiBoard.swift并在SetInitialState()的顶部调用clearBoard()
现在在开始的位置为初始化让每一个格子都清理一遍恢复为Empty。
Visualizing the board - 形象化棋盘
开始项目中包含了整个游戏需要的背景图片,然而,你需要去为每个棋格上都创建一个视图,用于包含适当的图片—白色,黑色,或者空。
添加一个Swift文件到View文件夹中,命名为BoardSquare.swift.补充代码如下:
import Foundation
import UIKit
class BoardSquare:UIView{
private let board: ReversiBoard
private let location:BoardLocation
private let blackView:UIImageView
private let whiteView:UIImageView
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init(frame: CGRect, location: BoardLocation,board: ReversiBoard) {
self.board = board
self.location = location
let blackImage = UIImage(named: "ReversiBlackPiece")
blackView = UIImageView(image: blackImage)
blackView.alpha = 0
let whiteImage = UIImage(named: "ReversiWhitePiece")
whiteView = UIImageView(image: whiteImage)
whiteView.alpha = 0
super.init(frame: frame)
backgroundColor = UIColor.clearColor()
addSubview(blackView)
addSubview(whiteView)
update()
}
private func update() {
let state = board[location]
whiteView.alpha = state == BoardCellState.White ? 1.0 : 0.0
blackView.alpha = state == BoardCellState.Black ? 1.0 : 0.0
}
}
这是个UIView的子类,用于呈现每一个格子的界面。init方法中存储各种初始化和创建UI的参数。update方法更新基于每个格子中的状态来控制白色和黑色棋子的图片的可见度。
现在你已经完成了一个单元格的处理,是时候关联整个棋盘了。
Views within views - 视图里的视图
为了呈现你的棋盘,你需要8*8个BoardSquare实例属性放置在正确的屏幕位置上。为了条理的管理,你需要建立另外一个view来专门展示这64个方格子棋格。
在View文件夹中,建立ReversiBoardView.swift ,添加代码:
import Foundation
import UIKit
class ReversiBoardView:UIView{
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
init(frame: CGRect,board:ReversiBoard) {
super.init(frame:frame)
let rowHeight = frame.size.height/CGFloat(board.boardSize)
let columnWidth = frame.size.width/CGFloat(board.boardSize)
board.cellVisitor { (location:BoardLocation) in
let left = CGFloat(location.column) * columnWidth
let top = CGFloat(location.row) * rowHeight
let squareFrame = CGRect(x: left,y: top,width: columnWidth,height: rowHeight)
let square = BoardSquare(frame: squareFrame,location: location,board: board)
self.addSubview(square)
}
}
}
上面的代码用每个单元格的宽度,高度大小以及数量来计算棋盘board的大小。注意Swift的Int常数和CGFloat需要显示转换。在这之后,代码使用cellVisitor去访问每个cell的位置,为每一个必须位置的格子都创建BoardSquare实例。
就像现在这样,你已经充分利用了你的cellVisitor方法!现在你已经设置好了视图,是时候将应用程序放在视图控制器中了。
打开ViewController.swift 并在类的定义后面添加代码:
private let board:ReversiBoard
required init?(coder aDecoder: NSCoder) {
board = ReversiBoard()
board.setInitialState()
super.init(coder: aDecoder)
}
上面简单的创建了一个ReversiBoard实例作为常数,在ViewController初始化的时候设置游戏的棋子的初始位置。
更新已经存在的viewDidLoad如下:
override func viewDidLoad() {
super.viewDidLoad()
let boardFrame = CGRect(x: 88, y: 152,width: 600, height: 600)
let boardView = ReversiBoardView(frame: boardFrame, board: board)
view.addSubview(boardView)
}
这创建了一个ReversiBoardView实例,添加了ReversiBoard,并将其加入到view的层级上。
是该见证奇迹的时候了,运行代码:
现在应用看起来已经很像一个真实的游戏了,但是玩家渴望马上有第一步操作。在下一节开始这个部分的开发。
Swift vs. Objective-C
Swift和Objective-C在实现ReversiBoardView和BoardSquare是相似的。这并没什么好奇怪的,因为虽然开发语言有了变化,但是UIKit没有变化。
一个明显的区别是两个版本的cellVisitor方法的实现,使用了函数式编程技术去实现访问者模式。你可以用Objective-C的函数技术,其中使用到很多的block。然而,就语法上来说,没有Swift简洁优雅,,Swift的实现:
cellVisitor { self[$0] = .Empty }
相同功能的Objective-C实现:
[self cellVisitor:^void (struct SHCBoardLocation location) { [self setCellState:BoardCellStateEmpty
forLocation:location];
}];
正如你在本书所看的那样,Swift让函数编程更加强大。
Adding user interaction - 添加用户交互
目前,64个BoardSquare的实例每个都与特殊的行和列设置了一个初始状态。你需要有个方法允许Board去通知每个视图的状态改变。
在Model的文件夹中添加Swift文件,命名为BoardDelegate.swift,并添加代码如下:
import Foundation
protocol BoardDelegate {
func cellStateChanged(location: BoardLocation)
}
你可以用这个代理在棋子状态改变时通知视图view。
然而,Board有64个关联的BoardSquare实例。标准的代理模板因为要传递代理类型,所以只能支持单个调用。在这个黑白棋应用中,你需要多点传播的能力。
在Model文件夹中添加Swift文件,命名为:DelegateMulticast.swift ,添加代码如下:
class DelegateMulticast<T> {
private var delegates = [T]()
func addDelegate(delegate: T) {
delegates.append(delegate)
}
func invokeDelegates(invocation: (T) -> ()) {
for delegate in delegates {
invocation(delegate)
}
}
}
DelegateMulticast 类包含了代理的数组,用类型参数T来确保所有的格式都符合正确的协议。addDelegate 往监听的数组中添加代理,invokeDelegates为每一个代理调用指定的函数。
如果你有过C#的经验,你可能会意识到多点传播模板就像c#中的“event”。否则的话,看他的触发情况你可能能更容易的理解这个类的工作。
打开Board.swift并在后面添加属性:
private let boardDelegates = DelegateMulticast<BoardDelegate>()
类型参数确保只有代理实现了BoardDelegate协议的可以被添加到多点传播的数组中。在这个文件中更近一步,添加代码如下:
func addDelegate(delegate: BoardDelegate) {
boardDelegates.addDelegate(delegate)
}
这提供了一个公共方法,运行Board类去添加他们自己的代理。最后一步是在棋子状态改变时通知符合这些代理的方法。
在Board参数类型的下标subscript中,底部添加setter实现:
boardDelegates.invokeDelegates { $0.cellStateChanged(location) }
上面创建了一个闭包函数用于调用BoardDelegate方法,并将cellStateChanged代理方法作为参数传递到内敛闭包。由于在invokeDelegate 使用了循环,所以会调用每一个代理。
现在你已经对多个代理提供了多点传播的机制,是时候使用了。
打开BoardSquare.swift并更行类的声明采用BoardDelegate协议:
class BoardSquare: UIView, BoardDelegate {
现在这个类符合了这个协议,你可以添加他自己为代理,在init的初始化后面添加代码如下:
board.addDelegate(self)
这将在棋子中添加多点传播代理以便在棋子状态发生变化时受到通知。
每一个BoardSquare实例在棋盘上关联一个特殊位置。他只需要在cellStateChange代理被调用时在对应的位置更新即可。
在类的定义里面添加一个代理方法的实现:
func cellStateChanged(location: BoardLocation) {
if self.location == location {
update()
}
}
这样以确保view只有在关联的棋子状态更新时更新。然而,你将发现上面的代码无法通过编译,因为你没有为这个BoardLocation结构提供一个相等符==的操作。
打开BoardLocation.swift并更新结构的定义采用Equatable协议:
struct BoardLocation: Equatable {
这个协议只定义了一个函数,相等操作符==。在结构定义的下边,添加实现:
struct BoardLocation : Equatable {
let row:Int, column:Int
init(row:Int,column:Int){
self.row = row
self.column = column
}
}
上面将两个BoardLocation实例在行和列都相等的情况下定义为相同的实例。注意你必须把这个函数定义在结构定义外,让他处于全局范围。
你已经做了不少的编码,以及一些游戏模型的准备,是时候来添加一些游戏逻辑了!
Adding the game logic - 添加游戏逻辑
黑白棋是一个回合制的游戏,也就是说白棋和黑旗交替。为了支持此功能,打开BoardCellState.swift并在后面添加枚举:
func invert() -> BoardCellState {
if self == Black {
return White
} else if self == White {
return Black
}
assert(self != Empty, "cannot invert the empty state")
return Empty
}
BoardCellState跟踪当前的状态,用于翻转棋子的状体从黑到白,从白到黑。这是一个很有用的便利方法,在后面的处理玩家间的交替上也很有用。你很快就能学以致用打开ReversiBoard.swift并在ReversiBoard类中添加如下属性和方法
func isValidMove(location:BoardLocation) -> Bool {
return self[location] == BoardCellState.Empty
}
func makeMove(location:BoardLocation) {
self[location] = nextMove
nextMove = nextMove.invert()
}
nextMove跟踪该轮到谁了。默认的游戏规则是,一般控制白棋的一方先走。
第一个方法isValidMove,决定哪一个球员可以在棋盘上移动棋子。现在,你可以让玩家移动到任何一个空的棋格子上。
这第二个方法makeMove,设置给定的位置为当前棋子的颜色,然后使用翻转切换到其他颜色,最后一步是处理用户的交互,将上面的代码执行。
Handling tap gestures - 处理单击手势
打开BoardSquare.swift,在init后面添加:
let tapRecognizer = UITapGestureRecognizer(target: self,action: #selector(BoardSquare.cellTapped))
addGestureRecognizer(tapRecognizer)
addGestureRecognizer(tapRecognizer)
这里添加了一个单击手势在手势触发时调用cellTapped方法。
现在,在这个相同的类里,添加一个处理单击手势的方法:
func cellTapped() {
if board.isValidMove(location) {
board.makeMove(location)
}
}
当用户点击了棋盘后,游戏会执行上面的代码。这个代码会检查此举是否有效。如果是,则将棋子变为白色或黑色。让我们实际运行看看!
编译并运行程序。点击棋盘上任何一个空白的棋格处。
他还不是完全的黑白棋,但有了进步了!
Swift vs. Objective-C
可能Swift和Objective-C最显著的区别就在于实现黑白棋关系的多点传播消息的代理。Objective-C要使用消息的转发需要更多的代码,相比较下,Swift就简单的多。
Swift的DelegateMulticast类通过类的所有者调用避开了动态调用代理。一旦你用特殊的类型初始化了DelegateMulticast,编译器会处理检查类型。
在这个app中,你可以确保所有的对象都遵循BoardDelegate协议实现了多点传播。ReversiBoard类察觉到代理协议然后直接调用
cellStateChange:
boardDelegates.invokeDelegates{ $0.cellStateChanged(location)
}
因此,DelegateMulticast就是泛型,没有复杂的消息转发逻辑。
这段代码还涉及到了Swift的协议。在本章中,你了解了类采用Sequence协议去使他们使用for-each循环。这里,BoardLocation采用了Equatable让你可以很容易的使用==操作符来进行实例间的比较。
还有一些其他的协议让Swift完成类似的目的。例如,类可以采用LogicValue,允许你在一个if语句里作为条件。
Detailed game logic - 详细的游戏逻辑
在黑白棋中,当该你下棋时,在你下棋的地方,必须在水平,垂直或者斜线上包围了一个或多个对手的棋子。
为了说明下游戏规则,可参考下面的棋盘,现在该黑棋落子。
把棋子放在a处是有效的,因为在他的左边包围了一颗白棋。然而,b位置是无效的,在其任何方向上都没有一个包围的白棋。
所以为了确定移动棋子的地方是否有效,你需要去检查其包围着他的八个方向。
虽然这听起来会有很多工作要做,但判断所有的八个方向的逻辑是一样的:必须围绕着一个或多个棋子。你可以使用共通点来想出一个简洁的有效性检查。首先,添加通用的概念,一个“移动的方向”。在Model组中,添加Swift文件,命名为MoveDirection.swift。文件下添加代码:
import Foundation
enum MoveDirection {
case North, South, East, West,
NorthEast, NorthWest, SouthEast, SouthWest;
func move(loc: BoardLocation) -> BoardLocation {
switch self {
case .North:
return BoardLocation(row: loc.row-1, column: loc.column)
case .South:
return BoardLocation(row: loc.row+1, column: loc.column)
case .East:
return BoardLocation(row: loc.row, column: loc.column-1)
case .West:
return BoardLocation(row: loc.row, column: loc.column+1)
case .NorthEast:
return BoardLocation(row: loc.row-1, column: loc.column-1)
case .NorthWest:
return BoardLocation(row: loc.row-1, column: loc.column+1)
case .SouthEast:
return BoardLocation(row: loc.row+1, column: loc.column-1)
case .SouthWest:
return BoardLocation(row: loc.row+1, column: loc.column+1)
}
}
static let directions: [MoveDirection] = [
.North, .South, .East, .West,
.NorthEast, .NorthWest, .SouthWest, .SouthEast
]
}
这个枚举定义了从棋子位置开始相邻的八个方向。他也有个便利方法用于计算新的BoardLocation从一个现在又存在位置到当前的方向。
你需要静态方向常数来包含每个方向。因为Swift目前没有机制用来便利枚举的值。
在ReversiBoard.swift中,在类的定义后面添加方法:
func moveSurroundsCounters(location: BoardLocation,direction: MoveDirection, toState: BoardCellState) -> Bool {
var index = 1
var currentLocation = direction.move(location)
while isWithinBounds(currentLocation) {
let currentState = self[currentLocation]
if index == 1 {
// 附近必须是对手的棋子颜色
if currentState != toState.invert() {
return false
}
} else {
// if the player’s color is reached, the move is valid
if currentState == toState {
return true
}
// if an empty cell is reached give up!
if currentState == BoardCellState.Empty {
return false
}
}
index+=1
// move to the next cell
currentLocation = direction.move(currentLocation)
}
return false
}
这个方法决定是否能移动到棋盘的某个有效位置,包围对手的一个或多个棋子。在while循环里,代码要检查几个必须的条件:
*相邻的棋子必须是相反的颜色
*并且要落子的地方相邻的也是对手的颜色。while循环继续
*沿着落子的地方平行线上必须有自己的棋子,将对手的颜色的棋子包围起来。
最后,你需要更新下isValidMove中的逻辑。目前他只检查要落子的地方是不是空的棋格。
替换isValidMove代码如下:
func isValidMove(location: BoardLocation) -> Bool {
return isValidMove(location, toState: nextMove)
}
private func isValidMove(location: BoardLocation,
toState: BoardCellState) -> Bool {
// check the cell is empty
if self[location] != BoardCellState.Empty {
return false
}
// test whether the move surrounds any of the opponents pieces
for direction in MoveDirection.directions {
if moveSurroundsCounters(location,
direction: direction, toState: toState) {
return true
}
}
return false
}
正如你看到的,上面的代码将isValidMove方法分成了两个,马上你就知道为什么了。原来检查给定位置是否为空的条件还在,但添加了第二个条件。使用MoveDirections.directions数组遍历所有有可能的方向,这个方法用于包围对手的棋子。不久这个方法能发现匹配这个条件的方向,会返回true。否则继续下去只是没有意义的进一步检查。
编译并运行应用可看到如下:
你会发现你在棋盘上下棋变得更受限制了。
但这还不是完全的黑白棋!
黑白棋的关键规则是你必须将任何被包围的颜色翻转为相反的颜色。因为此操作是在玩家落完棋子之后,所以我们把此触发添加出来。
在ReversiBoard类的后面添加方法:
//翻转包围的棋子
private func flipOpponentsCounters(location: BoardLocation, direction: MoveDirection, toState: BoardCellState) {
// is this a valid move?
if !moveSurroundsCounters(location,direction: direction, toState: toState) {
return
}
let opponentsState = toState.invert()
var currentState = BoardCellState.Empty
var currentLocation = location
// flip counters until the edge of the board is reached or
// a piece with the current state is reached
repeat {
currentLocation = direction.move(currentLocation)
currentState = self[currentLocation]
self[currentLocation] = toState
} while (isWithinBounds(currentLocation) && currentState == opponentsState)
}
上诉方法促进了MoveDirection枚举的进一步使用,在检查是否将包围在给定方向上的任何棋子。一个简短的do-while循环会翻转对手的棋子。
使用这种方法,更新makMove如下:
func makeMove(location: BoardLocation) {
self[location] = nextMove
for direction in MoveDirection.directions {
flipOpponentsCounters(location, direction: direction, toState: nextMove)
}
nextMove = nextMove.invert()
}
现在翻转方法任何部分在任何的八个被包围的方向。编译并运行!
现在更像是个黑白棋了!
但是谁赢了呢?接下来进行分数统计
Swift vs. Objective-C
Swift和Objective-C的黑白棋版本中都将统计被包围棋子的个数。然而,两者的实现是不同的。
Objective-C代码使用了很多的block来传递行和列数值的引用。
BoardNavigationFunction BoardNavigationFunctionRight = ^(NSInteger* c, NSInteger* r) {
(*c)++;
};
swift的实现,用枚举来表示方向,此提供了清晰和更好的封装。
Keeping score - 计分
黑白棋是一个比赛游戏,所以你的app版本需要定义一个计分的机制。每个玩家的分数就是棋盘上的棋子数。Board类已经有了一个访问附近棋格的概念,你可以利用来好好计算分数。
打开Board.swift并在类的后面添加方法:
func countMatches(fn: (BoardLocation) -> Bool) -> Int {
var count = 0
cellVisitor {
if fn($0) { count++ }
}
return count
}
这个方法用来计算匹配条件的棋子数
打开ReversiBoard.swift并在makeMove后面添加:
whiteScore = countMatches { self[$0] == BoardCellState.White }
blackScore = countMatches { self[$0] == BoardCellState.Black }
上面的代码在玩家每次移动棋子后都执行。你用countMatches去计算分别包含的白子和黑子的数量。是不是很简单!
在Model文件夹中添加Swift文件ReversiBoardDelegate.swift,添加代码:
import Foundation
protocol ReversiBoardDelegate {
func boardStateChanged()
}
这定义了一个你用来实现代理模板的协议。
打开ReversiBoard.swift并在类的顶部添加代码:
private let reversiBoardDelegates = DelegateMulticast<ReversiBoardDelegate>()
这使用了与前面完全相同的多点传播的代理。
在相同的文件下添加如下方法:
func addDelegate(delegate: ReversiBoardDelegate) {
reversiBoardDelegates.addDelegate(delegate)
}
提供了一个添加代理的机制以便你可以通知状态的变更。
在相同的文件内,在makeMove的地步添加:
reversiBoardDelegates.invokeDelegates { $0.boardStateChanged() }
此时,你已经更新了棋子的状态和分数因此需要通知每个代理。提醒一下,上面的代码调用了你添加的boardStateChanged代理。
你现在有了棋子更新时分数也跟着更新的通知,但你还没是有添加任何实现的代理,是时候来完成了。
打开ViewController.swift并更新类的定义并采用协议
class ViewController: UIViewController, ReversiBoardDelegate {
在ViewController的内部,添加协议的实现:
func boardStateChanged() {
blackScore.text = "\(board.blackScore)"
whiteScore.text = "\(board.whiteScore)"
}
简单的更新两个UILabel的值显示当前的分数。
在结尾处添加init:
board.addDelegate(self)
添加ViewController作为代理以便激活你刚刚添加的方法。
最后的一步用于确保UI反映了当用户启动应用程序是的初始状态。在同一个文件中,在viewDidLoad的后面添加:
boardStateChanged()
因为一开始每个玩家都有两个棋子在棋盘上,所以开始时ui上每个玩家都有两分。
编译并运行:
游戏基本完成了!
Swift vs. Objective-C
再一次,你在你的黑白棋的Swift实现中使用了闭包和函数编程,让你的代码更加的简洁优雅。使用countMatches确定每一个玩家的得分也是一种乐趣。
然而,这里还有一点点小东西不一样。
这里是Objective-C实现计分的格式:
_whiteScore.text = [NSString stringWithFormat:@"%d", _board.whiteScore];
_blackScore.text = [NSString stringWithFormat:@"%d", _board.blackScore];
下面的是Swift的计分代码:
blackScore.text = "\(board.blackScore)"
whiteScore.text = "\(board.whiteScore)"
我知道我喜欢哪个!
Adding UI flair - 添加UI动画
现在从游戏逻辑上出来休息下做点UI上的动画。
打开BoardSquare.swift并改变update()如下:
//根据棋子状态显示不同的图片
private func update() {
let state = board[location]
UIView.animateWithDuration(0.2, animations: {
switch state {
case .White:
self.whiteView.alpha = 1.0
self.blackView.alpha = 0.0
self.whiteView.transform = CGAffineTransformIdentity
self.blackView.transform = CGAffineTransformMakeTranslation(0, 20)
case .Black:
self.whiteView.alpha = 0.0
self.blackView.alpha = 1.0
self.whiteView.transform = CGAffineTransformMakeTranslation(0, -20)
self.blackView.transform = CGAffineTransformIdentity
case .Empty:
self.whiteView.alpha = 0.0
self.blackView.alpha = 0.0
}
})
}
如上,方法直接设置了黑色和白色图片属性的透明度。用了一个简短的0.2s左右的动画来更新透明度。
编译并运行可见如下:
现在有了个很微妙但是过度棋子颜色很棒的动画。上面的图片只是运行时的截图。
这个改变很简单,但是效果显著。我相信你能想出更多的变化。
现在回到核心业务上!最后一步操作是当一个玩家获得胜利时的结束展示了。
Handling the end-game - 处理游戏的结束状态
关于判断游戏结束的条件有一个好消息和一个坏消息。
首先,坏消息是在黑白棋中,确定最终条件不是简单的检查棋盘上是否都有了棋子。游戏结束时的时间应该是其中一个玩家没有可包围翻转一个或多个对手棋子的时候。
好消息是,这个检查条件很好写!
打开Board.swift并在类的结尾处添加:
func anyCellsMatchCondition(fn: (BoardLocation)->Bool) -> Bool {
for column in 0..<boardSize {
for row in 0..<boardSize {
if fn(BoardLocation(row: row, column: column)) {
return true
}
}
}
return false
}
上面的方法遍历每个棋子直到函数返回true。如果发生这种情况,进一步检查也毫无意义所以立即退出anyCellsMatchCondition。
打开ReversiBoard.swift并在类的顶部定义变量:
private (set) var gameHasFinished = false
在同一个文件中,继续在makeMove中白棋和黑旗分数计算的前面添加:
gameHasFinished = checkIfGameHasFinished()
每次玩家轮流回合时,都会用上面的anyCellsMatchCondition去检查是否满足游戏结束的条件并更新刚刚添加的变量。你所必须要做的就是添加所需的逻辑。
接着,在ReversiBoard类的实现中添加方法:
private func checkIfGameHasFinished() -> Bool {
return !canPlayerMakeMove(BoardCellState.Black) && !canPlayerMakeMove(BoardCellState.White)
}
private func canPlayerMakeMove(toState:BoardCellState) -> Bool {
return anyCellsMatchCondition
}
第一个方法检查是否还有黑旗或者白棋可以移动。如果没有的话,游戏就结束。第二种方法确定玩家是否还有任何有效的落子位置。anyCellsMatchContion便于实现最后的游戏逻辑!
在游戏逻辑中还有个小插曲:有可能一个玩家下了棋后改变了对手的棋子,所以还可以继续。例如,如果在黑子回合而白棋没有可以移动的位置了,但如果白棋被变成黑旗后又出现了白棋可落子的位置,则可以继续。
在ReversiBoard类去处理回合结束时的情况:
func switchTurns() {
let intendedNextMove = nextMove.invert()
// only switch turns if the player can make a move
if canPlayerMakeMove(intendedNextMove) {
nextMove = intendedNextMove
}
}
在下一个玩家执行nextMove前,这个方法用于检测玩家是否有有效的落子位置可执行。
现在你需要调用这个方法。在makeMove中,替换nextMove.inver()行直接如下:
switchTurns()
这将调用新的方法处理棋子翻转后的逻辑。
仍然只需要更新UI。打开ViewController.swift并在boardStateChanged后面添加:
restartButton.hidden = !board.gameHasFinished
当游戏结束时显示重新开始的按钮。
编译并运行app,你可以完整的完成一个游戏了。虽然你周围如果没有朋友的话你得自个儿玩…
在上面的截图中,黑色棋子相当的可怜,棋盘上全是白子。你可以看到app正确的判断了游戏结束的时间并显示了重新开始的按钮。
这个按钮表示当你点击后重新开始游戏,你现在可以给按钮添加处理事件了。
在ViewController.swift里,在ViewDidLoad的后面添加:
restartButton.addTarget(self, action: #selector(ViewController.restartTapped), forControlEvents: UIControlEvents.TouchUpInside)
在这个相同的文件中,添加如下:
func restartTapped() {
if board.gameHasFinished {
board.setInitialState()
boardStateChanged()
}
}
你现在可以一遍一遍又一遍的玩了,玩了几场后都是你决定输赢,明显木得意思!
在下面的最后一节教程中,你将用Swift来创建一个电脑对手实现简单的下棋功能。
Adding a computer opponent - 添加计算机对手
你的电脑对手将依赖于粗暴的策略,评估所有可能的行动和选择后得分高的一个结果。为了支持这个,你将添加代码去克隆当前棋盘上状态以便你测试不同的场景而不影响游戏真实的状态。
打开Board.swift并添加初始化:
init(board: Board) {
cells = board.cells;
}
这个初始化器给你提供了棋盘的初始化状态。记住Swift的数组是结构,这也就是说创建board会复制现有Board的所有存在的棋子。
Note:Swift使用copy-on-write结构,也就是说cell数组在初始化时不会进行复制,当数组内容进行变换时才会复制。这是非常强大的性能优化!
接着,打开ReversiBoard.swift并添加初始化内容:
override init() {
super.init()
}
init(board: ReversiBoard) {
super.init(board: board)
nextMove = board.nextMove
blackScore = board.blackScore
whiteScore = board.whiteScore
}
第一个初始化没有参数仅仅只是初始化个Board。创建了一个普通的空白Board。
第二个初始化调用了Board的父类去复制cells然后复制黑白棋特殊的属性。
是时候去添加计算机对手了!
在Model文件夹下添加Swift文件ComputerOpponet.swift.并在文件后面添加代码
import Foundation
class ComputerOpponent : ReversiBoardDelegate {
private let board: ReversiBoard
private
let color: BoardCellState
init(board: ReversiBoard, color: BoardCellState) {
self.board = board
self.color = color
board.addDelegate(self)
}
}
你用一个白色或黑色颜色代表电脑玩家。电脑玩家也将自己作为ReversiBoard的代理用于决定何时移动棋子。
接着,在上面的类里添加全局通用的函数声明。
func delay(delay: Double, closure: ()->()) {
let time = dispatch_time( DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC)) )
dispatch_after(time, dispatch_get_main_queue(), closure)
}
用了一个优雅的代码机制封装GCD Api来实现在几秒后返回闭包里的表达式。
在ComputerOpponent类中添加方法:
func boardStateChanged() {
if board.nextMove == color {
delay(1.0) {
self.makeNextMove()
}
}
}
上面的ReversiBoard的调用,通过ReversiBoardDelegate协议。每次玩家需要他们。如果下一步属于电脑的颜色,你用你的延迟函数在电脑操作前暂停。这个暂停纯粹只是让它看起来像是电脑在思考!这样的小技巧能让你的应用程序更友好。
最后,添加以下方法:
private func makeNextMove() {
var bestScore = Int.min
var bestLocation: BoardLocation?
board.cellVisitor {
(location: BoardLocation)
in
if self.board.isValidMove(location) {
let score = self.scoreForMove(location) if (score > bestScore) {
bestScore = score
bestLocation = location }
}
}
if bestScore > Int.min {
board.makeMove(bestLocation!)
}
}
private func scoreForMove(location:BoardLocation) -> Int {
let testBoard = ReversiBoard(board: board)
testBoard.makeMove(location)
let score = color == BoardCellState.White ?
testBoard.whiteScore - testBoard.blackScore : testBoard.blackScore - testBoard.whiteScore;
return score
}
第一个方法使用了cellVisitor去迭代遍历所有的cells。在每一个位置,移动结果都决定了分数,跟踪移动给最后的分数。一旦访问了所有的cells计算比较后,便进行最好的移动。
黑白棋,找到“最好”的位置移动是十分简单的。游戏的目标是尽可能在棋盘上占领更多的位置,所以最后的移动一般都是尽可能多的玩家棋子。
第二个方法计算特定的移动,他通过复制棋盘,移动棋子后根据黑色和白色玩家的分数差异进行决断。
现在把电脑玩家加入实际操作中。打开ViewController.swift并在后添加属性。
private let computer: ComputerOpponent
在init中,在super.init前添加:
computer = ComputerOpponent(board: board,color: BoardCellState.Black)
你的电脑玩家已经准备好开始了。
编译并运行,看看你如何对付电脑!
击败电脑的技巧:棋盘的棱角处是最有价值的位置,占领了之后,你的对手更难包围你。占领了个好的角落后,你会有很大的机会战胜电脑!
Swift vs. Objective-C
board的复制代码在Objective-C的实现中使用memcpy函数:
SHCBoard* board = [[[self class] allocWithZone:zone] init]; memcpy(board->_board, _board, sizeof(NSUInteger) * 8 * 8);
看上去有点丑!Swift的就简洁多了:
cells = board.cells;
上面的代码就是为什么Swift的实现使用的是1维数组而不是2维数组。电脑对手多次克隆棋盘来决定潜在的分数。所以复制操作的性能是至关重要的。复制2维数组需要复制的数组更多,对性能损耗很大。
Challenge - 挑战
我希望你喜欢这个最后教程中用Swift实现的一个Objective-C应用。一路上,你见证了很多这两种语言的差异,也看到了如何用Swift实现更优雅,更容易理解。
最后一个比较,你有没有注意到,黑白棋的Swift实现代码要远远少于Objective-C?更少的代码也就一位置更少的代码编写和更快的应用程序开发。祝你好运!
你准备好最后的挑战了吗?Objective-C版本的黑白棋使用了更先进的电脑玩家,是有了极大极小算法(https://en.wikipedia.org/wiki/Minimax),为何不用Swift来实现看看呢。
查看原文的第二部分(http://www.raywenderlich.com/29248/how- to-develop-an-ipad-board-game-app-part-22)Objective-C实现的更多的细节
源码百度云下载 密码:tpi4