Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 71 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
71
Dung lượng
383,97 KB
Nội dung
356 if( result < count ) // turn off the mutex and throw an error - could not send all data. if( result == SOCKET_ERROR ) // turn off the mutex and throw an error - sendto() failed. #if defined( _DEBUG_DROPTEST ) } #endif if( pStr ) pHost->GetOutQueue().AddPacket( pStr, length ); cMonitor::MutexOff(); } Since I've covered most of this before, there are only four new and interesting things. The first is _DEBUG_DROPTEST. This function will cause a random packet to not be sent, which is equivalent to playing on a really bad network. If your game can still play on a LAN with a _DEBUG_DROPTEST as high as four, then you have done a really good job, because that's more than you would ever see in a real game. The second new thing is sendto(). I think any logically minded person can look at the bind() code, look at the clearly named variables, and understand how sendto() works. It may surprise you to see that the mutex is held for so long, directly contradicting what I said earlier. As you can see, pHost is still being used on the next-to-last line of the program, so the mutex has to be held in case the other thread calls MTUDP::HostDestroy(). Of course, the only reason it has to be held so long is because of HostDestroy(). The third new thing is MTUDPMSGTYPE_RELIABLE. I'll get to that a little later. The last and most important new item is cHost::GetOutQueue(). Just like its counterpart, GetOutQueue provides access to an instance of cQueueOut, which is remarkably similar (but not identical) to cQueueIn. class cQueueOut : public cMonitor { protected: list<cDataPacket *> d_packets; 357 DWORD d_currentPacketID, d_count; // number of packets added to this queue. public: cQueueOut(); virtual ~cQueueOut(); void Clear(); void AddPacket( const char * const pData, unsigned short len ); void RemovePacket( DWORD packetID ); bool GetPacketForResend( DWORD waitTime, cDataPacket *pPacket ); bool GetPreviousPacket( DWORD packetID, cDataPacket *pPacket ); cDataPacket *BorrowPacket( DWORD packetID ); void ReturnPacket(); DWORD GetLowestID(); bool IsEmpty(); inline DWORD GetCurrentID(); // returns d_currentPacketID. inline DWORD GetCount(); // returns d_count. }; There are several crucial differences between cQueueIn and cQueueOut: d_currentPacketID is the ID of the last packet sent/added to the queue; GetLowestID() returns the ID of the first packet in the list (which, incidentally, would also be the packet that has been in the list the longest); AddPacket() just adds a packet to the far end of the list and assigns it the next d_currentPacketID; and RemovePacket() removes the packet with d_id == packetID. The four new functions are GetPacketForResend(), GetPrevious-Packet(), BorrowPacket(), and ReturnPacket(), of which the first two require a brief overview and the last two require a big warning. GetPacketForResend() checks if there are any packets that were last sent more than waitTime milliseconds ago. If there are, it copies that packet to pPacket and updates the original packet's d_lastTime. This way, if you know the ping to some other computer, then you know how long to wait before you can assume the packet was dropped. GetPreviousPacket() is far simpler; it returns the packet that was sent just before the packet with d_id == packetID. This is used by ReliableSendTo() to 358 "piggyback" an old packet with a new one in the hopes that it will reduce the number of resends caused by packet drops. BorrowPacket() and ReturnPacket() are evil incarnate. I say this because they really, really bend the unwritten mutex rule: Lock and release a mutex in the same function. I know I should have gotten rid of them, but when you see how they are used in the code (later), I hope you'll agree it was the most straightforward implementation. I put it to you as a challenge to remove them. Nevermore shall I mention the functions-that-cannot-be-named(). Now, about that MTUDPMSGTYPE_RELIABLE: The longer I think about MTUDPMSGTYPE_RELIABLE, the more I think I should have given an edited version of ReliableSendTo() and then gone back and introduced it later. But then a little voice says, "Hey! That's why they put ADVANCED on the cover!" The point of MTUDPMSGTYPE_RELIABLE is that it is an identifier that would be read by ProcessIncomingData(). When Process-IncomingData() sees MTUDPMSGTYPE_RELIABLE, it would call pHost->ProcessIncomingReliable(). The benefit of doing things this way is that it means I can send other stuff in the same message and piggyback it just like I did with the old messages and GetPreviousPacket(). In fact, I could send a message that had all kinds of data and no MTUDPMSGTYPE_RELIABLE (madness! utter madness!). Of course, in order to be able to process these different message types I'd better make some improvements, the first of which is to define all the different types. enum eMTUDPMsgType { MTUDPMSGTYPE_ACKS = 0, MTUDPMSGTYPE_RELIABLE = 1, MTUDPMSGTYPE_UNRELIABLE = 2, MTUDPMSGTYPE_CLOCK = 3, MTUDPMSGTYPE_NUMMESSAGES = 4, }; I defined this enum in MTUDP.cpp because it's a completely internal matter that no other class should be messing with. Although you're not going to work with most of these types (just yet) here's a brief overview of what they're for: MTUDPMSGTYPE_CLOCK is for a really cool clock I'm going to add later. "I'm sorry, did you say cool?" Well, okay, it's not cool in a Pulp Fiction/Fight Club kind of cool, but it is pretty neat when you consider that the clock will read almost exactly the same value on all clients and the server. This is a critical feature of real-time games because it makes sure that you can say "this thing happened at this time" and everyone can correctly duplicate the effect. 359 MTUDPMSGTYPE_UNRELIABLE is an unreliable message. When a computer sends an unreliable message it doesn't expect any kind of confirmation because it isn't very concerned if the message doesn't reach the intended destination. A good example of this would be the update messages in a game—if you're sending 20 messages a second, a packet drop here and a packet drop there is no reason to have a nervous breakdown. That's part of the reason we made _DEBUG_DROPTEST in the first place! MTUDPMSGTYPE_ACKS is vital to reliable message transmission. If my computer sends a reliable message to your computer, I need to get a message back saying "yes, I got that message!" If I don't get that message, then I have to resend it after a certain amount of time (hence GetPacketForResend()). Now, before I start implementing the stuff associated with eMTUDPMsgType, let me go back and improve MTUDP::ProcessIncomingData(). assert( pHost != NULL ); // Process the header for this packet. bool bMessageArrived; unsigned char code; char *ptr; bMessageArrived = false; ptr = pData; while( ptr < pData + length ) { code = *ptr; ptr++; switch( code ) { case MTUDPMSGTYPE_ACKS: // Process any ACKs in the packet. ptr += pHost->ProcessIncomingACKs( ptr, pData + length - ptr, 360 receiveTime ); break; case MTUDPMSGTYPE_RELIABLE: bMessageArrived = true; // Process reliable message in the packet. ptr += pHost->ProcessIncomingReliable( ptr, pData + length - ptr, receiveTime ); break; case MTUDPMSGTYPE_UNRELIABLE: // Process UNreliable message in the packet. ptr += pHost->ProcessIncomingUnreliable( ptr, pData + length - ptr, receiveTime ); break; case MTUDPMSGTYPE_CLOCK: ptr += ProcessIncomingClockData( ptr, pData + length - ptr, pHost, receiveTime ); break; default: // turn mutex off, throw an error. something VERY BAD has happened, // probably a write to bad memory (such as to an uninitialized // pointer). break; } } cMonitor::MutexOff(); 361 if( bMessageArrived == true ) { // Send an ACK immediately. If this machine is the // server, also send a timestamp of the server clock. ReliableSendTo( NULL, 0, pHost->GetAddress() ); } } So ProcessIncomingData() reads in the message type then sends the remaining data off to be processed. It repeats this until there's no data left to be processed. At the end, if a new message arrived, it calls Reliable-SendTo() again. Why? Because I'm going to make more improvements to it! // some code we've seen before memset( outBuffer, 0, MAX_UDPBUFFERSIZE ); // Attach the ACKs. if( pHost->GetInQueue().GetCount() != 0 ) { // Flag indicating this block is a set of ACKs. outBuffer[ count ] = MTUDPMSGTYPE_ACKS; count++; count += pHost->AddACKMessage( &outBuffer[ count ], MAX_UDPBUFFERSIZE ); } count += AddClockData( &outBuffer[ count ], MAX_UDPBUFFERSIZE - count, pHost ); // some code we've seen before. So now it is sending clock data, ACK messages, and as many as two reliable packets in every message sent out. Unfortunately, there are now a number of outstanding issues: ProcessIncomingUnreliable() is all well and good, but how do you send unreliable data? 362 How do cHost::AddACKMessage() and cHost::ProcessingIncoming-ACKs() work? Ok, so I ACK the messages. But you said I should only resend packets if I haven't received an ACK within a few milliseconds of the ping to that computer. So how do I calculate ping? How do AddClockData() and ProcessIncomingClockData() work? Unfortunately, most of those questions have answers that overlap, so I apologize in advance if things get a little confusing. Remember how I said there were four more classes to be defined? The class cQueueOut was one and here come two more. cUnreliableQueueIn class cUnreliableQueueIn : public cMonitor { list<cDataPacket *> d_packets; DWORD d_currentPacketID; public: cUnreliableQueueIn(); virtual ~cUnreliableQueueIn(); void Clear(); void AddPacket( DWORD packetID, const char * const pData, unsigned short len, DWORD receiveTime ); cDataPacket *GetPacket(); }; cUnreliableQueueOut class cUnreliableQueueOut : public cMonitor { list<cDataPacket *> d_packets; DWORD d_currentPacketID; unsigned char d_maxPackets, d_numPackets; 363 public: cUnreliableQueueOut(); virtual ~cUnreliableQueueOut(); void Clear(); void AddPacket( const char * const pData, unsigned short len ); bool GetPreviousPacket( DWORD packetID, cDataPacket *pPacket ); void SetMaxPackets( unsigned char maxPackets ); inline DWORD GetCurrentID(); // returns d_currentPacketID. }; They certainly share a lot of traits with their reliable counterparts. The two differences are that I don't want to hang on to a huge number of outgoing packets, and I only have to sort incoming packets into one list. In fact, my unreliable packet sorting is really lazy—if the packets don't arrive in the right order, the packet with the lower ID gets deleted. As you can see, cQueueOut has a function called SetMaxPackets() so you can control how many packets are queued. Frankly, you'd only ever set it to 0, 1, or 2. Now that that's been explained, let's look at MTUDP::Unreliable-SendTo(). UnreliableSendTo() is almost identical to ReliableSendTo(). The only two differences are that unreliable queues are used instead of the reliable ones and the previous packet (if any) is put into the outBuffer first, followed by the new packet. This is done so that if packet N is dropped, when packet N arrives with packet N+1, my lazy packet queuing won't destroy packet N. cHost::AddACKMessage()/cHost::ProcessIncomingACKs() Aside from these two functions, there's a few other things that have to be added to cHost with regard to ACKs. #define ACK_MAXPERMSG 256 #define ACK_BUFFERLENGTH 48 class cHost : public cMonitor { protected: // A buffer of the latest ACK message for this host 364 char d_ackBuffer[ ACK_BUFFERLENGTH ]; unsigned short d_ackLength; // amount of the buffer actually used. void ACKPacket( DWORD packetID, DWORD receiveTime ); public: unsigned short ProcessIncomingACKs( char * const pBuffer, unsigned short len, DWORD receiveTime ); unsigned short AddACKMessage( char * const pBuffer, unsigned short maxLen ); } The idea here is that I'll probably be sending more ACKs than receiving packets, so it only makes sense to save time by generating the ACK message when required and then using a cut and paste. In fact, that's what AddACKMessage() does—it copies d_ackLength bytes of d_ackBuffer into pBuffer. The actual ACK message is generated at the end of cHost::Process-IncomingReliable(). Now you'll finally learn what cQueueIn::d_count, cQueueIn::GetHighestID(), cQueueIn::GetCurrentID(), and cQueueIn:: UnorderedPacketIsQueued() are for. // some code we've seen before. d_inQueue.AddPacket( packetID, (char *)readPtr, length, receiveTime ); readPtr += length; // Should we build an ACK message? if( d_inQueue.GetCount() == 0 ) return ( readPtr - pBuffer ); // Build the new ACK message. DWORD lowest, highest, ackID; unsigned char mask, *ptr; lowest = d_inQueue.GetCurrentID(); 365 highest = d_inQueue.GetHighestID(); // Cap the highest so as not to overflow the ACK buffer // (or spend too much time building ACK messages). if( highest > lowest + ACK_MAXPERMSG ) highest = lowest + ACK_MAXPERMSG; ptr = (unsigned char *)d_ackBuffer; // Send the base packet ID, which is the // ID of the last ordered packet received. memcpy( ptr, &lowest, sizeof( DWORD ) ); ptr += sizeof( DWORD ); // Add the number of additional ACKs. *ptr = highest - lowest; ptr++; ackID = lowest; mask = 0x80; while( ackID < highest ) { if( mask == 0 ) { mask = 0x80; ptr++; } // Is there a packet with id 'i' ? if( d_inQueue.UnorderedPacketIsQueued( ackID ) == true ) *ptr |= mask; // There is else *ptr &= ~mask; // There isn't [...]... and so forth See the SDK documentation for further information on particular states Table 8.1: Direct3D render states D3DRS_ZENABLE Depth buffering state defined with a member of the D3DZBUFFERTYPE enumeration D3DRS_FILLMODE The fill mode; specified with the D3DFILLMODE enumeration D3DRS_SHADEMODE The shade mode; specified with the D3DSHADEMODE enumeration D3DRS_LINEPATTERN A D3DLINEPATTERN structure... types of lights I discussed are the same ones Direct3D supports In order to grasp the practical concepts of Direct3D, I needed to first show you the essentials of 3D programming With that in your back pocket you can start exploring the concepts that drive Direct3D programming The Direct3D9 Object The Direct3D object is the way you can talk to the 3D capabilities of the video card, asking it what kinds... acceleration, etc.), or requesting interfaces to a particular type of device To get a IDirect3D9 pointer, all you need to do is call Direct3D-Create9() I covered this back in Chapter 2 The Direct3DDevice9 Object All of the real work in Direct3D is pushed through the Direct3D device In earlier versions of Direct3D, the D3DDevice interface was actually implemented by the same object that implemented IDirectDrawSurface... receiveTime ) 366 { cDataPacket *pPacket; pPacket = d_outQueue.BorrowPacket( packetID ); if( pPacket == NULL ) return; // the mutex was not locked DWORD time; time = receiveTime - pPacket->d_firstTime; d_outQueue.ReturnPacket(); unsigned int i; if( pPacket->d_timesSent == 1 ) { for(i=0;i< PING_RECORDLENGTH - 1; i++ ) d_pingLink[i]= d_pingLink[i+1]; d_pingLink[i]= time; } for(i=0;i< PING_RECORDLENGTH - 1; i++... GetLastClockTime(); // self-explanatory void SetLastClockTime( DWORD time ); 373 // self-explanatory inline bool WasClockTimeSet(); // returns d_bClockTimeSet } And that appears to be that In just about 35 pages I've shown you how to set up all the harder parts of network game programming In the next section I'll show you how to use the MTUDP class to achieve first-rate, super-smooth game play Implementation... frame rate Because of this, Direct3D has built in several different types of devices to do rendering Hardware The HAL (or hardware abstraction layer) is a device-specific interface, provided by the device manufacturer, that Direct3D uses to work directly with the display hardware Applications never interact with the HAL With the infrastructure that the HAL provides, Direct3D exposes a consistent set of... to create a HAL device, you call IDirect3D9::CreateDevice with D3DDEVTYPE_HAL as the second parameter This step will be discussed in the "Direct3D Initialization" section later in this chapter Software A software device is a pluggable software device that has been registered with IDirect3D9::RegisterSoftwareDevice Ramp (and Other Legacy Devices) Older books on D3D discuss other device types, specifically... to D3D There are two major interfaces that are all-important in Direct3D: the Direct3D object and the Direct3D device You came across both of these peripherally in Chapter 2 The Direct3D object is communicated with through the IDirect3D9 interface It handles creation of the Direct3D device, enumeration of devices and z-buffer formats, and eviction of managed textures You essentially create it during... support for hardware rasterization This is a far cry from the way games used to be written, with developers pouring months of work into hand-optimized texture mapping routines and geometry engines, and supporting each 3D accelerator individually Aside If you've ever played the old game Forsaken, you know what the old way was like— the game had a separate executable for each hardware accelerator that... supported in Direct3D 9.0 If you wish to access them, you must use a previous version of the Direct3D interfaces (5.0, for example) The MMX device was a different type of software accelerator that was specifically optimized for MMX machines MMX (and Katmai/3DNow) support is now intrinsically supported in the software device The Ramp device was used for drawing 3D graphics in 2 5 6- color displays In this . up all the harder parts of network game programming. In the next section I'll show you how to use the MTUDP class to achieve first-rate, super-smooth game play. Implementation 2: Smooth. implementation. I put it to you as a challenge to remove them. Nevermore shall I mention the functions-that-cannot-be-named(). Now, about that MTUDPMSGTYPE_RELIABLE: The longer I think about MTUDPMSGTYPE_RELIABLE,. solution for any one game probably wouldn't work for another game. Geographic and Temporal Independence Although in this book I am going to write a real-time, networked game, it is important