- 原文链接 : Build Tic Tac Toe with AI Using Swift
- 译文出自 : 掘金翻译计划
- 译者 : Nicolas(Yifei) Li
- 校对者: cyseria, jamweak
用 Swift 创建有AI(人工智能)的 Tic Tac Toe 游戏
我对(自我)学习有着很强的热情并且非常着迷。最近,我提出了一个利用制作游戏的理论应用到应用程序开发中来提高用户体验的假说。很多人提出“游戏化”这类的流行词,通过应用程序与用户之间的交互,以及让用户主动参与的方式去取悦用户,达到解决应用程序的痛点的难题。无论你的应用程序到底提供了什么内容。我们今天不会讨论这个(我甚至都不会提起增加游戏感行为的倡导者们是否玩游戏这样的问题。) 取而代之,我们会使用 SpriteKit,GameplayKit 和 Swift 来建立一个游戏。
抑制下你的期望
在你野心勃勃(准备)创建一个高居榜单的应用程序之前,我要告诉你这不是我们今天的目标。我们准备只看冰山一角,创建一个简单的井字游戏 Tic Tac Toe。在我们着手工作后,我会增加一个由计算机控制的AI(人工智能) 对手(供)允许你对战。
第一部分 - 原理
Apple 公司在召开 WWDC2013 期间发布了 SpriteKit,这给开发者一个比玩转自己(创建的)框架更快建立游戏应用程序的可选方案。由于游戏应用程序这个类别在 Apple 的生态系统中占据了大部分的下载量,这就一点儿都不奇怪 Apple 公司致力于游戏社区的发展并且从让程序开发者们更加简单的创建新的 iOS,macOS,watchOS 和 tvOS 游戏获得巨大的利益。
SpriteKit 也通常被引用为 sprites,是一个处理渲染,图形动画和图片的框架。作为一个程序开发者,你决定了改变什么,SpriteKit 就去处理显示这些变化的工作。你能在这里读到更多有关SpriteKit的内容。我也强烈推荐你去阅读 SpriteKit编程指导 获取更多框架提供的其他特性,例如处理声音的播放和 Sprite 物理模型。
SpriteKit 处理你游戏的运行循环并提供多个地方让开发者在每一帧更新游戏内容。下图展示了每一帧从开始更新到最终渲染发生了什么。本质上来说,在每一帧你有很多机会来调整你的游戏。

GameplayKit 是另外一个能使用的框架。 GameplayKit 是在去年的 WWDC 被引进的,它提供了很多制作游戏使用的通用方法的实现,例如创建随机数,创建人工智能对手,或者障碍物的寻路系统。他们是非常有用的工具,能做很多繁重的工作并且让游戏应用程序的开发者把精力放在怎么制作更有趣的游戏。我强烈推荐你阅读GameplayKit编程指导 去学习怎样利用这些框架中的技巧。回到我们这个简单的游戏,我们将只包含框架中的一小部分内容让我们的计算机对手更加“聪明”。
启动 Xcode

启动 XCode 并且通过为 iOS 设计的模板创建一个游戏项目。 命名游戏为 TicTacToe 并且确认编程语言设定为 Swift。在项目的创建过程中, XCode 创建了 SKScene 文件,它展示了游戏的初始视图,连同一个视图控制器文件用于初始化你的游戏场景并且处理在应用程序启动的时候展现在屏幕上。如果你现在启动应用程序,你会看到Hello World标签,它让你所有的东西都立即可以使用了。另外,如果你点击了视图,一个宇宙飞船会增加在你点击的位置。我们已经不再需要那个标签和宇宙飞船了,让我们移除那部分代码。切换到 GameScene.swift 文件,移除 didMoveToView 和 touchesBegan 方法中的代码。

让我们来花点时间并强调一些场景编辑器的特性。视图的中心是显示了场景,黄色的轮廓围绕着我们的 tic tac toe 游戏棋盘展现了我们的视口,它让我们的游戏可见。我们能改变游戏的视口或者增加摄像机,让我们实时得看见更多游戏的内容。在 platformer 中,我们也可以创建一个很大的很多敌人点散列在场景终的背景图片。我们也可以使用一个摄像机节点横跨整个场景随着时间去显示更多新的背景部分。然而,对于这个游戏,我们的视图将会时静止在棋盘附近。

场景的底部是节点编辑器。我们可以使用这个编辑器为节点增加功能或者在场景中更容易的选择他们。我们用增加一个节点来表示游戏棋盘,标签和每一格游戏棋盘的占位节点。最后,每一个在场景中的节点都有一个名字,它用于在代码中引用回去。
为了考虑写这篇文章的时间我已经将整个游戏项目提交到了 Github,所以你可以追随并且研究我略过的这个区域。
回到代码
让我们切换回到 GameViewController.swift 去看看怎样建立我们的场景和让我们的游戏干点事情。在 viewDidLoad 方法中我们配置并且装载我们的场景。我们也增加了调试语句所以我们能追踪代码和每秒多少帧。在一个动作游戏中,我们对监控每秒钟多少个节点同时出现在屏幕上连同我们是否可以保持理想上的60fps的帧率感兴趣。
override func viewDidLoad() {
super.viewDidLoad()
if let scene = GameScene(fileNamed:"GameScene") {
// Configure the view.
let skView = self.view as! SKView
skView.showsFPS = true
skView.showsNodeCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .AspectFill
skView.presentScene(scene)
}
}
再看 GameScene.swift 文件,我们需要检查以下三个方法: didMoveToView, tochesBegan 和 update。当场景既要显示在我们的视图控制器的视图内时,didMoveToView 方法被调用。我们的 GameScene 视图最炫的是我们有好几种选择访问视图里的节点。在我们的方法里,我们通过移除游戏棋盘单元格背景的颜色初始化场景。我们也干了点其他事情,但是我们将在之后的文章里面讨论这些。
override func didMoveToView(view: SKView) {
/* Setup your scene here */
self.enumerateChildNodesWithName("//grid*") { (node, stop) in
if let node = node as? SKSpriteNode{
node.color = UIColor.clearColor()
}
}
let top_left: BoardCell = BoardCell(value: .None, node: "//*top_left")
let top_middle: BoardCell = BoardCell(value: .None, node: "//*top_middle")
let top_right: BoardCell = BoardCell(value: .None, node: "//*top_right")
let middle_left: BoardCell = BoardCell(value: .None, node: "//*middle_left")
let center: BoardCell = BoardCell(value: .None, node: "//*center")
let middle_right: BoardCell = BoardCell(value: .None, node: "//*middle_right")
let bottom_left: BoardCell = BoardCell(value: .None, node: "//*bottom_left")
let bottom_middle: BoardCell = BoardCell(value: .None, node: "//*bottom_middle")
let bottom_right: BoardCell = BoardCell(value: .None, node: "//*bottom_right")
let board = [top_left, top_middle, top_right, middle_left, center, middle_right, bottom_left, bottom_middle, bottom_right]
gameBoard = Board(gameboard: board)
}
下一个我们讨论的方法是 touchesBegan。这个方法处理用户选择移动和重置游戏的触摸事件。对场景上的每一个触摸事件,我们决定他们在场景上的位置和哪一个节点被选中。就我们的情况来说,我们要么放置玩家的棋子在一个单元格里,要么重置游戏。我们也更新内部的游戏棋盘状态。
“`
override func touchesBegan(touches: Set, withEvent event: UIEvent?) {
for touch in touches {
let location = touch.locationInNode(self)
let selectedNode = self.nodeAtPoint(location)
var node: SKSpriteNode
if let name = selectedNode.name {
if name == "Reset" || name == "reset_label"{
self.stateMachine.enterState(StartGameState.self)
return
}
}
if gameBoard.isPlayerOne(){
let cross = SKSpriteNode(imageNamed: "X_symbol")
cross.size = CGSize(width: 75, height: 75