Overview

The Blizzard proyect is a solution to building multiplayer games in an authorative client-server architecture. This proyect consists of a Server and a Game Engine.

The proyect is still in development and very new, it is not a full-featured solution. As of now only the following are covered:

  • ECS Game Engine (only data, no rendering)
  • TCP authorative server
  • Handling connections + disconnections
  • Example client

There are many areas of improvement and for further development. The development roadmap can be seen in the next section!

Roadmap

There are many features missing in the proyect, here are some of the contemplated features for future development.

Server

  • Cheating prevention
  • Optimization
  • Client prediction
  • UDP server (now it's only TCP)
  • Better communication protocols
  • AI / ML enhancements
  • Good logger
  • Server metrics
  • Game saving (like game recording...)
  • Modularity (server customization)
  • More examples and docs

Game Engine

  • Improved ECS
  • Math / physics library
  • Out-of-the-box components for ECS
  • Good logger
  • Multi-platform support
  • Window abstraction layer
  • Renderer API (OpenGL or Vulkan)
  • Modularity (engine customization)
  • More examples and docs

See how to get started in the next section!

Getting Started

For a quick setup and seeing the game engine and server work together, try copying the example library in the GitHub repository. It is a library with two binaries, one for the server and one for the client example.

For now, there is not a quick "default" way to get started, because the Game and World need to be manually configured, so please see the learn section!

Server Architecture

The architecture decided for any game using this,is for the server to be an authorative. This means that the server receives user data, and decides how to update the game according to the inputs. After the update, it dispatches the updated game data to all players.

Client-Server Architecture

Client-Server Diagram


The server is missing many features like cheating prevention, player prediction, a better game loop, and metrics.

The server is made up of many parts:

  • Connection listener (handles initial connections)
  • Pool (Reference to connectors)
  • Connector (Bridge between controller and pool)
  • Controller (Handles application, player connections, message passing)
  • Application (Runs the game, listens for controller messages)
Server Architecture

Server Architecture Diagram

Connection Listener

When creating a server, it opens a port for listening to player connections. The server also creates a Game Pool. When a player connects to the main TCP port of the server, the server tries to find an available game inside the Game Pool. If it finds an available game, it returns the port number where the game is hosted (another TCP connection). If no game is found, it returns 0.

Pool

The Game Pool creates as many games as specified in separate threads. It actually creates Connectors. Each Connector is like a wrapper around the Controller. The Game Pool stores each Connector, so it can talk to the Server about empty games, available ports, and so on.

Connector

The Connector as said before, is like a wrapper around aController. It just holds information like port number, max players and current player count. When a Connector is created, it creates it's corresponding Controller in a new thread.

Controller

The Controller is where the most important things happen. When a Controller is created, it opens a new TCP listener to a new port. It then runs the Application on a separate thread, with capabilites to passing Messages to it. When a player conencts to this new port, the Controller will update the Connector information as well as the Application information. The Controller opens two new threads for handling the player's input and also writing data back to the player. The Controller serves as the entire communication controller between Application and Client.

Application

An Application is a Network Application made by the Blizzard Game Engine. It handles the entire Game logic as well as handling the player's inputs. More on this in the next section about the engine's architecture.

Engine Architecture

The engine is meant to be used with ECS architecture (Entity Component System). A game engine consists of many parts, which are not yet implemented. As of now, there is only:

  • Core
    • Logger
    • Parsing
  • Application Layer
    • Event loop
    • Network Input
    • ECS

This is because the MVP was meant for integration with the server. This is the minimum needed for integration.

The future architecture will consist of more parts:

  • Platform layer
    • Multi-platform support
    • File I/O
  • Core
    • Engine configuration
    • Tests
    • Math library
    • System time
  • Renderer
    • OpenGL abstraction
    • GUI
    • Camera
    • Sceneing
    • Many other rendering goodies
  • Application Layer
    • Physics
    • State machine
    • Time
    • Lifecycles
    • Shutdown
  • AI layer
  • Tools
    • Debugging
    • Build system

Application

The Application which as of now is only a Network Application (although it can be used without it's networking capabilities), is where the most logic occurs.

It defines how fast to run the game, the order of function calling of the game, the game loop and how to pass messages and handle client input.

The application takes most importantly a Game argument when creating one. The Game follows ECS architecture. When creating a Game, a World is configured. A World can have Entities, Components, and Systems. The World also accepts inputs from the Game, which really means it accepts inputs from the Application.

To understand better the ECS architecture implemented, check out the next section!

Learn

This is a tutorial to build the Example project in the GitHub repository. This will teach you the fundamentals to create your own TCP multiplayer data games using Blizzard's Game Engine and Server.

Configuration

Open a new terminal and create a new library crate:

cargo new my_game --lib cd my_game

Inside the src folder, create a bin directory and add two files:

server.rs client.rs

Make sure that each file has a main() function!

This will be all there is to the folder and file structure.

Inside the root cargo.toml file, add the following dependencies:

[dependencies] serde = "1.0.13" serde_json = "1.0" serde_derive = "1.0" blizzard-server = "0.1" blizzard-engine = "0.1" blizzard-engine_derive = "0.1"

Then install the dependencies / build the proyect in your terminal

cargo build

Your file structure should look like the following:

my_game/ src/ bin/ client.rs server.rs lib.rs cargo.toml cargo.lock .gitignore target/

Now let's make a simple multiplayer game where you can move a player's position!

World

The Blizzard Game Engine works with the Entity Component System architecture. The server runs a Game, said game contains a World, which contains all the Entities and Components. The World also runs the Systems, which process all of the Components to run logic and update the game. You can read more about the ECS architecture online.

Import World

Inside the server.rs file we will write all the game and server logic. At the top of the file add the following:

extern crate blizzard_engine; use blizzard_engine::ecs::{World};

The first two line is for adding the blizzard engine to the binary. The second line is to use the World trait definied for the ECS.

Create your world

Create your own world by creating any struct that implements Debug and Clone. Then simply apply the World trait to your World:

#[derive(Debug, Clone)] struct MyWorld { } impl World<Input> for MyWorld { fn new() -> Self { Self { } } fn run_systems(&mut self, input: Input) { // runs future systems } }

This won't compile yet! The World trait takes a generic type I, used for sending client (player) data to the world! Here we called it Input. You can define input any way you like, it is meant to be flexible to however you like! For now, let's define Input as the following tuple struct:

#[derive(Debug, Clone, Copy)] struct Input(usize);

Next step: Creating Entities and Components!

Entities and Components

Hopefully by now you have read a little about Entities and Components from the ECS architecture.

Necessary imports

The Blizzard Engine comes with a default way of creating Entities and Components. Add the following to the top of your server.rs file:

extern crate blizzard_engine_derive; use blizzard_engine::ecs::{ComponentRegistry, EntityManager, World}; use blizzard_engine_derive::ComponentRegistry; use std::collections::HashMap;

The top of your file should now look something like the following:

extern crate blizzard_engine; extern crate blizzard_engine_derive; use blizzard_engine::ecs::{ComponentRegistry, EntityManager, World}; use blizzard_engine_derive::ComponentRegistry; use std::collections::HashMap;

Add Entities to your world

Adding entities to the world is very simple:

#[derive(Debug, Clone)] struct MyWorld { entity_manager: EntityManager, } impl World<Input> for MyWorld { fn new() -> Self { Self { entity_manager: EntityManager::new(), } } fn run_systems(&mut self, input: Input) { // runs future systems } }

See the ecs part of the Blizzard Game Engine docs to see all the methods available for adding and managing entities.

Let's add components to our World.

Adding Components

Adding components is very easy, that's what the engine_derive library is for, it provides a macro to generate components easily! Let's create two types of components, a Player and a Position:

#[derive(ComponentRegistry, Debug, Clone)] struct PositionRegistry { components: HashMap<u32, Position>, } #[derive(ComponentRegistry, Debug, Clone)] struct PlayerRegistry { components: HashMap<u32, usize>, }

Our components are stored in a registry, hence the struct names end with "Registry". Components are stored inside a HashMap, the first key MUST be of type u32, since that is the UID that is associated to an Entity when a Component is created. The second key of the HashMap is of any type that is needed. This won't compile yet because Position is not defined. Let's define it:

use std::ops::AddAssign; #[derive(Debug, Clone, Copy)] pub struct Position { x: i32, y: i32, } impl Position { pub fn new() -> Self { Self { x: 0, y: 0 } } pub fn displacement(x: i32, y: i32) -> Self { Self { x, y } } } impl AddAssign for Position { fn add_assign(&mut self, other: Self) { *self = Self { x: self.x + other.x, y: self.y + other.y, }; } }

Now the only thing left to do is add the component registries to the World:

#[derive(Debug, Clone)] struct MyWorld { entity_manager: EntityManager, positions: PositionRegistry, players: PlayerRegistry, }

Great! Let's now learn how to add Systems in order to manipulate components.

Systems

Systems are a way of changing Components. Systems are very broad in this implementation, and can be pretty much defined any way you like. For this example, let's add a simple counter Component and a System that increments this counter.

#[derive(Debug, Clone)] struct MyWorld { entity_manager: EntityManager, positions: PositionRegistry, counters: CounterRegistry, players: PlayerRegistry, } ... #[derive(ComponentRegistry, Debug, Clone)] struct CounterRegistry { components: HashMap<u32, u32>, }

Creating a System

Systems have must process a specific Component, hence their function signature must match the component:

fn counter_system(counters: &mut HashMap<u32, u32>) { for (_, c) in counters.iter_mut() { *c += 1; } }

This system will iterate all the counters inside the CounterRegistry and increment their value by one.

This system will not run if it is not added to the World...

Adding Systems to the world

Addidng a System is very easy, you just call the System inside the World's run_systems function:

fn run_systems(&mut self, input: Input) { // Systems to always run counter_system(&mut self.counters.components); }

The next step is to use the Game trait to create your own game, which will be run on the server!

Game

The Game is in charge of setting up the World's initial state, defining how to update the World, define global resources / state, setting up the input, and defining when the game should end.

To create your own Game, it must be imported from the engine:

use blizzard_engine::game::Game;

Creating a Game

Game is a trait. You can define your own game struct and implement this trait and also Clone:

#[derive(Clone)] struct MyGame { world: MyWorld, counter: i32, } impl Game<SharedState, Input> for MyGame { fn world_config(&mut self) { } fn update(&mut self, input: Input, shared_state: Arc<Mutex<SharedState>>) { } fn reset_input(&mut self, input: Arc<Mutex<Input>>) { } fn render(&mut self) {} fn end_game(&self) -> bool { false } }

The struct MyGame has the world. We also defined another property called counter, just to show that you can create and customize your game however you wish.

The Game trait has two generic types, the first is K. K is used for sharing information with the client, it is a way to define which data is sent off to the client. For now, we defined the generic K as a struct called SharedState. We will talk about the shared state later. For now, let's define it as an emtpy struct:

#[derive(Debug, Serialize, Deserialize, Clone)] pub struct SharedState { } impl SharedState { pub fn new() -> Self { Self { } } }

The shared state must implement these traits, hence we need to add Serde to our proyect in order to serialize/deserialize the data sent. At the top of the file add the following:

#[macro_use] extern crate serde_derive; extern crate serde; extern crate serde_json;

The second generic type is I which was used before, we defined it as the Input tuple struct.

If the function signatures are confusing, here is the generic definition:

pub trait Game<K, I> { fn world_config(&mut self); fn update(&mut self, input: I, shared_state: Arc<Mutex<K>>); fn reset_input(&mut self, input: Arc<Mutex<I>>); fn render(&mut self); fn end_game(&self) -> bool; }

Let's talk about each function and implement it in our game.

World Configuration

The world configuration function is the first function to be called when starting the game. It is useful to define initial states, entities and components. This function is only called once, at the beginning of the Game lifecycle.

Let's define a basic initial entity with a counter component:

fn world_config(&mut self) { // Create counter entity let entities = self.world.entity_manager.create_n_entities(1); // Add components to many entities self.world.counters.add_many(&entities, 0); }

Updating the world

After the world configuration is run, the game loop is started. The update function is the first function that is called in the game loop. It takes an argument of the input, however we are not using it now.

fn update(&mut self, input: Input, shared_state: Arc<Mutex<SharedState>>) { // Update components self.world.run_systems(input); self.counter += 1; }

This function should be used to tell the World to run it's systems.

Reseting user input

After the update function is called, the reset_input function is called. For now we can leave it empty, but it is just a way to define how the input for the next loop should look like if no client input is detected.

fn reset_input(&mut self, input: Arc<Mutex<Input>>) { }

Render

This function is called after updating and reseting the user input. It is meant to be called for rendering the game, however the engine does not have a Renderer API yet, so it is pretty much useless:

fn render(&mut self) {}

Ending the game

This function determines wheter the game should end or not. Here logic can be inserted to end the game. However, we will create an endless game:

fn end_game(&self) -> bool { false }

Game creator helper function

It is recommended to make a game creator helper function:

fn new_game(world: MyWorld) -> MyGame { MyGame { counter: 0, world: world, } }

Perfect, we are almost ready to deploy our server, let's learn how to add start a server with this game!

TCP Server

For now, only TCP servers can be created. To make a server we need to add the following to the top of our server.rs file:

use blizzard_server::server::Server; use std::sync::mpsc::Receiver; use std::sync::{Arc, Mutex};

The top should now look like this:

extern crate blizzard_engine; extern crate blizzard_engine_derive; use blizzard_engine::ecs::{ComponentRegistry, EntityManager, World}; use blizzard_engine::game::Game; use blizzard_engine_derive::ComponentRegistry; use blizzard_server::server::Server; use std::collections::HashMap; use std::sync::mpsc::Receiver; use std::sync::{Arc, Mutex}; use std::ops::AddAssign;

Make the server

In the file's main function (or create the function), we will configure our server. We need to define:

  • Port: a port where our server will listen to incoming connections
  • Max games
  • Max players per game
  • The game
  • Shared state (to send to client)
  • Client input type
  • Rate at which data is sent to client (FPS or HZ)
  • Rate at which the game updates (FPS or HZ)
  • How to handle client input

The server's new function has the following signature:

pub fn new<T: Game<K, I>, K, I, M>( port: i32, max_games: i32, max_players: i32, game: T, shared_state: K, input: I, handle_input: &'static (dyn Fn(Receiver<(M, usize)>, Arc<Mutex<I>>) -> I + Sync), send_data_rate: i32, game_update_rate: i32, ) ...

The new type is M, which is a type used for the Messages sent from client to the game.

Let's create our basic configuration:

fn main() { let port = 8888; let max_games = 4; let max_players = 2; let world = MyWorld::new(); let shared_state = SharedState::new(); let game = new_game(world); // The data that a client message will manipulate let input_type = Input::default(); let hanlde_input = &handle_client_message; // Engine speeds let send_data_from_server_rate = 1; let server_game_update_rate = 2; // 2 times per second // Start server + games Server::new( port, max_games, max_players, game, shared_state, input_type, hanlde_input, send_data_from_server_rate, server_game_update_rate, ); }

We have a couple of missing definitions, let's create them.

Input:

#[derive(Debug, Clone, Copy)] struct Input(Message, usize); impl Input { fn default() -> Self { Self(Message::None, 0) } fn from(m: Message, id: usize) -> Self { Self(m, id) } }

We are changing the input to contain a Message, and a number of type usize. This second one is for identification, it will be passed by the client to identify the player generating the Input.

Message:

#[derive(Serialize, Deserialize, Clone, Copy, Debug)] pub enum Message { None, W, A, S, D, AddPlayer, RemovePlayer, }

SharedState:

#[derive(Debug, Serialize, Deserialize, Clone)] pub struct SharedState { pub counters: Vec<u32>, pub registry: Vec<Position>, } impl SharedState { pub fn new() -> Self { Self { registry: vec![], counters: vec![], } } }

The shared state will be to show the counters and the positions of players.

Handle client message:

fn handle_client_message(receiver: Receiver<(Message, usize)>, input: Arc<Mutex<Input>>) -> Input { for (message, id) in receiver { println!("Player {} called {:?}", id, message); *input.lock().unwrap() = Input::from(message, id); } Input::default() }

This is an interesting code bit. To handle a client message, when creating the server, an Application is created which wraps the Game, and is in charge for calling the game loop and also connecting all inputs and communications. Hence this function is to handle inputs. It actually handles messages. A Message will be sent from the client. The Receiver will receive the Message and decide what Input to generate based on the message. For now, we are just passing the message on to the Input which will be used in the update_systems function inside the World. The Input is wrapped in a Arc and Mutex.

Adding game logic

Let's make some systems that will be used.

Adding a player entity, with a player component and position:

fn add_player_system(world: &mut MyWorld, player_id: usize) { let ent = world.entity_manager.create_entity(); world.players.add(ent, player_id); world.positions.add(ent, Position::new()); world.player_id_map.players.insert(player_id, ent); }

When a player joins a game, they get a UID defined by the server. However, the World has no way to know which entity belongs to a player. Hence we created the PlayerIdMap:

#[derive(Debug, Clone)] struct MyWorld { entity_manager: EntityManager, positions: PositionRegistry, counters: CounterRegistry, players: PlayerRegistry, player_id_map: PlayerIdMap, } impl World<Input> for MyWorld { fn new() -> Self { Self { entity_manager: EntityManager::new(), positions: PositionRegistry::new(), counters: CounterRegistry::new(), players: PlayerRegistry::new(), player_id_map: PlayerIdMap::new(), } } fn run_systems(&mut self, input: Input) { ... } } #[derive(Debug, Clone)] struct PlayerIdMap { players: HashMap<usize, u32>, } impl PlayerIdMap { fn new() -> Self { Self { players: HashMap::new(), } } }

Updating player position system:

fn update_player_pos_system(world: &mut MyWorld, player_id: usize, displacement: Position) { if let Some(ent) = world.player_id_map.players.get(&player_id) { *world .positions .components .entry(*ent) .or_insert(displacement) += displacement; } }

Removing a player system:

fn remove_player_system(world: &mut MyWorld, player_id: usize) { if let Some(ent) = world.player_id_map.players.get(&player_id) { world.players.components.remove(ent); world.positions.components.remove(ent); world.entity_manager.remove_entity(*ent); world.player_id_map.players.remove(&player_id); } }

Updating games using run_systems and input

We can now do a lot of basic functionality using this. Let's add our systems to our run_systems:

fn run_systems(&mut self, input: Input) { // Systems to run conditionally match input.0 { Message::AddPlayer => add_player_system(self, input.1), Message::W => { update_player_pos_system(self, input.1, Position::displacement(0, 1)); } Message::A => { update_player_pos_system(self, input.1, Position::displacement(-1, 0)); } Message::S => { update_player_pos_system(self, input.1, Position::displacement(0, -1)); } Message::D => { update_player_pos_system(self, input.1, Position::displacement(1, 0)); } Message::RemovePlayer => { remove_player_system(self, input.1); } _ => {} } // Systems to always run counter_system(&mut self.counters.components); }

Defining the data to be sent to the client

Inside the Game's update function, we can now copy the desired data over to the SharedState, which is sent off to the client:

fn update(&mut self, input: Input, shared_state: Arc<Mutex<SharedState>>) { // Update states self.world.run_systems(input); self.counter += 1; // Update shared state: for client reception shared_state.lock().unwrap().counters = self .world .counters .components .iter() .map(|(_, counter)| *counter) .collect(); shared_state.lock().unwrap().registry = self .world .positions .components .iter() .map(|(_, positions)| *positions) .collect(); }

Here we are just copying the data from the counter and position components into vectors of information.

Next steps

This is everything! You have created your own multiplayer game server! You can start the server in your terminal by typing:

cargo run --bin server

You will see a couple of logs from the server. However nothing exciting really happens. This is because there are no players connecting to any games! Let's now write a client that connects to a game. We will be writing the client inside the client.rs file. However, now we will use the library we created, my_game. The library is a suggestion, because many of the client's data structs are the same as the game's. For example:

  • Message
  • SharedState
  • Position

Let's write a client that connects to our server.

Client

The client will be a terminal based client.

We will move the following on to our my_game library:

  • Message
  • SharedState
  • Position

MyGame library

Inside the lib.rs file:

#[macro_use] extern crate serde_derive; extern crate serde; extern crate serde_json; use std::ops::AddAssign; // Message definition #[derive(Serialize, Deserialize, Clone, Copy, Debug)] pub enum Message { None, W, A, S, D, AddPlayer, RemovePlayer, } // Shared state definition #[derive(Debug, Serialize, Deserialize, Clone)] pub struct SharedState { pub counters: Vec<u32>, pub registry: Vec<Position>, } impl SharedState { pub fn new() -> Self { Self { registry: vec![], counters: vec![], } } } // Position component #[derive(Serialize, Deserialize, Debug, Clone, Copy)] pub struct Position { x: i32, y: i32, } impl Position { pub fn new() -> Self { Self { x: 0, y: 0 } } pub fn displacement(x: i32, y: i32) -> Self { Self { x, y } } } impl AddAssign for Position { fn add_assign(&mut self, other: Self) { *self = Self { x: self.x + other.x, y: self.y + other.y, }; } }

Let's update our server.rs by importing these definitions and removing the old ones that were in the file. Inside: server.rs:

extern crate my_game; use example::{Message, Position, SharedState};

The beggining of your server.rs file should now look like the following:

extern crate blizzard_engine; extern crate blizzard_engine_derive; extern crate my_game; use blizzard_engine::ecs::{ComponentRegistry, EntityManager, World}; use blizzard_engine::game::Game; use blizzard_engine_derive::ComponentRegistry; use blizzard_server::server::Server; use std::collections::HashMap; use std::sync::mpsc::Receiver; use std::sync::{Arc, Mutex}; use example::{Message, Position, SharedState};

Creating our client

Now we are ready to make our client!

In the beggining of the file write the following:

extern crate example; use example::Message; use example::SharedState; use std::io::{self, BufRead, BufReader, Write}; use std::net::{Shutdown, TcpStream}; use std::str; use std::sync::{Arc, Mutex}; use std::thread; struct Client {}

Our Client struct will be used to define related functionality.

The way the server works is the following:

  • Server is started listening to a TCP port
  • Server creates threads based on how many games were specified
  • Server opens different TCP listeners (on different ports) per game

When a client connects to the server, the following happens:

  1. Server looks for an available game (based of player capacity)
  2. If it finds a game, it returns the TCP port of said game
  3. In none are found, the port returned is 0

Based on this we can create a basic client that tries to connect to the server and receive a port number.

impl Client { fn start() { let mut stream = TcpStream::connect("0.0.0.0:8888").expect("Could not connect to server"); let port: i32; let mut input = String::new(); let mut buffer: Vec<u8> = Vec::new(); println!("Enter your username: "); io::stdin() .read_line(&mut input) .expect("Failed to read from stdin"); println!("Finding an available lobby..."); stream .write(input.as_bytes()) .expect("Failed to write to server"); let mut reader = BufReader::new(&stream); reader .read_until(b'\n', &mut buffer) .expect("Could not read into buffer"); port = str::from_utf8(&buffer) .expect("Could not write buffer as string") .replace("\n", "") .parse() .expect("Could not parse port"); if port == 0 { println!("No game available, please try again later"); return; } stream .shutdown(Shutdown::Both) .expect("Could not disconnect from original server"); let tcp = format!("0.0.0.0:{}", port); Client::run_game(tcp); } }

The server needs to receive an input in order to acknowledge that a player wants to connect, hence we are defining a username, but it will actually never be read. If no game is found, a port of 0 will be returned. We can check this to see if the client should continue to try and connect. At the end, Client::run_game(tcp) starts the game connection. Let's see how we can define such game.

Implementing player control

fn run_game(tcp: String) { // Try to connect to game let mut stream = TcpStream::connect(tcp).expect("Could not connect to server"); let stream_clone = stream.try_clone().unwrap(); // Tell the server to add a player let data = Message::AddPlayer; let json = serde_json::to_string(&data).unwrap() + "\n"; let should_close = Arc::new(Mutex::new(false)); let should_close_copy = Arc::clone(&should_close); // Write add player message stream .write(json.as_bytes()) .expect("Failed to write to server"); println!("data written"); // User Input thread thread::spawn(move || { let shoud_close = should_close_copy; loop { let mut input = String::new(); io::stdin() .read_line(&mut input) .expect("Failed to read from stdin"); let input = input.trim(); let mut data = Message::None; // Player controls if input == "w" { data = Message::W; } else if input == "a" { data = Message::A; } else if input == "s" { data = Message::S; } else if input == "d" { data = Message::D; } else if input == "close" { data = Message::RemovePlayer; *shoud_close.lock().unwrap() = true; } let json = serde_json::to_string(&data).unwrap() + "\n"; stream .write(json.as_bytes()) .expect("Failed to write to server"); } }); // Stream Reader thread thread::spawn(move || { let stream = stream_clone; loop { let mut buffer: Vec<u8> = Vec::new(); let mut reader = BufReader::new(&stream); reader .read_until(b'\n', &mut buffer) .expect("Could not read into buffer"); let json = str::from_utf8(&buffer).unwrap(); let state: SharedState = serde_json::from_str(&json).unwrap(); // Print shared state! println!("{:?}", state); } }); // Keep thread alive, so TCP connection on other threads doesn't reset loop { if *should_close.lock().unwrap() { return; } } }

Running everything togehter

In a terminal, start the server:

cargo run --bin server

In a new terminal, start a client:

cargo run --bin client

You should input your username, and the game will connect. You should then see positions logged to the console. Try writing to the server "w", "a", "s", "d", or "close". What happens?

Now without closing the current client, open y a 3rd terminal and connect another player!

What happens if you connect a 4th player? What happens if you keep going until all games are full? Find out!

GitHub repo

All of this code is the same code from the Example library in the official GitHub repository. You can check it out if anything is not working properly.

Congratulations!

You just made your own very basic multiplayer game! Many other languages and frameworks have TCP connections. For example C# has a TCP class. You could connect your server to a Unity game! The possibilities are endless.

Origin

This proyect started as my curiosity to develop a multiplayer game. I ended up deciding to write my own game server using Rust. After some succesfull connections and data passing between threads, I realised that the server needed a game engine in order to implement an authorative client-server achitecture. This gave birth to the Blizzard Game Engine and Server Engine! If you are curious about my learning process and development process, please see the development process section.

Development Process

Overview

When starting this project, I really had no idea of what I was doing. I did a lot of researching, reading, book finding, and video watching. I needed to learn the following:

  • Networking
    • TCP & UDP networking
    • Data serialization
  • Rust
    • Threads
    • Safe types (Arc, Mutex)
    • Generics
    • Traits
    • Type traits
    • Macros (Procedural and Derivative)
    • Closures
    • Iterators
    • Packages and crates
    • Pretty much everything from The-Book except lifetimes
  • Game engine architectures
  • Game server architectures

Recap

I first developed the TCP server. After good data passing between threads and handling connections, I realized I needed a sort of game engine running with my server, so it could be authorative. I ended up also making a small game engine.

Resources

Here are some resources I strongly recommend (they aren't even half of what I read and watched):

  • Rust
  • Networking & Serialization
    • Book: "Network Programming with Rust" - Abhishek Chanda
  • ECS
    • Paper: "ECS Game Engine Design" - Daniel Hall
  • Game Engine YouTube series