You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

520 lines
21 KiB
C++

#if defined(_WIN32)
#define NOGDI // All GDI defines and routines
#define NOUSER // All USER defines and routines
#define WIN32_LEAN_AND_MEAN
#include <windows.h> // or any library that uses Windows.h
#endif
#if defined(_WIN32) // raylib uses these names as function parameters
#undef near
#undef far
#endif
#include <iostream>
#define _USE_MATH_DEFINES
#include <cmath>
#include <cstring>
#include <ctime>
#include <cerrno>
#include <sstream>
#include "includes/raylib-cpp/raylib-cpp.hpp"
#define RAYGUI_IMPLEMENTATION
#include "includes/raygui/raygui.h"
#include "includes/raygui/style_dark.h"
#include "includes/paddle.hpp"
#include "includes/ball.hpp"
#include "includes/connect_code.hpp"
#include "includes/client.hpp"
#include "includes/server.hpp"
#include "includes/exception_consts.hpp"
#include "includes/check_input.hpp"
#include "includes/display_text.hpp"
#include "includes/easysock.h"
#include "netpong-serialization/includes/serialization.h"
#include "includes/timer.h"
/* Global variables used to instantiate structs */
const int WIDTH = 1500;
const int HEIGHT = 600;
const int RECT_H = HEIGHT / 3;
const int RECT_W = 30;
const int PADDLE_SPEED = 8;
const int CIRC_RAD = 10;
const float BASE_BOUNCE_DEG = 45;
const float BASE_BOUNCE_RAD = (BASE_BOUNCE_DEG / 180.0) * M_PI;
const float BASE_SPEED_COMPONENTS = 15;
const float BASE_SPEED = sqrt(powf(BASE_SPEED_COMPONENTS, 2) * 2);
std::string HELP_TEXT = "\nnetpong - A networked pong game for the internet era.\n"
"\n"
"Usage: \n"
"netpong [MODE] [ADDRESS PORT]|[CODE]\n"
"\n"
"MODE: \n"
"-S : Server mode. Starts a server to allow the other player to connect.\n"
"IP address and port must be specified.\n"
"\n"
"-C: Client mode. Connects to a server, using the provided connection code.\n"
"\n"
"If no mode is specified, single player mode is used as default.\n"
"\n"
"CONTROLS:"
"\'W\' and \'S\' control left paddle (AKA client paddle)\n"
"Up and Down arrow keys control right paddle (AKA server paddle)\n";
/* Simple function to return 1 if a value is positive, and -1 if it is negative */
int signum(int num) {
int retval = 0;
(num > 0) ? retval = 1 : retval = -1;
return retval;
}
raylib::Vector2 changeVelocityAfterCollision(Paddle paddle, Ball ball) {
float paddle_mid_y = (paddle.getRect().y + paddle.getRect().GetHeight()) / 2.0; /* Middle y value of rectangle */
float ball_y = ball.pos.y; /* Y co-ordinate of ball */
float offset = paddle_mid_y - ball_y; /* Subtracting the ball coordinate will give us a value between -paddle_mid_y (represents bottom of paddle) and +paddle_mid_y (represents top of paddle) */
offset /= (paddle.getRect().GetHeight()); /* Normalize the value, by dividing it by its maximum magnitude. It is now a value between -1 and 1. */
offset *= 0.8 + (float)(std::rand()) / (float) (RAND_MAX / ( 1.2 - 0.8)); // Generate a random float from 0.8 to 1.2
float bounce_angle = offset * BASE_BOUNCE_RAD; /* Calculate the actual bounce angle from the base bounce angle. */
/* Calculate new velocities as multiples of the original velocity. I use sine and cosine, because when the ball hits the paddle
perpendicular to it (bounce angle is 0), the y_velocity should be 0 (i.e. It should bounce straight back). The sin function does
this for us. A similar reasoning was employed for the use of cosine */
float new_x_vel = abs(BASE_SPEED * cosf(bounce_angle)) * (-1 * signum(ball.vel.x)); /* Reverse the sign of the x-velocity */
float new_y_vel = abs(BASE_SPEED * sinf(bounce_angle)) * signum(ball.vel.y); /* Keep the sign of the y-velocity */
return raylib::Vector2(new_x_vel, new_y_vel);
}
/* Checks the number and type of the command-line arguments. Throws an exception
if the args are invalid. DOES NOT PROCESS VALID ARGUMENTS. */
void check_num_args(int argc, char** argv) {
if (argc > 4) {
throw std::invalid_argument("ARGUMENT ERROR: Too many arguments. To view syntax, use -h or --help.");
}
if (argc > 1) { // Either server or client mode
if (std::string(argv[1]) == "-S") {
if (argc < 4) { // Server mode but no address and/or port
throw std::invalid_argument("ARGUMENT ERROR: Server mode specified without any address or port.");
}
}
else if (std::string(argv[1]) == "-C") {
if (argc < 3) { // Client mode but no code
throw std::invalid_argument("ARGUMENT ERRROR: Client mode specified without any code.");
}
}
else if (std::string(argv[1]) == "-h" || std::string(argv[1]) == "--help") {
throw std::invalid_argument(HELP_TEXT); // I am abusing the exception mechanism here, so that I can ensure that the caller quits the program after printing the help message.
} else {
throw std::invalid_argument("Unrecognized argument.");
}
}
return;
}
int main(int argc, char** argv) {
/* Check the number and validity of command-line arguments. Invalid arguments
will throw an exception. */
try {
check_num_args(argc, argv);
} catch (std::invalid_argument& inv) {
std::cout << inv.what() << std::endl;
return -1;
}
/* From here on, we assume that:
a. The program was started with no arguments (User is prompted in GUI), OR
b. The program was started in server mode, and an additional was given, OR
c. The program was started in client mode, and an additional argument was given. */
/* GameType struct, to define whether the game is in single or multi-player mode, and
to hold the appropriate socket */
GameType type;
/* Check if game was started in server or client mode, and call the appropriate function to process the arguments.
If game was started in single-player mode (i.e. with no arguments), then the user is prompted in the GUI. */
try { // I put this try-catch block outside the if-statement because the exception handling is the same for both client and server.
if (argc > 1) { // Server or client mode
if (std::string(argv[1]) == "-S") { // Server mode
type = check_server(argv[2], argv[3], IF_CLI);
}
if (std::string(argv[1]) == "-C") { // Client mode
type = check_client(argv[2], IF_CLI);
}
}
} catch (std::invalid_argument& inv) {
std::cout << inv.what() << std::endl;
return -1;
} catch (int err) {
std::cout << strerror(err) << std::endl;
return -1;
}
/* Initialize window and other variables */
SetTraceLogLevel(LOG_NONE);
raylib::Window window = raylib::Window(WIDTH, HEIGHT, "Pong");
window.ClearBackground(BLACK);
SetTargetFPS(60);
SetExitKey(KEY_Q);
std::string points_str = std::string("0\t\t0");
bool game_started = false;
srand(std::time(NULL));
/* If there were no command-line arguments, the user is prompted in the GUI */
if (argc == 1) {
/* Display a drop-down menu, to allow user to pick between Single player, server and client. This section of the code uses the raygui library, and is written in C. */
GuiLoadStyleDark(); // Load the dark theme style
/* Modify the default style, by changing font size and spacing */
int font_size = 25;
int font_spacing = 2;
GuiSetStyle(DEFAULT, TEXT_SIZE, font_size);
GuiSetStyle(DEFAULT, TEXT_SPACING, font_spacing);
/* Set variables to position objects on screen */
int selected_item = 0; // variable to hold the index of the selected item
const char* text_to_display = "Select Game Mode"; // Text to display
/* Size of the label, drop down box and button */
Vector2 label_size = MeasureTextEx(GetFontDefault(), text_to_display, font_size, font_spacing); // Set the size based on the width of the string to print, the font size and the text spacing. I added 1 to font_size and font_spacing, to account for any possible rounding errors, since the function expects floats.
Vector2 box_size = Vector2{label_size.x, HEIGHT / 20};
bool is_being_edited = false; // Indicates whether the drop-down menu is being 'edited' i.e. whether an option is being selected
bool button_pressed = false; // Indicates whether the submit button has been pressed
while (button_pressed == false) {
if (WindowShouldClose()) {
CloseWindow();
return 0;
}
BeginDrawing();
ClearBackground(BLACK);
GuiLabel(Rectangle{(WIDTH/2)-(label_size.x/2), (HEIGHT/8), label_size.x, label_size.y}, text_to_display); // Label to display text on top
if (is_being_edited) {
GuiLock(); // If the drop-down menu is being 'edited', we need to prevent the user from modifying any other aspect of the UI
}
/* Button that allows user to proceed */
button_pressed = GuiButton(Rectangle{(WIDTH/2)-(box_size.x/2), (HEIGHT/2) + (HEIGHT/8), box_size.x, box_size.y}, "Continue");
/* Drop-down menu, that allows user to select game mode */
if (GuiDropdownBox(Rectangle{(WIDTH/2) - (box_size.x/2), (HEIGHT/2) - (HEIGHT/8), box_size.x, box_size.y}, "SINGLE;CLIENT;SERVER", &selected_item, is_being_edited)) { // This function returns != 0 if there was a mouse click inside the dropdown area
is_being_edited = !is_being_edited; // If the dropdown menu was selected, then it is being edited (or not being edited, if it previously was).
}
GuiUnlock();
EndDrawing();
}
/* Single player mode */
if (selected_item == M_SINGLE) {
type.mode = M_SINGLE;
type.netsock = NULL;
GuiSetStyle(DEFAULT, TEXT_WRAP_MODE, TEXT_WRAP_WORD); // Enable text wrapping so that the long text, displayed below, will be wrapped
BeginDrawing();
ClearBackground(BLACK);
GuiLabel(Rectangle{(WIDTH/2)-(WIDTH/8), (HEIGHT/2)-(HEIGHT/8), WIDTH/4, HEIGHT/4}, "W and S control left paddle, Up and Down arrow keys control right paddle. Good luck!");
EndDrawing();
Timer timer = timer_init(5);
while (!timer_done(timer));
}
/* Server mode, ask user to input IP address and port */
if (selected_item == M_SERVER) {
button_pressed = false; // Whether submit button is pressed
char* ip_text = (char *)calloc(150, sizeof(char)); // Holds input of IP text box
char* port_text = (char *)calloc(20, sizeof(char)); // Holds input of port text box
const char* ip_label = "Local IP address";
const char* port_label = "Port number (1024 - 65535)";
int port_label_x_size = MeasureTextEx(GetFontDefault(), port_label, font_size, font_spacing).x; // Custom size for port label, because it's long
bool editing_ip = false; // Indicates whether the IP address text box is being edited
bool editing_port = false; // Indicates whether the port text box is being edited
while (button_pressed == false || ((strlen(ip_text) == 0) || (strlen(port_text) == 0))) {
if (WindowShouldClose()) {
CloseWindow();
return 0;
}
BeginDrawing();
ClearBackground(BLACK);
/* Label and text box for IP address */
GuiLabel(Rectangle{(WIDTH/2)-(label_size.x/2), (HEIGHT/2) - (HEIGHT/6) - label_size.y - 10, label_size.x, label_size.y}, ip_label); // Label to display text on top
/* The reason this if statement exists, is largely the same as the reasoning for the drop-down menu. We want to make the text box editable
if it has been clicked. If it is already editable, we want to make it read-only if the user clicks outside the box. This functionality
is mostly handled in the GuiTextBox function. If the text box is in edit mode, this function returns nonzero if the user clicks INSIDE
the box. If the text box is in editable mode, this function returns nonzero if the user clicks OUTSIDE the box. */
if (GuiTextBox(Rectangle{(WIDTH/2) - (box_size.x/2), (HEIGHT/2) - (HEIGHT/6), box_size.x, box_size.y}, ip_text, 100, editing_ip)) {
editing_ip = !editing_ip;
}
/* Label and text box for port. See above for explanation of if statement. */
GuiLabel(Rectangle{(WIDTH/2)-(label_size.x/2), (HEIGHT/2) - label_size.y, port_label_x_size }, port_label); // Label to display text on top
if (GuiTextBox(Rectangle{(WIDTH/2) - (box_size.x/2), (HEIGHT/2), box_size.x, box_size.y}, port_text, 100, editing_port)) {
editing_port = !editing_port;
}
button_pressed = GuiButton(Rectangle{(WIDTH/2) - (box_size.x/2), (HEIGHT/2) + (HEIGHT/6), box_size.x, box_size.y}, "Start Server");
EndDrawing();
}
try {
type = check_server(ip_text, port_text, IF_GUI);
} catch (int e) {
display_and_exit_raygui(std::string(std::strerror(e)) + "\nClosing game...", 2); // The server constructor throws the errno if it cannot create a socket
free(ip_text);
free(port_text);
return -1;
} catch (std::invalid_argument& inv) {
display_and_exit_raygui(std::string(inv.what()) + "\nClosing game...", 2);
free(ip_text);
free(port_text);
return -1;
}
free(ip_text);
free(port_text);
}
if (selected_item == M_CLIENT) {
button_pressed = false; // Whether submit button is pressed
char* code_text = (char *)calloc(150, sizeof(char)); // Holds the connect code
const char* code_label = "Enter code:";
bool editing_code = false; // Indicates whether the port text box is being edited
while (button_pressed == false || ((strlen(code_text) == 0))) {
if (WindowShouldClose()) {
CloseWindow();
return 0;
}
BeginDrawing();
ClearBackground(BLACK);
/* Label and text box for IP address */
GuiLabel(Rectangle{(WIDTH/2)-(label_size.x/2), (HEIGHT/2) - (HEIGHT/6) - label_size.y - 10, label_size.x, label_size.y}, code_label);
if (GuiTextBox(Rectangle{(WIDTH/2) - (box_size.x/2), (HEIGHT/2) - (HEIGHT/6), box_size.x, box_size.y}, code_text, 100, editing_code)) {
editing_code = !editing_code;
}
button_pressed = GuiButton(Rectangle{(WIDTH/2) - (box_size.x/2), (HEIGHT/2) + (HEIGHT/6), box_size.x, box_size.y}, "Connect");
EndDrawing();
}
try {
type = check_client(code_text, IF_GUI);
} catch (int e) {
display_and_exit_raygui(std::string(std::strerror(e)) + "\nClosing game...", 2); // The client constructor throws the errno if it cannot create a socket
return -1;
} catch (std::invalid_argument& inv) {
display_and_exit_raygui(std::string(inv.what()) + "\nClosing game...", 2);
return -1;
}
free(code_text);
}
}
/* Variable to store the response given by the other player */
std::string response;
Serial_Data response_data;
/* Vector to store peer paddle position */
raylib::Vector2 peer_pos;
/* Byte array to hold the result of serializing a struct (in order to send it through a socket) */
Serial_Data to_send_data;
std::string to_send_string;
/* Instantiate Paddle and Ball objects */
Paddle pad1 = Paddle(10, (HEIGHT / 2) - (RECT_H / 2), RECT_W, RECT_H);
Paddle pad2 = Paddle(window.GetWidth() - RECT_W - 10, (HEIGHT / 2) - (RECT_H / 2), RECT_W, RECT_H);
Ball ball = Ball(window.GetWidth()/2, window.GetHeight()/2, CIRC_RAD, BASE_SPEED, 0);
window.BeginDrawing();
window.ClearBackground(BLACK);
pad1.draw();
pad2.draw();
ball.draw();
window.EndDrawing();
/* Main loop */
while (!window.ShouldClose()) {
if (!game_started) {
/* For the server, or if game is being played in single-player mode */
if ((type.mode == M_SERVER || type.mode == M_SINGLE) && IsKeyDown(KEY_SPACE)) {
game_started = true;
/* Send a start message to the client */
if (type.mode == M_SERVER) {
type.netsock->sendAll("S");
}
}
/* For client (wait for start or quit message from server): When the peer quits the
game, it sends a serialized struct, containing all zeros, with the last bit turned
on as a flag. We catch this zero bit, as it indicates that the peer quit the game. */
if (type.mode == M_CLIENT) {
do {
response = type.netsock->recvAll();
} while (response[0] != 'S' && response[0] != 0);
if (response[0] == 0) {
CloseWindow();
std::cout << "Peer unexpectedly quit game." << std::endl;
return -1;
}
game_started = true;
std::cout << "Game has been started by server." << std::endl;
}
}
if (game_started) {
/* Serialize the data that we need to send, and then send it to the peer paddle */
if (type.mode == M_SERVER) {
/* Serial_create_data creates a Serial_Data struct from our values.
Paddle 2 is controlled by the server, Paddle 1, by the client.*/
to_send_data = Serial_create_data(pad2.getRect().x, pad2.getRect().y, ball.pos.x, ball.pos.y, false);
}
else if (type.mode == M_CLIENT) {
/* The _server_ is the authoritative peer for the ball position, so the client sends (0, 0) as the ball position instead of actually sending a position */
to_send_data = Serial_create_data(pad1.getRect().x, pad1.getRect().y, 0, 0, false);
}
/* Only send and receive data if the game is not in single player mode */
if (type.mode != M_SINGLE) {
/* Serial_serialize serializes the struct into a byte_array. Since sendAll accepts a string, we have to convert this byte array into a string. */
type.netsock->sendAll((char *)Serial_serialize(to_send_data), sizeof(Serial_Data) + 1);
/* Create Serial_data struct from the response of the server. Since recvAll returns a char*, we need to convert it to a byte array */
uint8_t* response_array = (uint8_t *)(type.netsock->recvAll());
if (response_array != NULL) {
response_data = Serial_deserialize(response_array);
} else {
/* If the response is NULL, that means it timed-out. In this case, there's no value to print */
std::cout << "NOTHING RECEIVED" << std::endl;
}
free(response_array);
}
/* Check to see if peer has quit the game */
if (response_data.should_quit == true) {
std::cout << "Peer unexpectedly quit game." << std::endl;
break; // Break out of main game loop
}
/* Left paddle (controlled by client) - I use type.mode != M_SERVER, because I also want the single player
mode to be able to control the paddle. Therefore, the only mode that _can't_ control the paddle is the server
mode. */
/* Down motion */
if (IsKeyPressed(KEY_S) && type.mode != M_SERVER) {
pad1.velocity.y = PADDLE_SPEED; /* Set positive (downward) velocity, since (0,0) is top-left */
}
/* Up motion */
if (IsKeyPressed(KEY_W) && type.mode != M_SERVER) {
pad1.velocity.y = (-1) * PADDLE_SPEED; /* Set negative (upward) velocity */
}
/* Stop */
if (((IsKeyReleased(KEY_S) || IsKeyReleased(KEY_W))) && (type.mode != M_SERVER)) {
pad1.velocity.y = 0;
}
/* Right paddle - controlled by server - See above for why I used '!= M_CLIENT' instead of '== M_SERVER' */
/* Down */
if (IsKeyPressed(KEY_DOWN) && type.mode != M_CLIENT) {
pad2.velocity.y = PADDLE_SPEED;
}
/* Up */
if (IsKeyPressed(KEY_UP) && type.mode != M_CLIENT) {
pad2.velocity.y = (-1) * PADDLE_SPEED;
}
/* Stop */
if ((IsKeyReleased(KEY_UP) || IsKeyReleased(KEY_DOWN)) && type.mode != M_CLIENT) {
pad2.velocity.y = 0;
}
/* Why did I use 'type.mode != M_CLIENT'? - The client should set the ball position solely based
on the data sent by the server. It doesn't have to do any calculations of its own. */
if (type.mode != M_CLIENT) {
/* Update ball velocity based on collision detection */
if (pad1.getRect().CheckCollision(ball.pos, ball.radius)) { /* Collision with paddle 1 */
ball.pos.x = pad1.getRect().x + pad1.getRect().GetWidth() + ball.radius + 1; /* Ensuring that the ball doesn't get stuck inside the paddle */
ball.vel = changeVelocityAfterCollision(pad1, ball);
}
if (pad2.getRect().CheckCollision(ball.pos, ball.radius)) { /* Collision with paddle 2 */
ball.pos.x = pad2.getRect().x - ball.radius - 1;
ball.vel = changeVelocityAfterCollision(pad2, ball);
}
} else {
ball.setPosition(raylib::Vector2(response_data.ball_x, response_data.ball_y));
}
if (ball.pos.x + ball.radius >= window.GetWidth()) { /* Collision with right wall */
pad1.incrementPoints();
game_started = false;
ball.reset();
pad1.reset();
pad2.reset();
}
if (ball.pos.x - ball.radius <= 0) { /* Collision with left wall */
pad2.incrementPoints();
game_started = false;
ball.reset();
pad1.reset();
pad2.reset();
}
if (ball.pos.y - ball.radius <= 0) { /* Collision with top wall */
ball.pos.y = ball.radius + 1;
ball.vel.y = ball.vel.y * -1;
}
if (ball.pos.y + ball.radius >= window.GetHeight()) { /* Collision with bottom wall */
ball.pos.y = HEIGHT - ball.radius - 1;
ball.vel.y = ball.vel.y * -1;
}
/* Update positions based on velocities - Client only updates pad1 (and receives data for pad2),
server updates pad2 and ball (and receives data for pad1) */
if (type.mode != M_CLIENT) {
ball.updatePosition();
pad2.updatePosition();
} else {
pad2.setPosition(response_data.pad_x, response_data.pad_y);
}
if (type.mode != M_SERVER) {
pad1.updatePosition();
} else {
pad1.setPosition(response_data.pad_x, response_data.pad_y);
}
}
/* Draw objects */
window.BeginDrawing();
window.ClearBackground(BLACK);
points_str = std::to_string(pad1.getPoints()) + "\t\t" + std::to_string(pad2.getPoints());
raylib::Text::Draw( points_str, (WIDTH / 2) - 30, HEIGHT / 10, 30, raylib::Color::White() );
pad1.draw();
pad2.draw();
ball.draw();
window.EndDrawing();
}
/* If the game has been quit, ask the peer to quit as well */
if (type.mode != M_SINGLE) {
to_send_data = Serial_create_data(0, 0, 0, 0, true);
type.netsock->sendAll((char *)Serial_serialize(to_send_data), sizeof(Serial_Data) + 1);
sock_quit();
}
window.Close();
return 0;
}