ios 棋類游戲對戰的實現

棋類游戲對戰的實現

  • 六洲棋
  • 五子棋
  • AI對戰
  • 藍牙對戰
  • 在線對戰

六洲棋

六洲棋,又稱:泥棋、插方、來馬、五福棋,中國民間傳統棋類體育形式。源于民間,簡便、通俗、易學,在民間廣為流行,深受社會底層大眾的喜愛。龍其在淮河流域的安徽省、河南省、江蘇省、以及湖北省、山東省非常普及,并流傳到中國各地,包括港、澳、臺地區。起源于勞動人民生活,根植于民間大眾之中,它簡捷、明快,趣味性、競技性強,是一項長期流行于民間,富有傳統文化色彩的競技項目。對于啟迪智慧,休閑娛樂,增進交流非常有益。列安徽省第二批省級非物質文化遺產。
6*6縱橫線組成,共三十六個棋點。每方十八枚棋子,以兩色區分敵我。

規則

對弈過程分三階段。(鳳陽下法)放子:對弈雙方依次將己子放入空棋點,將手上的棋子放完才開始走子。逼子:若無棋子被吃,使得棋子放滿棋盤。則兩人各選對方一枚敵子移出游戲。走子:由后手方開始輪流移動己棋,沿線直橫線一格。吃子:無論是下子或走子階段,只要己方棋子排成以下排列稱為成城,就要吃掉一定數量的敵子,但不可吃掉已成城子的敵棋。在放子階段,被吃的子先作記號,等走子階段開始才一齊提取。
成六:六枚棋子以縱、橫和斜3個方向連成直線(除了四條邊的直線)。吃掉敵方三子。

six
six_two

斜五:連子的2頭都靠棋盤邊緣,吃掉敵方兩子。

five

斜四:連子的2頭都靠棋盤邊緣,吃掉敵方一子。

four

斜三:連子的2頭都靠棋盤邊緣,吃掉敵方一子。

three

成方:四枚棋子組成一個緊鄰相連的小正方形,吃掉敵方一子。

check

使對方只剩下三枚以下則獲勝。因為是民間文化,各地稍有差異。

棋型的算法實現

//是否形成斜子棋(三子棋,四子棋,五子棋,六子棋)
   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
    }
image

實現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進行配置(如果沒安裝,自己谷歌安裝)

映射相應語言的文件

  • 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服務器下的即時通信結構和邏輯的設定。

最后

代碼具體實現地址
代碼中具體實現了兩個棋類游戲(有時間會持續添加游戲種類),包括在線對戰,人機對戰(算法不錯哦),藍牙對戰。
代碼編寫不易,喜歡的請點贊,謝謝!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,363評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,497評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,305評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,962評論 1 311
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,727評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,193評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,257評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,411評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,945評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,777評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,978評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,519評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,216評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,657評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,960評論 2 373

推薦閱讀更多精彩內容

  • 五子棋 五子棋五子棋是比較流行的棋類游戲了,玩法簡單,基本上人人會玩,在此就不介紹游戲規則了。下面使用 swift...
    天機否閱讀 20,554評論 3 29
  • 實時消息協議---流的分塊 版權聲明: 版權(c)2009 Adobe系統有限公司。全權所有。 摘要: 本備忘錄描...
    一個人zy閱讀 1,921評論 0 9
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,782評論 18 139
  • 針對曾經火爆的2048游戲,有人實現了一個AI程序,可以以較大概率(高于90%)贏得游戲,并且作者在stackov...
    GarfieldEr007閱讀 2,739評論 1 18
  • 01砍掉中間環節▲ 曾經,我有一個愛看書的同事。 我也挺愛看書,不同的是,她分享好書給我時,我總是把書名寫在紙上,...
    三人行必有吳師閱讀 519評論 0 4