Choose the Objective-C category template. Name the category STDataMessaging and make it a category of STGame, as shown in Figure 14-20.
Figure 14-20. Creating the STDataMessaging category
The category is going to implement three methods that send game information to the other player:
-sendGameStart, -sendStrike:, and -sendCaptureForSunIndex:. The category also implements the CGMatchDelegate methods to receive data from the other player. In the new STGame+STDataMessaging.h interface file, edit the categorydeclaration so it looks like this (new code in bold):
@interface STGame (STDataMessaging) <GKMatchDelegate>
- (void)sendGameStart;
- (void)sendStrike:(STStrike*)strike;
- (void)sendCaptureForSunIndex:(NSUInteger)index;;
@end
The category declaration will need a definition of the STStrike class and few constants, so add this to the beginning of the file:
#import "STGameDefs.h" @class STStrike;
Defining the Data Format
One of the design tasks I outlined earlier was deciding “how your data is formatted.” This is a critical part of your communications design. The GKMatch object will transport an array of bytes from your app to another device,possibly halfway around the world, but what’s in that array of bytes is entirely up to you. It has to contain the information you want to communicate with the other app, and it has to be organized in such a way that the other appcan understand it when it’s received. The sidebar “Serialization and Cross-Platform Communications” describes some of the challenges involved.
Converting information (numbers, objects, properties, and so on) into a format that’s transportable is generically referred to as serialization, marshaling, or deflating. You must do this whenever you exchange information withanother computer system or process, which includes storing information in a file. Cocoa and Objective-C provide a number of tools to help serialize your data, and then turn that serialized data back into the objects andproperties your app can use—a process called deserialization, unmarshaling, or inflating.
There are three aspects about the information in your app that can present a barrier to exchanging it with another app or device: memory addresses, word size, and byte order.
The biggest problem is memory addresses. An object in Objective-C is a small region of dynamic RAM that stores the properties of that instance. In your app, you refer to that object using its address. The memory address ofan object is utterly meaningless to another process or computer system. Another device can’t access the
memory of your app—at least I hope it can’t.
Giving the address of an object to another process is akin to giving someoneyour telephone number...
in a parallel universe; they have no way to use it.
The solution is to convert the properties of your object(s) into a sequence of bytes that can be used to assemble equivalent objects by the recipient. Let’s say you had a Person object that had name (string) and age (integer)properties. You can’t pass the address of the Person, or string, object to another process. Instead, you serialize the object by creating an array of bytes and filling those bytes with the characters of the person’s name and thebinary value of their age. The computer
receiving these bytes can use that data to construct a new string object and a new Person object with the same properties.
When it comes to exchanging that person’s age, there are two additional issues to contend with. Different computer systems, and even different compilers, use different word sizes. A “word” in computer architecture is asequence of bytes used to store a single number, such as an int. An int may be 16 bits (2 bytes) long on one computer system and 64 bits (8 bytes) long on another. So you can’t simply write code that copies an int into an array of bytes and then extracts it again on the other system, because on one computer that means 2 bytes and on the other that means 8 bytes.
Mismatched word sizes are typically solved by using the fixed-size variable types in C and Objective-C. For example, int32_t is a variable type (just like int and char) that defines an integer that’s always 32 bits
(4 bytes) long. It doesn’tmatter on what kind of computer system you compile this on, or what kind of CPU it’s running, an int32_t variable will always be 32 bits long.
The final problem is byte order. Different CPU architectures choose to store the bytes of a single integer in different orders. CPUs that store the least significant bits of the integer in the first (lowest) byte of memory are called little-endian machines. If the first byte contains the most significant bits of the integer, it’s a big-endian machine. If you transmit the value of an integer, least significant byte first, to a system that expects the first byte to be the mostsignificant, the integer value will arrive scrambled.
Byte order isn’t a problem (yet) for SunTouch. As of this writing, all iOS devices are built with similar CPU architectures that all use the same (little-endian) byte order. Be aware that this could change in the future.
Word size, however, is not the same on all iOS devices. With the introduction of the A7 processor, some
iOS devices have a 32bit CPU while others have a 64 bit CPU. This means an NSInteger variable occupies
4 bytes (32 bits) when running on an iPhone 4S, but occupies 8 bytes (64 bits) when running on an iPhone 5S. (This statementpresumes that you’ve compiled your app for both 32 and 64 bit architectures,which
is the default Xcode build setting.) The length of all pointer and CGFloat variables will also be different. Any integer or floating point values you exchange between iOS devices will have to agree on a consistent word size.
If your app wanted to communicate with a different kind of computer system, running a different operating
system, you’d need to concern yourself with both word size and byte order differences.
Chapters 18 and 19 explain the built-in Objective C tools for serializing objects.
These tools take care of all of the word size, byte order, and object inflating for you.
When the SunTouch app receives data, it must be able to determine what kind of information the data block contains. The simplest approach, when doing this yourself, is to start every data block with an integer thatdescribed what kind of information the rest of the data block contains. In STGame+STDataMessaging.h, add this declaration:
typedef uint32_t STMessage; enum {
kSTStartGameMessage, kSTStrikeMessage, kSTCaptureMessage
};
This code defines a new integer variable type (STMessage) that is guaranteed to be 32 bits long regardless of what computer system it’s compiled for. It then defines three constants, one for each type of data messageSunTouch sends.
The rest of the declarations in STGame+STDataMessaging.h define the structures uses to exchange data between games:
typedef float STFloat; typedef struct {
STFloat x; STFloat y;
} attribute ((aligned(4), packed)) STMessagePoint;
struct STStartGameMessage { STMessage message; uint32_t coinToss; STMessagePoint sun[kSunCount];
} attribute ((aligned(4), packed));
struct STStrikeMessage { STMessage message; STMessagePoint location; STFloat radius;
} attribute ((aligned(4), packed));
struct STCaptureMessage { STMessage message; uint32_t sunIndex; STFloat gameTime;
} attribute ((aligned(4), packed));
The first two declarations create two new variable types, STFloat and STMessagePoint. STFloat defines a single coordinate or distance variable and STMessagePoint defines a pair of STFloat values used to describe a coordinate.
The next three structures (STStartGameMessage, STStrikeMessage, and STCaptureMessage) define the
organization of the data blocks that will be exchanged.
Notice that every structure starts with an STMessage integer field. Thiswill contain the appropriate message
type constant. When your app receives a data block from another player, you know that the first 32 bits of the message will contain a number. You’ll examine that number to determine whatthe data contains.
The rest of the fields should be obvious. The attribute ((aligned(4),packed)) gibberish is a special directive that tells the compiler exactly how to align and pack the fields within the structure. Just as word size and byte orderchange from one computer to another, so does the byte alignment of fields within a structure.
By being explicit, SunTouch makes sure that—should the compiler’s structure alignment rules change in the future—all versions ofSunTouch will still be able to communicate with each other.
That’s all the declarations you need. Now you can write the methods that send and receive data from theremote app.
Sending Data to a Player
Switch to the STGame+STDataMessaging.m implementation file. Start by #importing the definitions of the STStrike and STSun classes; you’re going to need them.
#import "STStrike.h"
#import "STSun.h"
Implement the -sendGameStart method:
- (void)sendGameStart
{
struct STStartGameMessage message; message.message = kSTStartGameMessage; message.coinToss = coinToss;
for ( NSUInteger i=0; i<kSunCount; i++ )
{
STSun *sun = suns[i]; message.sun[i].x = sun.location.x; message.sun[i].y = sun.location.y;
}
NSData *data = [NSData dataWithBytes:&message length:sizeof(message)]; [multiPlayerMatch sendDataToAllPlayers:data
withDataMode:GKMatchSendDataReliable error:NULL];
}
All of your methods to send data to the other players will follow this same pattern.
Your method starts by allocating the appropriate data structure (STStartGameMessage, in this case).
It sets the message field to the constant(kSTStartGameMessage) that identifies what kind of data it contains.
It then fills in the remaining values of the structure.
The last step is to transmit the finished structure to the other player. The NSData class converts the bytes of the structure into an NSData object—which is nothing more than an object that contains an array of bytes. You then send the -sendDataToAllPlayers:withDataMode:error: message to the GKMatch object. This method transmits those bytes to all other participating players. Since SunTouch is only a two-player game, the sole recipient is the opposing player’s app.
The GKMatchSendDataReliable mode tells GKMatch that it’s important that this data arrive. This might seem a silly thing to request—wouldn’t you want all data to arrive? But not all game data is important enough to worryabout whether it arrives safely or not.
Wireless communications can be spotty and unreliable. Data can get lost due to interference. If it’s not critical
that the message arrives, pass GKMatchSendDataUnreliable. This sends the data quickly, but makes no
guarantees. Thiswould be appropriate for status updates that occur continuously.
It won’t hurt (too much) if a few of them got lost; the next one will catch the game up. Messages that communicate vital information—such as a chess move—should besent using GKMatchSendDataReliable. If there’s a problem sending the message, GKMatch will try again until successful.
This adds overhead, and it may take a while before it’s delivered, but the message will get there.
You’ve implemented the code to send “game start” data to the other player.
Now write the code to receive “game start” data from the other player.
Receiving Data from a Player
When a block of data is received from the remote player, your STGame object receives a
-match:didReceiveData:fromPlayer: message. This will be the central location where you handle all received data:
- (void) match:(GKMatch*)match didReceiveData:(NSData*)data
fromPlayer:(NSString*)playerID
{
STMessage message = *((STMessage*)data.bytes); switch (message) {
case kSTStartGameMessage: {
const struct STStartGameMessage *message = data.bytes;
if (message->coinToss>coinToss)
{
STSun *otherSuns[kSunCount];
for ( NSUInteger i=0; i<kSunCount; i++ ) otherSuns[i] = [STSun sunAt:message->sun[i].x
:message->sun[i].y];
suns = [NSArray arrayWithObjects:otherSuns count:kSunCount];
}
else if (message->coinToss==coinToss)
{
coinToss = arc4random(); [self sendGameStart]; return;
}
startTime = [NSDate timeIntervalSinceReferenceDate]; multiPlayStarted();
} break;
}
}
The first step is to examine the 32-bit integer value that occupies the first four bytes of the received data block. The C syntax *((STMessage*) treats the first four bytes of the received data as an STMessage integer, and then gets that value and stores it in the message variable. Now the method knows what kind of data it just received. The rest is just a matter of handling each type.
The kSTStartGameMessage case treats (casts) the data bytes received as if it were an
STStartGameMessages structure—which it is.
When your game receives “game start” data, it compares the coinToss value chosen by the other player with the one your game engine picked in -startMultiplayerWithMatch:started:. If the opponent’s coinToss is bigger, theopponent won the coin toss. Discard the sun locations we picked and replace them with the ones the opponent picked. Now both games have the same sun locations.
In the case where your game picked the higher coinToss number, there’s nothing to do, as you already have the sun locations. There is, however, a one-in-a-billion chance that both apps picked the same value forcoinToss. If that happens, both apps pick a new random number and try again.
Once the coin toss is over and both games are using the same sun locations, the game is started.
If you’re looking for a function named multiPlayStarted, you can stop. It isn’t a function. It’s the name of the code block variable that STGameViewController passed to STGame when it originally sent the -startMultiplayerWithMatch:started: message. It looks like a C function call, but what it’s doing is executing the code block saved in the multiPlayStarted instance variable.
Executing the “game did start” code block is the last step in starting the game. The game is now running and both players are using the same list of hidden sun locations. The next thing that will happen is that one, probablyboth, of the players will touch their interface and cause a strike
to occur.
Sending Strike Data
When a player touches the game view, a strike is initiated. But that same information must be communicated to the other player, so it can animate its opponent game view. Add the send strike data method to theSTGame+STDataMessaging.m implementation file:
- (void)sendStrike:(STStrike*)strike
{
if (multiPlayerMatch==nil) return;
struct STStrikeMessage message; message.message = kSTStrikeMessage; message.location.x = strike.location.x; message.location.y = strike.location.y; message.radius = strike.radius;
NSData *data = [NSData dataWithBytes:&message length:sizeof(message)]; [multiPlayerMatch sendDataToAllPlayers:data
withDataMode:GKMatchSendDataReliable error:NULL];
}
The first statement does nothing if the multiPlayerMatch property is nil, inferring that this is a single-player game and there is no remote player to send data to. The rest of the method looks just like -sendGameStart, except thedata consists of the location of the strike and its radius.
This method must be invoked every time the local user strikes. Switch to the STGame.m implementation file, find the -strike:radius:inView: method and change the beginning so it looks like this (new code in bold):
- (void)strike:(CGPoint)viewLocation radius:(CGFloat)viewRadius inView:(STGameView*)gameView
{
STStrike* strike = [STStrike new];
strike.location = [gameView unitPointFromPoint:viewLocation]; strike.radius = [gameView unitRadiusFromRadius:viewRadius]; [self sendStrike:strike];
Now every time the game engine receives a -strike:radius:inView: message, it will report that strike to the opposing player (assuming it’s a two-player game). The -sendStrike: method is part of the STDataMessaging category. Addthis #import towards the beginning of the file so STGame.m will recognize the new method:
#import "STGame+STDataMessaging.h"
Anything you send to the other player, you have to expect to receive.
Receiving Strike Data
Return to STGame+STDataMessaging.m, locate the -match:didReceiveData:fromPlayer: method, and add a new case to the switch statement:
case kSTStrikeMessage: {
const struct STStrikeMessage *message = data.bytes; STStrike *strike = [STStrike new];
strike.location = CGPointMake(message->location.x,message->location.y); strike.radius = message->radius;
NSDictionary *strikeInfo = @{ kGameInfoStrike: strike, kGameInfoOpponent: @YES };
[[NSNotificationCenter defaultCenter] postNotificationName:kGameStrikeNotification object:self
userInfo:strikeInfo];
} break;
When a block of data identifying itself with the kSTStrikeMessage value is received, an STStrike
object is constructed from the location and radius information in the data. This is then posted as
a strike notification, with the kGameInfoOpponent property set to YES. The game views observe this notification, causing the opponent game view to animate a strike in the background view.
The game engine isn’t interested in opponent strikes; suns captured by the opponent will be communicated separately. And there’s no time like the present to write that code.
Sending Sun Capture Data
This is starting to get monotonous, but you’re almost done. Still in STGame+STDataMessaging.m, add the -sendCaptureForSunIndex: method:
- (void)sendCaptureForSunIndex:(NSUInteger)index
{
if (multiPlayerMatch==nil) return;
struct STCaptureMessage message; STSun *sun = suns[index]; message.message = kSTCaptureMessage; message.sunIndex = (uint32_t)index; message.gameTime = sun.time;
NSData *data = [NSData dataWithBytes:&message length:sizeof(message)]; [multiPlayerMatch sendDataToAllPlayers:data
withDataMode:GKMatchSendDataReliable error:NULL];
}
You’ve got this: check multiPlayerMatch, fill an STCaptureMessage structure, convert it to NSData, and send that data to the other players. Done.
When does this happen? When the -strike:radius:inView: method determines that a strike will capture a sun. Switch to STGame.m, find -strike:radius:inView:, find the if block that determines when a sun is captured, and change itso it now reads (new code in bold):
if (sunDistance<=viewRadius)
{
NSTimeInterval strikeTime = self.gameTime+kStrikeAnimationDuration/2
*(sunDistance/viewRadius); [self willCaptureSunAtIndex:i gameTime:strikeTime localPlayer:YES]; [self sendCaptureForSunIndex:i];
}
Receiving Sun Capture Data
Back in the STGame+STDataMessaging.m file, add one last case to the switch statement in the
-match:didReceiveData:fromPlayer: method:
case kSTCaptureMessage: {
const struct STCaptureMessage *message = data.bytes; [self willCaptureSunAtIndex:message->sunIndex
gameTime:message->gameTime localPlayer:NO];
} break;
This is the simplest one yet. The captured sun information from the opponent is passed to the game engine. Remember that you added an additional parameter to willCaptureSunAtIndex:gameTime:localPlayer: so the method can
distinguishbetween suns captured by the local player and sun captured by the opponent. When sun captured data is received from the remote game, you send the same message, but this time pass NO for the localPlayer.
Handling Match Disruption
All of your communications logic is finished, but there are a few additional GKMatchDelegate methods you should implement. Add them to your STGame+STDataMessaging.m file:
- (void) match:(GKMatch*)match player:(NSString*)playerID
didChangeState:(GKPlayerConnectionState)state
{
}
- (void)match:(GKMatch*)match didFailWithError:(NSError*)error
{
[[NSNotificationCenter defaultCenter] postNotificationName:kGameDidEndNotifcation object:self];
}
- (BOOL)match:(GKMatch *)match shouldReinvitePlayer:(NSString*)playerID
{
return YES;
}
As mentioned earlier, wireless communication is subject to a variable quality of service. (The technical term is “flaky.”) When iOS loses, or reestablishes, a connection with one of the other players, your delegate method willreceive a -match:player:didChangeState: message. What you do depends on the type of game. SunTouch doesn’t do anything. (It’s sad that the other player can’t capture your suns, but that’s about it.) If this was a two-playerbattling robot game, it might make sense to pause the game until the connection can be reestablished.
The more dire -match:didFailWithError: message is received when a serious networking problem prevents the game from keeping, or reestablishing, a connection with one or more players. In this situation, the link to the otherplayers is probably broken. SunTouch responds by ending the game.
Finally, the -match:shouldReinvitePlayer: method is received whenever a two-player game looses connection with the other player. If this method returns YES, the GKMatch object will automatically attempt to reestablish a connection withthe other player. If you return NO, or there are more than two players, it’s up to you to
reconnect the disconnected players in your -match:player:didChangeState: method.

No comments:
Post a Comment