I'm trying to connect two players with each other using GameKit in a very simple game. I want to use GKMatchmaker.shared().findMatch as I don't want to show any GameCenter related view controllers. (to keep it simple)
Problem:
Even though GameKit creates a match after finding two players, an error occurs that prevents either player from sending any message to the others.
Current Situation:
The basic code is as follows (based on the docs described here: https://developer.apple.com/documentation/gamekit/finding_multiple_players_for_a_game)
print("Requesting multiplayer match")
let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 2
request.recipientResponseHandler = {(player: GKPlayer, respnse: GKInviteRecipientResponse) -> Void in
print("new player about to join")
print(player.alias)
print(respnse)
}
GKMatchmaker.shared().findMatch(for: request, withCompletionHandler: {
(match: GKMatch?, error: Error?) -> Void in
if error != nil {
// Handle the error that occurred finding a match.
print("error during matchmaking")
print(error as Any)
} else if match != nil {
guard let match = match else { return }
print("connected to \(match.players.count) players")
// load the multiplayer data handler
let handler = MultiMatchHandler()
match.delegate = handler
// load the multiplayer service
let service = MultiMatchService(match: match)
service.sendMessageToAll(text: "Hello from the other side")
// finish the match making
GKMatchmaker.shared().finishMatchmaking(for: match)
// Start the game with the players in the match.
self.view?.presentScene(GameScene.newScene(multiplayer: service))
}
})
The output of that is
Requesting multiplayer match
2022-01-05 01:19:16.554959+0100 Grapefruit[38300:10026027] [Match] cannot set connecting state for players: (
"<GKPlayer: 0x282add280>(alias:... gamePlayerID:... teamPlayerID:... name:... status:(null) friendBiDirectional:0 friendPlayedWith:1 friendPlayedNearby:0 acceptedGameInviteFromThisFriend:0 initiatedGameInviteToThisFriend:0 automatchedTogether:1)"
), as there is no inviteDelegate set yet. The state might directly change to Ready when we set the inviteDelegate later and call sendQueuedStatesAndPackets.
2022-01-05 01:19:16.557002+0100 Grapefruit[38300:10026027] [Match] syncPlayers failed to loadPlayersForLegacyIdentifiers: (
"..."
)
connected to 0 players
sending text Hello from the other side failed
Findings:
minPlayers is set to 2. As the completion handler is called this means that at least one more player was found. But the number of players returned in match.players.count is 0
The matcher shows an error saying that cannot set connecting state for players ... as there is no inviteDelegate set yet. I can't find any info about this invite delegate.
Actual Question:
What is an inviteDelegate? Do I really need to implement such (if yes, then how?)? (I don't think so as the docs state that the match only starts after the invites are accepted).
How can I resolve this issue?
here is a working example for you. open on two machines, make sure both are authenticated, press "findMatch()" on both machines (and wait for confirmation), then ping baby ping
i believe the "no inviteDelegate set yet" error doesn't mean the match making necessary failed, and can safely be ignored, as mentioned here
you'll want to implement more of the GKMatchDelegate protocol, but this is a skeleton for demonstration purposes
import SwiftUI
import GameKit
import SpriteKit
class MyGameScene: SKScene, GKMatchDelegate {
override func didMove(to view: SKView) {
self.backgroundColor = .yellow
}
//GKMatchDelegate protocol
func match(_ match: GKMatch, didReceive data: Data, forRecipient recipient: GKPlayer, fromRemotePlayer player: GKPlayer) {
print("\(Self.self) \(#function) -- ping received")
}
}
struct Matchmaker: View {
#State var isAuthenticated:Bool = false
#State var scene = MyGameScene()
#State var match:GKMatch? = nil
var body: some View {
ZStack {
Color.clear
SpriteView(scene: scene)
VStack(alignment: .leading, spacing: 20) {
Text("1) authenticate() \(Image(systemName: isAuthenticated ? "checkmark.icloud" : "xmark.icloud"))")
Button { findMatch() } label: {
Text("2) findMatch() \(Image(systemName: (match != nil) ? "person.fill.checkmark" : "person.fill.xmark"))")
}
Button { ping() } label: {
Text("3) ping()")
}
}
}
.onAppear() {
authenticate()
}
}
func authenticate() {
GKLocalPlayer.local.authenticateHandler = { viewController, error in
if let error = error { print(error) }
isAuthenticated = (error == nil)
}
}
func findMatch() {
guard isAuthenticated else { return }
let request = GKMatchRequest()
request.minPlayers = 2
request.maxPlayers = 2
request.playerAttributes = 0xFFFFFFFF //mask for "i'll match with anyone"
GKMatchmaker.shared().findMatch (for: request) { match, error in
if let error = error { print(error) }
self.match = match
self.match?.delegate = scene
}
}
func ping() {
let players = match?.players ?? [];
let data = Data()
do {
try match?.send(data, to: players, dataMode: .reliable)
} catch {
print("Sending failed")
}
}
}
Related
I'm a student studying iOS development currently working on a simple AI project that utilizes SNAudioStreamAnalyzer to classify an incoming audio stream from the device's microphone. I can start the stream and analyze audio no problem, but I've noticed I can't seem to get my app to stop analyzing and close the audio input stream when I'm done. At the beginning, I initialize the audio engine and create the classification request like so:
private func startAudioEngine() {
do {
// start the stream of audio data
try audioEngine.start()
let snoreClassifier = try? SnoringClassifier2_0().model
let classifySoundRequest = try audioAnalyzer.makeRequest(snoreClassifier)
try streamAnalyzer.add(classifySoundRequest,
withObserver: self.audioAnalyzer)
} catch {
print("Unable to start AVAudioEngine: \(error.localizedDescription)")
}
}
After I'm done classifying my audio stream, I attempt to stop the audio engine and close the stream like so:
private func terminateNight() {
streamAnalyzer.removeAllRequests()
audioEngine.stop()
stopAndSaveNight()
do {
let session = AVAudioSession.sharedInstance()
try session.setActive(false)
} catch {
print("unable to terminate audio session")
}
nightSummary = true
}
However, after I call the terminateNight() function my app will continue using the microphone and classifying the incoming audio. Here's my SNResultsObserving implementation:
class AudioAnalyzer: NSObject, SNResultsObserving {
var prediction: String?
var confidence: Double?
let snoringEventManager: SnoringEventManager
internal init(prediction: String? = nil, confidence: Double? = nil, snoringEventManager: SnoringEventManager) {
self.prediction = prediction
self.confidence = confidence
self.snoringEventManager = snoringEventManager
}
func makeRequest(_ customModel: MLModel? = nil) throws -> SNClassifySoundRequest {
if let model = customModel {
let customRequest = try SNClassifySoundRequest(mlModel: model)
return customRequest
} else {
throw AudioAnalysisErrors.ModelInterpretationError
}
}
func request(_ request: SNRequest, didProduce: SNResult) {
guard let classificationResult = didProduce as? SNClassificationResult else { return }
let topClassification = classificationResult.classifications.first
let timeRange = classificationResult.timeRange
self.prediction = topClassification?.identifier
self.confidence = topClassification?.confidence
if self.prediction! == "snoring" {
self.snoringEventManager.snoringDetected()
} else {
self.snoringEventManager.nonSnoringDetected()
}
}
func request(_ request: SNRequest, didFailWithError: Error) {
print("ended with error \(didFailWithError)")
}
func requestDidComplete(_ request: SNRequest) {
print("request finished")
}
}
It was my understanding that upon calling streamAnalyzer.removeAllRequests() and audioEngine.stop() the app would stop streaming from the microphone and call the requestDidComplete function, but this isn't the behavior I'm getting. Any help is appreciated!
From OP's edition:
So I've realized it was a SwiftUI problem. I was calling the startAudioEngine() function in the initializer of the view it was declared on. I thought this would be fine, but since this view was embedded in a parent view when SwiftUI updated the parent it was re-initializing my view and as such calling startAudioEngine() again. The solution was to call this function in on onAppear block so that it activates the audio engine only when the view appears, and not when SwiftUI initializes it.
I don't believe you should expect to receive requestDidComplete due to removing a request. You'd expect to receive that when you call completeAnalysis.
I can detect workout started on backgroun with apple watch, with below code
let workoutevent = HKObjectType.workoutType()
if store.authorizationStatus(for: workoutevent) != HKAuthorizationStatus.notDetermined {
store.enableBackgroundDelivery(for: workoutevent, frequency: .immediate, withCompletion: { (worked, error) in
print(worked)
print(error)
print("workoutevent enableBackgroundDelivery")
guard worked else {
self.logger.error("Unable to set up background delivery from HealthKit: \(error!.localizedDescription)")
print("workoutevent unable to set up background ")
fatalError()
}
if error != nil {
print("workoutevent error is ")
print(error)
}
})
backgroundObserver3 =
HKObserverQuery(sampleType: workoutevent,
predicate: nil,
updateHandler: processUpdate3(query:completionHandler3:error:))
if let queryworkout = backgroundObserver3 {
print("Starting workoutevent333 the background observer query.\(queryworkout)")
store.execute(queryworkout)
}
}else{
print("not determined....")
}
whenever I started workout on apple watch, it goes to
processUpdate3
very well,
but what I need to know is to when user finish workout.
how can I detect it ?
func processUpdate3(query: HKObserverQuery,
completionHandler3: #escaping () -> Void,
error: Error?) {
print("come here when work out started ")
...........
}
I don't see it in your code. But somewhere you must have an HKWorkoutSession. My app is set up to track running and I configure the session to begin like so;
let configuration = HKWorkoutConfiguration()
configuration.activityType = .running
do {
// HKWorkoutSession is set up here.
session = try HKWorkoutSession(healthStore: healthStore, configuration: configuration)
workoutBuilder = session.associatedWorkoutBuilder()
} catch {
// handle errors
}
When the users taps the end workout button I call session.end()
Here is a link to the documentation.
After staring motionActivityUpdates, any attempt to stop it is not working. Here is the code that I am using for starting the motionactivity. There is a similar question without any answers and it is in C. Reading the documentation this is supposed to stop activity updates
Call this method to stop the delivery of updates that you started by calling the startActivityUpdates(to:withHandler:) method. This method does not stop queries started using the queryActivityStarting(from:to:to:withHandler:) method.
var manager: CMMotionActivityManager?
var motionManager: CMMotionManager?
func startMotionMonitoring() {
motionManager = CMMotionManager()
manager = CMMotionActivityManager()
startActivityMonitoring()
}
func startActivityMonitoring() {
if !CMMotionActivityManager.isActivityAvailable() {
return
}
if CMMotionActivityManager.authorizationStatus() != .authorized {
return
}
if let man = manager {
man.startActivityUpdates(to: .main) { (activity) in
guard let a = activity else {
return
}
print("motionActive")
}
}
}
I have another function I call to stop everything
func endMotionMonitoring(){
if manager == nil && motionManager == nil { return }
manager!.stopActivityUpdates()
manager = nil
motionManager!.stopAccelerometerUpdates()
motionManager = nil
}
But it is not stopping the motion activity updates. Every time I move the phone it prints out the statement "motionActive"
Anyone know how to fix this?
First of all I'm really new to Firestore and its functionalities, so I apologize if some of this might feel obvious to others. These things are just not registering in my mind yet.
I'm trying to create a 2 person multiplayer game using the language Swift and Firestore as the backend. However I'm not to sure how to create this functionality of only allowing two players inside a single game at a given time. How would I go about restricting each game to only allowing two players inside one game? Would this be something I need to set up within the security and rules portion of Firestore? Or would I need to create this functionality within how I model my data?
My current setup for how I'm modeling the data includes creating a collection of "Games" where each "Game" has two documents for "player1" and "player2". Then, within each one of those players/documents I store the values of each players functionalities. But with this approach, I still haven't solved the issue of only allowing two players within a single "Game"/collection. How do I prevent a third player from entering the game? or how would I handle the situation when more than one person enters a game at the same time?
Thank you for any advice possible.
You can use Cloud Functions to assign players to a game, manage when a game is full and then start it. Take a look at this article on Medium, Building a multi-player board game with Firebase Firestore & Functions
This is the code I ended up creating in Swift using Firestore to create the 2 person multiplayer game.
import UIKit
import Firebase
class WaitForOpponentViewController: UIViewController
{
#IBOutlet weak var activityIndicator: UIActivityIndicatorView!
var battleRoomDocumentReference : DocumentReference!
var battleRoomListenerRegistration : ListenerRegistration!
override func viewDidLoad(){
super.viewDidLoad()
activityIndicator.hidesWhenStopped = true
activityIndicator.startAnimating()
Firestore.firestore().collection(BATTLEROOMS_Collection)
.whereField(BATTLEROOMFULL_FIELD, isEqualTo: false)
.whereField(NUMOFPLAYERS_FIELD, isLessThan: 2)
.limit(to: 1)
.getDocuments { (snapShot, error) in
if let error = error
{
print("There was a error while fetching the documents: \(error)")
}
else
{
guard let snap = snapShot else {return}
if(snap.documents.count > 0)
{
//Update the current battle room
for document in snap.documents
{
Firestore.firestore().collection(BATTLEROOMS_Collection)
.document(document.documentID)
.setData(
[
BATTLEROOMFULL_FIELD : true,
NUMOFPLAYERS_FIELD : 2, //Note: Player1Id is not changed because there is already a player1Id when this document is updated
PLAYER2ID_FIELD : Auth.auth().currentUser?.uid ?? "AnonymousNum2"
], options: SetOptions.merge(), completion: { (error) in
if let error = error
{
print("There was an error while adding the second player to the battle room document : \(error)")
}
self.addBattleRoomListener(battleRoomDocumentId: document.documentID)
})
}
}
else
{
//Create a new battle room
self.battleRoomDocumentReference = Firestore.firestore().collection(BATTLEROOMS_Collection)
.addDocument(data:
[
BATTLEROOMFULL_FIELD: false,
NUMOFPLAYERS_FIELD : 1,
PLAYER1ID_FIELD : Auth.auth().currentUser?.uid ?? "AnonymousNum1",
PLAYER2ID_FIELD : ""
], completion: { (error) in
if let error = error
{
print("Error while adding a new battle room/player 1 to the battle room document : \(error)")
}
})
self.addBattleRoomListener(battleRoomDocumentId: self.battleRoomDocumentReference.documentID)
}
}
}
}
override func viewWillDisappear(_ animated: Bool) {
//Remove Battle Room Listener
battleRoomListenerRegistration.remove()
activityIndicator.stopAnimating()
}
func addBattleRoomListener(battleRoomDocumentId : String)
{
battleRoomListenerRegistration = Firestore.firestore().collection(BATTLEROOMS_Collection)
.document(battleRoomDocumentId)
.addSnapshotListener { (documentSnapshot, error) in
guard let snapshot = documentSnapshot else { return }
guard let documentData = snapshot.data() else { return }
let battleRoomFullData = documentData[BATTLEROOMFULL_FIELD] as? Bool ?? false
let numOfPlayerData = documentData[NUMOFPLAYERS_FIELD] as? Int ?? 0
if(battleRoomFullData == true && numOfPlayerData == 2)
{
print("Two Players in the Game, HURRAY. Segue to GAME VIEW CONTROLLER")
}
else
{
return
}
}
}
#IBAction func cancelBattle(_ sender: UIButton) {
//NOTE: Canceling is only allowed for the first user thats creates the Battle Room, once the Second Person enters the Battle Room the system will automatically segue to the Game View Controller sending both players into the game VC
Firestore.firestore().collection(BATTLEROOMS_Collection)
.document(battleRoomDocumentReference.documentID)
.delete { (error) in
if let error = error
{
print("There was an error while trying to delete a document: \(error)")
}
}
activityIndicator.stopAnimating()
self.dismiss(animated: true, completion: nil)
}
}
I am trying to do a MUC on iOS using xmpp_messenger_ios & XMPPFramework
Here is the code to join the room.
func createOrJoinRoomOnXMPP(){
// location has named array of lat and long
NSLog("Creating room on XMPP")
let roomJID: XMPPJID = XMPPJID.jidWithString(self.roomID + "#conference.ip-172-31-41-100")
let roomData: XMPPRoomCoreDataStorage = XMPPRoomCoreDataStorage.sharedInstance()
let chatRoom = XMPPRoom.init(roomStorage: roomData, jid: roomJID, dispatchQueue: dispatch_get_main_queue())
chatRoom.activate(OneChat.sharedInstance.xmppStream)
chatRoom.addDelegate(self, delegateQueue: dispatch_get_main_queue())
// let history = DDXMLElement.elementWithName("history")
// // Get lst messegs of the room
// history.addAttributeWithName("maxstanzas", stringValue: "10")
chatRoom.joinRoomUsingNickname(OneChat.sharedInstance.xmppStream!.myJID.user, history: nil)
}
as soon as this block executes I get an error in this code:
extension OneMessage: XMPPStreamDelegate {
public func xmppStream(sender: XMPPStream, didSendMessage message: XMPPMessage) {
if let completion = OneMessage.sharedInstance.didSendMessageCompletionBlock {
completion(stream: sender, message: message)
}
//OneMessage.sharedInstance.didSendMessageCompletionBlock!(stream: sender, message: message)
}
public func xmppStream(sender: XMPPStream, didReceiveMessage message: XMPPMessage) {
let user = OneChat.sharedInstance.xmppRosterStorage.userForJID(message.from(), xmppStream: OneChat.sharedInstance.xmppStream, managedObjectContext: OneRoster.sharedInstance.managedObjectContext_roster())
if !OneChats.knownUserForJid(jidStr: user.jidStr) { // <<< ERROR LINE
OneChats.addUserToChatList(jidStr: user.jidStr)
}
if message.isChatMessageWithBody() {
OneMessage.sharedInstance.delegate?.oneStream(sender, didReceiveMessage: message, from: user)
} else {
//was composing
if let _ = message.elementForName("composing") {
OneMessage.sharedInstance.delegate?.oneStream(sender, userIsComposing: user)
}
}
}
}
fatal error: unexpectedly found nil while unwrapping an Optional value
I have noticed that as soon as the connection is made to chat room it fetches previous messages, and thus the above code is executed.
Please help me out is doing a MUC for room chat on ios. I have searched and have not found any solution.
thanks
I solved this by this temporary solution.
extension OneMessage: XMPPStreamDelegate {
public func xmppStream(sender: XMPPStream, didSendMessage message: XMPPMessage) {
if let completion = OneMessage.sharedInstance.didSendMessageCompletionBlock {
completion(stream: sender, message: message)
}
//OneMessage.sharedInstance.didSendMessageCompletionBlock!(stream: sender, message: message)
}
public func xmppStream(sender: XMPPStream, didReceiveMessage message: XMPPMessage) {
NSLog("This is blocked")
// let user = OneChat.sharedInstance.xmppRosterStorage.userForJID(message.from(), xmppStream: OneChat.sharedInstance.xmppStream, managedObjectContext: OneRoster.sharedInstance.managedObjectContext_roster())
//
// if !OneChats.knownUserForJid(jidStr: user.jidStr) {
// OneChats.addUserToChatList(jidStr: user.jidStr)
// }
//
// if message.isChatMessageWithBody() {
// OneMessage.sharedInstance.delegate?.oneStream(sender, didReceiveMessage: message, from: user)
// } else {
// //was composing
// if let _ = message.elementForName("composing") {
// OneMessage.sharedInstance.delegate?.oneStream(sender, userIsComposing: user)
// }
// }
}
}
Blocking the OneMessage.swift code.
and handling the incoming messages in my ViewController.
This is not the right way to do it. but until ProcessOne give support for MUC this can be done.
Unwrapping that causes nil happens on:
user (return value of userForJID method is XMPPUserCoreDataStorageObject! )
jidStr (the type is String!)
Investigate which one happens to be nil.
Possible causes of user to be nil
- Nil value of jid or managedObjectContext is used in userForJID(:xmppStream:managedObjectContext)`
To find out which one is nil, simply do this:
guard let user = OneChat.sharedInstance.xmppRosterStorage.userForJID(message.from(), xmppStream: OneChat.sharedInstance.xmppStream, managedObjectContext: OneRoster.sharedInstance.managedObjectContext_roster())
else { fatalError("user is nil") }
guard let userJIDStr = user.jidStr
else { fatalError("jidStr is nil") }
I think you need to understand XMPP MUC first, read this doc.
When you send a message to MUCRoom, the serve will broadcast the message to all the members, including yourself.
And here message.from() = room.jid BUT NOT user.jid.
That's why the user you tried to get from roster is nil.