Compare commits

25 Commits

Author SHA1 Message Date
f4c3ef9b19 Renamed file 2024-03-06 15:59:46 -06:00
bb4601c5bd Chenged FPS to 60 for release build 2024-03-06 15:59:12 -06:00
45aa6ba4bc Added version number, added an additional compiler argument for Windows (doesn't work yet) 2024-03-06 15:58:47 -06:00
c3f6ae0ae9 Added .gitignore 2024-03-06 13:09:06 -06:00
21c864da60 Created script to copy DLLs into application fodler on mingw 2024-03-06 13:06:29 -06:00
a2fed8e4b0 Updated meson.build to include additional DLLs needed on Windows 2024-03-06 12:47:58 -06:00
1196ebd228 Updated README 2024-03-05 22:42:28 -05:00
3a9a32d7e3 Updated README 2024-03-05 22:29:09 -05:00
a5202ad85a Add support for building a statically linked version of the game, by specifying a command-line flag 2024-03-05 22:26:40 -05:00
61a856e88f Added raylib submodule, under subprojects directory 2024-03-05 21:03:08 -05:00
6170c95666 Delete raylib submodule and move it inside 'subprojects' 2024-03-05 20:58:41 -05:00
d6f597d8c0 Added raylib submodule 2024-03-05 20:47:18 -05:00
591c3b16a2 Fixed stupid error (using 'meson' instead of 'compiler') 2024-03-05 16:39:10 -05:00
2ea5bb4fe2 Find raylib even if pkg-config is not found 2024-03-05 16:36:43 -05:00
f961db5e58 Added code to send quit message only if game is not in single player mode 2024-03-05 07:57:20 -05:00
64aa4b1850 Replaced 'linux' with '__unix__' because the same header files are included on macOS as well. 2024-03-05 07:50:32 -05:00
0e504060cf Modified README 2024-03-04 23:51:48 -05:00
d69b627bb1 Added README 2024-03-04 23:50:16 -05:00
91bf5e2ce1 Fixed some commenting issues; Added code to detect if game was quit by peer and, if so, quit the game locally 2024-03-01 22:35:31 -05:00
50c090cd88 Added a boolean field to the struct, to indicate whether the game should be quit or not 2024-03-01 22:34:33 -05:00
2c735896df Used unsigned int instead of int when converting to base-10 2024-03-01 11:31:18 -05:00
24c1dd6391 Removed unnecessary variable, and added a (optional) dependency for the ws2_32 library, which is required on Windows 2024-03-01 11:31:18 -05:00
8e3488b904 Added a virtual destructor to the Sock class, which would allow Server and Client to override it 2024-03-01 11:31:15 -05:00
c2c095dfa7 Added code to get a non-const char* from a std::string 2024-03-01 11:31:08 -05:00
047ff602ed Changed 'char*' to 'const char*' because that's what the underlying functions use 2024-03-01 11:31:01 -05:00
17 changed files with 163 additions and 50 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
main.o
easysock.o
pong
release/

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "subprojects/raylib"]
path = subprojects/raylib
url = https://github.com/raysan5/raylib.git

32
README.md Normal file
View File

@@ -0,0 +1,32 @@
## Netpong - A Pong game for the internet era
__Netpong__ is a network-enabled Pong game, written in C++. It enables two players to play against each other, provided an IP address and a port. It also supports a single-player mode.
## How it works
The game has only one runtime dependency: The [raylib](https://www.raylib.com/) graphics system. In order to write idiomatic C++, I chose to use the [raylib-cpp](https://robloach.github.io/raylib-cpp/) wrapper, which provides an object-oriented interface to the Raylib library. However, this wrapper is bundled with the project, and is thus not required to be installed.
## Building
This application uses [Meson](https://mesonbuild.com/) as a build system. To build the application:
1. Install __meson__ from the link above.
2. Install __raylib__ from the link above (THIS IS OPTIONAL, SEE STEP 5)
3. Set up the build directory with the `meson setup build` command.
4. Compile the application, with the existing raylib installation, using `meson compile -C build`.
5. If you don't have raylib installed, you can create a statically linked version of the library on Linux by running the following commands:
```
meson configure -Ddefault_library=static build/
meson compile -C build -Ddefault_library=static
```
## Running
- To run in single-player mode:
- Run the application with no arguments: `build/pong`
- Left paddle is controlled with `W` and `S` keys, right paddle is controlled with `Up` and `Down` arrow keys.
- To run in multi-player mode:
- One player runs the application in Server mode, specifying their IP address and a port: `build/pong -S <ip_address> <port>`
- The other player connects to the first player by running in Client mode, specifying the first player's IP address and port: `build/pong -C <ip_address> <port>`.
- The server controls the left paddle by default (WIP to allow the user to modify this), and the client controls the right paddle.

View File

@@ -11,8 +11,10 @@ namespace connect_code {
/* Tokenizes a string, based on the given delimiter */
std::vector<std::string> tokenize_str(std::string str, std::string delim) {
std::vector<std::string> result;
char* c_str = str.data();
char* c_delim = delim.data();
/* &str[0] is used to convert an std::string to a char*. I tried using string.data(),
but that appears to return a const char*. */
char* c_str = &str[0];
char* c_delim = &delim[0];
char* tok = strtok(c_str, c_delim);
while (tok != NULL) {

21
create_release_build.sh Normal file
View File

@@ -0,0 +1,21 @@
#!/bin/bash
# This script copies required DLLs, and the application itself into a folder called 'release'. It only runs on MinGW.
BASE_DIR=$(dirname $0)
REL_DIR="$BASE_DIR/release/dist"
mkdir -p "$REL_DIR"
# Parse the output of the 'ldd' command, and create a file with the required DLL paths.
ldd build/pong.exe | awk ' NF == 4 {print $3}' > "$BASE_DIR/tmp_file.txt"
# Copy the required DLLs.
cp $(cat "$BASE_DIR/tmp_file.txt") "$REL_DIR"
# Copy the executable itself
cp "$BASE_DIR/build/pong" "$REL_DIR"
# Remove the temporary file.
rm "$BASE_DIR/tmp_file.txt"

View File

@@ -59,7 +59,7 @@ SOCKET create_socket(int network, char transport) {
}
int create_addr(int network, char* address, int port,struct sockaddr* dest) {
int create_addr(int network, const char* address, int port,struct sockaddr* dest) {
if (network == 4) {
struct sockaddr_in listen_address;
@@ -83,7 +83,7 @@ int create_addr(int network, char* address, int port,struct sockaddr* dest) {
}
SOCKET create_local (int network, char transport, char* address, int port,struct sockaddr* addr_struct) {
SOCKET create_local (int network, char transport, const char* address, int port,struct sockaddr* addr_struct) {
int socket = create_socket(network,transport);
if (socket < 0) {
return (-1 * errno);
@@ -109,7 +109,7 @@ SOCKET create_local (int network, char transport, char* address, int port,struct
return socket;
}
SOCKET create_remote (int network,char transport,char* address,int port,struct sockaddr* remote_addr_struct) {
SOCKET create_remote (int network,char transport, const char* address,int port,struct sockaddr* remote_addr_struct) {
struct addrinfo hints; /* Used to tell getaddrinfo what kind of address we want */
struct addrinfo* results; /* Used by getaddrinfo to store the addresses */

View File

@@ -6,7 +6,7 @@
#include <winsock.h>
#include <ws2tcpip.h>
#endif
#ifdef linux
#ifdef __unix__
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
@@ -44,7 +44,7 @@ and dest is a pointer to the sockaddr struct that will be filled in.
The function returns with -202 if the network parameter contained neither '4'
nor '6'. */
int create_addr(int network, char* address, int port,struct sockaddr* dest);
int create_addr(int network, const char* address, int port,struct sockaddr* dest);
@@ -54,7 +54,7 @@ same as above.
It prints the error returned by 'bind' if something went wrong, and returns ( -1 * errno ).*/
SOCKET create_local (int network, char transport, char* address, int port,struct sockaddr* addr_struct);
SOCKET create_local (int network, char transport, const char* address, int port,struct sockaddr* addr_struct);
/* This function utilizes the same functions as 'create_local' but _connects_ to the
@@ -63,7 +63,7 @@ as above. This function needs an empty 'sockaddr *' structure passed to it, whic
If something goes wrong, this function returns with ( -1 * errno ). */
SOCKET create_remote (int network,char transport,char* address,int port,struct sockaddr* remote_addr_struct);
SOCKET create_remote (int network,char transport, const char* address,int port,struct sockaddr* remote_addr_struct);
/* check_ip_ver - This function checks if the given string is an IPv4 address (returns 4),
IPv6 address (returns 6) or neither (returns -1). */

View File

@@ -3,10 +3,10 @@
#include <string>
/* Convert the given value from the given base, to base 10 */
int to_decimal(std::string num, int from_base);
unsigned int to_decimal(std::string num, int from_base);
/* Convert the given value from base 10 to the given base */
std::string from_decimal(int num, int to_base);
std::string from_decimal(unsigned int num, int to_base);
/* Convert the given value from 'from_base', to 'to_base' */
std::string base_convert(std::string num, int from_base, int to_base);

View File

@@ -6,17 +6,19 @@ extern "C" {
#endif
#include <stdint.h>
#include <stdbool.h>
/* Struct used to hold the data that will be sent between sockets */
typedef struct {
uint16_t pad_x;
uint16_t pad_y;
uint16_t ball_x;
uint16_t ball_y;
uint16_t pad_x; // X-coordinate of sending paddle
uint16_t pad_y; // Y-coordinate of sending paddle
uint16_t ball_x; // X-coordinate of ball (only the server fills this in)
uint16_t ball_y; // Y-coordinate of ball (only the server fills this in)
bool should_quit; // Flag to indicate whether game should be quit or not
} Serial_Data;
/* Create a Serial_Data struct from float values */
Serial_Data Serial_create_data(float pad_x, float pad_y, float ball_x, float ball_y);
Serial_Data Serial_create_data(float pad_x, float pad_y, float ball_x, float ball_y, bool should_quit);
/* Serialize a struct into a byte array, that can be sent through a socket */
uint8_t* Serial_serialize(Serial_Data data);

View File

@@ -2,7 +2,7 @@
#define _SOCK_CLASS
#include <string>
#ifdef linux
#ifdef __unix__
#include <sys/socket.h>
#endif
#ifdef _WIN32
@@ -34,6 +34,9 @@ public:
/* Default constructor */
Sock() {}
/* Virtual destructor */
virtual ~Sock();
/* Regular constructor - defined in sock.cpp */
Sock(int ip_ver, char protocol, const char* address, int port);

View File

@@ -211,7 +211,7 @@ int main(int argc, char** argv) {
SetTraceLogLevel(LOG_NONE);
raylib::Window window = raylib::Window(WIDTH, HEIGHT, "Pong");
window.ClearBackground(BLACK);
SetTargetFPS(10);
SetTargetFPS(60);
SetExitKey(KEY_Q);
std::string points_str = std::string("0\t\t0");
bool game_started = false;
@@ -264,34 +264,36 @@ int main(int argc, char** argv) {
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, and Serial_serialize serializes it Paddle 2 is controled 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);
/* 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);
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) {
type.netsock->sendAll((char *)Serial_serialize(to_send_data), 9);
/* 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) {
/* If the response is NULL, that means it timed-out. In this case, there's no value to print */
response_data = Serial_deserialize(response_array);
std::cout << response_data.pad_x << "\t" << response_data.pad_y << "\t" << response_data.ball_x << "\t" << response_data.ball_y << std::endl;
} 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;
}
}
/* When updating the paddle positions, update the peer paddle's positions based on the vector set earlier */
std::string to_send = "";
/* Update paddle velocity */
/* 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. */
@@ -327,7 +329,8 @@ int main(int argc, char** argv) {
pad2.velocity.y = 0;
}
/* The client should set the ball position based on the data sent by the server. It doesn't have to do any calculations of its own. */
/* 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 */
@@ -366,7 +369,8 @@ int main(int argc, char** argv) {
ball.vel.y = ball.vel.y * -1;
}
/* Update positions based on velocities - Client only updates pad1, server updates pad2 and ball */
/* 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();
@@ -390,9 +394,15 @@ int main(int argc, char** argv) {
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();
sock_quit();
return 0;
}

View File

@@ -1,11 +1,36 @@
project('Pong', ['cpp', 'c'])
add_global_arguments('-g', '-Wall', '-pedantic', '-Wno-unused-function', '-Werror', language : ['cpp', 'c'])
project('Pong', ['cpp', 'c'], version: '0.1')
add_global_arguments('-g', '-Wall', '-pedantic', '-Wno-unused-function', language : ['cpp', 'c'])
compiler = meson.get_compiler('cpp')
cmake = import('cmake')
raylib = dependency('raylib', native: true)
static_lib_path = ''
if get_option('default_library') == 'shared'
raylib = dependency('raylib', required: false) # Try to find dependency with pkg-config
if not raylib.found()
raylib = compiler.find_library('raylib', has_headers: ['raylib.h', 'raymath.h'], required: true) # Try to manually search for the dependency
endif
# if not raylib.found()
# opt_var = cmake.subproject_options()
# opt_var.add_cmake_defines({'BUILD_SHARED_LIBS' : true})
# opt_var.add_cmake_defines({'CMAKE_SKIP_RPATH' : true})
# raylib_proj = cmake.subproject('raylib', options: opt_var)
# raylib = raylib_proj.dependency('raylib')
# endif
endif
if get_option('default_library') == 'static'
raylib_proj = cmake.subproject('raylib')
raylib = raylib_proj.dependency('raylib')
endif
#For Windows only
ws2_dep = compiler.find_library('ws2_32', required: false)
winmm = compiler.find_library('winmm', required: false)
if build_machine.system() == 'windows'
add_global_arguments('-Wl,--subsystem,windows', '-mwindows', language: ['cpp', 'c']) # Prevent opening console when game is run
endif
executable('pong',
'main.cpp', 'easysock.cpp', 'sock.cpp','paddle.cpp', 'ball.cpp', 'numeric_base.cpp', 'connect_code.cpp', 'server.cpp', 'client.cpp',
'serialization.c',
dependencies: raylib,
dependencies: [raylib, ws2_dep, winmm]
)

View File

@@ -5,10 +5,10 @@
std::string possible_chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
int to_decimal(std::string num, int from_base) {
unsigned int to_decimal(std::string num, int from_base) {
char current_char = 0;
int index = 0;
int value = 0;
unsigned int value = 0;
/* Here, we convert 'num' to decimal (base 10) - Find the index of
every character in the string, in 'possible_chars' and
@@ -23,7 +23,7 @@ int to_decimal(std::string num, int from_base) {
}
/* Convert the given value from base 10 to the given base */
std::string from_decimal(int num, int to_base) {
std::string from_decimal(unsigned int num, int to_base) {
std::string return_val;
int val = 0;
while (num > 0) {
@@ -40,7 +40,7 @@ std::string from_decimal(int num, int to_base) {
/* Convert the given value from 'from_base', to 'to_base' */
std::string base_convert(std::string num, int from_base, int to_base) {
int temp = to_decimal(num, from_base);
unsigned int temp = to_decimal(num, from_base);
std::string result = from_decimal(temp, to_base);
return result;

View File

@@ -1,7 +1,8 @@
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#ifdef linux
#ifdef __unix__
#include <arpa/inet.h>
#endif
#ifdef _WIN32
@@ -11,29 +12,32 @@
/* Takes in float values, casts them to uint16_t and creates a Serial_Data struct */
Serial_Data Serial_create_data(float pad_x, float pad_y, float ball_x, float ball_y) {
Serial_Data Serial_create_data(float pad_x, float pad_y, float ball_x, float ball_y, bool should_quit) {
Serial_Data data;
data.pad_x = (uint16_t)pad_x;
data.pad_y = (uint16_t)pad_y;
data.ball_x = (uint16_t)ball_x;
data.ball_y = (uint16_t)ball_y;
data.should_quit = should_quit;
return data;
}
/* Serializes a 'Data' struct into a byte array, converted to network-byte order */
uint8_t* Serial_serialize(Serial_Data data) {
/* Create a pointer that can fit the entire struct */
uint8_t* serialized = malloc(4 * sizeof(uint16_t) + 1);
uint8_t* serialized = malloc(sizeof(Serial_Data) + 1);
uint8_t* pad_x_ptr;
uint8_t* pad_y_ptr;
uint8_t* ball_x_ptr;
uint8_t* ball_y_ptr;
memset(serialized, 0, 4 * sizeof(uint16_t) + 1); // Zero out the memory
uint8_t* should_quit_ptr;
memset(serialized, 0, sizeof(Serial_Data) + 1); // Zero out the memory
pad_x_ptr = serialized;
pad_y_ptr = pad_x_ptr + sizeof(uint16_t);
ball_x_ptr = pad_y_ptr + sizeof(uint16_t);
ball_y_ptr = ball_x_ptr + sizeof(uint16_t);
should_quit_ptr = ball_y_ptr + sizeof(uint16_t);
*((uint16_t *)pad_x_ptr) = data.pad_x;
*((uint16_t *)pad_x_ptr) = htons(*((uint16_t *)pad_x_ptr));
@@ -46,7 +50,9 @@ uint8_t* Serial_serialize(Serial_Data data) {
*((uint16_t *)ball_y_ptr) = data.ball_y;
*((uint16_t *)ball_y_ptr) = htons(*((uint16_t *)ball_y_ptr));
*(ball_y_ptr + sizeof(uint16_t)) = '\0';
*((bool *)should_quit_ptr) = data.should_quit;
*(should_quit_ptr + sizeof(bool)) = '\0';
return serialized;
}
@@ -59,6 +65,7 @@ Serial_Data Serial_deserialize(uint8_t* serialized) {
uint8_t* pad_y_ptr = serialized + sizeof(uint16_t);
uint8_t* ball_x_ptr = pad_y_ptr + sizeof(uint16_t);
uint8_t* ball_y_ptr = ball_x_ptr + sizeof(uint16_t);
uint8_t* should_quit_ptr = ball_y_ptr + sizeof(uint16_t);
/* Dereference (and cast) the pointers, and store them into the struct */
deserialized.pad_x = *((uint16_t *)pad_x_ptr);
@@ -73,6 +80,8 @@ Serial_Data Serial_deserialize(uint8_t* serialized) {
deserialized.ball_y = *((uint16_t *)ball_y_ptr);
deserialized.ball_y = ntohs(deserialized.ball_y);
deserialized.should_quit = *((bool *)should_quit_ptr);
return deserialized;
}

View File

@@ -1,4 +1,4 @@
#ifdef linux
#ifdef __unix__
#include <sys/socket.h>
#endif
#ifdef _WIN32

View File

@@ -13,6 +13,10 @@ void Sock::create_socket() {
addrlen = sizeof(*dest);
}
/* Virtual destructor, allows 'Server' and 'Client' to override this destructor */
Sock::~Sock() {}
/* Constructor - This function initializes the object attributes with the given
parameters. It throws an exception if an IPv4 address was given, but the type
given is IPv6 (or the other way around). */

1
subprojects/raylib Submodule

Submodule subprojects/raylib added at c7b362d19d