棋類游戲對戰的實現
- 六洲棋
- 五子棋
- AI對戰
- 藍牙對戰
- 在線對戰
六洲棋
六洲棋,又稱:泥棋、插方、來馬、五福棋,中國民間傳統棋類體育形式。源于民間,簡便、通俗、易學,在民間廣為流行,深受社會底層大眾的喜愛。龍其在淮河流域的安徽省、河南省、江蘇省、以及湖北省、山東省非常普及,并流傳到中國各地,包括港、澳、臺地區。起源于勞動人民生活,根植于民間大眾之中,它簡捷、明快,趣味性、競技性強,是一項長期流行于民間,富有傳統文化色彩的競技項目。對于啟迪智慧,休閑娛樂,增進交流非常有益。列安徽省第二批省級非物質文化遺產。
6*6縱橫線組成,共三十六個棋點。每方十八枚棋子,以兩色區分敵我。
規則
對弈過程分三階段。(鳳陽下法)放子:對弈雙方依次將己子放入空棋點,將手上的棋子放完才開始走子。逼子:若無棋子被吃,使得棋子放滿棋盤。則兩人各選對方一枚敵子移出游戲。走子:由后手方開始輪流移動己棋,沿線直橫線一格。吃子:無論是下子或走子階段,只要己方棋子排成以下排列稱為成城,就要吃掉一定數量的敵子,但不可吃掉已成城子的敵棋。在放子階段,被吃的子先作記號,等走子階段開始才一齊提取。
成六:六枚棋子以縱、橫和斜3個方向連成直線(除了四條邊的直線)。吃掉敵方三子。
斜五:連子的2頭都靠棋盤邊緣,吃掉敵方兩子。
斜四:連子的2頭都靠棋盤邊緣,吃掉敵方一子。
斜三:連子的2頭都靠棋盤邊緣,吃掉敵方一子。
成方:四枚棋子組成一個緊鄰相連的小正方形,吃掉敵方一子。
使對方只剩下三枚以下則獲勝。因為是民間文化,各地稍有差異。
棋型的算法實現
//是否形成斜子棋(三子棋,四子棋,五子棋,六子棋)
static func isXiZiChess(_ point:SWSPoint,_ chessArray: [[FlagType]]) -> LianZhuState?{
let type = chessArray[point.x][point.y]
let pointLeft = SWSPoint()
let pointRight = SWSPoint()
let ponitTop = SWSPoint()
let pointBottom = SWSPoint()
// 東北方向
var i = 0
while point.x - i >= 0 && point.y + i <= 5 && chessArray[point.x - i][point.y + i] == type {
pointLeft.x = point.x - i
pointLeft.y = point.y + i
i += 1
}
i = 0
while point.x + i <= 5 && point.y - i >= 0 && chessArray[point.x + i][point.y - i] == type {
pointRight.x = point.x + i
pointRight.y = point.y - i
i += 1
}
//西北方向
i = 0
while point.x - i >= 0 && point.y - i >= 0 && chessArray[point.x - i][point.y - i] == type {
ponitTop.x = point.x - i
ponitTop.y = point.y - i
i += 1
}
i = 0
while point.x + i <= 5 && point.y + i <= 5 && chessArray[point.x + i][point.y + i] == type {
pointBottom.x = point.x + i
pointBottom.y = point.y + i
i += 1
}
print(pointRight.x,pointRight.y,pointLeft.x,pointLeft.y,ponitTop.x,ponitTop.y,pointBottom.x,pointBottom.y)
let arr = [3,2,1,0]
for index in arr {
func condition() -> Bool {
if pointRight.x == 2+index && pointRight.y == 0 && pointLeft.x == 0 && pointLeft.y == 2+index {
return true
}
if pointRight.x == 5 && pointRight.y == 3 - index && pointLeft.x == 3 - index && pointLeft.y == 5 {
return true
}
if ponitTop.x == 0 && ponitTop.y == 3-index && pointBottom.x == 2+index && pointBottom.y == 5 {
return true
}
if ponitTop.x == 3-index && ponitTop.y == 0 && pointBottom.x == 5 && pointBottom.y == 2+index {
return true
}
return false
}
if condition() {
switch index {
case 0:
return .threeChess
case 1:
return .fourChess
case 2:
return .fiveChess
case 3:
return .sixChess
default:()
}
}
}
return nil
}
//是否形成方格棋
static func isCheckChess(_ point:SWSPoint,_ chessArray: [[FlagType]]) ->LianZhuState? {
let type = chessArray[point.x][point.y]
//左上
if point.x - 1 >= 0 && point.y - 1 >= 0 && chessArray[point.x][point.y-1] == type &&
chessArray[point.x-1][point.y] == type && chessArray[point.x-1][point.y-1] == type {
return .checkChess
}
//左下
if point.x - 1 >= 0 && point.y + 1 <= 5 && chessArray[point.x][point.y+1] == type &&
chessArray[point.x-1][point.y] == type && chessArray[point.x-1][point.y+1] == type {
return .checkChess
}
//右上
if point.x + 1 <= 5 && point.y - 1 >= 0 && chessArray[point.x][point.y-1] == type &&
chessArray[point.x+1][point.y] == type && chessArray[point.x+1][point.y-1] == type {
return .checkChess
}
//右下
if point.x + 1 <= 5 && point.y + 1 <= 5 && chessArray[point.x][point.y+1] == type &&
chessArray[point.x+1][point.y] == type && chessArray[point.x+1][point.y+1] == type {
return .checkChess
}
return nil
}
小結
六洲棋,在我們老家被稱為泥棋,小時候經常玩的一種棋,偶有回憶,因此實現下這個游戲,望能找到個棋友沒事玩玩,這種棋,玩法多種,很有趣。
五子棋
五子棋五子棋是比較流行的棋類游戲了,玩法簡單,基本上人人會玩,在此就不介紹游戲規則了。下面使用 swift實現五子棋這個游戲,主要實現AI算法,包括極大值極小值算法,深度搜索算法,估值函數,Alpha Beta 剪枝算法等等。
//橫向五子連珠(除去四邊線的五子連珠)
static func isFiveChess(_ point:SWSPoint,_ chessArray: [[FlagType]]) -> Bool {
let type = chessArray[point.x][point.y]
let pointLeft = SWSPoint()
let pointRight = SWSPoint()
let pointTop = SWSPoint()
let pointBottom = SWSPoint()
let pointLeft45 = SWSPoint()
let pointRight45 = SWSPoint()
let pointTop135 = SWSPoint()
let pointBottom135 = SWSPoint()
//東西方向
var i = 0
while point.x - i >= 0 && chessArray[point.x - i][point.y] == type {
pointLeft.x = point.x - i
i += 1
}
i = 0
while point.x + i <= 14 && chessArray[point.x + i][point.y] == type {
pointRight.x = point.x + i
i += 1
}
if pointRight.x - pointLeft.x == 4 && (pointLeft.y != 15 || pointLeft.y != 0){
return true
}
//南北方向
i = 0
while point.y - i >= 0 && chessArray[point.x][point.y-i] == type {
pointTop.y = point.y - i
i += 1
}
i = 0
while point.y + i <= 14 && chessArray[point.x][point.y+i] == type {
pointBottom.y = point.y + i
i += 1
}
if pointBottom.y - pointTop.y == 4 && (pointTop.x != 15 || pointTop.x != 0) {
return true
}
// 東北方向
i = 0
while point.x - i >= 0 && point.y + i <= 14 && chessArray[point.x - i][point.y + i] == type {
pointLeft45.x = point.x - i
pointLeft45.y = point.y + i
i += 1
}
i = 0
while point.x + i <= 14 && point.y - i >= 0 && chessArray[point.x + i][point.y - i] == type {
pointRight45.x = point.x + i
pointRight45.y = point.y - i
i += 1
}
if pointLeft45.y - pointRight45.y == 4{
return true
}
//西北方向
i = 0
while point.x - i >= 0 && point.y - i >= 0 && chessArray[point.x - i][point.y - i] == type {
pointTop135.x = point.x - i
pointTop135.y = point.y - i
i += 1
}
i = 0
while point.x + i <= 14 && point.y + i <= 14 && chessArray[point.x + i][point.y + i] == type {
pointBottom135.x = point.x + i
pointBottom135.y = point.y + i
i += 1
}
if pointBottom135.y - pointTop135.y == 4{
return true
}
return false
}
在 demo中實現了五子棋的AI、同機、藍牙、在線對戰,下面重點介紹AI對戰。
五子棋的AI算法實現
2017年互聯網最火的技術毫無疑問就是AI了,在此嘗試寫了個算法來和人腦來pk。五子棋屬于零和游戲:一方勝利代表另一方失敗,而零和游戲的代表算法就是極大值極小值搜索算法。
極大值極小值搜索算法
A、B二人對弈,A先走,A始終選擇使局面對自己最有利的位置,然后B根據A的選擇,在剩下的位置中選擇對A最不利的位置,以此類推下去直到到達我們定義的最大搜索深度。所以每一層輪流從子節點選擇最大值-最小值-最大值-最小值...
我們如何知道哪個位置最有利和最不利呢?在此我們引入一套評估函數,來對棋盤上每個位置進行分數評估
//活一、活二、活三、活四、連五、眠一,眠二、眠三、眠四
enum FiveChessType:Int {
case liveOne = 0
case liveTwo
case liveThree
case liveFour
case liveFive
case sleepOne
case sleepTwo
case sleepThree
case sleepFour
case unknown
var score:Int {
switch self {
case .unknown:
return un_known
case .sleepOne:
return sleep_One
case .liveOne,.sleepTwo:
return live_One
case .liveTwo,.sleepThree:
return live_Two
case .liveThree:
return live_Three
case .sleepFour:
return sleep_Four
case .liveFour:
return live_Four
case .liveFive:
return live_Five
}
}
}
let live_Five = 1000000
let live_Four = 100000
let sleep_Four = 10000
let live_Three = 1000
let live_Two = 100
let sleep_Three = 100
let live_One = 10
let sleep_Two = 10
let sleep_One = 1
let un_known = 0
在使用極大值極小值進行深度搜索時,遍歷節點是指數增長的,如果不進行算法優化,將會導致電腦計算時間過長,影響下棋體驗,所以這里引入 Alpha Beta 剪枝原理。
Alpha Beta 剪枝原理
AlphaBeta剪枝算法是一個搜索算法旨在減少在其搜索樹中,被極大極小算法評估的節點數。
Alpha-Beta只能用遞歸來實現。這個思想是在搜索中傳遞兩個值,第一個值是Alpha,即搜索到的最好值,任何比它更小的值就沒用了,因為策略就是知道Alpha的值,任何小于或等于Alpha的值都不會有所提高。
第二個值是Beta,即對于對手來說最壞的值。這是對手所能承受的最壞的結果,因為我們知道在對手看來,他總是會找到一個對策不比Beta更壞的。如果搜索過程中返回Beta或比Beta更好的值,那就夠好的了,走棋的一方就沒有機會使用這種策略了。
在搜索著法時,每個搜索過的著法都返回跟Alpha和Beta有關的值,它們之間的關系非常重要,或許意味著搜索可以停止并返回。
如果某個著法的結果小于或等于Alpha,那么它就是很差的著法,因此可以拋棄。因為我前面說過,在這個策略中,局面對走棋的一方來說是以Alpha為評價的。
如果某個著法的結果大于或等于Beta,那么整個節點就作廢了,因為對手不希望走到這個局面,而它有別的著法可以避免到達這個局面。因此如果我們找到的評價大于或等于Beta,就證明了這個結點是不會發生的,因此剩下的合理著法沒有必要再搜索。
如果某個著法的結果大于Alpha但小于Beta,那么這個著法就是走棋一方可以考慮走的,除非以后有所變化。因此Alpha會不斷增加以反映新的情況。有時候可能一個合理著法也不超過Alpha,這在實戰中是經常發生的,此時這種局面是不予考慮的,因此為了避免這樣的局面,我們必須在博弈樹的上一個層局面選擇另外一個著法。鏈接
c代碼實現原理
int AlphaBeta(int depth, int alpha, int beta)
{
if (depth == 0)
{
return Evaluate();
}
GenerateLegalMoves();
while (MovesLeft())
{
MakeNextMove();
val = -AlphaBeta(depth - 1, -beta, -alpha);
UnmakeMove();
if (val >= beta)
{
return beta;
}
if (val > alpha)
{
alpha = val;
}
}
return alpha;
}
實際在代碼中的運用,代碼比較復雜請結合項目理解。項目地址
static func getAIPoint(chessArray:inout[[FlagType]],role:FlagType,AIScore:inout [[Int]],humanScore:inout [[Int]],deep:Int) ->(Int,Int,Int)? {
let maxScore = 10*live_Five
let minScore = -1*maxScore
let checkmateDeep = self.checkmateDeep
var total=0, //總節點數
steps=0, //總步數
count = 0, //每次思考的節點數
ABcut = 0 //AB剪枝次數
func humMax(deep:Int)->(Int,Int,Int)? {
let points = self.getFiveChessType(chessArray: chessArray, AIScore: &AIScore, humanScore: &humanScore)
var bestPoint:[(Int,Int)] = []
var best = minScore
count = 0
ABcut = 0
for i in 0..<points.count {
let p = points[i]
chessArray[p.x][p.y] = role
self.updateOneEffectScore(chessArray: chessArray, point: (p.x,p.y), AIScore: &AIScore, humanScore: &humanScore)
var score = -aiMaxS(deep: deep-1, alpha: -maxScore, beta: -best, role: self.reverseRole(role: role))
if p.x < 3 || p.x > 11 || p.y < 3 || p.y > 11 {
score = score/2
}
if TJFTool.equal(a: Float(score), b: Float(best)){
bestPoint.append((p.x,p.y))
}
if TJFTool.greatThan(a: Float(score), b: Float(best)){
best = score
bestPoint.removeAll()
bestPoint.append((p.x,p.y))
}
chessArray[p.x][p.y] = .freeChess
self.updateOneEffectScore(chessArray: chessArray, point: (p.x,p.y), AIScore: &AIScore, humanScore: &humanScore)
}
steps += 1
total += count
if bestPoint.count > 0 {
let num = arc4random()%UInt32(bestPoint.count)
return (bestPoint[Int(num)].0,bestPoint[Int(num)].1,best)
}
return nil
}
func aiMaxS(deep:Int,alpha:Int,beta:Int,role:FlagType) -> Int{
var score = 0
var aiMax = 0
var humMax = 0
var best = minScore
for i in 0..<15{
for j in 0..<15{
if chessArray[i][j] == .freeChess{
aiMax = max(AIScore[i][j], aiMax)
humMax = max(humanScore[i][j], humMax)
}
}
}
score = (role == .blackChess ? 1 : -1) * (aiMax-humMax)
count += 1
if deep <= 0 || TJFTool.greatOrEqualThan(a: Float(score), b: Float(live_Five)){
return score
}
let points = self.getFiveChessType(chessArray: chessArray, AIScore: &AIScore, humanScore: &humanScore)
for i in 0..<points.count{
let p = points[i]
chessArray[p.x][p.y] = role
self.updateOneEffectScore(chessArray: chessArray, point: (p.x,p.y), AIScore: &AIScore, humanScore: &humanScore)
let some = -aiMaxS(deep: deep-1, alpha: -beta, beta: -1 * ( best > alpha ? best : alpha), role: self.reverseRole(role: role)) * deepDecrease
chessArray[p.x][p.y] = .freeChess
self.updateOneEffectScore(chessArray: chessArray, point: (p.x,p.y), AIScore: &AIScore, humanScore: &humanScore)
if TJFTool.greatThan(a: Float(some), b: Float(best)) {
best = some
}
//在這里進行ab 剪枝
if TJFTool.greatOrEqualThan(a: Float(some), b: Float(beta)){
ABcut += 1
return some
}
}
if (deep == 2 || deep == 3 || deep == 4) && TJFTool.littleThan(a: Float(best), b: Float(sleep_Four)) && TJFTool.greatThan(a: Float(best), b: -(Float)(sleep_Four)){
if let result = self.checkmateDeeping(chessArray: &chessArray, role: role, AIScore: &AIScore, humanScore: &humanScore, deep: checkmateDeep) {
return Int(Double(result[0].2) * pow(0.8, Double(result.count)) * (role == .blackChess ? 1:-1))
}
}
return best
}
var i = 2
var result:(Int,Int,Int)?
while i <= deep {
if let test = humMax(deep: i) {
result = test
if TJFTool.greatOrEqualThan(a: Float(test.2), b: Float(live_Four)) {
return test
}
}
i += 2
}
if result == nil {
var maxAiScore = 0
for i in 0..<15{
for j in 0..<15 {
if chessArray[i][j] == .freeChess && maxAiScore < AIScore[i][j] {
maxAiScore = AIScore[i][j]
result = (i,j,maxAiScore)
}
}
}
}
return result
}
經過Alpha Beta剪枝后,優化效果應該達到 1/2 次方,也就是說原來需要遍歷XY個節點,現在只需要遍歷X(Y/2)個節點,相比之前已經有了極大的提升。
不過即使經過了Alpha Beta 剪枝,思考層數也只能達到四層,也就是一個不怎么會玩五子棋的普通玩家的水平。而且每增加一層,所需要的時間或者說計算的節點數量是指數級增加的。所以目前的代碼想計算到第六層是很困難的。
我們的時間復雜度是一個指數函數 X^Y,其中底數X是每一層節點的子節點數,Y 是思考的層數。我們的剪枝算法能剪掉很多不用的分支,相當于減少了 Y,那么下一步我們需要減少 X,如果能把 X 減少一半,那么四層平均思考的時間能降低到 0.5^4 = 0.06 倍,也就是能從10秒降低到1秒以內。
如何減少X呢?我們知道五子棋中,成五、活四、雙三、雙眠四、眠四活三是必殺棋,于是我們遇到后就不用再往下搜索了。代碼如下:
static func getFiveChessType(chessArray:[[FlagType]],AIScore:inout [[Int]],humanScore:inout [[Int]]) ->[(x:Int,y:Int)]{
var twos:[(Int,Int)] = []
var threes:[(Int,Int)] = []
var doubleThrees:[(Int,Int)] = []
var sleepFours:[(Int,Int)] = []
var fours:[(Int,Int)] = []
var fives:[(Int,Int)] = []
var oters:[(Int,Int)] = []
for i in 0..<15{
for j in 0..<15{
if chessArray[i][j] == .freeChess && self.effectivePoint(chessArray: chessArray, point: (x: i, y: j)) {
let aiScore = AIScore[i][j]
let humScore = humanScore[i][j]
if aiScore>=live_Five {
return[(i,j)]
}else if humScore >= live_Five {
fives.append((i,j))
}else if aiScore >= live_Four {
fours.insert((i,j), at: 0)
}else if humScore >= live_Four {
fours.append((i,j))
}else if aiScore >= sleep_Four{
sleepFours.insert((i,j), at: 0)
}else if humScore >= sleep_Four{
sleepFours.append((i,j))
}else if aiScore >= 2*live_Three{
doubleThrees.insert((i,j), at: 0)
}else if humScore >= 2*live_Three{
doubleThrees.append((i,j))
}else if aiScore >= live_Three {
threes.insert((i,j), at: 0)
}else if humScore >= live_Three {
threes.append((i, j))
}else if aiScore >= live_Two{
twos.insert((i,j), at: 0)
}else if humScore >= live_Two{
twos.append((i,j))
}else {
oters.append((i,j))
}
}
}
}
if fives.count > 0 {
return [fives[0]]
}
if fours.count > 0 {
return fours
}
if sleepFours.count > 0{
return [sleepFours[0]]
}
if doubleThrees.count > 0{
return doubleThrees + threes
}
let result = threes + twos + oters
var realy:[(Int,Int)] = []
if result.count > limitNum {
realy += result.prefix(limitNum)
return realy
}
return result
}
五子棋是一種進攻優勢的棋,依靠連續不斷地活三或者沖四進攻,最后很容易會形成必殺棋,所以在進行深度搜索時,我們另開一種連續進攻的搜索,如果,電腦可以依靠連續進攻獲得勝利,我們可以直接走這條路勁。這條路勁,其實也是極大值極小值搜索算法的一種,只不過是只考慮活三沖四這兩種棋型,指數的底數較小,搜索的節點比較少,因此是效率很高的算法。代碼如下:
//有限考慮ai成五
static func findMaxScore(chessArray:[[FlagType]],role:FlagType,aiScore:[[Int]],humanScore:[[Int]],score:Int)->[(Int,Int,Int)]{
var result:[(Int,Int,Int)] = []
for i in 0..<15{
for j in 0..<15{
if chessArray[i][j] == .freeChess {
if self.effectivePoint(chessArray: chessArray, point: (i,j),chessCount: 1) {
let score1 = role == .blackChess ? aiScore[i][j] : humanScore[i][j]
if score1 >= live_Five {
return [(i,j,score1)]
}
if score1 >= score {
result.append((i,j,score1))
}
}
}
}
}
return result.sorted { (a, b) -> Bool in
return b.2 > a.2
}
}
//考慮活三,沖四
static func findEnemyMaxScore(chessArray:[[FlagType]],role:FlagType,aiScore:[[Int]],humanScore:[[Int]],score:Int)->[(Int,Int,Int)]{
var result:[(Int,Int,Int)] = []
var fours:[(Int,Int,Int)] = []
var fives:[(Int,Int,Int)] = []
for i in 0..<15{
for j in 0..<15{
if chessArray[i][j] == .freeChess {
if self.effectivePoint(chessArray: chessArray, point: (i,j),chessCount: 1) {
let score1 = role == .blackChess ? aiScore[i][j] : humanScore[i][j]
let score2 = role == .blackChess ? humanScore[i][j] : aiScore[i][j]
if score1 >= live_Five {
return [(i,j,-score1)]
}
if score1 >= live_Four {
fours.insert((i,j,-score1), at: 0)
continue
}
if score2 >= live_Five {
fives.append((i,j,score2))
continue
}
if score2 >= live_Four{
fours.append((i,j,score2))
continue
}
if score1 > score || score2 > score {
result.append((i,j,score1))
}
}
}
}
}
if fives.count > 0 {
return [fives[0]]
}
if fours.count > 0 {
return [fours[0]]
}
return result.sorted { (a, b) -> Bool in
return abs(b.2) > abs(a.2)
}
}
小結
本次編寫的AI還是比較強的,我勝利的機會很少,但還是存在贏的時候,因此AI算法還存在漏洞,主要表現在評分標準不準確和搜索深度不夠問題上,如何優化評分標準和搜索算法,是實現AI無敵的關鍵工作。
另外,在增加搜索深度的同時,遍歷的節點指數增長,計算時間增長,可以結合哈希算法,保存每次的棋盤評分,一定程度上提高計算時間,這也只是治標不治本的做法。
藍牙對戰
MultipeerConnectivity框架的使用
MultipeerConnectivity通過WiFi、P2P WiFi以及藍牙個人局域網進行通信的框架,從而無需聯網手機間就能傳遞消息。其原理是通過廣播作為服務器去發現附近的節點,每個節點都以設備名稱為標識。
myPeer = MCPeerID.init(displayName: UIDevice.current.name)
session = MCSession.init(peer: myPeer!, securityIdentity: nil, encryptionPreference: .none)
session?.delegate = self
MCSession的幾個代理方法必須實現,否則無法建立連接
//監聽連接狀態
func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
switch state {
case .notConnected:
print("未連接")
case .connecting:
print("正在連接中")
case .connected:
print("連接成功")
}
}
//發送Dada數據
func sendData(_ messageVo: GPBMessage, successBlock:(()->())?,errorBlock:((NSError)->())?) {
guard let session = session else {
return
}
guard let data = NSDataTool.shareInstance().returnData(messageVo, messageId: 0) else {return}
do {
try session.send(data as Data , toPeers: session.connectedPeers, with: .reliable)
}catch let error as NSError {
errorBlock?(error)
return
}
successBlock?()
}
//接收到的Data數據
func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
// 解析出過來的data數據包
NSDataTool.shareInstance().startParse(data) { (gpbMessage) in
self.getMessageBlock?(gpbMessage)
}
}
//接收到的流數據
func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
print("streamName")
}
//接收到的文件類型數據
func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
print("resourceName")
}
//接收到的文件類型數據,可將文件換路勁
func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL, withError error: Error?) {
}
我們通過MCAdvertiserAssistant(廣播)開啟搜索服務
advertiser = MCAdvertiserAssistant.init(serviceType: serviceStr, discoveryInfo: nil, session: session!)
//發出廣播
advertiser?.start()
advertiser?.delegate = self
MCBrowserViewControllerDelegate代理方法
/// 發出廣播請求
func advertiserAssistantWillPresentInvitation(_ advertiserAssistant: MCAdvertiserAssistant) {
print("advertiserAssistantWillPresentInvitation")
}
/// 結束廣播請求
func advertiserAssistantDidDismissInvitation(_ advertiserAssistant: MCAdvertiserAssistant) {
print("advertiserAssistantDidDismissInvitation")
}
設置藍牙連接頁面,顯示效果如圖所示:
func setupBrowserVC() {
guard let session = session else {
return
}
browser = MCBrowserViewController.init(serviceType: serviceStr, session: session)
browser?.delegate = self
}
實現MCBrowserViewControllerDelegate代理方法
func browserViewControllerDidFinish(_ browserViewController: MCBrowserViewController) {
print("藍牙連接完成")
browser?.dismiss(animated: true, completion: { [weak self] in
self?.browserBlock?()
})
}
func browserViewControllerWasCancelled(_ browserViewController: MCBrowserViewController) {
print("取消藍牙連接")
browser?.dismiss(animated: true, completion: nil)
}
小結
使用藍牙技術進行傳輸數據,盡管不需要連接網絡服務,但是真實因為這樣存在著許多安全隱患,為此我們引入Google Protobuf框架進行數據傳輸。下章會對該技術的運用進行詳解。
protobuf在iOS中的運用
protocolbuffer(以下簡稱protobuf)是google 的一種數據交換的格式,它獨立于語言,獨立于平臺。google 提供了多種語言的實現:java、c#、c++、oc、go 和 python,每一種實現都包含了相應語言的編譯器以及庫文件。由于它是一種二進制的格式,比使用 xml和json 進行數據交換快許多。可以把它用于分布式應用之間的數據通信或者異構環境下的數據交換。作為一種效率和兼容性都很優秀的二進制數據傳輸格式,可以用于諸如網絡傳輸、配置文件、數據存儲等諸多領域。
我們重點介紹protobuf在iOS中的運用,官方文檔
protobuf使用步驟
- 定義.proto文件
- 配置protobuf環境
- 映射相應語言的文件
- 導入第三方庫protobuf
.proto文件的定義
該文件主要是用來作為你傳遞數據的數據結構的文檔,然后通過終端命令生成我們相應語言的model類,導入項目中使用。
.proto的定義語法有官方文檔自己學習,在此不過多介紹,在此一定要注意的是,一定要使用proto3來定義,proto2已經在很多第三方庫中被淘汰使用(以前用的都是proto2,Proto3出來并不了解,報錯信息一度讓我懷疑人生)。定義文件類似下圖所示:
配置protobuf環境
使用homebrew進行配置(如果沒安裝,自己谷歌安裝)
- brew install automake
- brew install libtool
- brew install protobuf
- ln -s /usr/local/Cellar/protobuf/(上步中安裝protobuf的版本號)/bin/protoc /usr/local/bin
- git clone https://github.com/alexeyxo/protobuf-objc.git(oc版本)或者
git clone https://github.com/alexeyxo/protobuf-swift.git(swift版本) - cd protobuf-objc
- ./autogen.sh
- ./configure CXXFLAGS=-I/usr/local/include LDFLAGS=-L/usr/local/lib
- make install
映射相應語言的文件
- cd 到.proto文件的路勁中
- protoc --plugin=/usr/local/bin/protoc-gen-objc test.proto --objc_out=.
此為生成oc類的命令,其中test.proto是自己生成的proto文件的名字。相應swift類的命令為:
protoc --plugin=/usr/local/bin/protoc-gen-swift test.proto --swift_out=. - 將生成的文件導入項目中
導入第三方庫protobuf
這里建議使用pod管理:pod 'Protobuf'
Protobuf庫的使用
一般就是將Data類型的數據映射成model和將model生成data類型數據兩個方法,他們分別是
使用GPBMessage中的倆個方法
+ (instancetype)parseFromData:(NSData *)data error:(NSError **)errorPtr {
return [self parseFromData:data extensionRegistry:nil error:errorPtr];
}
- (nullable NSData *)data;
小結
使用protobuf傳輸還是存在安全問題和數據比較大時的耗能問題,于是我們想到了,在直播領域應用很普遍的RTMP協議。下章詳細講解,使用分包思想拆解數據包進行數據傳輸。
RTMP協議藍牙傳輸數據
RTMP傳統定義
rtmp協議中基本的數據單元被稱為消息(message)結構一般為:
- 時戳:4 byte,單位毫秒。超過最大值后會翻轉。
- 長度:消息負載的長度。
- 類型ID:Type Id 一部分ID范圍用于rtmp的控制信令。還有一部分可以供上層使用,rtmp只是透 傳。這樣可以方便的在rtmp上進行擴展。
- 消息流ID:Message Stream ID,用于區分不同流的消息。
消息在網絡中傳輸時,會被分割成很多小的消息塊,進行傳輸,增加傳輸的效率,而這些消息塊是由消息頭+消息體組成,消息頭就是制定的標識消息的協議,消息體就是所傳輸的消息內容。
RTMP在藍牙中的定義
手機藍牙傳輸數據,無法保證雙方手機時間同步,因此刨除時間戳定義改為固定字符串,因此messageHeader定義為:
struct message_header
{
uint32_t magic;//magic number, 0x98765432
uint32_t total;//包長度,從這一字段頭算起
uint32_t msgid;//消息ID
uint32_t seqnum;//客戶端使用,自增量
uint32_t version;//協議版本,目前為1
};
將需要傳輸的數據添加message_header
//GPBMEssage為protobuf庫里的類,請參考上篇文章
-(NSMutableData*)returnData:(GPBMessage*)req messageId:(int)messageId {
NSString *header=[NSString stringWithFormat:@"98765432%08lx%08x%08lx00000001",(unsigned long)req.data.length+20,messageId,(unsigned long)++self.header_count];
Byte bytes[40];
int j=0;
for(int i=0;i*2+1<header.length;i++)
{
int int_ch; /// 兩位16進制數轉化后的10進制數
const char* hex_char=[[header substringWithRange:NSMakeRange(i*2, 2)] UTF8String];
int_ch = (int)strtoul(hex_char, 0, 16);
// DLog(@"int_ch=%d",int_ch);
bytes[j] = int_ch; ///將轉化后的數放入Byte數組里
j++;
}
NSMutableData *data = [[NSMutableData alloc] init];
[data appendBytes:bytes length:j];
[data appendData:req.data];
return data;
}
接受到數據后,需要把長度小于message_header長度的數據進行拼包,并解析message_header結構
//解析數據message_header結構
-(void)parseSocketReceiveData:(NSData*)data result:(void (^)(NSData*result ,int messageId,int hearderId))resultBlock finish:(void(^)())finishBlockMessage{
if (_halfData.length>0) {
[_halfData appendData:data];
data=[_halfData copy];
_halfData =[[NSMutableData alloc]init];
}else{
data=[data copy];
}
if (data.length<20) {
[_halfData appendData:data];
if (finishBlockMessage) {
finishBlockMessage();
}
return;
}
Byte *testByte = (Byte*)[data bytes];
int length=(int) ((testByte[4] & 0xFF<<24)
| ((testByte[5] & 0xFF)<<16)
| ((testByte[6] & 0xFF)<<8)
| ((testByte[7] & 0xFF)));
int messageId=(int) ((testByte[8] & 0xFF<<24)
| ((testByte[9] & 0xFF)<<16)
| ((testByte[10] & 0xFF)<<8)
| ((testByte[11] & 0xFF)));
int headerId=(int)((testByte[12] & 0xFF<<24)
| ((testByte[13] & 0xFF)<<16)
| ((testByte[14] & 0xFF)<<8)
| ((testByte[15] & 0xFF)));
if(length==data.length){
if (resultBlock) {
resultBlock([data subdataWithRange:NSMakeRange(20, length-20)],messageId,headerId);
}
if (finishBlockMessage) {
finishBlockMessage();
}
}else if(length<data.length){
if (resultBlock) {
resultBlock([data subdataWithRange:NSMakeRange(20, length-20)],messageId,headerId);
}
[self parseSocketReceiveData:[data subdataWithRange:NSMakeRange(length, data.length-length)] result:resultBlock finish: finishBlockMessage];
}else{
[_halfData appendData:data];
if (finishBlockMessage) {
finishBlockMessage();
}
}
}
小結
rtmp協議雖然加快了數據傳輸的效率,一定程度上的安全,但是并不是特別的安全,為避免攻擊者攻擊,一些安全措施還是有必要的,在這里不過多介紹,有興趣自己調研。
在線對戰
IM采用的是環信SDK,環信作為免費的socket服務,相對已經很好了,功能也挺全面,但是,如果作為嚴謹的功能開發,他所暴露出來的api是遠遠不夠的,如傳輸的數據必須是它定好的結構,雖然有個自定義字典可以傳輸但是,這個字典也是僅僅限于幾種數據類型(做主要的DATA類型不接受)。導入SDK官方文檔
環信的主要用到的API
環信的主要用到的API需要實現的代理
//在初始化是設置代理
private override init() {
super.init()
EMClient.shared().add(self, delegateQueue: nil)
EMClient.shared().chatManager.add(self, delegateQueue: nil)
EMClient.shared().contactManager.add(self, delegateQueue: nil)
EMClient.shared().groupManager.add(self, delegateQueue: nil)
EMClient.shared().roomManager.add(self, delegateQueue: nil)
}
//在對象釋放時,釋放代理對象
deinit {
EMClient.shared().removeDelegate(self)
EMClient.shared().chatManager.remove(self)
EMClient.shared().contactManager.removeDelegate(self)
EMClient.shared().groupManager.removeDelegate(self)
EMClient.shared().roomManager.remove(self)
}
實現登錄異常的代理:服務器斷開,開啟定時器定時重連(環信并沒有給出重連的api,我發現調用環信的需要連接服務器的api,sdk會自動重連服務器,所以斷開服務器,定時調用上傳錯誤日志的api,機制吧。)
extension ChatHelpTool: EMClientDelegate{
//主要處理斷開服務器重連機制
func connectionStateDidChange(_ aConnectionState: EMConnectionState) {
networkState?(aConnectionState)
switch aConnectionState {
case EMConnectionConnected:
print("服務器已經連上")
if reconnectTimer != nil {
reconnectTimer.invalidate()
reconnectTimer = nil
}
case EMConnectionDisconnected:
print("服務器已斷開")
if reconnectTimer != nil {
reconnectTimer.invalidate()
reconnectTimer = nil
}
DispatchQueue.global().async {
self.reconnectTimer = Timer.weak_scheduledTimerWithTimeInterval(2, selector: { [weak self] in
self?.reconnectNetwork()
}, repeats: true)
self.reconnectTimer.fire()
RunLoop.current.add(self.reconnectTimer, forMode: RunLoopMode.defaultRunLoopMode)
RunLoop.current.run()
}
default:
()
}
}
func autoLoginDidCompleteWithError(_ aError: EMError!) {
if let error = aError {
TJFTool.errorForCode(code: error.code)
TJFTool.loginOutMessage(message: "自動登錄失敗,請重新登錄。")
}else {
PAMBManager.sharedInstance.showBriefMessage(message: "自動登錄成功")
}
}
//異地登錄
func userAccountDidLoginFromOtherDevice() {
TJFTool.loginOutMessage(message: "該賬號在其他設備上登錄,請重新登錄。")
}
func userAccountDidRemoveFromServer() {
TJFTool.loginOutMessage(message: "當前登錄賬號已經被從服務器端刪除,請重新登錄")
}
func userDidForbidByServer() {
TJFTool.loginOutMessage(message: "服務被禁用,請重新登錄")
}
}
實現發送消息的方法:因為是自定義的數據結構,所以使用消息的擴展,自定義字典傳遞數據。
//發送消息
extension ChatHelpTool {
// 定義消息model EMMessage
static func sendTextMessage(text:String,toUser:String,messageType:EMChatType,messageExt:[String:Any]?) ->EMMessage?{
let body = EMTextMessageBody.init(text: text)
let from = EMClient.shared().currentUsername
let message = EMMessage.init(conversationID: toUser, from: from, to: toUser, body: body, ext: messageExt)
message?.chatType = messageType
return message
}
//發送消息
static func senMessage(aMessage:EMMessage,progress aProgressBlock:(( _ progres: Int32)->())?,completion aCompletionBlock:((_ message:EMMessage?,_ error:EMError?)->())?) {
DispatchQueue.global().async {
EMClient.shared().chatManager.send(aMessage, progress: aProgressBlock,completion:aCompletionBlock)
}
}
}
實現接收消息的代理
extension ChatHelpTool: EMChatManagerDelegate{
//會話列表發生變化<EMConversation>
func conversationListDidUpdate(_ aConversationList: [Any]!) {
print("會話列表發生變化")
}
//收到消息
func messagesDidReceive(_ aMessages: [Any]!) {
aMessages.forEach { (message) in
if let message = message as? EMMessage {
if let data = message.ext as? [String:Any] {
let model = MessageModel.init(dictionary: data)
if model.gameType == "1" {
self.letterOfChallengeAction(["userName":message.from,"message":(model.challengeList?.message).noneNull,"chessType":model.chessType.noneNull])
}else if model.gameType == "2" {
var role:Role = .blacker
var gameType:GameType = .LiuZhouChess
if model.chessType == "1" {
role = .whiter
gameType = .fiveInRowChess
}
TJFTool.pushToChessChatRoom(message.from,role,chessType: gameType)
}else {
self.buZiChessMessage?(message)
}
}
}
}
}
//收到已讀回執
func messagesDidRead(_ aMessages: [Any]!) {
print("收到已讀回執")
}
//收到消息送達回執
func messagesDidDeliver(_ aMessages: [Any]!) {
print("收到消息送達回執")
aMessages.forEach { (message) in
if let message = message as? EMMessage {
if let data = message.ext as? [String:Any] {
let model = MessageModel.init(dictionary: data)
if model.gameType == "3" {
}
}
print(message.messageId)
print(TJFTool.timeWithTimeInterVal(time: message.timestamp),TJFTool.timeWithTimeInterVal(time: message.localTime))
}
}
}
//消息狀態發生變化
func messageStatusDidChange(_ aMessage: EMMessage!, error aError: EMError!){
print("消息狀態發生變化")
}
}
小結
IM在沒有服務器的情況下,使用第三方免費的最方便,但是同時并不能滿足產品的需求,有機會,我會為大家分享一篇自定義socket服務器下的即時通信結構和邏輯的設定。
最后
代碼具體實現地址
代碼中具體實現了兩個棋類游戲(有時間會持續添加游戲種類),包括在線對戰,人機對戰(算法不錯哦),藍牙對戰。
代碼編寫不易,喜歡的請點贊,謝謝!