Erstellen eines Echtzeit-iOS-Multiplayer-Spiels mit Swift und WebSockets (3)


Bitte beachten Sie: Dies ist Teil 3 eines 4-teiligen Tutorials. Für Teil 2 siehe hier.


Richten Sie das Serverprojekt ein

Wir basieren das Serverprojekt auf einem Perfect-Vorlagenprojekt. Der Server wird ziemlich einfach sein und nur ein Spiel zwischen zwei Spielern gleichzeitig unterstützen. Das Projekt verwendet 3 Dateien:

  • main.swift: Der Einstiegspunkt der App startet einen HTTP-Server.
  • GameHandler.swift: Diese Klasse verarbeitet die von den Clients gesendeten Anfragen und stellt das Routing bereit.
  • Game.swift: Ein Singleton, das den Spielstatus verwaltet.

Wir werden einige Änderungen am Template-Server-Projekt vornehmen. Der erste Schritt wäre, eine Datei hinzuzufügen Game.swift das die Spiellogik enthalten wird. Diese Klasse sieht folgendermaßen aus:

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)
        }
    }

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

    private func didPlayerWin() -> Bool {
        let winningTiles: [[Int]] = [
            [0, 1, 2], 
            [3, 4, 5], 
            [6, 7, 8], 
            [0, 3, 6], 
            [1, 4, 7], 
            [2, 5, 8], 
            [0, 4, 8], 
            [6, 4, 2], 
        ]
        
        for tileIdxs in winningTiles {
            let tileIdx0 = tileIdxs[0]
            let tileIdx1 = tileIdxs[1]
            let tileIdx2 = tileIdxs[2]
            
            
            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)")
            })
        })
    }
}

Der größte Teil der Spiellogik sollte leicht verständlich sein. Das Spiel verfolgt die Spieler und ihre jeweiligen Steckdosen. Die öffentlichen Funktionen werden von der genutzt GameHandler Klasse. Die private Funktion notifyPlayers(message:) wird verwendet, um eine JSON-codierte zu senden Message Objekt über die Socket-Verbindung an alle angeschlossenen Player.

Der nächste Schritt wäre, a hinzuzufügen GameHandler Klasse, die den Web-Socket-Handler enthält. Für die Kommunikation ein benutzerdefiniertes Protokoll tictactoe ist definiert. Wenn sich der Client mit dem Server verbindet, validiert der Server das Protokoll. Wenn der Client ein anderes Protokoll verwendet, wird die Verbindung zum Client getrennt.

Das GameHandler Klasse muss von Perfect erben WebSocketSessionHandler und überschreiben die socketProtocol Eigentum und die handleSession(request:, socket:) Funktion. Wann immer eine Nachricht empfangen wird, überprüfen wir, ob sie Daten enthält, andernfalls benachrichtigen wir die Clients, dass die Verbindung zu einem Spieler getrennt wurde. Wenn wir eine ordnungsgemäße Nachricht erhalten haben, werden wir entsprechend damit umgehen. Um eine Nachricht zu parsen, serialisieren wir sie natürlich zuerst in a Message Objekt. Wenn alles gesagt und getan ist, benachrichtigen wir unsere Superklasse, dass wir unsere Nachricht bearbeitet haben und bereit sind, die nächste zu verarbeiten.

import Foundation
import PerfectWebSockets
import PerfectHTTP
import TicTacToeShared

class GameHandler: WebSocketSessionHandler {
    
    
    let socketProtocol: String? = "tictactoe"
    
    
    func handleSession(request: HTTPRequest, socket: WebSocket) {        
        
        
        
        
        
        socket.readStringMessage { (string, op, fin) in
            
            
            
            guard let string = string else {
                
                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")
            }
            
            
            self.handleSession(request: request, socket: socket)
        }
    }
}

Schließlich müssen wir die ändern main.swift Datei. Wir müssen eine Funktion hinzufügen, um den Datenverkehr an unsere weiterzuleiten GameHandler Klasse und starten Sie den Webserver:

import PerfectHTTP
import PerfectHTTPServer
import PerfectWebSockets
import PerfectLib

func makeRoutes() -> Routes {
    var routes = Routes()
    
    
    routes.add(method: .get, uri: "/game", handler: {
        request, response in
        
        
        
        WebSocketHandler(handlerProducer: {
            (request: HTTPRequest, protocols: [String]) -> WebSocketSessionHandler? in
            
            
            return GameHandler()
        }).handleRequest(request: request, response: response)
    })
    
    return routes
}

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

Fahren Sie mit Teil 4 des Tutorials fort.


Similar Posts

Leave a Reply

Your email address will not be published.