余晖落尽暮晚霞,黄昏迟暮远山寻
本站
当前位置:网站首页 > 编程知识 > 正文

使用 Multipeer Connectivity 框架和 SwiftUI 4 构建游戏

xiyangw 2022-12-03 13:24 16 浏览 0 评论

我将演示如何使用 SwiftUI 4 实现一个基本的 Multipeer 连接应用程序,不需要 UIKit!

使用 Multipeer Connectivity 框架和 SwiftUI 4 构建游戏

事不宜迟,让我们开始吧!


应用结构

我们的应用程序的基本结构如下:

  • 一个 MultipeerSession 对象,用于处理与配对节点的配对和通信

  • PairView 将向用户显示可用同伴的列表,并允许他们邀请他们参加游戏

  • GameView 将显示游戏控件并向用户显示他们是赢还是输

游戏将是一个基本的“石头、纸、剪刀”游戏。用户将相互配对,然后他们将看到三个选项,石头、纸或剪刀。当用户选择一个动作时,它将被发送到对手的设备,一旦计时器到了,结果就会显示出来。

有了基本的概述,让我们深入研究一些代码。


代码

我们将从创建 MultipeerSession 对象开始。首先,我们需要将 MultipeerConnectivity 导入到我们的类中,并继承 NSObject 和 ObrvableObject。

class RPSMultipeerSession: NSObject, ObservableObject {
    private let serviceType = "rps-service"
    private var myPeerID: MCPeerID
    
    public let serviceAdvertiser: MCNearbyServiceAdvertiser
    public let serviceBrowser: MCNearbyServiceBrowser
    public let session: MCSession
}

在这里,我们创建了一个 serviceType 字符串,它将让其他正在扫描对等点的设备知道我们正在使用 RPS 应用程序并且仅在寻找 RPS 对等点。 这个字符串可以是任何东西来区分我们的 Multipeer 服务和其他服务。 然后我们创建一些实例变量来保存我们的 MCPeerID、MCNearbyServiceAdvertiser、MCNearbyServiceBrowser 和 MCSession。 这些字段需要公开,以便我们可以在 RPSMultipeerSession 类之外对它们执行操作。

在我们对象的 init() 内部,我们需要为上面创建的变量赋值。

init(username: String) {
    myPeerID = MCPeerID(displayName: username)
        
    session = MCSession(peer: peerID, securityIdentity: nil, encryptionPreference: .none)
    serviceAdvertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceType)
    serviceBrowser = MCNearbyServiceBrowser(peer: peerID, serviceType: serviceType)
    super.init()
}

在我们的应用程序中,我们将允许用户创建用户名,以便更轻松地发现对等点。在这里,我们将提供的用户名作为初始化程序中的参数,并从中创建一个 MCPeerID。

同样在初始化器内部,我们创建:

  • session:用于发送和接收 RPS 动作

  • serviceAdvertiser:用于向附近的玩家宣传我们自己

  • serviceBrowser:用于查找附近可用的玩家

并且不要忘记调用 super.init() 来调用超类的 init 方法!

接下来,我们需要考虑如何从对等方接收数据。稍后,我们将为我们的会话对象创建一个委托,该委托将从我们的对等方接收一个数据对象,然后我们可以将其转换为更易于使用的东西。

由于我们实际上只有四个选项(石头、纸、剪刀和无),我们将使用枚举来使处理响应更具可读性和更容易使用。

像这样放置在与我们的 RPSMultipeerSession 类相同的文件中就足够了:

enum Move: String, CaseIterable, CustomStringConvertible {
    case rock, paper, scissors, unknown
    
    var description : String {
        switch self {
        case .rock: return "Rock"
        case .paper: return "Paper"
        case .scissors: return "Scissors"
        default: return "Thinking"
        }
    }
}

稍后我们将使用移动的字符串表示来向我们的玩家显示图像。 通过使用 CustomStringConvertible,我们可以减少执行此操作所需的代码量。

现在我们已经创建并可用了 Move 枚举,我们应该考虑需要为我们的视图提供什么类型的数据。 我们知道,我们的 PairView 将允许玩家找到并与他们的朋友配对,它需要访问当前可用同伴的列表。

同样的视图需要知道我们何时收到来自其他玩家的邀请,以及该玩家是谁。 GameView 需要知道我们何时收到对手的动作。

我们的多个视图可能会发现了解我们当前是否与玩家配对很有用,最后,我们的 PairView 需要有某种方式来接受或拒绝来自另一个玩家的邀请。

@Published var availablePeers: [MCPeerID] = []
@Published var receivedMove: Move = .unknown
@Published var recvdInvite: Bool = false
@Published var recvdInviteFrom: MCPeerID? = nil
@Published var paired: Bool = false
@Published var invitationHandler: ((Bool, MCSession?) -> Void)?

总之,我们的 RPSMultipeerSession 中将有六个 @Published 属性。使这些变量@Published 使我们的视图不仅可以看到变量的值,而且可以在值更改时通知它们。

除此之外,我们需要为我们的会话、serviceAdvertiser 和 serviceBrowser 创建一些委托。让我们从最长的 MCSessionDelegate 开始。

会话委托具有处理方法:

  • 当对等点更改状态(已连接、已断开、正在连接)时

  • 当我们从对等点接收数据时

  • 当我们从对等点收到 InputStream

  • 当我们从对等点收到资源时(有或没有进展)

  • 当我们从对等端收到证书时(身份验证)

我们实际上只关心其中两种方法:当对等方更改状态时以及我们从用户接收数据时。即使是这种情况,这些方法中的每一个都需要在委托内部实现。

Swift 有一个简洁的特性,叫做扩展。如果您不熟悉,扩展基本上可以让您将代码添加到任何 Swift 类。

可以在 String 类上创建一个扩展来对字符串执行任何类型的操作。扩展非常强大,我强烈建议您查看细节,但现在这应该是让我们继续前进的充分介绍。

为了防止我们的 RPSMultipeerSession 变得太大而无法处理,我们将利用 Swift 的扩展来实现这些委托。我们可以简单地做:

extension RPSMultipeerSession: MCSessionDelegate

并在那里实现委托函数,在主类之外,但仍然在同一个文件中。可以将这些代表放在单独的文件中,但我个人选择将它们全部放在一个文件中。

extension RPSMultipeerSession: MCSessionDelegate {
    func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
        log.info("peer \(peerID) didChangeState: \(state.rawValue)")
        switch state {
        case MCSessionState.notConnected:
            // Peer disconnected
            break
        case MCSessionState.connected:
            // Peer connected
            break
        default:
            // Peer connecting or something else
            break
        }
    }
    
    func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
        if let string = String(data: data, encoding: .utf8), let move = Move(rawValue: string) {
            // Received move from peer
        } else {
            log.info("didReceive invalid value \(data.count) bytes")
        }
    }
    
    public func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
        log.error("Receiving streams is not supported")
    }
    
    public func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
        log.error("Receiving resources is not supported")
    }
    
    public func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
        log.error("Receiving resources is not supported")
    }
    
    public func session(_ session: MCSession, didReceiveCertificate certificate: [Any]?, fromPeer peerID: MCPeerID, certificateHandler: @escaping (Bool) -> Void) {
        certificateHandler(true)
    }
}

就像我之前说的,这是一个*大*。 确保使用 Xcode 的自动完成功能来获得声明的函数。

如您所见,大多数函数只是向控制台打印一行,实际上根本不做任何事情。 这是因为我们的应用程序不支持发送或接收流或资源。 不过,这可能会随着教程的进行而改变。

我们还没有完成! 如果您按照代码进行操作,您可能已经注意到委托实际上并没有做任何事情。 我们需要实现响应对等连接状态变化和接收对手数据的逻辑。 我将在第 2 部分详细介绍如何处理这些事件,所以现在,让我们继续。

接下来,我们将实现 MCNearbyServiceAdvertiserDelegate。 这个更容易消化:

extension RPSMultipeerSession: MCNearbyServiceAdvertiserDelegate {
    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: Error) {
        log.error("ServiceAdvertiser didNotStartAdvertisingPeer: \(String(describing: error))")
    }
    
    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
        log.info("didReceiveInvitationFromPeer \(peerID)")
    }
}

服务广告主有两种方法:一种是在广告主由于某种原因无法开始投放广告时调用,另一种是在我们收到其他玩家的邀请时调用。 后者将再次在第 2 部分中实施!

最后但同样重要的是,我们需要实现 MCNearbyServiceBrowserDelegate。

extension RPSMultipeerSession: MCNearbyServiceBrowserDelegate {
    func browser(_ browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: Error) {
        //TODO: Tell the user something went wrong and try again
        log.error("ServiceBroser didNotStartBrowsingForPeers: \(String(describing: error))")
    }
    
    func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) {
        log.info("ServiceBrowser found peer: \(peerID)")
        // Add the peer to the list of available peers
    }
    
    func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
        log.info("ServiceBrowser lost peer: \(peerID)")
        // Remove lost peer from list of available peers
    }
}

此委托具有在以下情况下调用的方法:

  • 浏览器由于某种原因无法开始浏览

  • 浏览器找到了一个附近的对等点,它正在宣传我们的 serviceType

  • 浏览器丢失了一个附近的对等点,该对等点正在宣传我们的 serviceType

现在我们已经设置好了代理,我们可以将它们应用到我们的会话、serviceAdvertiser 和 serviceBrowser。

session.delegate = self
serviceAdvertiser.delegate = self
serviceBrowser.delegate = self
                
serviceAdvertiser.startAdvertisingPeer()
serviceBrowser.startBrowsingForPeers()

在调用 super.init() 之后,我们将它添加到我们的 init() 中。 这将分配代表并开始向/为同行做广告和浏览。

我们快完成了,但我们不能忘记告诉我们的广告商和浏览器在 deinit() 内部停止

deinit {
    serviceAdvertiser.stopAdvertisingPeer()
    serviceBrowser.stopBrowsingForPeers()
}

现在我们已经完成了所有这些,我们的 RPSMultipeerSession.swift 文件应该如下所示:

//
//  RPSMultipeerSession.swift
//  RPS
//
//  Created by Joe Diragi on 7/28/22.
//

import MultipeerConnectivity
import os

enum Move: String, CaseIterable, CustomStringConvertible {
    case rock, paper, scissors, unknown
    
    var description : String {
        switch self {
        case .rock: return "Rock"
        case .paper: return "Paper"
        case .scissors: return "Scissors"
        default: return "Thinking"
        }
      }
}

class RPSMultipeerSession: NSObject, ObservableObject {
    private let serviceType = "rps-service"
    private var myPeerID: MCPeerID
    
    public let serviceAdvertiser: MCNearbyServiceAdvertiser
    public let serviceBrowser: MCNearbyServiceBrowser
    public let session: MCSession
        
    private let log = Logger()
    
    @Published var availablePeers: [MCPeerID] = []
    @Published var receivedMove: Move = .unknown
    @Published var recvdInvite: Bool = false
    @Published var recvdInviteFrom: MCPeerID? = nil
    @Published var paired: Bool = false
    @Published var invitationHandler: ((Bool, MCSession?) -> Void)?
    
    init(username: String) {
        let peerID = MCPeerID(displayName: username)
        self.myPeerID = peerID
        
        session = MCSession(peer: peerID, securityIdentity: nil, encryptionPreference: .none)
        serviceAdvertiser = MCNearbyServiceAdvertiser(peer: peerID, discoveryInfo: nil, serviceType: serviceType)
        serviceBrowser = MCNearbyServiceBrowser(peer: peerID, serviceType: serviceType)
        super.init()
        
        session.delegate = self
        serviceAdvertiser.delegate = self
        serviceBrowser.delegate = self
                
        serviceAdvertiser.startAdvertisingPeer()
        serviceBrowser.startBrowsingForPeers()
    }
    
    deinit {
        serviceAdvertiser.stopAdvertisingPeer()
        serviceBrowser.stopBrowsingForPeers()
    }
}

extension RPSMultipeerSession: MCSessionDelegate {
    func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) {
        log.info("peer \(peerID) didChangeState: \(state.rawValue)")
        switch state {
        case MCSessionState.notConnected:
            // Peer disconnected
            break
        case MCSessionState.connected:
            // Peer connected
            break
        default:
            // Peer connecting or something else
            break
        }
    }
    
    func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) {
        if let string = String(data: data, encoding: .utf8), let move = Move(rawValue: string) {
            // Received move from peer
        } else {
            log.info("didReceive invalid value \(data.count) bytes")
        }
    }
    
    public func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) {
        log.error("Receiving streams is not supported")
    }
    
    public func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) {
        log.error("Receiving resources is not supported")
    }
    
    public func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: Error?) {
        log.error("Receiving resources is not supported")
    }
    
    public func session(_ session: MCSession, didReceiveCertificate certificate: [Any]?, fromPeer peerID: MCPeerID, certificateHandler: @escaping (Bool) -> Void) {
        certificateHandler(true)
    }
}

extension RPSMultipeerSession: MCNearbyServiceAdvertiserDelegate {
    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didNotStartAdvertisingPeer error: Error) {
        log.error("ServiceAdvertiser didNotStartAdvertisingPeer: \(String(describing: error))")
    }
    
    func advertiser(_ advertiser: MCNearbyServiceAdvertiser, didReceiveInvitationFromPeer peerID: MCPeerID, withContext context: Data?, invitationHandler: @escaping (Bool, MCSession?) -> Void) {
        log.info("didReceiveInvitationFromPeer \(peerID)")
    }
}

extension RPSMultipeerSession: MCNearbyServiceBrowserDelegate {
    func browser(_ browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: Error) {
        //TODO: Tell the user something went wrong and try again
        log.error("ServiceBroser didNotStartBrowsingForPeers: \(String(describing: error))")
    }
    
    func browser(_ browser: MCNearbyServiceBrowser, foundPeer peerID: MCPeerID, withDiscoveryInfo info: [String : String]?) {
        log.info("ServiceBrowser found peer: \(peerID)")
        // Add the peer to the list of available peers
    }
    
    func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) {
        log.info("ServiceBrowser lost peer: \(peerID)")
        // Remove lost peer from list of available peers
    }
}

关注七爪网,获取更多APP/小程序/网站源码资源!

相关推荐

前后端分离 Vue + NodeJS(Koa) + MongoDB实践

作者:前端藏经阁转发链接:https://www.yuque.com/xwifrr/gr8qaw/vr51p4写在前面闲来无事,试了一下Koa,第一次搞感觉还不错,这个项目比较基础但还是比较完整了,...

MongoDB 集群如何工作?

一、什么是“MongoDB”?“MongoDB”是一个开源文档数据库,也是领先的“NoSQL”数据库,分别用“C++”“编程语言”编写,使用带有“Schema”的各种类似JSON的文档,是也分别被认为...

三部搭建mongo,和mongo UI界面

三步搭建mongo,和mongoUI界面安装首先你需要先有一个docker的环境检查你的到docker版本docker--versionDockerversion18.03.1-ce,b...

Mongodb 高可用落地方案

此落地方案,用于实现高可用。复制集这里部署相关的复制集,用于实现MongoDB的高可用。介绍MongoDB复制集用于提供相关的数据副本,当发生硬件或者服务中断的时候,将会从副本中恢复数据,并进行自动...

一次线上事故,我顿悟了MongoDB的精髓

大家好,我是哪吒,最近项目在使用MongoDB作为图片和文档的存储数据库,为啥不直接存MySQL里,还要搭个MongoDB集群,麻不麻烦?让我们一起,一探究竟,继续学习MongoDB分片的理论与实践,...

IDEA中安装MongoDB插件-再也无要nosql manager for mongodb

大家都知道MongoDB数据库作为典型的非关系型数据库被广泛使用,但基于MongoDB的可视化管理工具-nosqlmanagerformongodb也被用的较多,但此软件收费,所以国内的破解一般...

数据库监控软件Lepus安装部署详解

Lepus安装部署一、软件介绍Lepus是一套开源的数据库监控平台,目前已经支持MySQL、Oracle、SQLServer、MongoDB、Redis等数据库的基本监控和告警(MySQL已经支持复...

YAPI:从0搭建API文档管理工具

背景最近在找一款API文档管理工具,之前有用过Swagger、APIManager、Confluence,现在用的还是Confluence。我个人一直不喜欢用Swagger,感觉“代码即文档”,让代...

Mac安装使用MongoDB

下载MongoDB包:https://www.mongodb.com/download-center解压mongodb包手动解压到/usr/local/mongodb文件夹配置Mac环境变量打开环境...

保证数据安全,不可不知道的MongoDB备份与恢复

大家在项目中如果使用MongoDB作为NOsql数据库进行存储,那一定涉及到数据的备份与恢复,下面给大家介绍下:MongoDB数据备份方法在MongoDB中我们使用mongodump命令来备...

MongoDB数据备份、还原脚本和定时任务脚本

备注:mongodump和mongorestore命令需要在MongoDB的安装目录bin下备份脚本备份格式/usr/local/mongodb/bin/mongodump -h ...

等保2.0测评:mongoDB数据库

一、MongoDB介绍MongoDB是一个基于分布式文件存储的数据库。由C++语言编写。旨在为WEB应用提供可扩展的高性能数据存储解决方案。MongoDB是一个介于关系数据库和非关系数据库之间的产...

MongoDB入门实操《一》

什么是MongoDBMongoDB是一个基于分布式文件存储的数据库。由C++语言编写。旨在为WEB应用提供可扩展的高性能数据存储解决方案。MongoDB是一个介于关系数据库和非关系数据库之...

Python安装PyMongo的方法详细介绍

欢迎点击右上角关注小编,除了分享技术文章之外还有很多福利,私信学习资料可以领取包括不限于Python实战演练、PDF电子文档、面试集锦、学习资料等。前言本文主要给大家介绍的是关于安装PyMongo的...

第四篇:linux系统中mongodb的配置

建议使用普通用户进行以下操作。1、切换到普通用户odysee。2、准备mongodb安装包,自行去官网下载。3、解压安装包并重命名为mongodb4.04、配置mongodbcdmongod...

取消回复欢迎 发表评论: