Tuesday, 6 May 2014

Creating a Data Messaging Category [Networking, The Nerdy Kind]

You’re going to consolidate all of your remote communications logic in a category of STGame named STDataMessaging. Select the STGame.m file in the project navigator and choose the New File . . command, either from theFile menu or by right+clicking on the STGame.m file.

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 sengame information to the other player:
-sendGameStart, -sendStrike:, and -sendCaptureForSunIndex:. The category also implements the CGMatchDelegate methods to receive data from thother player. In thnew STGame+STDataMessaging.h interface file, edit the categorydeclaration sit 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 whats 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 its 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 thats 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—aleast I hopit can’t.
Giving the address of aobject 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 sequencof bytes that can be useto assemble equivalenobjects bthe recipient. Lets say you had a Person object that had name (string) and age (integer)properties. You canpass the address of the Person, or string, object to another process. Instead, you serialize the object bcreating an arraof bytes and filling those bytes with the characters of the persons 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 persons 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 typ(just like int and char) that defines an integer thats always 32 bits
(4 bytes) long. It doesn’tmatter on whakind of computer system you compile this on, or what kind of CPU its running, an int32_t variable will always be 32 bits long.
The final problem ibyte order. Different CPU architectures choose to storthe bytes of a single integer idifferent orders. CPUs that store thleast significant bits of thinteger in thfirst (lowest) byte of memory are called little-endian machines. If thfirst byte contains the most significant bits of the integer, its a big-endian machine. If you transmit the value of an integer, leassignificant byte first, to a systethat 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 its 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 (STStartGameMessageSTStrikeMessage, 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 compilers structure alignment rules change in the future—all versions ofSunTouch will still be able to communicate with each other.

Thats althe declarations you need. Now you can write the methods that send and receive datfrom 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 players app.

The GKMatchSendDataReliable  mode tells GKMatch that its important that this data arrive. This might seem a silly thing to request—wouldnt 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 its nocritical
that the message arrives, pass GKMatchSendDataUnreliable. This sends the data quickly, bumakes no 
guarantees. Thiswould be appropriate for status updates that occur continuously.
It wont hurt (too much) if a few of them  got lost; the next one will catch the game up. Messages that communicatvitainformation—such as a chess move—should besent using GKMatchSendDataReliable. If theres problem sending the message, GKMatch will try again until successful.
This adds overhead, and it matake a while beforits 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 datis received from the remote player, your STGame object receives a
-match:didReceiveData:fromPlayer: message. This will be the central location where you handle alreceived 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 opponents 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, theres nothing to do, as yoalready have the sun locations. There is, however, a one-in-a-billion chance that both apppicked 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 isnt a function. Its 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 its 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 its 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 isnt interested in opponent strikes; suns captured by the opponent will be communicated separately. And theres 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 doesnt do anything. (Its sad that the other player cant capture your suns, but thats 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 thimethod returns YES, the GKMatch object wilautomatically attempto reestablish a connection withthe other player. If you return NO, or there are more than two players, itup to you to
reconnect the disconnected players in your -match:player:didChangeState: method.

No comments:

Post a Comment