// Client-Side Prediction Test - Client // Barra Ó Catháin - 2023 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "../cspt-state.h" #include "../cspt-message.h" #define ENABLE_CLIENT_SIDE_PREDICTION #define ENABLE_SERVER_RECONCILLIATION uint8_t colours[16][3] = { {255, 255, 255}, {100, 176, 254}, {147, 122, 254}, {199, 119, 254}, {243, 106, 254}, {254, 110, 205}, {254, 130, 112}, {235, 159, 35}, {189, 191, 0}, {137, 217, 0}, {93 , 229, 48}, {69 , 225, 130}, {72 , 206, 223} }; // A structure for binding together the shared state between threads: struct threadParameters { char * ipAddress; bool * keepRunning; struct gameState * state; struct clientInput * message; struct inputHistory * inputBuffer; }; void DrawCircle(SDL_Renderer * renderer, int32_t centreX, int32_t centreY, int32_t radius) { const int32_t diameter = (radius * 2); int32_t x = (radius - 1); int32_t y = 0; int32_t tx = 1; int32_t ty = 1; int32_t error = (tx - diameter); while (x >= y) { // Each of the following renders an octant of the circle SDL_RenderDrawPoint(renderer, centreX + x, centreY - y); SDL_RenderDrawPoint(renderer, centreX + x, centreY + y); SDL_RenderDrawPoint(renderer, centreX - x, centreY - y); SDL_RenderDrawPoint(renderer, centreX - x, centreY + y); SDL_RenderDrawPoint(renderer, centreX + y, centreY - x); SDL_RenderDrawPoint(renderer, centreX + y, centreY + x); SDL_RenderDrawPoint(renderer, centreX - y, centreY - x); SDL_RenderDrawPoint(renderer, centreX - y, centreY + x); if (error <= 0) { ++y; error += ty; ty += 2; } if (error > 0) { --x; tx += 2; error += (tx - diameter); } } } void * networkHandler(void * parameters) { // Declare the needed variables for the thread: struct threadParameters * arguments = parameters; struct sockaddr_in serverAddress; int udpSocket = 0; // Point at the server: serverAddress.sin_family = AF_INET; serverAddress.sin_addr.s_addr = inet_addr(arguments->ipAddress); serverAddress.sin_port = htons(5200); // Create a UDP socket to send through: udpSocket = socket(AF_INET, SOCK_DGRAM, 0); // Configure a timeout for recieving: struct timeval timeout; timeout.tv_sec = 0; timeout.tv_usec = 1000; setsockopt(udpSocket, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout)); // Store the state we recieve from the network: struct gameState * updatedState = calloc(1, sizeof(struct gameState)); while (true) { // Send our input, recieve the state: sendto(udpSocket, arguments->message, sizeof(struct clientInput), 0, (struct sockaddr *)&serverAddress, sizeof(struct sockaddr_in)); recvfrom(udpSocket, updatedState, sizeof(struct gameState), 0, NULL, NULL); // Only update the state if the given state is more recent than the current state: if(updatedState->timestamp.tv_sec > arguments->state->timestamp.tv_sec) { memcpy(arguments->state, updatedState, sizeof(struct gameState)); #ifdef ENABLE_SERVER_RECONCILLIATION // Throw away any already acknowledged inputs: while (arguments->inputBuffer->start != -1 && arguments->inputBuffer->inputs[arguments->inputBuffer->start].tickNumber < arguments->state->tickNumber) { arguments->inputBuffer->start = (arguments->inputBuffer->start + 1) % 256; if(arguments->inputBuffer->start == arguments->inputBuffer->end) { arguments->inputBuffer->start = -1; } } uint8_t currentMessage = arguments->inputBuffer->start; uint64_t lastTickNumber = arguments->inputBuffer->inputs[arguments->inputBuffer->start].tickNumber; // Re-apply the currently unused messages: while (currentMessage != 1 && currentMessage != arguments->inputBuffer->end) { updateInput(arguments->state, &arguments->inputBuffer->inputs[currentMessage]); currentMessage = (currentMessage + 1) % 256; if (arguments->inputBuffer->inputs[currentMessage].tickNumber != lastTickNumber) { doGameTick(arguments->state); } } #endif } else if(updatedState->timestamp.tv_sec == arguments->state->timestamp.tv_sec && updatedState->timestamp.tv_usec > arguments->state->timestamp.tv_usec) { memcpy(arguments->state, updatedState, sizeof(struct gameState)); #ifdef ENABLE_SERVER_RECONCILLIATION // Throw away any already acknowledged inputs: while (arguments->inputBuffer->start != -1 && arguments->inputBuffer->inputs[arguments->inputBuffer->start].tickNumber < arguments->state->tickNumber) { arguments->inputBuffer->start = (arguments->inputBuffer->start + 1) % 256; if(arguments->inputBuffer->start == arguments->inputBuffer->end) { arguments->inputBuffer->start = -1; } } uint8_t currentMessage = arguments->inputBuffer->start; uint64_t lastTickNumber = arguments->inputBuffer->inputs[arguments->inputBuffer->start].tickNumber; // Re-apply the currently unused messages: while (currentMessage != 1 && currentMessage != arguments->inputBuffer->end) { updateInput(arguments->state, &arguments->inputBuffer->inputs[currentMessage]); currentMessage = (currentMessage + 1) % 256; if (arguments->inputBuffer->inputs[currentMessage].tickNumber != lastTickNumber) { doGameTick(arguments->state); } } #endif } } } void * gameThreadHandler(void * parameters) { struct threadParameters * arguments = parameters; #ifdef ENABLE_CLIENT_SIDE_PREDICTION while (true) { updateInput(arguments->state, arguments->message); #ifdef ENABLE_SERVER_RECONCILLIATION if(arguments->inputBuffer->start = -1) { memcpy(&arguments->inputBuffer->inputs[0], arguments->message, sizeof(struct clientInput)); arguments->inputBuffer->start = 0; arguments->inputBuffer->end = 1; } else { memcpy(&arguments->inputBuffer->inputs[arguments->inputBuffer->end], arguments->message, sizeof(struct clientInput)); arguments->inputBuffer->end = (arguments->inputBuffer->end + 1) % 256; } #endif doGameTick(arguments->state); usleep(15625); } #endif } void * graphicsThreadHandler(void * parameters) { bool * keepRunning = ((struct threadParameters *)parameters)->keepRunning; struct gameState * state = ((struct threadParameters *)parameters)->state; struct clientInput * message = ((struct threadParameters *)parameters)->message; uint32_t rendererFlags = SDL_RENDERER_ACCELERATED; // Create an SDL window and rendering context in that window: SDL_Window * window = SDL_CreateWindow("CSPT-Client", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 512, 512, 0); SDL_Renderer * renderer = SDL_CreateRenderer(window, -1, rendererFlags); SDL_Event event; while (true) { while (SDL_PollEvent(&event)) { switch (event.type) { case SDL_KEYDOWN: { switch (event.key.keysym.sym) { case SDLK_LEFT: { message->tickNumber = state->tickNumber; message->left = true; break; } case SDLK_RIGHT: { message->tickNumber = state->tickNumber; message->right = true; break; } case SDLK_UP: { message->tickNumber = state->tickNumber; message->up = true; break; } case SDLK_DOWN: { message->tickNumber = state->tickNumber; message->down = true; break; } default: { break; } } break; } case SDL_KEYUP: { switch (event.key.keysym.sym) { case SDLK_LEFT: { message->tickNumber = state->tickNumber; message->left = false; break; } case SDLK_RIGHT: { message->tickNumber = state->tickNumber; message->right = false; break; } case SDLK_UP: { message->tickNumber = state->tickNumber; message->up = false; break; } case SDLK_DOWN: { message->tickNumber = state->tickNumber; message->down = false; break; } } break; } case SDL_QUIT: { *keepRunning = false; break; } default: { break; } } } // Clear the screen, filling it with black: SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255); SDL_RenderClear(renderer); // Draw all the connected clients: for (int index = 0; index < 16; index++) { 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); // Draw the circle: DrawCircle(renderer, (long)(state->clients[index].xPosition), (long)(state->clients[index].yPosition), 10); } } // Present the rendered graphics: SDL_RenderPresent(renderer); // Delay enough so that we only hit 144 frames: SDL_Delay(1000/144); } return NULL; } int main(int argc, char ** argv) { int serverSocket = 0; bool keepRunning = true; uint8_t currentPlayerNumber = 0; struct sockaddr_in serverAddress; struct CsptMessage currentMessage; pthread_t graphicsThread, networkThread, gameThread; struct gameState * currentState = calloc(1, sizeof(struct gameState)); struct clientInput * clientInput = calloc(1, sizeof(struct gameState)); // Say hello: printf("Client-Side Prediction Test - Client Starting.\n"); // Give me a socket, and make sure it's working: serverSocket = socket(AF_INET, SOCK_STREAM, 0); if (serverSocket == -1) { printf("Socket creation failed.\n"); exit(EXIT_FAILURE); } // Set our IP address and port. Default to localhost for testing: char * ipAddress = calloc(46, sizeof(char)); if (argc < 2) { strncpy(ipAddress, "127.0.0.1", 10); } else { strncpy(ipAddress, argv[1], strlen(argv[1])); } // Create an address struct to point at the server: serverAddress.sin_family = AF_INET; serverAddress.sin_addr.s_addr = inet_addr(ipAddress); serverAddress.sin_port = htons(5200); // Connect to the server: if (connect(serverSocket, (struct sockaddr *)&serverAddress, sizeof(struct sockaddr_in)) != 0) { fprintf(stderr, "Connecting to the server failed.\n"); exit(0); } currentMessage.type = 0; currentMessage.content = 0; send(serverSocket, ¤tMessage, sizeof(struct CsptMessage), 0); recv(serverSocket, ¤tMessage, sizeof(struct CsptMessage), 0); if (currentMessage.type == 0) { currentPlayerNumber = currentMessage.content; clientInput->clientNumber = currentPlayerNumber; } printf("Registered as: %u\n", currentPlayerNumber); // Configure the thread parameters: struct threadParameters parameters; parameters.state = currentState; parameters.message = clientInput; 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: pthread_create(&gameThread, NULL, gameThreadHandler, ¶meters); pthread_create(&networkThread, NULL, networkHandler, ¶meters); pthread_create(&graphicsThread, NULL, graphicsThreadHandler, ¶meters); while (keepRunning) { if (recv(serverSocket, ¤tMessage, sizeof(struct CsptMessage), 0) > 0) { switch (currentMessage.type) { case 1: { // We've been told to disconnect: shutdown(serverSocket, SHUT_RDWR); serverSocket = 0; keepRunning = false; break; } case 2: { // Pinged, so we now must pong. currentMessage.type = 3; currentMessage.content = 0; send(serverSocket, ¤tMessage, sizeof(struct CsptMessage), 0); break; } } } else { // Say goodbye to the server: currentMessage.type = 1; currentMessage.content = 0; // Send the goodbye message and shutdown: send(serverSocket, ¤tMessage, sizeof(struct CsptMessage), 0); shutdown(serverSocket, SHUT_RDWR); serverSocket = 0; keepRunning = false; } } // Say goodbye to the server: currentMessage.type = 1; currentMessage.content = 0; // Send the goodbye message and shutdown: send(serverSocket, ¤tMessage, sizeof(struct CsptMessage), 0); shutdown(serverSocket, SHUT_RDWR); serverSocket = 0; keepRunning = false; return 0; }