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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How To Make a Simple Playing Card Game with Multiplayer and Bluetooth, Part 2
45 mins
A Simple State Machine
The next thing to do is make the client connect to the server. So far, your app hasn’t made any connections – the client has shown to the user what servers are available, but the server doesn’t know anything about the client yet. Only after you tap the name of an available server will the client announce itself to that server.
So the MatchmakingClient can do two different things. At first, it’s looking for servers to join. When you pick a server, it will try to connect to that server. At that point, assuming it connects successfully and stays connected, the MatchmakingClient is no longer interested in any of the other servers. So there’s no reason for it to keep looking for new servers or update its _availableServers list (nor to tell its delegate about this [about what?]).
These different “states of mind” occupied by the MatchmakingClient can be described using a diagram. The full state diagram for the MatchmakingClient looks like this:
That’s four states the MatchmakingClient can occupy. It starts out in the “idle” state, where it just sits there, doing nothing. When you call startSearchingForServersWithSessionID:, it moves to the “Searching for Servers” state. That’s the code you’ve written so far.
When the user decides to connect to a particular server, the client first goes into the “connecting” state, where it attempts to connect to the server, and finally to “connected” when the connection is successfully established. If at any time in these last two states the server drops the connection (or disappears altogether), you move the client back to the idle state.
The MatchmakingClient will behave differently depending on which state it’s occupying. In the “searching for servers” state, it will add to or remove servers from its _availableServers list, but in the “connecting” or “connected” states, it won’t.
Using such a diagram to describe the possible states of your objects, you can make it immediately clear what your objects are supposed to do in different circumstances. You’ll be using state diagrams a few more times in this tutorial, including when managing the game state (which is a bit more complex than what you have here).
The implementation of a state diagram is called a “state machine.” You’ll keep track of the state of the MatchmakingClient using just an enum and an instance variable. Add the following to the top of MatchmakingClient.m, above the @implementation line:
typedef enum
{
ClientStateIdle,
ClientStateSearchingForServers,
ClientStateConnecting,
ClientStateConnected,
}
ClientState;
These four values represent the different states for this object. Also add a new instance variable:
@implementation MatchmakingClient
{
. . .
ClientState _clientState;
}
The state is something internal to this object, so you don’t need to put it in a property. Initially, the state should be “idle,” so add an init method to this class:
- (id)init
{
if ((self = [super init]))
{
_clientState = ClientStateIdle;
}
return self;
}
Now you’ll change some of the methods you wrote earlier to respect the different states. First up is startSearchingForServersWithSessionID:. This method should only take effect if the MatchmakingClient is in the idle state, so change it to:
- (void)startSearchingForServersWithSessionID:(NSString *)sessionID
{
if (_clientState == ClientStateIdle)
{
_clientState = ClientStateSearchingForServers;
// ... existing code goes here ...
}
}
Finally, change the two case statements in session:peer:didChangeState::
// The client has discovered a new server.
case GKPeerStateAvailable:
if (_clientState == ClientStateSearchingForServers)
{
if (![_availableServers containsObject:peerID])
{
[_availableServers addObject:peerID];
[self.delegate matchmakingClient:self serverBecameAvailable:peerID];
}
}
break;
// The client sees that a server goes away.
case GKPeerStateUnavailable:
if (_clientState == ClientStateSearchingForServers)
{
if ([_availableServers containsObject:peerID])
{
[_availableServers removeObject:peerID];
[self.delegate matchmakingClient:self serverBecameUnavailable:peerID];
}
}
break;
You only care about the GKPeerStateAvailable and GKPeerStateUnavailable messages if you’re in the ClientStateSearchingForServers state. Note that there are two types of state here: the state of the peer, as reported by the delegate method, and the state of the MatchmakingClient. I named the latter _clientState, as not to confuse things too much.
Connecting to the Server
Add a new method signature to MatchmakingClient.h:
- (void)connectToServerWithPeerID:(NSString *)peerID;
As the name indicates, you’ll use this to connect the client to the specified server. Add the implementation of this method to the .m file:
- (void)connectToServerWithPeerID:(NSString *)peerID
{
NSAssert(_clientState == ClientStateSearchingForServers, @"Wrong state");
_clientState = ClientStateConnecting;
_serverPeerID = peerID;
[_session connectToPeer:peerID withTimeout:_session.disconnectTimeout];
}
You can only call this method from the “searching for servers” state. If you don’t, then the application will exit with an assertion failure. That’s just a bit of defensive programming to make sure the state machine works.
You change the state to “connecting,” save the server’s peer ID in a new instance variable named _serverPeerID, and tell the GKSession object to connect this client to that peerID. For the timeout value – how long the session waits before it disconnects a peer that doesn’t respond – you use the default disconnect timeout from GKSession.
Add the new _serverPeerID instance variable:
@implementation MatchmakingClient
{
. . .
NSString *_serverPeerID;
}
That’s it for the MatchmakingClient. Now you have to call this new connectToServerWithPeerID: method from somewhere. The obvious place is JoinViewController’s table view delegate. Add the following code to JoinViewController.m:
#pragma mark - UITableViewDelegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
if (_matchmakingClient != nil)
{
[self.view addSubview:self.waitView];
NSString *peerID = [_matchmakingClient peerIDForAvailableServerAtIndex:indexPath.row];
[_matchmakingClient connectToServerWithPeerID:peerID];
}
}
This should be quite straightforward. First you determine the server’s peer ID (by looking at indexPath.row), and then call the new method to make the connection. Note that you also add the UIView from the “waitView” outlet to the screen, in order to cover up the table view and the other controls. Recall from earlier that the waitView is a second top-level view in the nib that you’ll use as a progress indicator.
When you now run the app on a client and tap the name of a server, it looks like this:
The MatchmakingClient has moved into the “connecting” state and is waiting for confirmation from the server. You don’t want the user to try and join any other servers at this point, so you display this temporary waiting screen.
If you take a peek at the Debug Output pane for the server app, you’ll see it now says something to the effect of:
Snap[4503:707] MatchmakingServer: peer 1310979776 changed state 4
Snap[4503:707] MatchmakingServer: connection request from peer 1310979776
These are notifications from GKSession telling the server that the client (who apparently has ID “1310979776” in this example) is attempting to connect. In the next section, you’ll make MatchmakingServer a bit smarter so that it will accept those connections and display the connected clients on the screen.
Note: The debug output says “changed state 4.” This numeric value represents one of the GKPeerState constants:
- 0 = GKPeerStateAvailable
- 1 = GKPeerStateUnavailable
- 2 = GKPeerStateConnected
- 3 = GKPeerStateDisconnected
- 4 = GKPeerStateConnecting
Note: The debug output says “changed state 4.” This numeric value represents one of the GKPeerState constants:
- 0 = GKPeerStateAvailable
- 1 = GKPeerStateUnavailable
- 2 = GKPeerStateConnected
- 3 = GKPeerStateDisconnected
- 4 = GKPeerStateConnecting
Tip: If you run the app on several devices simultaneously from within Xcode, you can switch between debug output using the debugger bar: