How To Make a Simple Playing Card Game with Multiplayer and Bluetooth, Part 2

This is a post by iOS Tutorial Team member Matthijs Hollemans, an experienced iOS developer and designer. You can find him on Google+ and Twitter. Welcome back to our monster 7-part tutorial series on creating a multiplayer card game over Bluetooth or Wi-Fi using UIKit! If you are new to this series, check out the […] By Matthijs Hollemans.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 6 of this article. Click here to view the first page.

Accepting Connections on the Server

Now you have a client that will attempt to make a connection, but on the server side, you still have to accept that connection before everything is hunky-dory. That happens in the MatchmakingServer.

But before you get to that, put a state machine into the server side of things. Add the following typedef to the top of MatchmakingServer.m:

typedef enum
{
	ServerStateIdle,
	ServerStateAcceptingConnections,
	ServerStateIgnoringNewConnections,
}
ServerState;

Unlike the client, the server only has three states.

State diagram for MatchmakingServer

That’s pretty simple. The “ignoring new connections” state is for when the host starts the card game. From that point on, no new clients are allowed to connect. Add a new instance variable for keeping track of the current state:

@implementation MatchmakingServer
{
	. . .
	ServerState _serverState;
}

As with the client, give the server an init method that initializes the state to idle:

- (id)init
{
	if ((self = [super init]))
	{
		_serverState = ServerStateIdle;
	}
	return self;
}

Add an if-statement to startAcceptingConnectionsForSessionID: that checks whether the state is “idle,” and then changes it to “accepting connections”:

- (void)startAcceptingConnectionsForSessionID:(NSString *)sessionID
{
	if (_serverState == ServerStateIdle)
	{
		_serverState = ServerStateAcceptingConnections;

		// ... existing code here ...
	}
}

Cool, now why don’t you make the GKSessionDelegate methods do some work. Just as the client is notified of when new servers become available, so is the server notified when a client tries to connect. In MatchmakingServer.m, change session:peer:didChangeState: to:

- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state
{
	#ifdef DEBUG
	NSLog(@"MatchmakingServer: peer %@ changed state %d", peerID, state);
	#endif

	switch (state)
	{
		case GKPeerStateAvailable:
			break;

		case GKPeerStateUnavailable:
			break;

		// A new client has connected to the server.
		case GKPeerStateConnected:
			if (_serverState == ServerStateAcceptingConnections)
			{
				if (![_connectedClients containsObject:peerID])
				{
					[_connectedClients addObject:peerID];
					[self.delegate matchmakingServer:self clientDidConnect:peerID];
				}
			}
			break;

		// A client has disconnected from the server.
		case GKPeerStateDisconnected:
			if (_serverState != ServerStateIdle)
			{
				if ([_connectedClients containsObject:peerID])
				{
					[_connectedClients removeObject:peerID];
					[self.delegate matchmakingServer:self clientDidDisconnect:peerID];
				}
			}
			break;

		case GKPeerStateConnecting:
			break;
	}
}

This time you’re interested in the GKPeerStateConnected and GKPeerStateDisconnected states. The logic is very similar to what you’ve seen in the client: you simply add the peer ID to an array and notify the delegate.

Of course, you haven’t defined a delegate protocol for the MatchmakingServer yet. To do that, add the following to the top of MatchmakingServer.h:

@class MatchmakingServer;

@protocol MatchmakingServerDelegate <NSObject>

- (void)matchmakingServer:(MatchmakingServer *)server clientDidConnect:(NSString *)peerID;
- (void)matchmakingServer:(MatchmakingServer *)server clientDidDisconnect:(NSString *)peerID;

@end

You know the drill. Add a property to the @interface section:

@property (nonatomic, weak) id <MatchmakingServerDelegate> delegate;

And synthesize it in the .m file:

@synthesize delegate = _delegate;

But who will play the role of this delegate for the MatchmakingServer? The HostViewController, of course. Switch to HostViewController.h and add MatchmakingServerDelegate to the list of implemented protocols:

@interface HostViewController : UIViewController <UITableViewDataSource, UITableViewDelegate, UITextFieldDelegate, MatchmakingServerDelegate>

Add the following line to HostViewController.m’s viewDidAppear, right after the allocation of MatchmakingServer, in order to hook up the delegate property:

		_matchmakingServer.delegate = self;

And implement the delegate methods:

#pragma mark - MatchmakingServerDelegate

- (void)matchmakingServer:(MatchmakingServer *)server clientDidConnect:(NSString *)peerID
{
	[self.tableView reloadData];
}

- (void)matchmakingServer:(MatchmakingServer *)server clientDidDisconnect:(NSString *)peerID
{
	[self.tableView reloadData];
}

As you did with the MatchmakingClient and the JoinViewController, you simply refresh the contents of the table view. Speaking of which, you still need to implement the data source methods. Replace numberOfRowsInSection and cellForRowAtIndexPath with:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
	if (_matchmakingServer != nil)
		return [_matchmakingServer connectedClientCount];
	else
		return 0;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
	static NSString *CellIdentifier = @"CellIdentifier";

	UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
	if (cell == nil)
		cell = [[PeerCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];

	NSString *peerID = [_matchmakingServer peerIDForConnectedClientAtIndex:indexPath.row];
	cell.textLabel.text = [_matchmakingServer displayNameForPeerID:peerID];

	return cell;
}

This pretty much mirrors what you did before, except now you display the list of connected clients rather than the available servers. Because tapping a row in the table view has no effect on this screen, also add the following method to disable row selections:

#pragma mark - UITableViewDelegate

- (NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
	return nil;
}

And add an import for PeerCell to the top of the file:

#import "PeerCell.h"

You’re almost there. You just need to add the missing methods to the MatchmakingServer. Add the following signatures to MatchmakingServer.h:

- (NSUInteger)connectedClientCount;
- (NSString *)peerIDForConnectedClientAtIndex:(NSUInteger)index;
- (NSString *)displayNameForPeerID:(NSString *)peerID;

And add their implementations to the .m file:

- (NSUInteger)connectedClientCount
{
	return [_connectedClients count];
}

- (NSString *)peerIDForConnectedClientAtIndex:(NSUInteger)index
{
	return [_connectedClients objectAtIndex:index];
}

- (NSString *)displayNameForPeerID:(NSString *)peerID
{
	return [_session displayNameForPeer:peerID];
}

Whew, that was a lot of typing! Now you can run the app again. Restart it on the device that will function as the server (it’s OK to restart the app on the client device too, but you haven’t changed anything in the client code, so it’s not really necessary).

Now when you tap the name of a server on the client device, the name of that client device should appear in the table view on the server. Try it out.

Except that… nothing happens (got ya!). As I said before, at this point the client is still trying to make a connection to the server, but the connection isn’t fully established until the server accepts it.

GKSession has another delegate method for that, named session:didReceiveConnectionRequestFromPeer:. To accept the incoming connection, the server has to implement this method and call acceptConnectionFromPeer:error: on the session object.

You already have a placeholder implementation of this delegate method in MatchmakingServer.m, so replace it with the following:

- (void)session:(GKSession *)session didReceiveConnectionRequestFromPeer:(NSString *)peerID
{
	#ifdef DEBUG
	NSLog(@"MatchmakingServer: connection request from peer %@", peerID);
	#endif
	
	if (_serverState == ServerStateAcceptingConnections && [self connectedClientCount] < self.maxClients)
	{
		NSError *error;
		if ([session acceptConnectionFromPeer:peerID error:&error])
			NSLog(@"MatchmakingServer: Connection accepted from peer %@", peerID);
		else
			NSLog(@"MatchmakingServer: Error accepting connection from peer %@, %@", peerID, error);
	}
	else  // not accepting connections or too many clients
	{
		[session denyConnectionFromPeer:peerID];
	}
}

First you check if the server's state is "accepting connections." If not, then you obviously don't want to accept any new connections, so you call denyConnectionFromPeer:. You also do that when you already have the maximum number of connected clients, as specified by the maxClients property, which for Snap! is set to 3.

If everything checks out, you'll call acceptConnectionFromPeer:error:. After that, the other GKSession delegate method will be called and the new client will show up in the table view. Restart the app on the server device and try again.

The debug output on the server is now:

Snap[4541:707] MatchmakingServer: Connection accepted from peer 1803140173
Snap[4541:707] MatchmakingServer: peer 1803140173 changed state 2

State 2 corresponds to GKPeerStateConnected. Congrats! You have established a connection between the server and the client. Both devices can now send messages to each other through the GKSession object (something that you will do a lot of shortly).

This is a screenshot of my iPod (the host) with three clients connected:

Server with 3 clients

Note: Even though you can type another name in the text field at the top of the screen, what appears in the table views are always the names of the devices themselves (in other words, the placeholder text from the "Your Name:" text field).

Contributors

Over 300 content creators. Join our team.