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
Trang 1Network 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();
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
Trang 2easier 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
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
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)
Trang 3c[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:
Trang 4When 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
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)
Trang 5We’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
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)
Trang 6Now we get to NetPacker, whose function is to turn big data types into small data types and vice
versa It works fine as long as the data we’re looking at does not go beyond the bounds of the
smaller data types
Take a look at the first function, TinyFloatToByte() and its counterpart, ByteToTinyFloat():
Trang 7We’re also handling small floats, medium (mid) floats, and big floats Because the range of
a short is –32767 and 32767, our value conversion ranges are as shown in Table 12-1
If we keep using the best conversions (we’ll have to play it by ear), we’ll maximize width efficiency and minimize precision loss
public static short BigFloatToShort(float f)
Table 12-1. NetPacker Conversion Ranges
Big float –32767 32767
Mid float –6553 6553
Small float –1638 1638
Tiny float 0 1
Trang 8public static float ShortToMidFloat(short s)
Character Net Data
Let’s move on to the write and read functions for Character We’ll be sending references to a
packet reader and writer for ReadFromNet() and WriteToNet(), respectively Here’s WriteToNet():
public void WriteToNet(PacketWriter writer)
Trang 9writer.Write(keyRight);
writer.Write(keyLeft);
writer.Write(NetPacker.IntToShort(HP));
}
Take a look at how ReadFromNet() differs from WriteToNet():
public void ReadFromNet(PacketReader reader)
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:
Trang 10Client 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;
Trang 11public const byte PARTICLE_FOG = 5;
public const byte PARTICLE_HEAT = 6;
public const byte PARTICLE_HIT = 7;
public const byte PARTICLE_MUZZLEFLASH = 8;
public const byte PARTICLE_ROCKET = 9;
public const byte PARTICLE_SHOCKWAVE = 10;
public const byte PARTICLE_SMOKE = 11;
a PacketReader which, when read, will reveal all of these values
public Blood(Vector2 loc,
Trang 12As with our characters, we do a bit of extra writing here We need to specify the message
type, particle type, and background bit When we did our reading in the constructor, we just
started at the location because the previous three items are read in NetGame
public override void NetWrite(PacketWriter writer)
Let’s take a look at another one:
public Fog(PacketReader reader)
Trang 13this.traj = new Vector2(80f, -30f);
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)
Trang 14Here’s where we handle the scenarios laid out a few pages ago It looks much shorter in code!
We’ll send off any particles flagged for a send in NetWriteParticles(), and then unflag them
public void NetWriteParticles(PacketWriter writer)
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
Trang 15Adding the Second Player to the HUD
In HUD.Draw(), we modify our heart-drawing algorithm a little to turn it into a loop that allows for two players Remember our floating HP value that we used for a smoothly adjusting health bar? We had only one We need two All we need to do is declare it as a float array of size 2 and change all affected code (Update() would be a good place to start)
Our heart-drawing algorithm in HUD.Draw() is modified like this:
for (int p = 0; p < Game1.players; p++)
{
float fProg = fHP[p] / (float)character[p].MHP;
float prog = (float)character[p].HP / (float)character[p].MHP;
Here’s a tremendously ugly draw call:
sprite.Draw(spritesTex, new Vector2(t, 66f),
Trang 16Rectangle(i * 32 + (int)(32f * (1f - ta)),
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