Compare commits

16 Commits

Author SHA1 Message Date
f42ac94a45 Added check to release script, to check if DLL exists 2024-03-19 09:02:48 -05:00
4ff840e91e Fixed boneheaded mistake, where I set REUSEADDR for UDP instead of TCP 2024-03-18 16:03:19 -04:00
fd4ad04aeb Removed unnecessary comment 2024-03-18 16:02:57 -04:00
d842485103 Throw errno instead of errno * -1, if an error is encountered with sending or receiving 2024-03-18 13:43:09 -04:00
3bf65ab8f9 Updated TODO 2024-03-18 13:27:14 -04:00
f3dcbc3b3e Removed check_server_client() function.
It has been replaced by check_server() and check_client().
2024-03-18 13:25:51 -04:00
463dfbd3e5 IPv6 support for RecvAllNB() 2024-03-18 13:22:26 -04:00
d3716536f9 Minor changes 2024-03-18 13:22:12 -04:00
8805402241 Return after catching exception 2024-03-18 13:22:01 -04:00
3d0aeac943 Ensure that REUSEADDR flag is only applied for TCP 2024-03-18 13:21:42 -04:00
c490eaa301 Updated TODO 2024-03-17 23:35:05 -04:00
0e7ebb4d78 Finished integrating check_server() and check_client(), check_server_client() has been commented out. 2024-03-17 23:34:24 -04:00
c94138ad8b Included new file in compilation process 2024-03-17 23:32:56 -04:00
cfbc726dca Renamed functions to make their purpose more explicit 2024-03-17 23:32:37 -04:00
ec2f3320e3 Replaced display_text_centered() with the environment-agnostic display_text() function; Changed function to include parameter to indicate environment type 2024-03-17 23:31:56 -04:00
43ba4aba0c Created a file that contains functions for agnostic text output 2024-03-17 18:09:07 -04:00
13 changed files with 122 additions and 196 deletions

View File

@@ -8,19 +8,20 @@
#include "includes/raygui/raygui.h"
#include "includes/exception_consts.hpp"
#include "includes/raygui_helpers.hpp"
#include "includes/display_text.hpp"
#include "includes/timer.h"
GameType check_server(char* ip_text, char* port_text) {
GameType check_server(char* ip_text, char* port_text, const int if_mode) {
GameType type;
std::string addr;
uint16_t port;
/* Check if IP address and port are in valid forms */
if (check_ip_ver(ip_text) < 0) {
throw std::invalid_argument("Invalid IP address");
throw std::invalid_argument("Invalid IP address.");
}
if (port_to_num(port_text) < 0) {
throw std::invalid_argument("Invalid port");
throw std::invalid_argument("Invalid port.");
}
/* From here on, we assume that the IP and port are valid */
@@ -33,7 +34,7 @@ GameType check_server(char* ip_text, char* port_text) {
/* Create server socket and wait for client to connect */
Server* server = new Server(ES_UDP, addr.data(), port);
server->create_socket();
display_text_centered("Your code is " + code + "\nWaiting for connection...");
display_text("Your code is " + code + "\nWaiting for connection...", if_mode);
std::string response = "";
char* temp_response = NULL;
/* Wait for the client to connect. Since recvAll returns a char*, we need to create a temporary variable to check for NULL.
@@ -44,30 +45,29 @@ GameType check_server(char* ip_text, char* port_text) {
response = std::string(temp_response);
server->sendAll("U2");
display_text_centered("Connection received from " + server->get_peer_addr());
Timer timer = timer_init(3);
while (!timer_done(timer)); // Wait for five seconds
display_text("Connection received from " + server->get_peer_addr(), if_mode);
type.mode = M_SERVER;
type.netsock = server;
return type;
}
GameType check_client(char* code_text) {
GameType check_client(char* code_text, const int if_mode) {
GameType type;
std::vector<std::string> addr_port;
std::string connect_code = std::string(code_text); /* The connect code is a special string, that contains the server address and port. It is given by the server. */
try {
addr_port = connect_code::decode(connect_code);
if (check_ip_ver(addr_port[0].data()) < 0) {
throw std::invalid_argument("Invalid code entered.");
}
Client* client = new Client(ES_UDP, addr_port[0].data(), std::stoi(addr_port[1]));
client->create_socket();
/* Send a specific message to the server, and wait for the appropriate response, to know that the server is ready */
client->sendAll("GG");
std::string msg_from_server = client->recvAllNB();
std::string msg_from_server = client->recvAll();
if (msg_from_server == "U2") {
display_text_centered("Connection made");
Timer timer = timer_init(3);
while (!timer_done(timer));
display_text("Connection made", if_mode);
} else {
throw std::invalid_argument("Server didn't respond with correct message.");
}

View File

@@ -5,6 +5,7 @@
set -o errexit # Stop executing when a command fails
BASE_DIR=$(dirname $0)
REL_DIR="$BASE_DIR/release/dist"
RAYLIB_DLL="$BASE_DIR/build/subprojects/raylib/libraylib.dll"
mkdir -p "$REL_DIR"
@@ -14,12 +15,15 @@ meson setup build/
# Build the application
meson compile -C build/
# 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"
# Parse the output of the 'ldd' command (using only DLLs that are found) and create a file with the required DLL paths.
ldd build/pong.exe | awk ' NF == 4 {print $3}' | grep -i "dll" > "$BASE_DIR/tmp_file.txt"
# Copy the required DLLs.
cp $(cat "$BASE_DIR/tmp_file.txt") "$REL_DIR"
# Copy the raylib DLL, if it does not exist in the directory
cp -n "$RAYLIB_DLL" "$REL_DIR"
# Copy the executable itself
cp "$BASE_DIR/build/pong" "$REL_DIR"

13
display_text.cpp Normal file
View File

@@ -0,0 +1,13 @@
#include "includes/display_text.hpp"
#include <iostream>
#include <string>
void display_text(std::string to_disp, const int if_mode) {
if (if_mode == IF_CLI) {
std::cout << to_disp << std::endl;
}
if (if_mode == IF_GUI) {
display_text_raygui(to_disp);
}
return;
}

View File

@@ -52,10 +52,11 @@ SOCKET create_socket(int network, char transport) {
int newSock = socket(domain,type,0);
/* Set REUSEADDR flag, allowing program to be run twice */
int set_opt = 1;
setsockopt(newSock, SOL_SOCKET, SO_REUSEADDR, (char *)&set_opt, sizeof(set_opt));
/* Set REUSEADDR flag for TCP, allowing program to be run twice */
if (transport == ES_TCP) {
int set_opt = 1;
setsockopt(newSock, SOL_SOCKET, SO_REUSEADDR, (char *)&set_opt, sizeof(set_opt));
}
return newSock;
}

View File

@@ -17,12 +17,14 @@ typedef struct {
/* This function checks the IP address and port passed to it, and returns a struct,
that contains information about the game mode, and contains the server socket.
It assumes that both ip_text and port_text are non-null
It assumes that both ip_text and port_text are non-null.
Any errors are printed using the display_text function, with the given if_type.
TODO - Add better error checking. */
GameType check_server(char* ip_text, char* port_text);
GameType check_server(char* ip_text, char* port_text, const int if_type);
/* NOT IMPLEMENTED YET - This function checks the code given to it, and returns a struct
that contains information about the game mode, and contains the client socket. */
GameType check_client(char* code);
that contains information about the game mode, and contains the client socket.
Any errors are printed using the display_text function, with the given if_type. */
GameType check_client(char* code, const int if_type);
#endif

10
includes/display_text.hpp Normal file
View File

@@ -0,0 +1,10 @@
#include "includes/raygui_helpers.hpp"
/* Constants that can be used by caller function. */
const int IF_CLI = 1;
const int IF_GUI = 2;
/* This function is used to display text. It is used to abstract the differences
between GUI invocation and CLI invocation. The if_mode parameter is used to
determine whether the game was launched from GUI or CLI. */
void display_text(std::string to_disp, const int if_mode);

View File

@@ -4,8 +4,8 @@
/* Display the given text, centered on the screen, as a label.
NEEDS RAYGUI LIBRARY. */
void display_text_centered(std::string to_disp);
void display_text_raygui(std::string to_disp);
/* Display the given string, and exit the game after 'time' seconds. */
void display_and_exit(std::string to_disp, int time);
void display_and_exit_raygui(std::string to_disp, int time);
#endif

193
main.cpp
View File

@@ -32,7 +32,7 @@
#include "includes/server.hpp"
#include "includes/exception_consts.hpp"
#include "includes/check_input.hpp"
#include "includes/raygui_helpers.hpp"
#include "includes/display_text.hpp"
#include "includes/easysock.h"
#include "includes/serialization.h"
#include "includes/timer.h"
@@ -52,11 +52,11 @@ 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|CODE]\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 must be specified. Port is optional, 6500 is used as default.\n"
"IP address and port must be specified.\n"
"\n"
"-C: Client mode. Connects to a server, using the provided connection code.\n"
"\n"
@@ -97,128 +97,32 @@ raylib::Vector2 changeVelocityAfterCollision(Paddle paddle, Ball ball) {
/* 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 > 3) {
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" && argc < 3) { // Server mode but no address
throw std::invalid_argument("ARGUMENT ERROR: Server mode specified without any address.");
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.");
}
}
if (std::string(argv[1]) == "-C" && 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]) == "-C") {
if (argc < 3) { // Client mode but no code
throw std::invalid_argument("ARGUMENT ERRROR: Client mode specified without any code.");
}
}
if (std::string(argv[1]) == "-h" || std::string(argv[1]) == "--help") {
throw new std::invalid_argument(HELP_TEXT); // I am abusing the exception mechanism here, so that I can ensure that the caller quits the program.
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;
}
/* This function checks the command-line arguments passed to the program.
It then decides whether the game is in Server or Client mode (or neither), and
instantiates the appropriate object. The (uninitialized) objects are passed to the
function as pointers. It returns a GameType struct, that indicates whether the game
is in server, client or single player mode, and contains the appropriate socket object. */
GameType check_server_client(int argc, char** argv) {
std::string connect_code;
std::vector<std::string> addr_port; /* Vector to store (IPv4) address and port */
GameType type;
if (argc < 2) { /* Game was not started in client or server mode */
type.mode = M_SINGLE;
type.netsock = nullptr;
return type;
}
/* GAME STARTED IN CLIENT MODE */
if (strcmp(argv[1],"-C") == 0) {
if (argc < 3) { /* No address was provided */
throw EXCEPT_TOOFEWARGS;
}
connect_code = std::string(argv[2]); /* The connect code is a special string, that contains the server address and port. It is given by the server. */
try {
addr_port = connect_code::decode(connect_code);
/* Check IP address version */
if (check_ip_ver(addr_port[0].data()) < 0) {
throw std::invalid_argument("Invalid code entered.");
}
Client* client = new Client(ES_UDP, addr_port[0].data(), std::stoi(addr_port[1]));
client->create_socket();
/* Send a specific message to the server, and wait for the appropriate response, to know that the server is ready */
client->sendAll("GG");
std::string msg_from_server = client->recvAll();
if (msg_from_server == "U2") {
std::cout << "Connection made. Waiting for server to begin game..." << std::endl;
} else {
throw EXCEPT_WRONGRESPONSE;
}
type.mode = M_CLIENT;
type.netsock = client;
return type;
} catch (int e) {
throw;
} catch (std::exception& e) {
throw;
}
}
/* GAME STARTED IN SERVER MODE */
else if (strcmp(argv[1],"-S") == 0) {
std::string addr;
uint16_t port;
/* No IP address or port specified */
if (argc < 3) {
throw EXCEPT_TOOFEWARGS;
}
/* IP address but no port */
else if (argc < 4) {
std::cout << "No port specified, using 6500..." << std::endl;
addr = std::string(argv[2]);
port = 6500;
} else {
addr = std::string(argv[2]);
port = std::stoi(std::string(argv[3]));
}
/* Check if IP is valid */
if (check_ip_ver(addr.data()) < 0) {
throw EXCEPT_INVALIDIP;
}
std::string code = connect_code::encode(addr, std::to_string(port));
std::cout << "Your code is " << code << std::endl;
/* Create server socket and wait for client to connect */
Server* server = new Server(ES_UDP, addr.data(), port);
server->create_socket();
std::cout << "Waiting for connection..." << std::endl;
std::string response = "";
char* temp_response = NULL;
/* Wait for the right client to connect. Since recvAll returns a char*, we need to create a temporary variable to check for NULL.
TODO - Check that the client actually sends 'GG'. */
do {
temp_response = server->recvAll();
} while (temp_response == NULL);
response = std::string(temp_response);
std::cout << "Connection received from " << server->get_peer_addr() << std::endl;
server->sendAll("U2");
type.mode = M_SERVER;
type.netsock = server;
return type;
}
else {
throw EXCEPT_INVALIDARGS;
}
}
int main(int argc, char** argv) {
/* Check the number of command-line arguments */
/* 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) {
@@ -227,42 +131,31 @@ int main(int argc, char** argv) {
}
/* From here on, we assume that:
a. The program was started in single player mode, OR
b. The program was started in server mode, and an address was given, OR
c. The program was started in client mode, and a code was given. */
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. */
/* Check if game was started in server or client mode, and set appropriate variables */
/* GameType struct, to define whether the game is in single or multi-player mode, and
to hold the appropriate socket */
GameType type;
try {
type = check_server_client(argc, argv);
} catch(int e) {
if (e == EXCEPT_TOOFEWARGS) {
std::cout << "Started in client mode, but no address was specified." << std::endl;
return -1;
/* 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);
}
}
if (e == EXCEPT_INVALIDARGS) {
std::cout << "Invalid argument." << std::endl;
return -2;
}
if (e == EXCEPT_INVALIDIP) {
std::cout << "Invalid IP address provided." << std::endl;
return -5;
}
if (e == EXCEPT_WRONGRESPONSE) {
std::cout << "The server didn't respond with the correct message. Are you sure you have used the right server?" << std::endl;
return -6;
}
else {
std::cout << strerror(e) << std::endl;
return -7;
}
} catch(std::invalid_argument& inv) {
std::cout << inv.what() << std::endl;
return -8;
} 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 */
@@ -371,14 +264,14 @@ int main(int argc, char** argv) {
}
try {
type = check_server(ip_text, port_text);
type = check_server(ip_text, port_text, IF_GUI);
} catch (int e) {
display_and_exit(std::string(std::strerror(e)) + "\nClosing game...", 2); // The server constructor throws the errno if it cannot create a socket
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(std::string(inv.what()) + "\nClosing game...", 2);
display_and_exit_raygui(std::string(inv.what()) + "\nClosing game...", 2);
free(ip_text);
free(port_text);
return -1;
@@ -411,12 +304,12 @@ int main(int argc, char** argv) {
EndDrawing();
}
try {
type = check_client(code_text);
type = check_client(code_text, IF_GUI);
} catch (int e) {
display_and_exit(std::string(std::strerror(e)) + "\nClosing game...", 2); // The client constructor throws the errno if it cannot create a socket
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(std::string(inv.what()) + "\nClosing game...", 2);
display_and_exit_raygui(std::string(inv.what()) + "\nClosing game...", 2);
return -1;
}
free(code_text);

View File

@@ -34,7 +34,7 @@ if build_machine.system() == 'windows'
endif
executable('pong',
'main.cpp', 'sock.cpp','paddle.cpp', 'ball.cpp', 'numeric_base.cpp', 'connect_code.cpp', 'server.cpp', 'client.cpp', 'check_input.cpp', 'raygui_helpers.cpp',
'main.cpp', 'sock.cpp','paddle.cpp', 'ball.cpp', 'numeric_base.cpp', 'connect_code.cpp', 'server.cpp', 'client.cpp', 'check_input.cpp', 'raygui_helpers.cpp', 'display_text.cpp',
'serialization.c', 'timer.c', 'easysock.c',
dependencies: [raylib, ws2_dep, winmm]
)

View File

@@ -1,7 +1,7 @@
#include "includes/raygui_helpers.hpp"
#include "includes/raygui/raygui.h"
#include "includes/timer.h"
void display_text_centered(std::string to_disp) {
void display_text_raygui(std::string to_disp) {
const char* to_disp_cstr = to_disp.c_str();
Vector2 label_size = MeasureTextEx(GetFontDefault(), to_disp_cstr, GuiGetStyle(DEFAULT, TEXT_SIZE)+1, GuiGetStyle(DEFAULT, TEXT_SPACING)+1); // The '+1' is there to account for any rounding errors
@@ -12,8 +12,8 @@ void display_text_centered(std::string to_disp) {
return;
}
void display_and_exit(std::string to_disp, int time) {
display_text_centered(to_disp);
void display_and_exit_raygui(std::string to_disp, int time) {
display_text_raygui(to_disp);
Timer timer = timer_init(time);
while (!timer_done(timer));
return;

View File

@@ -72,8 +72,11 @@ char* Server::recvAllNB() {
/* Convert the s_addr field of the caseted struct to host network-byte, and convert it to a dotted decimal */
peer_addr = connect_code::dec_to_dotted_dec(std::to_string(htonl(temp_struct->sin_addr.s_addr)));
} else {
/* FOR IPv6 */
peer_addr = "IPV6 NOT SUPPORTED YET";
/* FOR IPv6 - Use the inet_ntop function, and convert the struct's address into a string */
struct sockaddr_in6* temp_struct = (struct sockaddr_in6*)this->dest;
char* temp_buf = (char *)malloc(sizeof(char) * (INET6_ADDRSTRLEN + 1));
peer_addr = std::string(inet_ntop(AF_INET6, temp_struct->sin6_addr.s6_addr, temp_buf, INET6_ADDRSTRLEN));
free(temp_buf);
}
return to_return;
@@ -98,8 +101,7 @@ called immediately after the constructor. If the socket is TCP, it also sets the
socket to listen for incoming connections. This function throws an exception if
the socket could not be created. The excpetion is an integer corresponding to the errno
of the failing function, and enables the caller to print a corresponding error message by
'catching' the thrown exception and using strerror().
This function also sets a timeout of 100ms for UDP sockets */
'catching' the thrown exception and using strerror().*/
void Server::create_socket() {
Sock::create_socket();

View File

@@ -20,7 +20,6 @@ Sock::~Sock() {}
parameters. The address version (IPv4 or IPv6) is determined based on the given address. */
Sock::Sock(char protocol, const char* address, int port) {
/* Error checking */
this->ip_ver = check_ip_ver(address);
if (ip_ver != 4 && ip_ver != 6) {
@@ -33,7 +32,6 @@ Sock::Sock(char protocol, const char* address, int port) {
throw std::invalid_argument("Invalid protocol");
}
this->ip_ver = ip_ver;
this->protocol = protocol;
this->port = port;
this->address = std::string(address);
@@ -50,7 +48,9 @@ void Sock::sendAll(std::string to_send) {
/* For UDP sockets */
if (this->protocol == ES_UDP) {
sendto(this->sock_fd, to_send.data(), str_length, 0, (struct sockaddr *)dest, addrlen);
if (sendto(this->sock_fd, to_send.data(), str_length, 0, (struct sockaddr *)dest, addrlen) == -1) {
throw errno;
}
}
/* For TCP sockets */
else {
@@ -58,7 +58,7 @@ void Sock::sendAll(std::string to_send) {
/* Send the data to the 'other_socket' variable, which should be set by the client and server methods */
num_bytes_sent = send(this->other_socket, to_send.substr(total_bytes_sent).c_str(), str_length - total_bytes_sent, 0);
if (num_bytes_sent < 0) {
throw errno * -1;
throw errno;
}
total_bytes_sent += num_bytes_sent;
}
@@ -89,6 +89,9 @@ char* Sock::recvAll() {
if (num_bytes_received == 0) {
return NULL;
}
if (num_bytes_received < 0) {
throw errno;
}
/* Null-terminate the string */
*(buffer + num_bytes_received) = '\0';
return buffer;
@@ -106,7 +109,7 @@ char* Sock::recvAll() {
}
if (num_bytes_received < 0) {
throw errno * -1;
throw errno;
}
total_bytes_received += num_bytes_received;
has_been_read = true;

View File

@@ -1,12 +1,10 @@
1. Sign Windows executable, to remove 'Unknown Publisher' warnings.
2. Add 'install' target to Meson, to allow the user to install the game. This should also copy the .so files to the right locations.
3. Use free() to free allocated memory.
4. Use check_client() and check_server() for CLI invocation as well, and pass a flag that indicataes whether the parameters were entered through GUI or CLI (also probably create a function to handle printing vs. GUI display).
5. Use the struct to establish a connection, and to start each round (instead of sending strings).
6. Figure out how to build statically-linked Mac binary, and create a build script for packaging it.
7. Communicate the paddle reset position to the peer, after a round.
8. Clean up / refactor the raygui code in main.cpp, that asks user for game mode. Instead of just having a giant blob of code in main.cpp, maybe split it into a function, or move it to another file. It should be easy to split it into a different function, since none of the functions take any specific parameters. The text box function, for example, only takes in the rectangle coordinates, and the text to display. I can move the code to a function, and then pass in any parameters that I need to pass in (I don't think I need to pass many parameters, though).
9. Allow the user to specify which paddle they want to control, in multi-player mode.
10. Try to make the ball go between screens.
11. Change the networking code, so that a single server can connect two clients with each other. The server should provide player 1 with a code, and player 2 can connect with player 1 using that code (essentially like a room).
12. Add a --help option, that displays information about the game and how to run it.
4. Use the struct to establish a connection, and to start each round (instead of sending strings).
5. Figure out how to build statically-linked Mac binary, and create a build script for packaging it.
6. Communicate the paddle reset position to the peer, after a round.
7. Clean up / refactor the raygui code in main.cpp, that asks user for game mode. Instead of just having a giant blob of code in main.cpp, maybe split it into a function, or move it to another file. It should be easy to split it into a different function, since none of the functions take any specific parameters. The text box function, for example, only takes in the rectangle coordinates, and the text to display. I can move the code to a function, and then pass in any parameters that I need to pass in (I don't think I need to pass many parameters, though).
8. Allow the user to specify which paddle they want to control, in multi-player mode.
9. Try to make the ball go between screens.
10. Change the networking code, so that a single server can connect two clients with each other. The server should provide player 1 with a code, and player 2 can connect with player 1 using that code (essentially like a room).