Building XNA 2.0 Games- P14

Building XNA 2.0 Games- P14

Building XNA 2.0 Games- P14: I would like to acknowledge John Sedlak, who saved this book from certain doom, as well as
all of the great guys in the XNA community and Microsoft XNA team, who helped me with all
of my stupid programming questions. (That is actually the term used—“stupid programming
question”—and it is a question that one should not have to ask if one has been approached to
write a book about the subject.)

Nội dung Text: Building XNA 2.0 Games- P14

378 CHAPTER 12 ■ NETWORKING
Network Game Interaction
The following is our NetGame class, which concerns itself with message composing, sending,
receiving, and parsing:
public class NetGame
{
NetPlay netPlay;
public const byte MSG_SERVER_DATA = 0;
public const byte MSG_CLIENT_DATA = 1;
public const byte MSG_CHARACTER = 2;
public const byte MSG_PARTICLE = 3;
public const byte MSG_END = 4;
PacketWriter writer;
PacketReader reader;
float frame;
public float frameTime;
Our constructor, besides taking a reference to our overarching NetPlay class, initializes
our PacketReader and PacketWriter. We’ll be using the writer and reader to send and receive
messages, respectively.
public NetGame(NetPlay _netPlay)
{
netPlay = _netPlay;
writer = new PacketWriter();
reader = new PacketReader();
}
public void Update(Character[] c, ParticleManager pMan)
{
LocalNetworkGamer gamer will handle all of our reading and writing; gamer can send and receive
messages. The GetGamer() function is defined later. Its purpose is to find the LocalNetworkGamer
at player index 1.
LocalNetworkGamer gamer = GetGamer();
if (gamer == null)
return;
We’re updating every frame, but we don’t want to send data every frame. If you think of the
Internet as a large series of tubes, we need to send the data just fast enough so that it doesn’t
clog up on one end. If we send too much data, it will not fit in the pipe. If we send too little data
at too great a speed, it will just pile up somewhere. The goal is to get the perfect amount across
with the perfect timing, so that the players don’t notice anything whatsoever. That’s a little

CHAPTER 12 ■ NETWORKING 379
easier said than done. Since we’re just testing a basic game, we don’t need to concern ourselves
with the problem. If you plan on making this game available over the Live platform, this is a
problem you will need to tackle.
For the time being, we’ll set it up to send data every 0.05 second, or at 20 frames per
second. This is too fast for most, if not all, Live matches, but will work fine for System Link.
frame -= frameTime;
if (frame < 0f)
{
frame = .05f;
if (netPlay.hosting)
{
if (c[0] != null)
{
writer.Write(MSG_SERVER_DATA);
As the host, we’ll send data about our own character as well as every non-null character
other than index 1. The character at index 1 is controlled by the client. This is a fairly simple
client/server setup, in that the clients all report to a single server, and then the server relays
data back to all the clients. This works in most cases; however, you may find that a more peer-
to-peer setup works better.
c[0].WriteToNet(writer);
for (int i = 2; i < c.Length; i++)
if (c[i] != null)
c[i].WriteToNet(writer);
After our characters have been written, we’ll write particles, finish off with an end-message
byte, and send our data off with SendDataOptions.None, meaning we don’t care if it reaches its
destination or it arrives at its destination out of order.
pMan.NetWriteParticles(writer);
writer.Write(MSG_END);
gamer.SendData(writer, SendDataOptions.None);
}
}
if (netPlay.joined)
{
if (c[1] != null)
{
writer.Write(MSG_CLIENT_DATA);
Likewise, our client writes the character only at index 1 (himself), as well as any particles
he may have spawned (more on the particles in the “Particle Net Data” section later in this
chapter).

380 CHAPTER 12 ■ NETWORKING
c[1].WriteToNet(writer);
pMan.NetWriteParticles(writer);
writer.Write(MSG_END);
gamer.SendData(writer, SendDataOptions.None);
}
}
}
If any data has been sent to us and is ready for processing, gamer.IsDataAvailable will
be true.
if (gamer.IsDataAvailable)
{
NetworkGamer sender;
gamer.ReceiveData(reader, out sender);
if (!sender.IsLocal)
{
byte type = reader.ReadByte();
Here’s a tricky bit: it’s the host’s responsibility to send out data on all currently active
(non-null) characters. So, in order to handle character death, we’ll set a flag in all characters
to false and check it again after processing the update. Any character not updated by the
message will be presumed dead and made null.
if (netPlay.joined)
{
for (int i = 0; i < c.Length; i++)
if (i != 1)
if (c[i] != null)
c[i].receivedNetUpdate = false;
}
We enter a while loop in which we process each portion of the incoming message until we
read a MSG_END. All bit-by-bit processing is done within the classes that are updated.
bool end = false;
while (!end)
{
byte msg = reader.ReadByte();
switch (msg)
{
case MSG_END:
end = true;
break;
case MSG_CHARACTER:

CHAPTER 12 ■ NETWORKING 381
When we read a character, we’ll read off the first three fields from this method before
passing the reader to the character to finish processing the update. These three fields— defID,
team, and ID—are used to create the character if this is the first time the reader has seen the
character.
int defID = NetPacker.SbyteToInt
(reader.ReadSByte());
int team = NetPacker.SbyteToInt
(reader.ReadSByte());
int ID = NetPacker.SbyteToInt
(reader.ReadSByte());
if (c[ID] == null)
{
c[ID] = new Character(new Vector2(),
Game1.charDef[defID],
ID, team);
}
c[ID].ReadFromNet(reader);
c[ID].receivedNetUpdate = true;
break;
case MSG_PARTICLE:
byte pType = reader.ReadByte();
bool bg = reader.ReadBoolean();
This is the first time we use NetPacker, which we’ll define in the next section. As we’ve said,
essentially, NetPacker’s function is to pack and unpack big data types into small data types. Here,
we see an 8-bit signed byte (Sbyte) being turned into a 32-bit integer. This will be fine as long
as we never have any defID, team, or ID fields greater than 127. It’s easy to just use 32-bit inte-
gers for everything in our game, but when bandwidth is at a premium, we take what we can get!
For parsing particles, we first read the type and a bit to specify whether it’s a background
particle (remember that we use this field for our AddParticle() method).
switch (pType)
{
case Particle.PARTICLE_NONE:
//
break;
case Particle.PARTICLE_BLOOD:
pMan.AddParticle(new
Blood(reader), bg, true);
break;
case Particle.PARTICLE_BLOOD_DUST:
pMan.AddParticle(new
BloodDust(reader), bg, true);
break;

382 CHAPTER 12 ■ NETWORKING
case Particle.PARTICLE_BULLET:
pMan.AddParticle(new
Bullet(reader), bg, true);
break;
...
case Particle.PARTICLE_SMOKE:
pMan.AddParticle(new
Smoke(reader), bg, true);
break;
default:
//Error!
break;
}
break;
}
}
We’re being a bit sneaky here: particles are sent only when they are created. All particles
that aren’t owned by the client are created and sent by the host, while all particles that are
owned by the client (for example, bullets that the client spawns) are sent from the client to the
server. At the same time, it’s important for the server to abort any client-owned particles that
the game might try to spawn outside a network read. Likewise, the client must abort all particle
spawns that it does not own unless they come through the network.
The client will iterate through its characters again to see if any have not been updated in
the last update, killing off those that have not been updated.
if (netPlay.joined)
{
for (int i = 0; i < c.Length; i++)
if (i != 1)
if (c[i] != null)
if (c[i].receivedNetUpdate == false)
{
c[i] = null;
}
}
}
}
}
Finally, here’s our GetGamer() method. It uses a bit of trickery to figure out which
LocalNetworkGamer is at player index 1.
private LocalNetworkGamer GetGamer()
{
foreach (LocalNetworkGamer gamer in
netPlay.netSession.LocalGamers)

386 CHAPTER 12 ■ NETWORKING
writer.Write(keyRight);
writer.Write(keyLeft);
writer.Write(NetPacker.IntToShort(HP));
}
Take a look at how ReadFromNet() differs from WriteToNet():
public void ReadFromNet(PacketReader reader)
{
loc.X = NetPacker.ShortToBigFloat(reader.ReadInt16());
loc.Y = NetPacker.ShortToBigFloat(reader.ReadInt16());
anim = NetPacker.ShortToInt(reader.ReadInt16());
animFrame = NetPacker.ShortToInt(reader.ReadInt16());
animName = charDef.GetAnimation(anim).name;
frame = NetPacker.ShortToMidFloat(reader.ReadInt16());
state = NetPacker.SbyteToInt(reader.ReadSByte());
face = NetPacker.SbyteToInt(reader.ReadSByte());
trajectory.X = NetPacker.ShortToBigFloat(reader.ReadInt16());
trajectory.Y = NetPacker.ShortToBigFloat(reader.ReadInt16());
keyRight = reader.ReadBoolean();
keyLeft = reader.ReadBoolean();
HP = NetPacker.ShortToInt(reader.ReadInt16());
receivedNetUpdate = true;
}
We’re starting by reading the location data, because from NetGame, we already read the first
four items. First, we read the message type, before our switch block, and then the next three for
use in a case where we needed to spawn a new character.
There’s a bit of noticeable waste here. Fields like defID, team, and ID don’t change every
frame, if ever. If we wanted to optimize more, we would include these as a separate message.
This could get a bit hairy though. We would need to flag new characters to make sure we send
out this data, we would need to account for special cases where packets arrived out of order
and the recipient received the character location data before the character ID data, and so on
and so forth.
Particle Net Data
Getting our particles in shape is a much uglier task. We broke down our strategy for dealing
with particles in a multiplayer setting a few pages earlier, but let’s lay it down again in a series
of scenarios:

CHAPTER 12 ■ NETWORKING 387
Client adds particle that client owns: This happens when the client fires bullets, swings his
wrench, or creates any other particle where owner = 1. The client spawns the particle and
flags it for a network send. At the next network write, the client sends the particle and unchecks
its flag, signifying that it no longer needs to be sent. The server receives and spawns the
particle.
Client adds particle that client does not own: This happens when the client’s game tries
to spawn explosions, blood, and so on its own. For instance, if a bullet hits a zombie, the
game will try to spawn blood. However, if the server doesn’t think the bullet hit the zombie,
we don’t want blood being spawned on the client and not on the server. The server is final
arbiter for particles that the client does not own. The client does not spawn the particle.
Hopefully, at the next network update, the client will receive the particle data that it tried
to spawn. This time, because the data is from a network source, the client will create the
particle.
Server adds particle that client owns: This happens when a client tries to create a particle,
like firing a bullet, on the server machine. Because we’re constantly updating all characters
on both machines, and because the FireTrig() call in the character is called from the
update, a client updated on the host will attempt to fire bullets if in the right animation.
However, if there’s a bit of a network hiccup, the server could end up seeing the client skip
over the fire frame or hit it twice, so we want to make sure we spawn bullet particles only
when the client sends them. In this case, the server does not spawn the particle. Again,
hopefully at the next network update, the server will receive the particle data from the
client and create it.
Server adds a particle that client does not own: This happens when the server spawns
anything that is not owned by the client. The server spawns the particle and flags it for
a network send. At the next network write, the server sends the particle and unchecks its
flag, signifying that it no longer needs to be sent. The client receives and spawns the
particle.
The big omission in this is that particle data is sent only at creation and is not updated. We
figured we could get away with this for now—we don’t have any particles change trajectory
mid-flight. If we included homing rockets, collectable items, or anything else that lingered for
longer than a second, we would definitely need to implement some sort of particle-updating
messaging functionality.
To allow particles to be sent and received, we’ll need particle-specific code in every particle
class. We’ll put a virtual NetWrite() method in the base Particle class, which will be over-
loaded from each class that extends Particle, and as you may have noticed from the NetGame
code, we’ll be making a new constructor for every type of particle that will accept a PacketReader.
We’ll also define some constant values for our particle types. We use these from NetGame as
well. Let’s start in Particle.
public const byte PARTICLE_NONE = 0;
public const byte PARTICLE_BLOOD = 1;
public const byte PARTICLE_BLOOD_DUST = 2;
public const byte PARTICLE_BULLET = 3;
public const byte PARTICLE_FIRE = 4;

390 CHAPTER 12 ■ NETWORKING
this.traj = new Vector2(80f, -30f);
this.size = Rand.getRandomFloat(6f, 8f);
this.flag = Rand.getRandomInt(0, 4);
this.owner = -1;
this.exists = true;
this.frame = (float)Math.PI * 2f;
this.additive = true;
this.rotation = Rand.getRandomFloat(0f, 6.28f);
}
public override void NetWrite(PacketWriter writer)
{
writer.Write(NetGame.MSG_PARTICLE);
writer.Write(Particle.PARTICLE_FOG);
writer.Write(background);
writer.Write(NetPacker.BigFloatToShort(loc.X));
writer.Write(NetPacker.BigFloatToShort(loc.Y));
}
All Fog really needed was location data.
Because all of the particles have different constructors and need to be constructed with
different data, we (groan) must add this overloaded constructor/overloaded NetWrite() combo
for every last particle. What’s more, one misstep along the way will mess up everything. If
we try to read the wrong amount of bits, every subsequent read will have an incorrect offset,
leading to weird performance (most likely in the form of crashes). When we implemented this,
we started with just Fire, then tried to implement another one, caused a crash, fixed the crash,
and moved on. One suggestion to change this would be to keep track of how many bits we have
read in and at what offset the new particle needs to be. This way, we could fix reading errors as
they happen. However, because of time issues, we will just get down and dirty while hoping we
haven’t made a mistake.
We need to update ParticleManager. First off, we add an overload to AddParticle() to
allow us to add a particle specified as sent through the network.
public void AddParticle(Particle newParticle, bool background)
{
AddParticle(newParticle, background, false);
}
public void AddParticle(Particle newParticle, bool background,
bool netSent)
{
for (int i = 0; i < particle.Length; i++)
{
if (particle[i] == null)
{
particle[i] = newParticle;
particle[i].background = background;

CHAPTER 12 ■ NETWORKING 391
Here’s where we handle the scenarios laid out a few pages ago. It looks much shorter in code!
if (!netSent)
{
if (Game1.netPlay.joined)
{
if (particle[i].GetOwner() == 1)
particle[i].netSend = true;
else
particle[i] = null;
}
else if (Game1.netPlay.hosting)
{
if (particle[i].GetOwner() != 1)
particle[i].netSend = true;
else
particle[i] = null;
}
}
break;
}
}
}
We’ll send off any particles flagged for a send in NetWriteParticles(), and then unflag them.
public void NetWriteParticles(PacketWriter writer)
{
for (int i = 0; i < particle.Length; i++)
if (particle[i] != null)
{
if (particle[i].netSend)
{
particle[i].NetWrite(writer);
particle[i].netSend = false;
}
}
}
We’ll need to round up a few more odds and ends before everything is ready for prime
time. We need to add player 2’s health to the HUD. We need to give player 2 a different skin so
that we don’t end up with two clones running around together. We should turn off bucket
monster spawning from the client side. Lastly, we need to plug everything into Game1.

CHAPTER 12 ■ NETWORKING 393
Rectangle(i * 32 + (int)(32f * (1f - ta)),
192, (int)(32f * ta), 32)
),
new Color(new Vector4(1f, 0f, 0f, .75f)),
r, new Vector2(16f
- (p == 1 ? 32f * (1f - ta) : 0f), 16f), 1.25f,
SpriteEffects.None, 1f);
}
ta = prog - (float)i;
if (ta > 1f) ta = 1f;
if (ta > 0f)
...
}
The two big conditionals involve the source rectangle and the center vector. The first
conditional chooses between the rectangle we were using originally (for player 1), in which the
width scales as the heart changes sizes, and a rectangle for player 2, in which the x coordinate
shifts and the width scales.
The second conditional is required for player 2’s heart. This causes the center to shift as
well. While changing the width of the hearts on player 1’s health bar involved changing only the
source rectangle width, doing this for player 2 involves changing the source rectangle width
and x coordinate, as well as the x coordinate of the center vector.
Giving the Second Player a Skin
We need to get a new skin for player 2. We’ll call him Esteban. Esteban is a well-seasoned
zombie smasher. He wears a hoodie and looks slightly emo. We made some new images: head,
torso, and legs. He can use a wrench and revolver as well for now. The new images are shown
in Figure 12-6.
Figure 12-6. Player 2 images

394 CHAPTER 12 ■ NETWORKING
We’ll add these files to our Content project and load them from Game1 as normal. We called
them head4.png, torso4.png, and legs3.png.
In Character.Draw(), we’ll just hard-code the skin change. Esteban is exactly the same
character as Guy, only with a skin swap, so the change can really be this superficial:
if (ID == 1 && Game1.players == 2)
{
switch (t)
{
case 0:
texture = headTex[3];
break;
case 1:
texture = torsoTex[3];
break;
case 2:
texture = legsTex[2];
break;
}
}
Plugging Everything into the Game
To turn off bucket monster spawning, we just add the following to the beginning of
Bucket.Update().
if (Game1.netPlay.joined)
return;
Lastly, we’ll make the necessary changes in Game1. We need to update the scroll value to
follow the correct player (host follows Guy; client follows Esteban) and make sure our input
is sent to the correct player (host to index 0; client to index 1). We’ll make some changes in
UpdateGame():
int idx = 0;
if (netPlay.joined)
idx = 1;
if (character[idx] != null)
{
scroll += ((character[idx].loc -
new Vector2(400f, 400f)) - scroll) * frameTime * 20f;
}
...

CHAPTER 12 ■ NETWORKING 395
if (map.transOutFrame 0f)
{
slowTime -= frameTime;
frameTime /= 10f;
}
netPlay.Update(character, pManager);
Once everything is plugged in, we should be ready to roll. You will not be blown away
by performance, and we aren’t set up to handle some odd situations, but it’s a fantastic start for
30 pages of networking crash course. Guy and Esteban battle it out with some monsters in
Figure 12-7. (OK, they’re not really battling it out, but you get the idea.)

396 CHAPTER 12 ■ NETWORKING
Figure 12-7. Network play in action
Conclusion
We’ve implemented a fairly rough, if functional, networking engine for Zombie Smashers XNA.
It doesn’t have any prediction, smoothing, or optimal sending strategy, but it’s as good a place
to start as any.
To recap, we added hosting functionality to our menu system. We implemented a network
connection management class to allow us to create, find, and join sessions and maintain a
current session. And we implemented a network game management class to facilitate message
sending and receipt over our character and particle classes.
There are a number of ways to improve on our networking system. We’ll leave you with
a few ideas:
Implement more efficient sending: You can check NetworkGamer.RoundTripTime to get a
rough estimate of how often you can send data before choking up the system.
Send less: We use a lot of cosmetic particles, like fire and smoke from the torch and map
fog. If you spawn these from the map with netSent = true, they’ll be spawned indepen-
dently on the client and server side with no sending in between. You’ll save on
transmitting a bit of data, without losing any information.

CHAPTER 12 ■ NETWORKING 397
Smooth movement: Here’s a pretty sneaky strategy that was implemented in The Dish-
washer game: keep two separate vectors for character location—one for the true location
and one that sort of plays catch-up with the true location. As you get updates from the
other side of the network, you’ll immediately update the true location (which you’ll use for
all game logic), but the draw location will set its location to a weighted average of the
previous draw location and the new true location. This will make performance look a bit
smoother.
Predict movement: If your character is moving left with a trajectory of –100, and packets
take 100 milliseconds to arrive, you can assume that by the time you get the packet, the
character has moved –100 0.1 on the remote machine.
All of these items involve varying degrees of complexity (heck, you could write a thesis on
prediction algorithms), but you have a good start, so it should be fairly easy to experiment in
network land.
A Parting Word
Well, here we are at the end of the last chapter in the book.
We’ve built a fairly robust infrastructure for what could be an amazing game. We have a
neat character format and editor that give us the means to create beautifully animated characters
with a high level of interactivity. We have a versatile map format that allows us to create rich
maps with parallax scrolling, collision data, and a simple yet extremely functional scripting
language.
We implemented side-scrolling game play (with collision detection); a particle engine and
numerous cool-looking particle effects; sound effects and music; snazzy, next-gen postprocessing
(including heat haze!); and rudimentary networking.
Now you’re on your own.
You have a great start. You can work with Zombie Smashers XNA, adding new skins,
monsters, maps, and so on. This is a good place to begin to get a feel for the techniques and
styles we’ve used.
Once you have a handle on the capabilities and strengths of the project’s codebase (which,
if you really pored over the text, could be as early as now!), you can get to work on a new game
using what we’ve covered here—as long as it’s 2D. Nowadays, when the market is awash with
AAA first-person shooters developed by teams of hundreds, it’s easy to think of 2D as restricted.
You’ll just have to remind yourself that the video game industry once thrived on a flat plane.
What major obstacles will we have to clear to pay homage to great design sans the third
dimension? Here are some ideas:
2D fighter (Street Fighter 2): This would be the easiest implementation. You would just
need to create new attack animations and tie them to different buttons—attack and
second would now be jab, strong, fierce, short, front, and roundhouse (assuming Street
Fighter 2 attack names).