Codementor Events

Building a Real Time iOS multiplayer game with Swift and WebSockets (3)

Published Jul 03, 2018Last updated Dec 30, 2018
Building a Real Time iOS multiplayer game with Swift and WebSockets (3)

Please note: this is part 3 of a 4-part tutorial. For part 2, look here.


Set up the server project

We'll base the server project on a Perfect template project. The server will be pretty simple, supporting only one game between two players at any time. The project will make use of 3 files:

  • main.swift: The entry point of the app, will launch a HTTP server.
  • GameHandler.swift: This class will handle the requests sent from the clients and provide the routing.
  • Game.swift: A singleton that will manage the game state.

We will be making quite a few changes to the template server project. First step would be to add a file Game.swift that will contain the game logic. This class will look as such:

import Foundation
import PerfectWebSockets
import TicTacToeShared

enum GameError: Error {
    case failedToSerializeMessageToJsonString(message: Message)
}

class Game {
    static let shared = Game()
    
    private var playerSocketInfo: [Player: WebSocket] = [:]
    private var activePlayer: Player?
    private var board = [Tile](repeating: Tile.none, count: 9)
    
    private var players: [Player] {
        return Array(self.playerSocketInfo.keys)
    }
        
    private init() {}
    
    func playerForSocket(_ aSocket: WebSocket) -> Player? {
        var aPlayer: Player? = nil

        self.playerSocketInfo.forEach { (player, socket) in
            if aSocket == socket {
                aPlayer = player
            }
        }
        
        return aPlayer
    }
    
    func handlePlayerLeft(player: Player) throws {
        if self.playerSocketInfo[player] != nil {
            self.playerSocketInfo.removeValue(forKey: player)
            
            let message = Message.stop()
            try notifyPlayers(message: message)
        }
    }
    
    func handleJoin(player: Player, socket: WebSocket) throws {
        if self.playerSocketInfo.count > 2 {
            return
        }
        
        self.playerSocketInfo[player] = socket

        if self.playerSocketInfo.count == 2 {
            try startGame()
        }
    }
    
    func handleTurn(_ board: [Tile]) throws {
        self.board = board

        if didPlayerWin() {
            let message = Message.finish(board: self.board, winningPlayer: self.activePlayer!)
            try notifyPlayers(message: message)
        } else if board.filter({ $0 == Tile.none }).count == 0 {
            let message = Message.finish(board: board, winningPlayer: nil)
            try notifyPlayers(message: message)
        } else {
            self.activePlayer = nextActivePlayer()!
            let message = Message.turn(board: self.board, player: self.activePlayer!)
            try notifyPlayers(message: message)
        }
    }

    // MARK: - Private
    
    private func setupBoard() {
        (0 ..< 9).forEach { (i) in
            board[i] = Tile.none
        }
    }

    private func didPlayerWin() -> Bool {
        let winningTiles: [[Int]] = [
            [0, 1, 2], // the bottm row
            [3, 4, 5], // the middle row
            [6, 7, 8], // the top row
            [0, 3, 6], // the left column
            [1, 4, 7], // the middle column
            [2, 5, 8], // the right column
            [0, 4, 8], // diagonally bottom-left to top-right
            [6, 4, 2], // diagonally top-left to bottom-right
        ]
        
        for tileIdxs in winningTiles {
            let tileIdx0 = tileIdxs[0]
            let tileIdx1 = tileIdxs[1]
            let tileIdx2 = tileIdxs[2]
            
            // Check if the 3 tiles are set and are all equal
            if (self.board[tileIdx0] != Tile.none &&
                self.board[tileIdx0] == self.board[tileIdx1] &&
                self.board[tileIdx1] == self.board[tileIdx2]) {
                return true
            }
        }
        
        return false
    }
    
    private func startGame() throws {
        setupBoard()
        
        self.activePlayer = randomPlayer()
        let message = Message.turn(board: self.board, player: self.activePlayer!)
        try notifyPlayers(message: message)
    }
    
    private func randomPlayer() -> Player {
        let randomIdx = Int(arc4random() % UInt32(self.players.count))
        return players[randomIdx]
    }
    
    private func nextActivePlayer() -> Player? {
        return self.players.filter({ $0 != self.activePlayer }).first
    }
    
    private func notifyPlayers(message: Message) throws {
        let jsonEncoder = JSONEncoder()
        let jsonData = try jsonEncoder.encode(message)
        
        guard let jsonString = String(data: jsonData, encoding: .utf8) else {
            throw GameError.failedToSerializeMessageToJsonString(message: message)
        }
        
        self.playerSocketInfo.values.forEach({
            $0.sendStringMessage(string: jsonString, final: true, completion: {
                print("did send message: \(message.type)")
            })
        })
    }
}

Most of the game logic should be easy to understand. The game keeps track of players and their respective sockets. The public functions will be used by the GameHandler class. The private function notifyPlayers(message:) is used to send a JSON-encoded Message object over the socket connection to all connected players.

Next step would be to add a GameHandler class that will contain the web socket handler. For communication, a custom protocol tictactoe is defined. When the client connects with the server, the server validates the protocol. If the client uses a different protocol, the client will be disconnected.

The GameHandler class has to inherit from Perfect's WebSocketSessionHandler and override the socketProtocol property and the handleSession(request:, socket:) function. Whenever a message is received we verify it contains some data, otherwise we will notify the clients that a player was disconnected. If we did receive a proper message, we will handle it accordingly. Of course, in order to parse a message we first serialize it into a Message object. When all is said and done, we notify our superclass that we did handle our message and we're ready to process the next one.

import Foundation
import PerfectWebSockets
import PerfectHTTP
import TicTacToeShared

class GameHandler: WebSocketSessionHandler {
    // The name of the super-protocol we implement.
    // This is optional, but it should match whatever the client-side WebSocket is initialized with.
    let socketProtocol: String? = "tictactoe"
    
    // This function is called by the WebSocketHandler once the connection has been established.
    func handleSession(request: HTTPRequest, socket: WebSocket) {        
        // This callback is provided:
        //  the received data
        //  the message's op-code
        //  a boolean indicating if the message is complete
        // (as opposed to fragmented)
        socket.readStringMessage { (string, op, fin) in
            // The data parameter might be nil here if either a timeout
            // or a network error, such as the client disconnecting, occurred.
            // By default there is no timeout.
            guard let string = string else {
                // This block will be executed if, for example, the game was closed.
                if let player = Game.shared.playerForSocket(socket) {
                    print("socket closed for \(player.id)")
                    
                    do {
                        try Game.shared.handlePlayerLeft(player: player)
                    } catch let error {
                        print("error: \(error)")
                    }
                }
                
                return socket.close()
            }
            
            do {
                let decoder = JSONDecoder()
                guard let data = string.data(using: .utf8) else {
                    return print("failed to covert string into data object: \(string)")
                }
                
                let message: Message = try decoder.decode(Message.self, from: data)
                switch message.type {
                case .join:
                    guard let player = message.player else {
                        return print("missing player in join message")
                    }
                    
                    try Game.shared.handleJoin(player: player, socket: socket)
                case .turn:
                    guard let board = message.board else {
                        return print("board not provided")
                    }
                    
                    try Game.shared.handleTurn(board)
                default:
                    break
                }
            } catch {
                print("Failed to decode JSON from Received Socket Message")
            }
            
            // Done working on this message? Loop back around and read the next message.
            self.handleSession(request: request, socket: socket)
        }
    }
}

Finally we need to change the main.swift file. We will need to add a function to route traffic to our GameHandler class and launch the web server:

import PerfectHTTP
import PerfectHTTPServer
import PerfectWebSockets
import PerfectLib

func makeRoutes() -> Routes {
    var routes = Routes()
    
    // Add the endpoint for the WebSocket example system
    routes.add(method: .get, uri: "/game", handler: {
        request, response in
        
        // To add a WebSocket service, set the handler to WebSocketHandler.
        // Provide your closure which will return your service handler.
        WebSocketHandler(handlerProducer: {
            (request: HTTPRequest, protocols: [String]) -> WebSocketSessionHandler? in
            
            // Return our service handler.
            return GameHandler()
        }).handleRequest(request: request, response: response)
    })
    
    return routes
}

do {
    // Launch the HTTP server on port 8181
    try HTTPServer.launch(name: "localhost", port: 8181, routes: makeRoutes())
} catch PerfectError.networkError(let err, let msg) {
    print("Network error thrown: \(err) \(msg)")
}

Continue to part 4 of the tutorial.


Discover and read more posts from Wolfgang Schreurs
get started
post commentsBe the first to share your opinion
sagar harish
5 years ago

I must be glad to using the link and finding the dvd player window 10 here http://dvdplayerwindows10.com/ this is the awesome feature in windon 10 i really like to trying the post thanks.

Show more replies