In the last tutorial we setup a basic TCP server. In this tutorial we will start by modifying the server we setup and finish by writing a player client application. In order to write a client application with SDL 2.0 we will need a lot of the ideas which where presented in my first string of SDL tutorials (here). A lot of my personal preferences for setting up SDL have changed since writing those tutorials so instead of using code found within those tutorials I will be using a collection of C++ files I have settled into using (found here).
First let’s start with a few server-side modifications. Let’s start by adding a few extra packet flags:
//-----------------------------------------------------------------------------
#define FLAG_QUIT 0x0000
#define FLAG_WOOD_QUEST 0x0011
#define FLAG_WOOD_UPDATE 0x0010
#define FLAG_WOOD_GETTIME 0x0012
Now we have a packet flag for starting and completing a quest, as well as a packet flag for getting the amount of time left until the quest is completed. Next we will update our server-side “RecvData” function:
uint8_t* RecvData(int index, uint16_t* length) {
uint8_t temp_data[MAX_PACKET];
int num_recv = SDLNet_TCP_Recv(sockets[index], temp_data, MAX_PACKET);
if(num_recv <= 0) {
CloseSocket(index);
const char* err = SDLNet_GetError();
if(strlen(err) == 0) {
printf("DB: client disconnectedn");
} else {
fprintf(stderr, "ER: SDLNet_TCP_Recv: %sn", err);
}
return NULL;
} else {
*length = num_recv;
uint8_t* data = (uint8_t*) malloc(num_recv*sizeof(uint8_t));
memcpy(data, temp_data, num_recv);
return data;
}
}
What I have done here is removed the work this function was doing to grab the packet flag from the start of the incoming socket data. This is because when a socket is ready for us to process it may have several packets all lined together. Therefore the data buffer returned by “RecvData” may contain several packets depending on how quickly the client is sending packets to us; so any work “RecvData” does to grab the flag from the first packet doesn’t help us identify the packet flags for all the other packets possibly following the first. Therefore the extra work being done isn’t necessary at this level.
Next we will add a new function called “ProcessData.” This function will continuously run on each packet we find within the data buffer returned by “RecvData.”
void ProcessData(int index, uint8_t* data, uint16_t* offset) {
if(data == NULL) return;
uint16_t flag = *(uint16_t*) &data[*offset];
*offset += sizeof(uint16_t);
switch(flag) {
case FLAG_WOOD_UPDATE: {
uint16_t send_offset = 0;
uint8_t send_data[MAX_PACKET];
memcpy(send_data+send_offset, &clients[index].amt_wood, sizeof(uint8_t));
send_offset += sizeof(uint8_t);
SendData(index, send_data, send_offset, FLAG_WOOD_UPDATE);
} break;
case FLAG_WOOD_GETTIME: {
uint16_t send_offset = 0;
uint8_t send_data[MAX_PACKET];
uint32_t time_left;
if(clients[index].questing) {
time_left = WOOD_WAIT_TIME-(SDL_GetTicks()-clients[index].timer_wood);
} else time_left = 0;
memcpy(send_data+send_offset, &time_left, sizeof(uint32_t));
send_offset += sizeof(uint32_t);
SendData(index, send_data, send_offset, FLAG_WOOD_GETTIME);
} break;
case FLAG_WOOD_QUEST: {
if(!clients[index].questing) {
clients[index].questing = 1;
clients[index].timer_wood = SDL_GetTicks();
}
} break;
case FLAG_QUIT: {
running = 0;
printf("DB: shutdown by client id: %dn", index);
} break;
}
}
This is where we do the work and grab the packet’s flag. Everything here should look similar to what we where doing before when the server found client sockets which were ready with information to be processed – except for the fact that we have added two extra flags. The “GETTIME” flag checks if the player is currently questing; if not then the time left on the quest is 0, otherwise the server sends the time left on the quest to the player. The other flag is the “QUEST” flag; this flag checks whether the client is currently questing and starts the quest if they aren’t currently in a quest.
The finial server-side update we make will be to the main processing loop. First let’s crank up how often we check for ready sockets:
int num_rdy = SDLNet_CheckSockets(socket_set, 50);
If num_rdy <= 0 then we do not have any sockets ready for processing so we enter the server’s idle tasks. Previously we were simply adding four wood to the client’s resource count continuously. This is quite a silly thing to do! Instead let’s check for whether a client has completed any quests and if so we’ll send out a quest completed packet and a wood update packet. In addition, we’ll continuously send out “GETTIME” packets to keep the client updated with the server’s timer.
int ind;
for(ind=0; ind<MAX_SOCKETS; ++ind) {
if(!clients[ind].in_use) continue;
if(clients[ind].questing &&
(SDL_GetTicks()-clients[ind].timer_wood)>WOOD_WAIT_TIME
) {
clients[ind].questing = 0;
clients[ind].amt_wood += 4;
SendData(ind, NULL, 0, FLAG_WOOD_QUEST);
uint16_t send_offset = 0;
uint8_t send_data[MAX_PACKET];
memcpy(send_data+send_offset, &clients[ind].amt_wood, sizeof(uint8_t));
send_offset += sizeof(uint8_t);
SendData(ind, send_data, send_offset, FLAG_WOOD_UPDATE);
}
uint16_t send_offset = 0;
uint8_t send_data[MAX_PACKET];
uint32_t time_left;
if(clients[ind].questing) {
time_left = WOOD_WAIT_TIME-(SDL_GetTicks()-clients[ind].timer_wood);
} else time_left = 0;
memcpy(send_data+send_offset, &time_left, sizeof(uint32_t));
send_offset += sizeof(uint32_t);
SendData(ind, send_data, send_offset, FLAG_WOOD_GETTIME);
}
And for the final server touch we’ll update the code which runs if there are sockets ready to be processed. Notice that how we handle the server socket (for new connections) isn’t changing but how we handle the client sockets is changing.
if(SDLNet_SocketReady(server_socket)) {
int got_socket = AcceptSocket(next_ind);
if(!got_socket) {
num_rdy--;
continue;
}
// NOTE: get a new index
int chk_count;
for(chk_count=0; chk_count<MAX_SOCKETS; ++chk_count) {
if(sockets[(next_ind+chk_count)%MAX_SOCKETS] == NULL) break;
}
next_ind = (next_ind+chk_count)%MAX_SOCKETS;
printf("DB: new connection (next_ind = %d)n", next_ind);
num_rdy--;
}
int ind;
for(ind=0; (ind<MAX_SOCKETS) && num_rdy; ++ind) {
if(sockets[ind] == NULL) continue;
if(!SDLNet_SocketReady(sockets[ind])) continue;
uint8_t* data;
uint16_t length;
data = RecvData(ind, &length);
if(data == NULL) {
num_rdy--;
continue;
}
int num_processed = 0;
uint16_t offset = 0;
while(offset < length) {
num_processed++;
ProcessData(ind, data, &offset);
}
printf("num_processed from ID: %d was %dn", ind, num_processed);
fflush(stdout);
free(data);
num_rdy--;
}
I am also keeping track of the number of packets which the server processes for each of the ready clients for debugging purposes. Now the client-side stuff isn’t that different when it comes to the network side of things. A lot of the helper functions we have server-side will have analogous client-server versions. Here is the client-side code in its entirety:
/*
*/
#include "AdBase.h"
#include "AdLevel.h"
#include "AdScreen.h"
#include "AdSpriteManager.h"
//-----------------------------------------------------------------------------
#define MAX_PACKET 0xFF
//-----------------------------------------------------------------------------
#define FLAG_QUIT 0x0000
#define FLAG_WOOD_QUEST 0x0011
#define FLAG_WOOD_UPDATE 0x0010
#define FLAG_WOOD_GETTIME 0x0012
//-----------------------------------------------------------------------------
TCPsocket socket;
SDLNet_SocketSet socket_set;
//-----------------------------------------------------------------------------
int questing;
uint8_t amt_wood;
uint32_t timer_wood;
//-----------------------------------------------------------------------------
void CloseSocket(void) {
if(SDLNet_TCP_DelSocket(socket_set, socket) == -1) {
fprintf(stderr, "%sn", SDLNet_GetError());
system("pause");
exit(-1);
}
SDLNet_FreeSocketSet(socket_set);
SDLNet_TCP_Close(socket);
}
//-----------------------------------------------------------------------------
void SendData(uint8_t* data, uint16_t length, uint16_t flag) {
uint8_t temp_data[MAX_PACKET];
int offset = 0;
memcpy(temp_data+offset, &flag, sizeof(uint16_t));
offset += sizeof(uint16_t);
memcpy(temp_data+offset, data, length);
offset += length;
int num_sent = SDLNet_TCP_Send(socket, temp_data, offset);
if(num_sent < offset) {
fprintf(stderr, "ER: SDLNet_TCP_Send: %sn", SDLNet_GetError());
CloseSocket();
}
}
//-----------------------------------------------------------------------------
uint8_t* RecvData(uint16_t* length) {
uint8_t temp_data[MAX_PACKET];
int num_recv = SDLNet_TCP_Recv(socket, temp_data, MAX_PACKET);
if(num_recv <= 0) {
CloseSocket();
const char* err = SDLNet_GetError();
if(strlen(err) == 0) {
printf("DB: server shutdownn");
} else {
fprintf(stderr, "ER: SDLNet_TCP_Recv: %sn", err);
}
return NULL;
} else {
*length = num_recv;
uint8_t* data = (uint8_t*) malloc(num_recv*sizeof(uint8_t));
memcpy(data, temp_data, num_recv);
return data;
}
}
//-----------------------------------------------------------------------------
void ProcessData(uint8_t* data, uint16_t* offset) {
if(data == NULL) return;
uint16_t flag = *(uint16_t*) &data[*offset];
*offset += sizeof(uint16_t);
switch(flag) {
case FLAG_WOOD_UPDATE: {
amt_wood = *(uint8_t*) &data[*offset];
*offset += sizeof(uint8_t);
} break;
case FLAG_WOOD_GETTIME: {
timer_wood = *(uint32_t*) &data[*offset];
*offset += sizeof(uint32_t);
} break;
case FLAG_WOOD_QUEST: {
// NOTE: quest completed
questing = 0;
} break;
}
}
//-----------------------------------------------------------------------------
void InitNetwork(const char* pIP, int iPort) {
IPaddress ip;
if(SDLNet_ResolveHost(&ip, pIP, iPort) == -1) {
fprintf(stderr, "%sn", SDLNet_GetError());
system("pause");
exit(-1);
}
socket = SDLNet_TCP_Open(&ip);
if(socket == NULL) {
fprintf(stderr, "%sn", SDLNet_GetError());
system("pause");
exit(-1);
}
socket_set = SDLNet_AllocSocketSet(1);
if(socket_set == NULL) {
fprintf(stderr, "%sn", SDLNet_GetError());
system("pause");
exit(-1);
}
if(SDLNet_TCP_AddSocket(socket_set, socket) == -1) {
fprintf(stderr, "%sn", SDLNet_GetError());
system("pause");
exit(-1);
}
}
//-----------------------------------------------------------------------------
bool CheckSocket(void) {
if(SDLNet_CheckSockets(socket_set, 0) == -1) {
fprintf(stderr, "%sn", SDLNet_GetError());
system("pause");
exit(-1);
}
return SDLNet_SocketReady(socket);
}
//-----------------------------------------------------------------------------
int SDL_main(int argc, char* argv[]) {
if(AdBase::Init(8*40, 8*30, 3) == false) {
fprintf(stderr, "ERROR: Failed to initiate.n");
system("pause");
return -1;
}
// TESTING
InitNetwork("PUT_SERVER_IP_HERE!!!", 8099); // arg #2 is the port
AdLevel* testLvl = new AdLevel();
//
SDL_Event sdlEvent = {};
while(sdlEvent.type != SDL_QUIT) {
SDL_PollEvent(&sdlEvent);
AdScreen::Clear();
// TESTING
if(CheckSocket()) {
uint16_t length, flag;
uint8_t* data = RecvData(&length);
uint16_t offset = 0;
while(offset < length) {
ProcessData(data, &offset);
}
free(data);
}
//
// TESTING
testLvl->Update(&sdlEvent);
char string[0xFF];
sprintf(string, "Wood: %d", amt_wood);
SDL_Point pnt1 = {0, 0};
SDL_Color color1 = {0xFF, 0x00, 0x00, 0x00};
SDL_Surface* pSurf = AdSpriteManager::BuildSprite(string, color1);
AdScreen::DrawSprite(pnt1, pSurf);
SDL_FreeSurface(pSurf);
pnt1.y += 8;
sprintf(string, "Timer: %d", (int) ceil((double)timer_wood/1000.0f));
pSurf = AdSpriteManager::BuildSprite(string, color1);
AdScreen::DrawSprite(pnt1, pSurf);
SDL_FreeSurface(pSurf);
//
//
if(timer_wood == 0) {
SDL_Point pnt2 = {128, 8};
SDL_Rect rec2 = {pnt2.x, pnt2.y, 5*8, 8};
if(
testLvl->m_iMouseX>=rec2.x && testLvl->m_iMouseX<=(rec2.x+rec2.w) &&
testLvl->m_iMouseY>=rec2.y && testLvl->m_iMouseY<=(rec2.y+rec2.h)
) {
SDL_Color color2 = {0x00, 0xFF, 0x00, 0x00};
pSurf = AdSpriteManager::BuildSprite("Quest", color2);
if(testLvl->m_bMouseLeft && !questing) {
questing = 1;
SendData(NULL, 0, FLAG_WOOD_QUEST);
}
} else {
SDL_Color color2 = {0x00, 0x00, 0xFF, 0x00};
pSurf = AdSpriteManager::BuildSprite("Quest", color2);
}
AdScreen::DrawSprite(pnt2, pSurf);
SDL_FreeSurface(pSurf);
}
//
AdScreen::Present();
}
// TESTING
delete testLvl;
SendData(NULL, 0, FLAG_QUIT);
CloseSocket();
//
AdBase::Quit();
return 0;
}
All the SDL graphics processing and input handling is being wrapped with the custom files I wrote and linked at the beginning of this post. If you find these posts helpful or would like me to dive deeper into a particular topic covered here please send me an email at “info@stephenmeier.net”.