Simple network scheme question

Started by
5 comments, last by xynapse 14 years ago
As i am coding something 'fast' for the first time, i better ask you guys for an opinion before i start to code the network engine to the game. Here is the deal: the game itself is an 'easy' top-down 3D shooter, players start at spawn points and by using "wsad" they: A-D : strafe left, strafe right W : move forward S : move backward Mouse is used to rotate the player. As for now, i have this and whole rendering framework completed. Time for a challenge. Till today i used mainly TCP as my online games were not 'fast' - typical RPG's with multi player support, so the scheme mainly looked like: Click on destination send this destination to server server generates pathfinder and steps to follow every X server sends a new position to player client moves the model to the step, server just sent. Very shortened, but it was really working fine even when we all played ( around 50people at once ). Now the real thing. I would like to stick to TCP really, due to ports on client side etc.. Here is how do i think this should be done, please highlight anything that might become a problem later on. My server runs on linux, i wrote non threaded server for this game. I send packets both ways using:

#define BYTE_SPACE 4
//prototype for the PDU
_u8 packetreply[BYTE_SPACE*6];
bzero(packetreply,sizeof(packetreply));
(*(int*)(packetreply))=NETWORKPACKET_START;
(*(int*)(packetreply+BYTE_SPACE))=NETWORKPACKET_PDU;
(*(float*)(packetreply+BYTE_SPACE*2))=client[cid].position[0];
(*(float*)(packetreply+BYTE_SPACE*3))=client[cid].position[1];
(*(int*)(packetreply+BYTE_SPACE*4))=client[cid].type;
(*(int*)(packetreply+BYTE_SPACE*5))=cid;
send(...)


1. Max players on server = 5 2. After successful login new player receives other's PDUs 3. After successful login server sends to others new player's PDU 4. At this time, everyone knows locations/models of the others This is working perfectly. When it comes to the movement. I understand it this way (not implemented yet). Theoretical "MOVE" packet would include: int w,s,a,d; int rotation;

//PSEUDOCODE//
1. Player 1 hits "W" (go forward) 
2. Player 1 sends packet including {W,S,A,D,rotation} to server
   so at this point Player 1 will send {1,0,0,0,rotation} to server
                                        w s a d


3. Server creates new packet including:
   int w,s,a,d;
   int clientid_that_is_moving;
   int rotation;
   float x,y;  //actual x,y of player

4. Server fills in that packet with data, 
   x and y values are always computed on server side,
   so players always use server side positions.

3. Server broadcasts that packet to all on line players
4. Server starts to increment the position of Player 1
5. Player 1 and others receive server broadcast 
6. Player 1 and others compare their x,y location of Player 1
   if there is a difference, they move Player 1 to server position
5. Player 1 and others start to increment the position of Player 1


The only 'bottleneck' here is the X,Y position that is always calculated on server side i believe, players are forced to stick to those values. STOP packet is being sent when a key is released.

Packet MOVE is being sent with relevant WSAD information = 0;
1. Server stops the movement calculation for such player
2. broadcasts it to all players
3. they move Player 1 position to the one received from server
4. they stop the calculation.


Now i cannot imagine how to handle the rotation with mouse, as if i imagine a player walking forward and continuously rotating the mouse, it will spawn hundredths of packets from him.. Well... hope you guys can enlighten me a bit, many many thanks for any comment on that. Any advantages/disadvantages? Any other idea how to network such an easy top-down ?
perfection.is.the.key
Advertisement
That's also a problem if players keep spamming the keyboard. That will increase the data for every key pressed and released.

an int for the state of the wsad keys and an int for the rotation is a bit extreme. an int if 4 bytes, so you could cram all the inputs into a single int (or even a short) quite easily.

Quote:
struct Inputs{	Inputs()	: m_controls(0)	{}	Inputs(int iw, int is, int ia, int id, int irotation)	: w((iw != 0)? 1 : 0)	, s((is != 0)? 1 : 0)	, a((ia != 0)? 1 : 0)	, d((id != 0)? 1 : 0)	, r((unsigned short)irotation)	{		// sanity check on rotation values.		ASSERT(irotation < (unsigned short) ((1 << 12)-1));	}	union	{ 		unsigned short m_controls; // pack of controls (two bytes).		struct		{			unsigned short w:1;  // one bit the 'w' key state.			unsigned short s:1;  // one bit the 's' key state.			unsigned short a:1;  // one bit the 'a' key state.			unsigned short d:1;  // one bit the 'd' key state.			unsigned short r:12; // 12 bits for encoding rotation of player.		};	};};



Now, everytime you poll the inputs, you check if the input changed, if it did, send the input.

Quote:
Input previous_inputs;Input current_inputs;void updateInputs(){	// get current input state.	current_inputs = getInputs();	// input changed, transmit them.	if(current_inputs != previous_inputs)	{		transmitInputs(current_inputs);	}	previous_inputs = current_inputs;}


Even in the case of the player rotating like an idiot, you're limiting your transmission to only two bytes (instead of the original 20, that's a factor of x10 compression). 12 bits for rotation gives you an accuracy of 1/10th of a degree, which should be enough for a 2D top-down shooter.

Furthermore, you can also reduce the rate of polling on your rotation, to reduce the risk even further.

Everything is better with Metal.

oliii - much appreciated.

I definitely make my packets too big, next step is to take everything down as you proposed.

What about the below scenario:

Server has a timer function with accuracy of Xms.
Within that timer function server is computing actual player position
by using state of WSAD/ROTATION on the client side.
This state comes to server as mentioned earlier, when MOVE/STOP packets are sent.


Now, when playerZ hits "W" - (previous scenario) he

1) sends a MOVE packet to server, to let server know he wants to move with WSAD,ROT state

2) gets a broadcast from server
that:

PlayerZ [ W=1 S=0 A=0 D=0 Rotation=r actual_server_position_x=X actual_server_position_y=Y ]

PlayerZ and other players are checking:

Ok, i have PlayerZ at X1 Y1
Server sent a packet and server has this player at X Y

#define MAX_DESYNCH_DISTANCE 1  //just for analysis it is 1 now.if (distance(client_x_on_server,client_y_on_server,client_x_on_client,client_y_on_client)> MAX_DESYNCH_DISTANCE){  snap_PlayerZ_to(client_x_on_server,client_y_on_server);


then the client timer kicks in ( it works in background in the same manner as servers one ) calculates the position of PlayerZ basing on his WSAD and ROTATION state.


STOP packet comes to server from PlayerZ

Server sets WSAD+ROT
Timer sees: ok, this player does not have any WSAD input, so calculation of his position does not change.

Broadcast that STOP to others
and the rest of the scenario is known:

- check distance
- snap if needed


So,
is this a suitable solution that all players calculations are computed on server side, and it is the server who exactly decides at which position player STARTS the move, CHANGES keystates, and ENDS ANY action?

It was good for multiplayer RPG where client send the destination x,y and all was doing was sitting in a loop awaiting steps to follow.
What about such topdown, wouldn't that cause too much desynchs ?


[Edited by - xynapse on March 18, 2010 12:20:37 PM]
perfection.is.the.key
Ok, switched to UDP.
No stress about 'delays' and retransmissions now. :)
perfection.is.the.key
Once you've made the controls be small (like olii's suggestion), you can send the last N steps in each packet with very little extra cost, because you can RLE encode them. You could put a "step counter" into the unused bits of the int, and then say that you send two or three ints, each with a "step counter" for how long that request has been in effect. Even the fastest users have a hard time spamming the keyboard with more than a handful of key events per second.

The benefit of doing this is that you become more tolerant to individual packet drops. You could even let the server ack whatever the latest step received was, and only send changes from that step up, which would send one or two ints in most cases, but could send bigger packets to compensate for longer intervals of packet loss.
enum Bool { True, False, FileNotFound };
hplus,

I'll take your advise into consideration,
I'm rewriting the UDP architecture now as it requires to handle few additional things that are not required while using TCP.

As soon as i get into keys and movements i will let you guys know,
so we can all together find out the best solution for a simple top-down shooter.


perfection.is.the.key
Ok, i have my UDP procedures ready,
the client connects by sending "AUTHORIZE" packet

struct PACKET_authentication{_u16 code;_u16 login;_u16 password;_u16 version;};



Server bound to the port listens for all incoming packets,
when he receives any packet it checks if a client is already authenticated
(flag client[x].status==CLIENT_AUTHENTICATED), if this is true,
packets are processed.

If client is not authorized but sending some data, connection is ignored
If client is not authorized but sending PACKET_authorization, we go through
a function checking MySQL for proper _u16 login and _u16 password and if
ok, client[x].status=CLIENT_AUTHENTICATED

From now on, server applies a flag called :

client[id].lastpacket_received=time(NULL)


and checks in a timer (running every second):

;#define MAX_PING 5  //seconds...for(int a=0;a<MAX_CLIENTS;a++){ if(time(NULL)-client[a].lastpacket_received > MAX_PING){   client[a].status=CLIENT_DISCONNECTED; }}


So we can ensure that client stay's online 5seconds without interruption.
This might be too small for final release, but it is okay for testing.


Hope this is okay for now, or am i missing anything in the beginning?


Next phase is to have the correct data sent/broadcasted.
What do you think guys, how should that work exactly when it comes to something like a fast topdown shooter?

[1] Scenario vulnerable for cheating

Players are sending their positions to server every Xms
Server broadcasts that to others


[2] Scenario most adequate?

Server runs his own calculations of players positions - the algorithm used is exactly the same on client's machines and server.

Lets say, a timer running with 10ms algorithm:

		 int a;		 for(a=0;a<MAX_CLIENTS;a++){				  if(client[a].W==1){					client[a].position[0]+=    ((float)sin(client[a].rotation*piover180))*PLAYER_SPEED;			// Move On The X-Plane Based On Player Direction					client[a].position[1]+=		((float)cos(client[a].rotation*piover180))*PLAYER_SPEED; 			  }			  if(client[a].S==1){					client[a].position[0]-=    ((float)sin(client[a].rotation*piover180))*PLAYER_SPEED;			// Move On The X-Plane Based On Player Direction					client[a].position[1]-=		((float)cos(client[a].rotation*piover180))*PLAYER_SPEED; 			  }			   if(client[a].A==1){						client[a].position[0]-=PLAYER_SPEED;			   }			   if(client[a].D==1){						client[a].position[0]+=PLAYER_SPEED;			   }		 }


Clients are sending their key inputs to server every Xms,
they locally move their characters without waiting for anything.

Server is receiving those packets and broadcasts actual server positions to the players

Clients are running a network procedure where both:

local positions
server received positions

are compared.

If distance between those two is > MAX ( lets say 2.0f )
client whose distance is > MAX is being snapped to the server received pos.




That is all i came up to right now,
how do you think, is there anything else that can be done in order to have it working smoothly for 5 players ?

Many thanks for all suggestions :)
perfection.is.the.key

This topic is closed to new replies.

Advertisement