init
This commit is contained in:
commit
f978b8af17
21 changed files with 4015 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
/target
|
3314
Cargo.lock
generated
Normal file
3314
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
25
Cargo.toml
Normal file
25
Cargo.toml
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
[package]
|
||||||
|
name = "d5"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bevy = { version = "0.14.2", default-features = false, features = [
|
||||||
|
"multi_threaded",
|
||||||
|
] }
|
||||||
|
bevy_eventwork = "0.9.0"
|
||||||
|
itermore = { version = "0.7.1", features = ["array_chunks"] }
|
||||||
|
rand = "0.8.5"
|
||||||
|
serde = { version = "1.0.215", features = ["derive"] }
|
||||||
|
sysinfo = { version = "0.32.0", default-features = false, features = [
|
||||||
|
"system",
|
||||||
|
] }
|
||||||
|
termion = "4.0.3"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "server"
|
||||||
|
path = "src/server.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "client"
|
||||||
|
path = "src/client.rs"
|
2
rustfmt.toml
Normal file
2
rustfmt.toml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
hard_tabs = true
|
||||||
|
max_width = 120
|
19
src/client.rs
Normal file
19
src/client.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use bevy::{app::ScheduleRunnerPlugin, prelude::*};
|
||||||
|
use lib_client::{display::DisplayPlugin, input::InputPlugin, net::NetPlugin};
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
mod lib_client;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new()
|
||||||
|
// Core.
|
||||||
|
.add_plugins(ScheduleRunnerPlugin::run_loop(Duration::from_millis(10)))
|
||||||
|
.add_plugins(NetPlugin)
|
||||||
|
.add_plugins(DisplayPlugin)
|
||||||
|
.add_plugins(InputPlugin)
|
||||||
|
.run();
|
||||||
|
}
|
31
src/common.rs
Normal file
31
src/common.rs
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
use bevy::math::IVec2;
|
||||||
|
use bevy_eventwork::NetworkMessage;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
pub mod server_msg {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct MapUpdates(pub Vec<(IVec2, (char, char))>);
|
||||||
|
netify!(MapUpdates);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod client_msg {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct Move(pub IVec2);
|
||||||
|
netify!(Move);
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! netify {
|
||||||
|
($name:ident) => {
|
||||||
|
impl NetworkMessage for $name {
|
||||||
|
const NAME: &'static str = stringify!($name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) use netify;
|
||||||
|
|
||||||
|
pub const RENDER_DISTANCE: i32 = 12;
|
48
src/lib_client/display.rs
Normal file
48
src/lib_client/display.rs
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy_eventwork::{tcp::TcpProvider, AppNetworkMessage, NetworkData};
|
||||||
|
|
||||||
|
use termion::{clear, cursor::Goto};
|
||||||
|
|
||||||
|
use crate::common::{self, server_msg::MapUpdates};
|
||||||
|
|
||||||
|
pub struct DisplayPlugin;
|
||||||
|
|
||||||
|
impl Plugin for DisplayPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.listen_for_message::<MapUpdates, TcpProvider>()
|
||||||
|
.add_systems(Update, handle_map_updates);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Display;
|
||||||
|
|
||||||
|
impl Display {
|
||||||
|
fn draw(&self, pos: IVec2, (l, r): (char, char)) {
|
||||||
|
let pos = pos + IVec2::new(RENDER_DISTANCE, RENDER_DISTANCE);
|
||||||
|
let x = (pos.x * 2) + 1;
|
||||||
|
let y = ((2 * RENDER_DISTANCE) - pos.y) + 1;
|
||||||
|
print!("{}{}{}", Goto(x as _, y as _), l, r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const RENDER_DISTANCE: i32 = common::RENDER_DISTANCE;
|
||||||
|
const RENDER_DISTANCE_SQ: i32 = RENDER_DISTANCE * RENDER_DISTANCE;
|
||||||
|
|
||||||
|
fn handle_map_updates(mut updates: EventReader<NetworkData<MapUpdates>>) {
|
||||||
|
for update in updates.read() {
|
||||||
|
print!("{}", clear::All);
|
||||||
|
for x in -RENDER_DISTANCE..RENDER_DISTANCE {
|
||||||
|
for y in -RENDER_DISTANCE..RENDER_DISTANCE {
|
||||||
|
let pos = IVec2::new(x, y);
|
||||||
|
if pos.distance_squared(IVec2::ZERO) <= RENDER_DISTANCE_SQ {
|
||||||
|
Display.draw(pos, ('.', ' '));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (pos, chars) in &update.0 {
|
||||||
|
Display.draw(*pos, *chars);
|
||||||
|
}
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
77
src/lib_client/input.rs
Normal file
77
src/lib_client/input.rs
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
use bevy_eventwork::{tcp::TcpProvider, ConnectionId, Network};
|
||||||
|
use std::{
|
||||||
|
io::{stdin, stdout, Stdout},
|
||||||
|
process,
|
||||||
|
sync::{mpsc, Arc, Mutex},
|
||||||
|
thread::{self, JoinHandle},
|
||||||
|
};
|
||||||
|
use termion::{
|
||||||
|
cursor::{self, HideCursor},
|
||||||
|
event::Key,
|
||||||
|
input::TermRead,
|
||||||
|
raw::{IntoRawMode, RawTerminal},
|
||||||
|
};
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use crate::common::client_msg::Move;
|
||||||
|
|
||||||
|
use super::net::Server;
|
||||||
|
|
||||||
|
pub struct InputPlugin;
|
||||||
|
|
||||||
|
impl Plugin for InputPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
let (rec, _thread) = spawn_reader();
|
||||||
|
let rec = Arc::new(Mutex::new(rec));
|
||||||
|
app.insert_resource(InputReceiver(rec))
|
||||||
|
.add_event::<KeyEvent>()
|
||||||
|
.add_systems(Update, try_read_keys)
|
||||||
|
.add_systems(Update, on_move);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct InputReceiver(Arc<Mutex<mpsc::Receiver<Key>>>);
|
||||||
|
|
||||||
|
#[derive(Debug, Event)]
|
||||||
|
pub struct KeyEvent(Key);
|
||||||
|
|
||||||
|
fn spawn_reader() -> (mpsc::Receiver<Key>, JoinHandle<()>) {
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
let thread = thread::spawn(move || {
|
||||||
|
let stdout = stdout().into_raw_mode().unwrap();
|
||||||
|
println!("{}", cursor::Hide);
|
||||||
|
let mut keys = stdin().keys();
|
||||||
|
while let Some(Ok(key)) = keys.next() {
|
||||||
|
if key == Key::Esc {
|
||||||
|
println!("exitting.");
|
||||||
|
println!("{}", cursor::Restore);
|
||||||
|
drop(stdout);
|
||||||
|
process::exit(0);
|
||||||
|
}
|
||||||
|
tx.send(key).ok();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
(rx, thread)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_read_keys(receiver: Res<InputReceiver>, mut writer: EventWriter<KeyEvent>) {
|
||||||
|
let receiver = receiver.0.lock().expect("Poisoned threads doomed the state.");
|
||||||
|
writer.send_batch(receiver.try_iter().map(KeyEvent));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_move(mut keys: EventReader<KeyEvent>, server: Res<Server>, net: Res<Network<TcpProvider>>) {
|
||||||
|
if let Some(server) = server.0 {
|
||||||
|
for key in keys.read() {
|
||||||
|
let dir = match key.0 {
|
||||||
|
Key::Char('z') => IVec2::Y,
|
||||||
|
Key::Char('s') => -IVec2::Y,
|
||||||
|
Key::Char('q') => -IVec2::X,
|
||||||
|
Key::Char('d') => IVec2::X,
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
net.send_message(server, Move(dir)).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
src/lib_client/mod.rs
Normal file
3
src/lib_client/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
pub mod display;
|
||||||
|
pub mod input;
|
||||||
|
pub mod net;
|
50
src/lib_client/net.rs
Normal file
50
src/lib_client/net.rs
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
use bevy::{
|
||||||
|
prelude::*,
|
||||||
|
tasks::{TaskPool, TaskPoolBuilder},
|
||||||
|
};
|
||||||
|
use bevy_eventwork::{
|
||||||
|
tcp::{NetworkSettings, TcpProvider},
|
||||||
|
ConnectionId, EventworkPlugin, EventworkRuntime, Network, NetworkEvent,
|
||||||
|
};
|
||||||
|
use termion::clear;
|
||||||
|
|
||||||
|
pub struct NetPlugin;
|
||||||
|
|
||||||
|
impl Plugin for NetPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_plugins(EventworkPlugin::<TcpProvider, TaskPool>::default())
|
||||||
|
.insert_resource(EventworkRuntime(TaskPoolBuilder::new().num_threads(2).build()))
|
||||||
|
.insert_resource(NetworkSettings::default())
|
||||||
|
.add_systems(Update, handle_net_event)
|
||||||
|
.init_resource::<Server>()
|
||||||
|
.add_systems(Startup, do_connect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Resource, Default)]
|
||||||
|
pub struct Server(pub Option<ConnectionId>);
|
||||||
|
|
||||||
|
fn handle_net_event(mut events: EventReader<NetworkEvent>, mut server: ResMut<Server>) {
|
||||||
|
for event in events.read() {
|
||||||
|
match event {
|
||||||
|
NetworkEvent::Connected(connection_id) => {
|
||||||
|
println!("Connected '{connection_id:?}'.");
|
||||||
|
server.0 = Some(*connection_id);
|
||||||
|
}
|
||||||
|
NetworkEvent::Disconnected(connection_id) => {
|
||||||
|
print!("{}", clear::All);
|
||||||
|
println!("Disconnected '{connection_id:?}'.");
|
||||||
|
panic!();
|
||||||
|
}
|
||||||
|
NetworkEvent::Error(network_error) => println!("Error '{network_error}'."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_connect(
|
||||||
|
net: Res<Network<TcpProvider>>,
|
||||||
|
settings: Res<NetworkSettings>,
|
||||||
|
task_pool: Res<EventworkRuntime<TaskPool>>,
|
||||||
|
) {
|
||||||
|
net.connect(([0, 0, 0, 0], 9000).into(), &task_pool.0, &settings);
|
||||||
|
}
|
24
src/lib_server/counter.rs
Normal file
24
src/lib_server/counter.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
pub struct CounterPlugin;
|
||||||
|
|
||||||
|
impl Plugin for CounterPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(Startup, sys_setup_counter)
|
||||||
|
.add_systems(Update, sys_count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Component)]
|
||||||
|
struct Counter(usize);
|
||||||
|
|
||||||
|
fn sys_count(mut query: Query<&mut Counter>) {
|
||||||
|
for mut c in &mut query {
|
||||||
|
println!("{c:?}");
|
||||||
|
c.0 += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sys_setup_counter(mut commands: Commands) {
|
||||||
|
commands.spawn(Counter(0));
|
||||||
|
}
|
67
src/lib_server/display.rs
Normal file
67
src/lib_server/display.rs
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
use crate::common::{self, server_msg::MapUpdates};
|
||||||
|
use bevy::{prelude::*, utils::hashbrown::HashSet};
|
||||||
|
use bevy_eventwork::{tcp::TcpProvider, ConnectionId, Network};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
physics::{update_physics_collisions, Pos},
|
||||||
|
player::Player,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct DisplayPlugin;
|
||||||
|
|
||||||
|
impl Plugin for DisplayPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_event::<RenderUpdate>()
|
||||||
|
// .add_systems(Update, rerender_players)
|
||||||
|
.add_systems(Update, on_updates.after(update_physics_collisions));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Component)]
|
||||||
|
pub struct Display(pub (char, char));
|
||||||
|
|
||||||
|
#[derive(Debug, Event)]
|
||||||
|
pub struct RenderUpdate(pub IVec2);
|
||||||
|
|
||||||
|
const RENDER_DISTANCE: i32 = common::RENDER_DISTANCE;
|
||||||
|
const RENDER_DISTANCE_SQ: i32 = RENDER_DISTANCE * RENDER_DISTANCE;
|
||||||
|
|
||||||
|
fn on_updates(
|
||||||
|
mut updates: EventReader<RenderUpdate>,
|
||||||
|
players: Query<(&Player, &Pos)>,
|
||||||
|
displayables: Query<(&Pos, &Display)>,
|
||||||
|
net: Res<Network<TcpProvider>>,
|
||||||
|
) {
|
||||||
|
let mut to_render = HashSet::new();
|
||||||
|
for RenderUpdate(event_pos) in updates.read() {
|
||||||
|
for (Player(connection_id), player_pos) in &players {
|
||||||
|
if player_pos.pos().distance_squared(*event_pos) <= RENDER_DISTANCE_SQ {
|
||||||
|
to_render.insert((*connection_id, player_pos.pos()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (id, pos) in to_render {
|
||||||
|
render(&net, id, pos, &displayables);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(
|
||||||
|
net: &Res<Network<TcpProvider>>,
|
||||||
|
id: ConnectionId,
|
||||||
|
player_pos: IVec2,
|
||||||
|
displayables: &Query<(&Pos, &Display)>,
|
||||||
|
) {
|
||||||
|
let updates = displayables
|
||||||
|
.iter()
|
||||||
|
.filter(|(pos, _)| pos.pos().distance_squared(player_pos) <= RENDER_DISTANCE_SQ)
|
||||||
|
.map(|(pos, Display(display))| (pos.pos() - player_pos, *display))
|
||||||
|
.collect();
|
||||||
|
net.send_message(id, MapUpdates(updates)).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rerender_players(players: Query<&Pos, With<Player>>, mut writer: EventWriter<RenderUpdate>) {
|
||||||
|
for Pos(pos) in &players {
|
||||||
|
writer.send(RenderUpdate(pos.as_ivec2()));
|
||||||
|
}
|
||||||
|
}
|
54
src/lib_server/map.rs
Normal file
54
src/lib_server/map.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
use std::ops::Not;
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use itermore::IterArrayChunks;
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
display::Display,
|
||||||
|
physics::{Mass, Pos},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn spawn_block(cmd: &mut Commands, pos: IVec2, display: (char, char), mass: usize) {
|
||||||
|
cmd.spawn((Pos(pos.as_vec2()), Mass(mass), Display(display)));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn str_to_struct(text: &str) -> Vec<(IVec2, (char, char), usize)> {
|
||||||
|
text.lines()
|
||||||
|
.enumerate()
|
||||||
|
.flat_map(|(y, line)| {
|
||||||
|
line.chars().array_chunks().enumerate().filter_map(move |(x, [l, r])| {
|
||||||
|
(l == ' ' && r == ' ')
|
||||||
|
.not()
|
||||||
|
.then_some((IVec2::new(x as _, y as _), (l, r), mass_of((l, r))))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mass_of(tiles: (char, char)) -> usize {
|
||||||
|
match tiles {
|
||||||
|
('#', '#') => 100,
|
||||||
|
('X', 'X') => 10,
|
||||||
|
('[', ']') => 5,
|
||||||
|
('(', ')') => 1,
|
||||||
|
_ => 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn_debug_map(mut cmd: Commands) {
|
||||||
|
let raw = "
|
||||||
|
()
|
||||||
|
|
||||||
|
########## ()
|
||||||
|
()## ##
|
||||||
|
## []
|
||||||
|
## ##
|
||||||
|
##########()
|
||||||
|
|
||||||
|
|
||||||
|
";
|
||||||
|
|
||||||
|
for (pos, display, mass) in str_to_struct(raw) {
|
||||||
|
spawn_block(&mut cmd, pos, display, mass);
|
||||||
|
}
|
||||||
|
}
|
27
src/lib_server/metrics.rs
Normal file
27
src/lib_server/metrics.rs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use sysinfo::{CpuRefreshKind, RefreshKind, System};
|
||||||
|
use termion::cursor;
|
||||||
|
|
||||||
|
use super::player::Player;
|
||||||
|
|
||||||
|
pub struct MetricsPlugin;
|
||||||
|
|
||||||
|
#[derive(Debug, Resource)]
|
||||||
|
pub struct SysRes(System);
|
||||||
|
|
||||||
|
impl Plugin for MetricsPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
let sys = System::new_with_specifics(RefreshKind::new().with_cpu(CpuRefreshKind::new().with_cpu_usage()));
|
||||||
|
app.add_systems(Update, show_metrics).insert_resource(SysRes(sys));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_metrics(time: Res<Time>, mut sys: ResMut<SysRes>, players: Query<(), With<Player>>) {
|
||||||
|
sys.0.refresh_all();
|
||||||
|
let cpu = sys.0.global_cpu_usage();
|
||||||
|
let player_count = players.iter().count();
|
||||||
|
let time_delta = time.delta_seconds();
|
||||||
|
|
||||||
|
let up = cursor::Up(1);
|
||||||
|
println!(" | players {player_count:2} | tick {time_delta:2.3} | cpu {cpu:2.1}{up} ");
|
||||||
|
}
|
7
src/lib_server/mod.rs
Normal file
7
src/lib_server/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
pub mod counter;
|
||||||
|
pub mod display;
|
||||||
|
pub mod map;
|
||||||
|
pub mod metrics;
|
||||||
|
pub mod net;
|
||||||
|
pub mod physics;
|
||||||
|
pub mod player;
|
55
src/lib_server/net.rs
Normal file
55
src/lib_server/net.rs
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
use bevy::{
|
||||||
|
prelude::*,
|
||||||
|
tasks::{TaskPool, TaskPoolBuilder},
|
||||||
|
};
|
||||||
|
use bevy_eventwork::{
|
||||||
|
tcp::{NetworkSettings, TcpProvider},
|
||||||
|
EventworkPlugin, EventworkRuntime, Network, NetworkEvent,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::lib_server::player::{remove_player, spawn_player};
|
||||||
|
|
||||||
|
use super::{display::RenderUpdate, player::Player};
|
||||||
|
|
||||||
|
pub struct NetPlugin;
|
||||||
|
|
||||||
|
impl Plugin for NetPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_plugins(EventworkPlugin::<TcpProvider, TaskPool>::default())
|
||||||
|
.insert_resource(EventworkRuntime(TaskPoolBuilder::new().num_threads(2).build()))
|
||||||
|
.insert_resource(NetworkSettings::default())
|
||||||
|
.add_systems(Update, handle_net_event)
|
||||||
|
.add_systems(Startup, do_listen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_net_event(
|
||||||
|
mut cmd: Commands,
|
||||||
|
mut events: EventReader<NetworkEvent>,
|
||||||
|
mut render_update: EventWriter<RenderUpdate>,
|
||||||
|
players: Query<(Entity, &Player)>,
|
||||||
|
) {
|
||||||
|
for event in events.read() {
|
||||||
|
match event {
|
||||||
|
NetworkEvent::Connected(conn_id) => {
|
||||||
|
println!("New connection, {conn_id:?}.");
|
||||||
|
spawn_player(&mut cmd, *conn_id, IVec2::ZERO);
|
||||||
|
render_update.send(RenderUpdate(IVec2::ZERO));
|
||||||
|
}
|
||||||
|
NetworkEvent::Disconnected(conn_id) => {
|
||||||
|
println!("Lost connection, {conn_id:?}.");
|
||||||
|
remove_player(&mut cmd, *conn_id, &players);
|
||||||
|
}
|
||||||
|
NetworkEvent::Error(network_error) => println!("Network failure '{network_error}'."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn do_listen(
|
||||||
|
mut net: ResMut<Network<TcpProvider>>,
|
||||||
|
settings: Res<NetworkSettings>,
|
||||||
|
task_pool: Res<EventworkRuntime<TaskPool>>,
|
||||||
|
) {
|
||||||
|
net.listen(([0, 0, 0, 0], 9000).into(), &task_pool.0, &settings)
|
||||||
|
.unwrap();
|
||||||
|
}
|
97
src/lib_server/physics.rs
Normal file
97
src/lib_server/physics.rs
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
use super::{display::RenderUpdate, player};
|
||||||
|
|
||||||
|
pub struct PhysicsPlugin;
|
||||||
|
|
||||||
|
impl Plugin for PhysicsPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(
|
||||||
|
Update,
|
||||||
|
(
|
||||||
|
update_physics_forces.after(player::handle_move),
|
||||||
|
update_physics_center.after(update_physics_forces),
|
||||||
|
update_physics_collisions.after(update_physics_center),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Component, Clone)]
|
||||||
|
pub struct Pos(pub Vec2);
|
||||||
|
|
||||||
|
impl Pos {
|
||||||
|
pub fn fpos(&self) -> Vec2 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pos_center(&self) -> Vec2 {
|
||||||
|
self.pos().as_vec2()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pos(&self) -> IVec2 {
|
||||||
|
self.0.round().as_ivec2()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dir_to_center(&self) -> Vec2 {
|
||||||
|
self.pos_center() - self.fpos()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Component)]
|
||||||
|
pub struct Mass(pub usize);
|
||||||
|
|
||||||
|
#[derive(Debug, Component)]
|
||||||
|
pub struct Forces(Vec2);
|
||||||
|
|
||||||
|
impl Forces {
|
||||||
|
pub fn zero() -> Self {
|
||||||
|
Self::new(Vec2::ZERO)
|
||||||
|
}
|
||||||
|
pub fn new(forces: Vec2) -> Self {
|
||||||
|
Self(forces)
|
||||||
|
}
|
||||||
|
pub fn add(&mut self, force: Vec2) {
|
||||||
|
self.0 += force;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MAX_MOVEMENT_PER_TICK: f32 = 0.3;
|
||||||
|
|
||||||
|
fn update_physics_forces(mut blocks: Query<(&mut Pos, &mut Forces)>, mut updater: EventWriter<RenderUpdate>) {
|
||||||
|
for (mut pos, mut forces) in &mut blocks {
|
||||||
|
let old_pos = pos.clone();
|
||||||
|
|
||||||
|
let applied = forces.0;
|
||||||
|
let applied = applied.clamp_length_max(MAX_MOVEMENT_PER_TICK);
|
||||||
|
pos.0 += applied;
|
||||||
|
forces.0 -= applied;
|
||||||
|
|
||||||
|
if old_pos.pos() != pos.pos() {
|
||||||
|
updater.send(RenderUpdate(old_pos.pos()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_physics_collisions(mut blocks: Query<(&mut Pos, &Mass)>) {
|
||||||
|
let mut combinations = blocks.iter_combinations_mut();
|
||||||
|
while let Some([(mut a_pos, Mass(a_mass)), (mut b_pos, Mass(b_mass))]) = combinations.fetch_next() {
|
||||||
|
if a_pos.pos() == b_pos.pos() {
|
||||||
|
let a_to_b = (b_pos.fpos() - a_pos.fpos()).normalize_or(Vec2::Y);
|
||||||
|
let total_mass = (a_mass + b_mass) as f32;
|
||||||
|
let a_mass_frac = (*a_mass) as f32 / total_mass;
|
||||||
|
let b_mass_frac = (*b_mass) as f32 / total_mass;
|
||||||
|
let a_movement = (-a_to_b) * b_mass_frac;
|
||||||
|
let b_movement = (a_to_b) * a_mass_frac;
|
||||||
|
a_pos.0 += a_movement;
|
||||||
|
b_pos.0 += b_movement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_physics_center(mut blocks: Query<(&Pos, &mut Forces)>) {
|
||||||
|
for (pos, mut forces) in &mut blocks {
|
||||||
|
let dir = pos.pos_center() - pos.0;
|
||||||
|
forces.add(dir * 0.75);
|
||||||
|
}
|
||||||
|
}
|
74
src/lib_server/player.rs
Normal file
74
src/lib_server/player.rs
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
use crate::common::client_msg::Move;
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use bevy_eventwork::{tcp::TcpProvider, AppNetworkMessage, ConnectionId, NetworkData};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
display::{Display, RenderUpdate},
|
||||||
|
physics::{Forces, Mass, Pos},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct PlayerPlugin;
|
||||||
|
|
||||||
|
impl Plugin for PlayerPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.listen_for_message::<Move, TcpProvider>()
|
||||||
|
.add_event::<RenderUpdate>()
|
||||||
|
.add_systems(Update, handle_move);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Component)]
|
||||||
|
pub struct Player(pub ConnectionId);
|
||||||
|
|
||||||
|
#[derive(Debug, Component)]
|
||||||
|
pub struct Dir(pub IVec2);
|
||||||
|
|
||||||
|
fn player_display_for_dir(dir: IVec2) -> (char, char) {
|
||||||
|
match dir {
|
||||||
|
IVec2::Y => ('&', '/'),
|
||||||
|
IVec2::NEG_X => ('_', '&'),
|
||||||
|
IVec2::X => ('&', '_'),
|
||||||
|
_ => ('&', '\\'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn_player(cmd: &mut Commands, conn_id: ConnectionId, pos: IVec2) {
|
||||||
|
cmd.spawn((
|
||||||
|
Player(conn_id),
|
||||||
|
Pos(pos.as_vec2()),
|
||||||
|
Display(player_display_for_dir(IVec2::X)),
|
||||||
|
Dir(IVec2::X),
|
||||||
|
Forces::zero(),
|
||||||
|
Mass(10),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_player(cmd: &mut Commands, conn_id: ConnectionId, players: &Query<(Entity, &Player)>) {
|
||||||
|
for (entity, player) in players {
|
||||||
|
if player.0 == conn_id {
|
||||||
|
cmd.entity(entity).despawn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_move(
|
||||||
|
mut messages: EventReader<NetworkData<Move>>,
|
||||||
|
mut players: Query<(&Player, &mut Forces, &Pos, &mut Dir, &mut Display)>,
|
||||||
|
) {
|
||||||
|
for message in messages.read() {
|
||||||
|
let conn_id = message.source();
|
||||||
|
for (Player(id), mut forces, pos, mut dir, mut display) in &mut players {
|
||||||
|
if conn_id == id {
|
||||||
|
let dir_movement = message.0.as_vec2().normalize_or_zero();
|
||||||
|
let added_force = dir_movement + pos.dir_to_center();
|
||||||
|
forces.add(added_force);
|
||||||
|
|
||||||
|
let idir = dir_movement.as_ivec2();
|
||||||
|
if !(idir == IVec2::ZERO) {
|
||||||
|
dir.0 = idir;
|
||||||
|
display.0 = player_display_for_dir(idir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
src/server.rs
Normal file
30
src/server.rs
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
#![allow(dead_code)]
|
||||||
|
#![allow(unstable_name_collisions)]
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use bevy::{app::ScheduleRunnerPlugin, prelude::*, time::TimePlugin};
|
||||||
|
use lib_server::{
|
||||||
|
display::DisplayPlugin, map, metrics::MetricsPlugin, net::NetPlugin, physics::PhysicsPlugin, player::PlayerPlugin,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
mod lib_server;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new()
|
||||||
|
// Core.
|
||||||
|
.add_plugins(ScheduleRunnerPlugin::run_loop(Duration::from_millis(50)))
|
||||||
|
.add_plugins(TimePlugin)
|
||||||
|
// World.
|
||||||
|
.add_plugins(PhysicsPlugin)
|
||||||
|
.add_plugins(DisplayPlugin)
|
||||||
|
.add_plugins(NetPlugin)
|
||||||
|
// Content.
|
||||||
|
.add_plugins(PlayerPlugin)
|
||||||
|
// Debug.
|
||||||
|
.add_plugins(MetricsPlugin)
|
||||||
|
// Startup.
|
||||||
|
.add_systems(Startup, map::spawn_debug_map)
|
||||||
|
.run();
|
||||||
|
}
|
5
watch_client
Executable file
5
watch_client
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/bash
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$(realpath "$0")")"
|
||||||
|
|
||||||
|
regar -sc 'sleep 1s; cargo run --release --bin=client' src
|
5
watch_server
Executable file
5
watch_server
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
#!/usr/bin/bash
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$(realpath "$0")")"
|
||||||
|
|
||||||
|
regar -sc 'cargo run --release --bin=server' src
|
Loading…
Add table
Add a link
Reference in a new issue