Skip to content

Commit 10a8def

Browse files
committed
feat: bot performance with improved tick rate, bandwidth tracking, and added test-demo
1 parent 8f6c6ca commit 10a8def

6 files changed

Lines changed: 116 additions & 41 deletions

File tree

README.md

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,38 @@
88
[![Reshiram](https://img.pokemondb.net/sprites/black-white/anim/normal/reshiram.gif)](https://pokemondb.net/pokedex/reshiram)
99
[![Kyurem](https://img.pokemondb.net/sprites/black-white/anim/normal/kyurem.gif)](https://pokemondb.net/pokedex/kyurem)
1010

11-
Networking in game is super frustrating, even a simple relay server takes lots of time.
12-
Forgot the actual game, now you are stuck with networking code chaos, forever and eternally, and you will never finish
13-
your game, and you will never be satisfied, and you will never be proud of your work, and you will never be able to show
14-
it to anyone or PLAY, and you will never be able to have fun at all......YOU WILL NEVER BE LOVED EVER....AGAIN....
15-
and you will just be stuck in this endless loop of networking code forever.
11+
Networking in games is pure suffering.
12+
13+
You start with “just a simple relay server.” *(alone let authoritative servers)*
14+
Just a few sockets. A couple messages. Clean.
15+
Few hours later:
16+
you’re debugging why one packet arrives in the future, one in the past, and one simply refuses to exist.
17+
You forget the game.
18+
The game forgets you.
19+
Now it’s just you, a growing pile of networking code,
20+
and a silent client that *definitely* connected but somehow didn’t.
21+
You promise yourself:
22+
“just one more fix”
23+
Congratulations.
24+
You are now in a long-term relationship with a race condition.
25+
Days pass.
26+
You no longer render frames.
27+
You render logs.
28+
At some point, you question reality:
29+
“is UDP unordered, or am I?”
30+
Eventually, you accept it.
31+
There is no game.
32+
There never was.
33+
Only packets.
34+
Only retries.
35+
Only pain.
1636

1737
**Dakara watashi ga anata o sukutte agemashou**
1838

1939
This will be a very simple AND opinionated game network libray not a daemon
20-
(You will still need to embed it in a server wrapper or something)
40+
`(You will still need to embed it in a server wrapper or something,
41+
and also this is not a Authoritative server library, thought it can be made with more hooks and callbacks,
42+
but the main point is effortlessly broadcasting with high tick rate and low-memory usage)`
2143

2244
> Note: This is very experimental.
2345
> I am making this for learning networking and get familiar with tokio ecosystem,
@@ -32,6 +54,15 @@ Usage (I don't like release cycles and versioning, so just use local)
3254
game-server = { git = "https://github.com/ronakgh97/ghost-sync" }
3355
```
3456

57+
![Bot-test](test-demo.gif)
58+
59+
Tick rate: 60 hz
60+
Bot-client: 128
61+
62+
`N * C msg/second for each broadcast, so (N - 1) * (N * C) msg/second in total broadcast`
63+
64+
*That's 58,521,600 msg/second with 512 mb buffer channel*
65+
3566
TODO
3667

3768
Better lib design, currently its just a mess of functions and structs, need to refactor it into a more usable and
@@ -40,3 +71,4 @@ Add more examples, maybe a mini-game?
4071
Experimental UDP support, maybe using QUIC?
4172
Add tuned buffering and improve performance by lessen serialization and deserialization, where possible (Zero-copy,
4273
etc.)
74+
Fixed the DAMN BACKPRESSURE, CHANNEL GETS FULL AND THEN EVERYTHING BLOWS UP

examples/bot.rs

Lines changed: 68 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,30 @@ use ::rand::RngExt;
22
use dashmap::DashMap;
33
use ghost_sync::{Client, ServerEvent};
44
use macroquad::prelude::*;
5+
use std::sync::atomic::{AtomicU64, Ordering};
56
use std::sync::Arc;
67
use std::thread;
78
use std::time::{Duration, Instant};
89
use uuid::Uuid;
910
use wincode::{SchemaRead, SchemaWrite};
1011

1112
const ADDR: &str = "127.0.0.1:7777";
12-
const ROOM: &str = "room-128";
13+
const ROOM: &str = "test-room";
1314
const BOT_COUNT: usize = 128;
14-
const TICK_RATE: u64 = 20;
15-
const MAX_SPEED: f32 = 100.0;
15+
const TICK_RATE: u64 = 60;
16+
const MAX_SPEED: f32 = 400.0;
1617
const SCREEN: f32 = 800.0;
1718

19+
static BYTES_SENT: AtomicU64 = AtomicU64::new(0);
20+
static BYTES_RECV: AtomicU64 = AtomicU64::new(0);
21+
1822
fn winconfig() -> Conf {
1923
Conf {
2024
window_title: "Bot Stress Test".into(),
2125
window_width: SCREEN as i32,
2226
window_height: SCREEN as i32,
2327
window_resizable: false,
24-
sample_count: BOT_COUNT as i32,
28+
sample_count: 512,
2529
..Default::default()
2630
}
2731
}
@@ -56,7 +60,7 @@ impl Bot {
5660
#[inline(always)]
5761
pub fn update(&mut self, dt: f32) {
5862
let target_pos = self.pos + self.vel * dt;
59-
self.pos = self.pos.lerp(target_pos, 0.225); // Smooth movement visual
63+
self.pos = self.pos.lerp(target_pos, 0.225); // Smooth visual (render only)
6064
self.pos.x = self.pos.x.rem_euclid(SCREEN);
6165
self.pos.y = self.pos.y.rem_euclid(SCREEN);
6266
}
@@ -76,12 +80,12 @@ async fn bot_loop(map: BotMap, bot_index: usize) {
7680
let mut client = match Client::connect(ADDR).await {
7781
Ok(c) => c,
7882
Err(e) => {
79-
eprintln!("[{bot_index}] connect error: {e}");
83+
eprintln!("Bot: [{bot_index}] connect error: {e}");
8084
return;
8185
}
8286
};
8387
if let Err(e) = client.join(ROOM).await {
84-
eprintln!("[{bot_index}] join error: {e}");
88+
eprintln!("Bot: [{bot_index}] join error: {e}");
8589
return;
8690
}
8791

@@ -92,7 +96,7 @@ async fn bot_loop(map: BotMap, bot_index: usize) {
9296

9397
map.insert(self_id, Bot::new(&mut rng));
9498

95-
// Tick rate limiter
99+
// Tick limit to avoid flooding the server with updates. The bot's movement is smoothed
96100
let send_interval = Duration::from_millis(1000 / TICK_RATE);
97101
let mut last_send = Instant::now();
98102

@@ -106,33 +110,41 @@ async fn bot_loop(map: BotMap, bot_index: usize) {
106110
vx: bot.vel.x,
107111
vy: bot.vel.y,
108112
};
109-
let _ = client
110-
.broadcast(&wincode::serialize(&payload).unwrap())
111-
.await;
113+
let data = wincode::serialize(&payload).unwrap();
114+
BYTES_SENT.fetch_add(data.len() as u64, Ordering::Relaxed);
115+
let _ = client.broadcast(&data).await;
112116

113117
bot.random_velo(&mut rng);
114118
}
115119
}
116120

117121
loop {
118-
// Draining incoming messages with a short timeout to avoid stalling the loop
119-
match tokio::time::timeout(Duration::from_micros(10), client.recv()).await {
122+
// Timeout for recv to avoid stalling the bot loop if the server is unresponsive.
123+
// The bot's movement is still updated and rendered during this time
124+
match tokio::time::timeout(Duration::from_millis(100), client.recv()).await {
120125
Ok(Ok(Some(ServerEvent::Broadcast { sender_id, data })))
121-
// Update the bot map, we will render them later
122126
if sender_id != self_id =>
123127
{
128+
BYTES_RECV.fetch_add(data.len() as u64, Ordering::Relaxed);
124129
if let Ok(v) = wincode::deserialize::<Payload>(&data) {
125130
map.entry(v.id)
126131
.and_modify(|b| b.vel = Vec2::new(v.vx, v.vy))
127132
.or_insert(Bot {
128133
pos: Vec2::new(
129-
rng.random::<f32>() * SCREEN,
130-
rng.random::<f32>() * SCREEN,
134+
rng.random_range(0.0..SCREEN),
135+
rng.random_range(0.0..SCREEN),
131136
),
132137
vel: Vec2::new(v.vx, v.vy),
133138
});
134139
}
135140
}
141+
Ok(Ok(Some(ServerEvent::PlayerLeft { client_id }))) => {
142+
map.remove(&client_id);
143+
eprintln!("Bot: {client_id} disconnected, removed from map");
144+
}
145+
Ok(Ok(Some(ServerEvent::Joined { client_id, .. }))) => {
146+
eprintln!("Bot: {client_id} joined");
147+
}
136148
_ => break,
137149
}
138150
}
@@ -143,28 +155,61 @@ async fn bot_loop(map: BotMap, bot_index: usize) {
143155

144156
#[macroquad::main(winconfig)]
145157
async fn main() {
146-
let map: BotMap = Arc::new(DashMap::new());
158+
let map: BotMap = Arc::new(DashMap::with_capacity(BOT_COUNT));
147159

148160
for i in 0..BOT_COUNT {
149161
let m = map.clone();
150-
thread::spawn(move || {
151-
let rt = tokio::runtime::Runtime::new().unwrap();
152-
rt.block_on(bot_loop(m, i));
162+
thread::spawn(move || match tokio::runtime::Runtime::new() {
163+
Ok(rt) => rt.block_on(bot_loop(m, i)),
164+
Err(e) => eprintln!("Tokio runtime error: {e}"),
153165
});
154166
}
155167

156-
let delta = get_frame_time();
168+
let mut last_bandwidth = Instant::now();
169+
let mut sent_acc = 0u64;
170+
let mut recv_acc = 0u64;
157171

158172
loop {
173+
let delta = get_frame_time();
174+
159175
clear_background(BLACK);
176+
177+
let connected = map.len();
160178
draw_text(
161-
&format!("Bots:{BOT_COUNT} Fps:{:.0}", 1.0 / delta.max(0.001)),
179+
&format!("Bots: {connected}/{BOT_COUNT}"),
162180
10.0,
163181
24.0,
164182
20.0,
165183
WHITE,
166184
);
167-
draw_text(&format!("Tick rate: {TICK_RATE}"), 10.0, 48.0, 20.0, WHITE);
185+
draw_text(
186+
&format!("FPS: {:.0}", 1.0 / delta.max(0.001)),
187+
10.0,
188+
48.0,
189+
20.0,
190+
WHITE,
191+
);
192+
draw_text(&format!("Tick rate: {TICK_RATE}"), 10.0, 72.0, 20.0, WHITE);
193+
194+
if last_bandwidth.elapsed().as_secs_f32() >= 1.0 {
195+
sent_acc = BYTES_SENT.swap(0, Ordering::Relaxed);
196+
recv_acc = BYTES_RECV.swap(0, Ordering::Relaxed);
197+
last_bandwidth = Instant::now();
198+
}
199+
draw_text(
200+
&format!("TX: {} KB/s", sent_acc / 1024),
201+
10.0,
202+
96.0,
203+
20.0,
204+
GREEN,
205+
);
206+
draw_text(
207+
&format!("RX: {} KB/s", recv_acc / 1024),
208+
10.0,
209+
120.0,
210+
20.0,
211+
BLUE,
212+
);
168213

169214
for mut entry in map.iter_mut() {
170215
let bot = entry.value_mut();

examples/daemon.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,9 +266,9 @@ async fn main() -> Result<()> {
266266
.bind("0.0.0.0:7777")
267267
.max_clients(1024)
268268
.max_payload(256 * 1024)
269-
.idle_timeout(Duration::from_secs(31))
270-
.ping_interval(Duration::from_secs(12))
271-
.channel_capacity(1024 * 1024 * 64)
269+
.idle_timeout(Duration::from_secs(25))
270+
.ping_interval(Duration::from_secs(15))
271+
.channel_capacity(1024 * 1024 * 512)
272272
.handler(DaemonHandler::new(daemon.clone()))
273273
.build();
274274

@@ -279,6 +279,8 @@ async fn main() -> Result<()> {
279279
server_handle.create_room(&format!("room-{}", i))?;
280280
}
281281

282+
server_handle.create_room("test-room")?;
283+
282284
let ctrl_listener = TcpListener::bind("0.0.0.0:8888").await?;
283285
info!("Control server listening on 0.0.0.0:8888");
284286

src/client.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,8 @@ impl ClientBuilder {
138138
let (read_half, write_half) = stream.into_split();
139139

140140
Ok(Client {
141-
reader: BufReader::with_capacity(64 * 1024, read_half),
142-
writer: BufWriter::with_capacity(64 * 1024, write_half),
141+
reader: BufReader::with_capacity(128 * 1024, read_half),
142+
writer: BufWriter::with_capacity(128 * 1024, write_half),
143143
max_payload: self.max_payload,
144144
})
145145
}

test-demo.gif

17.9 MB
Loading

tests/integration.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -426,18 +426,14 @@ async fn runtime_room_management() {
426426
}
427427

428428
assert_eq!(handle.room_client_count("match-1"), Some(1));
429-
430-
// Delete room at runtime (soft delete)
431429
assert!(handle.delete_room("match-1"));
432430
assert!(!handle.room_exists("match-1"));
433431
assert_eq!(handle.room_count(), 1);
434-
435-
// Delete non-existent room returns false
436432
assert!(!handle.delete_room("nonexistent"));
437433
}
438434

439-
const LOAD_CLIENTS: usize = 512;
440-
const LOAD_DURATION: Duration = Duration::from_secs(12);
435+
const LOAD_CLIENTS: usize = 256;
436+
const LOAD_DURATION: Duration = Duration::from_secs(8);
441437

442438
#[inline(always)]
443439
fn get_random_bytes(size: u32) -> Vec<u8> {
@@ -542,7 +538,7 @@ async fn server_load_test() {
542538
.max_payload(1024)
543539
.idle_timeout(Duration::from_secs(60))
544540
.ping_interval(Duration::from_secs(30))
545-
.channel_capacity(256)
541+
.channel_capacity(1024)
546542
.build();
547543

548544
server.pre_create_room("load_test").unwrap();

0 commit comments

Comments
 (0)