Compare commits

...

10 Commits

4 changed files with 255 additions and 59 deletions

View File

@ -63,6 +63,20 @@ to work on improving the client and separating out the techniques so at the end
just by changing preprocessor statements you'll be able to see the effects of just by changing preprocessor statements you'll be able to see the effects of
each technique. each technique.
** Entry 07: Server Reconcilliation:
Adding that in made it really seem a lot smoother. Last thing I want to add to
this for the moment is linear interpolation between states. Should make it
smooth for bigger tests.
** Entry 08: Linear Interpolation:
Added this mostly without incident, and my god did it improve things. It's very
close to essentially being frame-perfect for a round-trip to my server and
back. Should be excellent implemented in Spacewar! What I basically have is a
netcode that uses dead-reckoning client-side prediction, server reconcilliation,
and linear interpolation between states to make something fairly buttery smooth,
although I haven't tested over longer distances yet, which I really should
before declaring victory.
* Notes On Techniques: * Notes On Techniques:
** Entry 00: Where I'm Learning All This From: ** Entry 00: Where I'm Learning All This From:
I'm using a series of articles from gabrielgambetta.com, which seem to be well I'm using a series of articles from gabrielgambetta.com, which seem to be well

View File

@ -1,5 +1,7 @@
// Client-Side Prediction Test - Client /* /======================================\
// Barra Ó Catháin - 2023 | Client-Side Prediction Test - Client |
| Barra Ó Catháin - 2023 |
\======================================/ */
#include <netdb.h> #include <netdb.h>
#include <stdio.h> #include <stdio.h>
#include <errno.h> #include <errno.h>
@ -18,7 +20,21 @@
#include "../cspt-state.h" #include "../cspt-state.h"
#include "../cspt-message.h" #include "../cspt-message.h"
uint8_t colours[16][3] = #define ENABLE_CLIENT_SIDE_PREDICTION
#define ENABLE_SERVER_RECONCILLIATION
// A structure for binding together the shared state between threads:
struct ClientThreadParameters
{
char * ipAddress;
bool * keepRunning;
struct gameState * state;
struct clientInput * message;
struct inputHistory * inputBuffer;
};
// Seperate colours to distinguish each of the 16 possible clients:
static const uint8_t colours[16][3] =
{ {
{255, 255, 255}, {255, 255, 255},
{100, 176, 254}, {100, 176, 254},
@ -34,15 +50,9 @@ uint8_t colours[16][3] =
{69 , 225, 130}, {69 , 225, 130},
{72 , 206, 223} {72 , 206, 223}
}; };
// A structure for binding together the shared state between threads:
struct threadParameters
{
struct gameState * state;
struct clientInput * message;
char * ipAddress;
};
void DrawCircle(SDL_Renderer * renderer, int32_t centreX, int32_t centreY, int32_t radius) // Draws a circle based on the midpoint circle algorithm:
void drawCircle(SDL_Renderer * renderer, int32_t centreX, int32_t centreY, int32_t radius)
{ {
const int32_t diameter = (radius * 2); const int32_t diameter = (radius * 2);
@ -50,7 +60,7 @@ void DrawCircle(SDL_Renderer * renderer, int32_t centreX, int32_t centreY, int32
int32_t y = 0; int32_t y = 0;
int32_t tx = 1; int32_t tx = 1;
int32_t ty = 1; int32_t ty = 1;
int32_t error = (tx - diameter); int32_t error = (tx - diameter);
while (x >= y) while (x >= y)
{ {
@ -82,67 +92,129 @@ void DrawCircle(SDL_Renderer * renderer, int32_t centreX, int32_t centreY, int32
void * networkHandler(void * parameters) void * networkHandler(void * parameters)
{ {
// Declare the needed variables for the thread: // Unpack the variables passed to the thread:
struct threadParameters * arguments = parameters; char * ipAddress = ((struct ClientThreadParameters * )parameters)->ipAddress;
struct sockaddr_in serverAddress; bool * keepRunning = ((struct ClientThreadParameters * )parameters)->keepRunning;
int udpSocket = 0; struct gameState * state = ((struct ClientThreadParameters * )parameters)->state;
struct clientInput * message = ((struct ClientThreadParameters * )parameters)->message;
struct inputHistory * inputBuffer = ((struct ClientThreadParameters * )parameters)->inputBuffer;
// Point at the server: // Point at the server:
struct sockaddr_in serverAddress;
serverAddress.sin_family = AF_INET; serverAddress.sin_family = AF_INET;
serverAddress.sin_addr.s_addr = inet_addr(arguments->ipAddress); serverAddress.sin_addr.s_addr = inet_addr(ipAddress);
serverAddress.sin_port = htons(5200); serverAddress.sin_port = htons(5200);
// Create a UDP socket to send through: // Create a UDP socket to send through:
int udpSocket = 0;
udpSocket = socket(AF_INET, SOCK_DGRAM, 0); udpSocket = socket(AF_INET, SOCK_DGRAM, 0);
// Configure a timeout for recieving: // Configure a timeout for receiving:
struct timeval timeout; struct timeval receiveTimeout;
timeout.tv_sec = 0; receiveTimeout.tv_sec = 0;
timeout.tv_usec = 1000; receiveTimeout.tv_usec = 1000;
setsockopt(udpSocket, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); setsockopt(udpSocket, SOL_SOCKET, SO_RCVTIMEO, &receiveTimeout, sizeof(struct timeval));
// A structure to store the most recent state from the network:
struct gameState * updatedState = calloc(1, sizeof(struct gameState)); struct gameState * updatedState = calloc(1, sizeof(struct gameState));
while (true)
while (keepRunning)
{ {
// Send our input, recieve the state: // Send our input, recieve the state:
sendto(udpSocket, arguments->message, sizeof(struct clientInput), 0, (struct sockaddr *)&serverAddress, sizeof(struct sockaddr_in)); sendto(udpSocket, message, sizeof(struct clientInput), 0, (struct sockaddr *)&serverAddress, sizeof(struct sockaddr_in));
recvfrom(udpSocket, updatedState, sizeof(struct gameState), 0, NULL, NULL); recvfrom(udpSocket, updatedState, sizeof(struct gameState), 0, NULL, NULL);
if(updatedState->timestamp.tv_sec > arguments->state->timestamp.tv_sec)
// Only update the state if the given state is more recent than the current state:
if (updatedState->timestamp.tv_sec > state->timestamp.tv_sec ||
(updatedState->timestamp.tv_sec == state->timestamp.tv_sec &&
updatedState->timestamp.tv_usec > state->timestamp.tv_usec))
{ {
memcpy(arguments->state, updatedState, sizeof(struct gameState)); #ifdef ENABLE_SERVER_RECONCILLIATION
} // Throw away any already acknowledged inputs:
else if(updatedState->timestamp.tv_sec == arguments->state->timestamp.tv_sec && while (inputBuffer->start != -1 && inputBuffer->inputs[inputBuffer->start].tickNumber < state->tickNumber)
updatedState->timestamp.tv_usec > arguments->state->timestamp.tv_usec) {
{ inputBuffer->start = (inputBuffer->start + 1) % 256;
memcpy(arguments->state, updatedState, sizeof(struct gameState)); if(inputBuffer->start == inputBuffer->end)
{
inputBuffer->start = -1;
}
}
uint8_t currentMessage = inputBuffer->start;
uint64_t lastTickNumber = inputBuffer->inputs[inputBuffer->start].tickNumber;
// Re-apply the currently unused messages:
while (currentMessage != 1 && currentMessage != inputBuffer->end)
{
updateInput(state, &inputBuffer->inputs[currentMessage]);
currentMessage = (currentMessage + 1) % 256;
// When we get to the next tick in the inputs, apply a game tick:
if (inputBuffer->inputs[currentMessage].tickNumber != lastTickNumber)
{
doGameTick(state);
}
}
#endif
// Interpolate to the new state:
lerpStates(state, updatedState);
} }
} }
return NULL;
} }
void * gameThreadHandler(void * parameters) void * gameThreadHandler(void * parameters)
{ {
struct threadParameters * arguments = parameters; // Unpack the variables passed to the thread:
while (true) bool * keepRunning = ((struct ClientThreadParameters * )parameters)->keepRunning;
struct gameState * state = ((struct ClientThreadParameters * )parameters)->state;
struct clientInput * message = ((struct ClientThreadParameters * )parameters)->message;
struct inputHistory * inputBuffer = ((struct ClientThreadParameters * )parameters)->inputBuffer;
#ifdef ENABLE_CLIENT_SIDE_PREDICTION
struct gameState * nextStep = calloc(1, sizeof(struct gameState));
while (keepRunning)
{ {
updateInput(arguments->state, arguments->message); updateInput(state, message);
doGameTick(arguments->state);
#ifdef ENABLE_SERVER_RECONCILLIATION
if(inputBuffer->start == -1)
{
memcpy(&inputBuffer->inputs[0], message, sizeof(struct clientInput));
inputBuffer->start = 0;
inputBuffer->end = 1;
}
else
{
memcpy(&inputBuffer->inputs[inputBuffer->end], message, sizeof(struct clientInput));
inputBuffer->end = (inputBuffer->end + 1) % 256;
}
#endif
memcpy(nextStep, state, sizeof(struct gameState));
doGameTick(nextStep);
lerpStates(state, nextStep);
usleep(15625); usleep(15625);
} }
#endif
return NULL;
} }
void * graphicsThreadHandler(void * parameters) void * graphicsThreadHandler(void * parameters)
{ {
struct gameState * state = ((struct threadParameters *)parameters)->state; bool * keepRunning = ((struct ClientThreadParameters *)parameters)->keepRunning;
struct clientInput * message = ((struct threadParameters *)parameters)->message; struct gameState * state = ((struct ClientThreadParameters *)parameters)->state;
struct clientInput * message = ((struct ClientThreadParameters *)parameters)->message;
uint32_t rendererFlags = SDL_RENDERER_ACCELERATED; uint32_t rendererFlags = SDL_RENDERER_ACCELERATED;
// Create an SDL window and rendering context in that window: // Create an SDL window and rendering context in that window:
SDL_Window * window = SDL_CreateWindow("CSPT-Client", SDL_WINDOWPOS_CENTERED, SDL_Window * window = SDL_CreateWindow("CSPT-Client", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 512, 512, 0);
SDL_WINDOWPOS_CENTERED, 512, 512, 0);
SDL_Renderer * renderer = SDL_CreateRenderer(window, -1, rendererFlags); SDL_Renderer * renderer = SDL_CreateRenderer(window, -1, rendererFlags);
SDL_Event event; SDL_Event event;
while (true) while (keepRunning)
{ {
while (SDL_PollEvent(&event)) while (SDL_PollEvent(&event))
{ {
@ -154,21 +226,25 @@ void * graphicsThreadHandler(void * parameters)
{ {
case SDLK_LEFT: case SDLK_LEFT:
{ {
message->tickNumber = state->tickNumber;
message->left = true; message->left = true;
break; break;
} }
case SDLK_RIGHT: case SDLK_RIGHT:
{ {
message->tickNumber = state->tickNumber;
message->right = true; message->right = true;
break; break;
} }
case SDLK_UP: case SDLK_UP:
{ {
message->tickNumber = state->tickNumber;
message->up = true; message->up = true;
break; break;
} }
case SDLK_DOWN: case SDLK_DOWN:
{ {
message->tickNumber = state->tickNumber;
message->down = true; message->down = true;
break; break;
} }
@ -185,33 +261,43 @@ void * graphicsThreadHandler(void * parameters)
{ {
case SDLK_LEFT: case SDLK_LEFT:
{ {
message->tickNumber = state->tickNumber;
message->left = false; message->left = false;
break; break;
} }
case SDLK_RIGHT: case SDLK_RIGHT:
{ {
message->tickNumber = state->tickNumber;
message->right = false; message->right = false;
break; break;
} }
case SDLK_UP: case SDLK_UP:
{ {
message->tickNumber = state->tickNumber;
message->up = false; message->up = false;
break; break;
} }
case SDLK_DOWN: case SDLK_DOWN:
{ {
message->tickNumber = state->tickNumber;
message->down = false; message->down = false;
break; break;
} }
} }
break; break;
} }
case SDL_QUIT:
{
*keepRunning = false;
break;
}
default: default:
{ {
break; break;
} }
} }
} }
// Clear the screen, filling it with black: // Clear the screen, filling it with black:
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer); SDL_RenderClear(renderer);
@ -221,8 +307,17 @@ void * graphicsThreadHandler(void * parameters)
{ {
if (state->clients[index].registered == true) if (state->clients[index].registered == true)
{ {
// Set the colour to the correct one for the client:
SDL_SetRenderDrawColor(renderer, colours[index][0], colours[index][1], colours[index][2], 255); SDL_SetRenderDrawColor(renderer, colours[index][0], colours[index][1], colours[index][2], 255);
DrawCircle(renderer, (long)(state->clients[index].xPosition), (long)(state->clients[index].yPosition), 10);
// Draw the circle:
drawCircle(renderer, (long)(state->clients[index].xPosition), (long)(state->clients[index].yPosition), 10);
// Draw an additional circle so we can tell ourselves apart from the rest:
if (index == message->clientNumber)
{
drawCircle(renderer, (long)(state->clients[index].xPosition), (long)(state->clients[index].yPosition), 5);
}
} }
} }
@ -239,16 +334,24 @@ void * graphicsThreadHandler(void * parameters)
int main(int argc, char ** argv) int main(int argc, char ** argv)
{ {
int serverSocket = 0; int serverSocket = 0;
bool continueRunning = true; bool keepRunning = true;
uint8_t currentPlayerNumber = 0; uint8_t currentPlayerNumber = 0;
struct sockaddr_in serverAddress; struct sockaddr_in serverAddress;
struct CsptMessage currentMessage; struct CsptMessage currentMessage;
pthread_t graphicsThread, networkThread, gameThread;
struct gameState * currentState = calloc(1, sizeof(struct gameState)); struct gameState * currentState = calloc(1, sizeof(struct gameState));
struct clientInput * clientInput = calloc(1, sizeof(struct gameState)); struct clientInput * clientInput = calloc(1, sizeof(struct gameState));
// Say hello: // Print a welcome message:
printf("Client-Side Prediction Test - Client Starting.\n"); printf("Client-Side Prediction Test - Client Starting.\n");
printf("==============================================\n");
// Print a list of enabled features:
#ifdef ENABLE_CLIENT_SIDE_PREDICTION
printf("Client-side prediction is enabled in this build.\n");
#endif
#ifdef ENABLE_SERVER_RECONCILLIATION
printf("Server reconcilliation is enabled in this build.\n");
#endif
// Give me a socket, and make sure it's working: // Give me a socket, and make sure it's working:
serverSocket = socket(AF_INET, SOCK_STREAM, 0); serverSocket = socket(AF_INET, SOCK_STREAM, 0);
@ -293,56 +396,77 @@ int main(int argc, char ** argv)
clientInput->clientNumber = currentPlayerNumber; clientInput->clientNumber = currentPlayerNumber;
} }
printf("Registered as: %u\n", currentPlayerNumber); printf("Joined server as client: %u.\n", currentPlayerNumber);
// Configure the thread parameters: // Configure the thread parameters:
struct threadParameters parameters; struct ClientThreadParameters parameters;
parameters.message = clientInput;
parameters.state = currentState; parameters.state = currentState;
parameters.message = clientInput;
parameters.ipAddress = ipAddress; parameters.ipAddress = ipAddress;
parameters.keepRunning = &keepRunning;
parameters.inputBuffer = calloc(1, sizeof(struct inputHistory));
parameters.inputBuffer->start = -1;
parameters.inputBuffer->end = -1;
// Create all of our threads: // Create all of our threads:
pthread_t graphicsThread, networkThread, gameThread;
pthread_create(&gameThread, NULL, gameThreadHandler, &parameters); pthread_create(&gameThread, NULL, gameThreadHandler, &parameters);
pthread_create(&networkThread, NULL, networkHandler, &parameters); pthread_create(&networkThread, NULL, networkHandler, &parameters);
pthread_create(&graphicsThread, NULL, graphicsThreadHandler, &parameters); pthread_create(&graphicsThread, NULL, graphicsThreadHandler, &parameters);
while (continueRunning) while (keepRunning)
{ {
if (recv(serverSocket, &currentMessage, sizeof(struct CsptMessage), 0) > 0) if (recv(serverSocket, &currentMessage, sizeof(struct CsptMessage), 0) > 0)
{ {
switch (currentMessage.type) switch (currentMessage.type)
{ {
// Recieved a "GOODBYE" message:
case 1: case 1:
{ {
// We've been told to disconnect: // Close the socket, and stop the client:
shutdown(serverSocket, SHUT_RDWR); shutdown(serverSocket, SHUT_RDWR);
serverSocket = 0; serverSocket = 0;
continueRunning = false; keepRunning = false;
break; break;
} }
// Recieved a "PING" message:
case 2: case 2:
{ {
// Pinged, so we now must pong. // Setup and send a "PONG" message:
currentMessage.type = 3; currentMessage.type = 3;
currentMessage.content = 0; currentMessage.content = 0;
send(serverSocket, &currentMessage, sizeof(struct CsptMessage), 0); send(serverSocket, &currentMessage, sizeof(struct CsptMessage), 0);
break; break;
} }
} }
} }
// If we've lost connection for some reason:
else else
{ {
// Say goodbye to the server: // Setup a "GOODBYE" message:
currentMessage.type = 1; currentMessage.type = 1;
currentMessage.content = 0; currentMessage.content = 0;
// Send the goodbye message and shutdown: // Send the "GOODBYE" message and shutdown the socket:
send(serverSocket, &currentMessage, sizeof(struct CsptMessage), 0); send(serverSocket, &currentMessage, sizeof(struct CsptMessage), 0);
shutdown(serverSocket, SHUT_RDWR); shutdown(serverSocket, SHUT_RDWR);
serverSocket = 0; serverSocket = 0;
continueRunning = false; keepRunning = false;
} }
} }
// Say goodbye to the server:
currentMessage.type = 1;
currentMessage.content = 0;
// Send the goodbye message and shutdown:
send(serverSocket, &currentMessage, sizeof(struct CsptMessage), 0);
shutdown(serverSocket, SHUT_RDWR);
serverSocket = 0;
keepRunning = false;
return 0; return 0;
} }

View File

@ -1,5 +1,12 @@
#include <math.h>
#include <stdio.h> #include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <stdbool.h>
#include "cspt-state.h" #include "cspt-state.h"
void updateInput(struct gameState * state, struct clientInput * message) void updateInput(struct gameState * state, struct clientInput * message)
{ {
if(message->clientNumber < 16 && message->clientNumber >= 0) if(message->clientNumber < 16 && message->clientNumber >= 0)
@ -9,11 +16,50 @@ void updateInput(struct gameState * state, struct clientInput * message)
state->clients[message->clientNumber].upAcceleration = message->up; state->clients[message->clientNumber].upAcceleration = message->up;
state->clients[message->clientNumber].downAcceleration = message->down; state->clients[message->clientNumber].downAcceleration = message->down;
} }
}
void lerpStates (struct gameState * state, struct gameState * endState)
{
// Create a copy of the initial state for interpolating to the final state:
struct gameState * startState = calloc(1, sizeof(struct gameState));
memcpy(startState, state, sizeof(struct gameState));
for (double progress = 0.0; progress < 1.0; progress += 0.01)
{
for (uint8_t index = 0; index < 16; index++)
{
// Movement:
state->clients[index].xPosition = startState->clients[index].xPosition +
(progress * (endState->clients[index].xPosition - startState->clients[index].xPosition));
state->clients[index].yPosition = startState->clients[index].yPosition +
(progress * (endState->clients[index].yPosition - startState->clients[index].yPosition));
// If the movement is too dramatic, just go to the new position. Stops "zooming" when teleported:
if (fabs(startState->clients[index].xPosition - endState->clients[index].xPosition) >= 200)
{
state->clients[index].xPosition = endState->clients[index].xPosition;
}
if (fabs(startState->clients[index].yPosition - endState->clients[index].yPosition) >= 200)
{
state->clients[index].yPosition = endState->clients[index].yPosition;
}
}
usleep(100);
}
free(startState);
memcpy(state, endState, sizeof(struct gameState));
} }
void doGameTick(struct gameState * state) void doGameTick(struct gameState * state)
{ {
if ((state->tickNumber % UINT64_MAX) == 0)
{
state->tickNumber = 0;
}
else
{
state->tickNumber++;
}
for (int index = 0; index < 16; index++) for (int index = 0; index < 16; index++)
{ {
// Calculate acceleration: // Calculate acceleration:
@ -41,6 +87,7 @@ void doGameTick(struct gameState * state)
{ {
state->clients[index].yVelocity *= 0.9; state->clients[index].yVelocity *= 0.9;
} }
// Do movement: // Do movement:
state->clients[index].xPosition += state->clients[index].xVelocity; state->clients[index].xPosition += state->clients[index].xVelocity;
state->clients[index].yPosition += state->clients[index].yVelocity; state->clients[index].yPosition += state->clients[index].yVelocity;

View File

@ -4,6 +4,7 @@
#include <stdbool.h> #include <stdbool.h>
#include <sys/time.h> #include <sys/time.h>
#include <netinet/in.h> #include <netinet/in.h>
struct clientMovement struct clientMovement
{ {
double xPosition, yPosition, xVelocity, yVelocity; double xPosition, yPosition, xVelocity, yVelocity;
@ -13,11 +14,13 @@ struct clientMovement
struct clientInput struct clientInput
{ {
int clientNumber; int clientNumber;
uint64_t tickNumber;
bool left, right, up, down; bool left, right, up, down;
}; };
struct gameState struct gameState
{ {
uint64_t tickNumber;
struct timeval timestamp; struct timeval timestamp;
struct clientMovement clients[16]; struct clientMovement clients[16];
}; };
@ -28,8 +31,16 @@ struct networkThreadArguments
struct gameState * state; struct gameState * state;
}; };
struct inputHistory
{
int16_t start, end;
struct clientInput inputs[256];
};
void updateInput(struct gameState * state, struct clientInput * message); void updateInput(struct gameState * state, struct clientInput * message);
void lerpStates (struct gameState * state, struct gameState * endState);
void doGameTick(struct gameState * state); void doGameTick(struct gameState * state);
#endif #endif