network code and stuff

This commit is contained in:
Magnus von Wachenfeldt 2022-08-09 15:30:47 +02:00
parent e5a83229f1
commit b0eadd5420
Signed by: magnus
GPG Key ID: A469F7D71D09F795
8 changed files with 1510 additions and 89 deletions

805
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,3 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = ["client", "server"] members = ["client", "server", "shared"]

View File

@ -5,6 +5,11 @@ edition = "2021"
[dependencies] [dependencies]
bevy = "0.8" bevy = "0.8"
bevy_renet = "0.0.5"
bevy_egui = "0.15.0"
renet_visualizer = "0.0.2"
bincode = "1.3.1"
daggmask-shared = { path = "../shared" }
# Enable a small amount of optimization in debug mode # Enable a small amount of optimization in debug mode
[profile.dev] [profile.dev]

View File

@ -1,59 +1,246 @@
use bevy::prelude::*; use std::{collections::HashMap, net::UdpSocket, time::SystemTime};
fn main() { use bevy::{
App::new() diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
.add_plugins(DefaultPlugins) prelude::*,
.add_startup_system(setup) };
.add_startup_system(spawn_player)
.add_system(move_player) use bevy_egui::{EguiContext, EguiPlugin};
.run(); use bevy_renet::{
renet::{ClientAuthentication, RenetClient, RenetError},
run_if_client_connected, RenetClientPlugin,
};
use daggmask_shared::{
client_connection_config, setup_level, ClientChannel, NetworkFrame, PlayerCommand, PlayerInput,
ServerChannel, ServerMessages, PROTOCOL_ID,
};
use renet_visualizer::{RenetClientVisualizer, RenetVisualizerStyle};
#[derive(Component)]
struct ControlledPlayer;
#[derive(Default)]
struct NetworkMapping(HashMap<Entity, Entity>);
#[derive(Debug)]
struct PlayerInfo {
client_entity: Entity,
server_entity: Entity,
} }
fn setup(mut commands: Commands) { #[derive(Debug, Default)]
info!("hehe"); struct ClientLobby {
let mut camera_bundle = Camera2dBundle::default(); players: HashMap<u64, PlayerInfo>,
camera_bundle.projection.scale = 1. / 50.; }
commands.spawn_bundle(camera_bundle);
#[derive(Debug)]
struct MostRecentTick(Option<u32>);
fn new_renet_client() -> RenetClient {
let server_addr = "127.0.0.1:5000".parse().unwrap();
let socket = UdpSocket::bind("127.0.0.1:0").unwrap();
let connection_config = client_connection_config();
let current_time = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
let client_id = current_time.as_millis() as u64;
let authentication = ClientAuthentication::Unsecure {
client_id,
protocol_id: PROTOCOL_ID,
server_addr,
user_data: None,
};
RenetClient::new(current_time, socket, 1, connection_config, authentication).unwrap()
}
fn main() {
let mut app = App::new();
app.add_plugins(DefaultPlugins);
app.add_plugin(RenetClientPlugin);
app.add_plugin(TransformPlugin);
app.add_plugin(FrameTimeDiagnosticsPlugin::default());
app.add_plugin(LogDiagnosticsPlugin::default());
app.add_plugin(EguiPlugin);
app.add_event::<PlayerCommand>();
app.insert_resource(ClientLobby::default());
app.insert_resource(PlayerInput::default());
app.insert_resource(MostRecentTick(None));
app.insert_resource(new_renet_client());
app.insert_resource(RenetClientVisualizer::<200>::new(
RenetVisualizerStyle::default(),
));
app.insert_resource(NetworkMapping::default());
app.add_system(player_input);
app.add_system(client_send_input.with_run_criteria(run_if_client_connected));
app.add_system(client_send_player_commands.with_run_criteria(run_if_client_connected));
app.add_system(client_sync_players.with_run_criteria(run_if_client_connected));
app.add_system(update_visulizer_system);
app.add_startup_system(setup_level);
app.add_system(panic_on_error_system);
app.run();
}
// If any error is found we just panic
fn panic_on_error_system(mut renet_error: EventReader<RenetError>) {
for e in renet_error.iter() {
panic!("{}", e);
}
}
fn update_visulizer_system(
mut egui_context: ResMut<EguiContext>,
mut visualizer: ResMut<RenetClientVisualizer<200>>,
client: Res<RenetClient>,
mut show_visualizer: Local<bool>,
keyboard_input: Res<Input<KeyCode>>,
) {
visualizer.add_network_info(client.network_info());
if keyboard_input.just_pressed(KeyCode::F1) {
*show_visualizer = !*show_visualizer;
}
if *show_visualizer {
visualizer.show_window(egui_context.ctx_mut());
}
}
fn player_input(
keyboard_input: Res<Input<KeyCode>>,
mut player_input: ResMut<PlayerInput>,
mouse_button_input: Res<Input<MouseButton>>,
target_query: Query<&Transform, With<Target>>,
mut player_commands: EventWriter<PlayerCommand>,
most_recent_tick: Res<MostRecentTick>,
) {
player_input.left = keyboard_input.pressed(KeyCode::A) || keyboard_input.pressed(KeyCode::Left);
player_input.right =
keyboard_input.pressed(KeyCode::D) || keyboard_input.pressed(KeyCode::Right);
player_input.up = keyboard_input.pressed(KeyCode::W) || keyboard_input.pressed(KeyCode::Up);
player_input.down = keyboard_input.pressed(KeyCode::S) || keyboard_input.pressed(KeyCode::Down);
player_input.most_recent_tick = most_recent_tick.0;
if mouse_button_input.just_pressed(MouseButton::Left) {
player_commands.send(PlayerCommand::BasicAttack {
cast_at: Vec2::default(), // TODO: spawn projectiles correctly
});
}
}
fn client_send_input(player_input: Res<PlayerInput>, mut client: ResMut<RenetClient>) {
let input_message = bincode::serialize(&*player_input).unwrap();
client.send_message(ClientChannel::Input.id(), input_message);
}
fn client_send_player_commands(
mut player_commands: EventReader<PlayerCommand>,
mut client: ResMut<RenetClient>,
) {
for command in player_commands.iter() {
let command_message = bincode::serialize(command).unwrap();
client.send_message(ClientChannel::Command.id(), command_message);
}
}
fn client_sync_players(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut client: ResMut<RenetClient>,
mut lobby: ResMut<ClientLobby>,
mut network_mapping: ResMut<NetworkMapping>,
mut most_recent_tick: ResMut<MostRecentTick>,
) {
let client_id = client.client_id();
while let Some(message) = client.receive_message(ServerChannel::ServerMessages.id()) {
let server_message = bincode::deserialize(&message).unwrap();
match server_message {
ServerMessages::PlayerCreate {
id,
translation,
entity,
} => {
println!("Player {} connected.", id);
let mut client_entity = commands.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Capsule::default())),
material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()),
transform: Transform::from_xyz(translation[0], translation[1], translation[2]),
..Default::default()
});
if client_id == id {
client_entity.insert(ControlledPlayer);
}
let player_info = PlayerInfo {
server_entity: entity,
client_entity: client_entity.id(),
};
lobby.players.insert(id, player_info);
network_mapping.0.insert(entity, client_entity.id());
}
ServerMessages::PlayerRemove { id } => {
println!("Player {} disconnected.", id);
if let Some(PlayerInfo {
server_entity,
client_entity,
}) = lobby.players.remove(&id)
{
commands.entity(client_entity).despawn();
network_mapping.0.remove(&server_entity);
}
}
ServerMessages::SpawnProjectile {
entity,
location,
direction,
} => {
let projectile_entity = commands.spawn_bundle(SpriteBundle {
sprite: Sprite {
color: Color::rgb(0.25, 0.25, 0.75),
custom_size: Some(Vec2::new(50.0, 100.0)),
..default()
},
..default()
});
network_mapping.0.insert(entity, projectile_entity.id());
}
ServerMessages::DespawnProjectile { entity } => {
if let Some(entity) = network_mapping.0.remove(&entity) {
commands.entity(entity).despawn();
}
}
}
}
while let Some(message) = client.receive_message(ServerChannel::NetworkFrame.id()) {
let frame: NetworkFrame = bincode::deserialize(&message).unwrap();
match most_recent_tick.0 {
None => most_recent_tick.0 = Some(frame.tick),
Some(tick) if tick < frame.tick => most_recent_tick.0 = Some(frame.tick),
_ => continue,
}
for i in 0..frame.entities.entities.len() {
if let Some(entity) = network_mapping.0.get(&frame.entities.entities[i]) {
let translation = frame.entities.translations[i].into();
let transform = Transform {
translation,
..Default::default()
};
commands.entity(*entity).insert(transform);
}
}
}
} }
#[derive(Component)] #[derive(Component)]
struct Player; struct Target;
fn spawn_player(mut commands: Commands) {
commands
.spawn_bundle(SpriteBundle {
sprite: Sprite {
color: Color::rgb(0., 0.47, 1.),
custom_size: Some(Vec2::new(1., 1.)),
..Default::default()
},
..Default::default()
})
.insert(Player);
}
fn move_player(keys: Res<Input<KeyCode>>, mut player_query: Query<&mut Transform, With<Player>>) {
let mut direction = Vec2::ZERO;
if keys.any_pressed([KeyCode::Up, KeyCode::W]) {
direction.y += 1.;
}
if keys.any_pressed([KeyCode::Down, KeyCode::S]) {
direction.y -= 1.;
}
if keys.any_pressed([KeyCode::Right, KeyCode::D]) {
direction.x += 1.;
}
if keys.any_pressed([KeyCode::Left, KeyCode::A]) {
direction.x -= 1.;
}
if direction == Vec2::ZERO {
return;
}
let move_speed = 0.13;
let move_delta = (direction * move_speed).extend(0.);
for mut transform in player_query.iter_mut() {
transform.translation += move_delta;
}
}

View File

@ -3,5 +3,12 @@ name = "daggmask-server"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies]
bevy = "0.8.0"
bevy_renet = "0.0.5"
bevy_egui = "0.15.0"
bevy_rapier2d = "0.16.0"
renet_visualizer = "0.0.2"
bincode = "1.3.1"
daggmask-shared = { path = "../shared" }

View File

@ -1,45 +1,291 @@
use std::net::UdpSocket; use std::{collections::HashMap, net::UdpSocket, time::SystemTime};
fn main() -> std::io::Result<()> { use bevy::{
// replace xxxx with your desired port diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
// replace S.S.S.S with your server address prelude::*,
let socket = UdpSocket::bind("S.S.S.S:xxxx")?; };
let mut peers: Vec<String> = vec![];
loop { use bevy_egui::{EguiContext, EguiPlugin};
let mut buf = [0; 1024]; use bevy_rapier2d::prelude::*;
let (_, src) = socket.recv_from(&mut buf)?; use bevy_renet::{
let stringified_buff = String::from_utf8(buf.to_vec()).unwrap(); renet::{RenetServer, ServerAuthentication, ServerConfig, ServerEvent},
let stringified_buff = stringified_buff.trim_matches(char::from(0)); RenetServerPlugin,
};
println!("[NEW MESSAGE]{:?} => {:?}", src, stringified_buff); use daggmask_shared::{
server_connection_config, setup_level, spawn_projectile, ClientChannel, NetworkFrame, Player,
PlayerCommand, PlayerInput, Projectile, ServerChannel, ServerMessages, PROTOCOL_ID,
};
if stringified_buff != "register" { use renet_visualizer::RenetServerVisualizer;
continue;
#[derive(Debug, Default)]
pub struct ServerLobby {
pub players: HashMap<u64, Entity>,
} }
if !peers.contains(&format!("{}", src)) { #[derive(Debug, Default)]
peers.push(format!("{}", src)); struct NetworkTick(u32);
// Clients last received ticks
#[derive(Debug, Default)]
struct ClientTicks(HashMap<u64, Option<u32>>);
const PLAYER_MOVE_SPEED: f32 = 5.0;
fn new_renet_server() -> RenetServer {
let server_addr = "127.0.0.1:5000".parse().unwrap();
let socket = UdpSocket::bind(server_addr).unwrap();
let connection_config = server_connection_config();
let server_config =
ServerConfig::new(64, PROTOCOL_ID, server_addr, ServerAuthentication::Unsecure);
let current_time = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap();
RenetServer::new(current_time, server_config, connection_config, socket).unwrap()
} }
for p in &peers { fn main() {
let filtered_peers = filter_peers(&peers, p); let mut app = App::new();
app.add_plugins(DefaultPlugins);
if !filtered_peers.is_empty() { app.add_plugin(RenetServerPlugin);
socket.send_to(filtered_peers.join(",").as_bytes(), p)?; app.add_plugin(RapierPhysicsPlugin::<NoUserData>::default());
app.add_plugin(RapierDebugRenderPlugin::default());
app.add_plugin(FrameTimeDiagnosticsPlugin::default());
app.add_plugin(LogDiagnosticsPlugin::default());
app.add_plugin(EguiPlugin);
app.insert_resource(ServerLobby::default());
app.insert_resource(NetworkTick(0));
app.insert_resource(ClientTicks::default());
app.insert_resource(new_renet_server());
app.insert_resource(RenetServerVisualizer::<200>::default());
app.add_system(server_update_system);
app.add_system(server_network_sync);
app.add_system(move_players_system);
app.add_system(update_projectiles_system);
app.add_system(update_visulizer_system);
app.add_system(despawn_projectile_system);
app.add_system_to_stage(CoreStage::PostUpdate, projectile_on_removal_system);
app.add_startup_system(setup_level);
app.add_startup_system(setup_simple_camera);
app.run();
}
#[allow(clippy::too_many_arguments)]
fn server_update_system(
mut server_events: EventReader<ServerEvent>,
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut lobby: ResMut<ServerLobby>,
mut server: ResMut<RenetServer>,
mut visualizer: ResMut<RenetServerVisualizer<200>>,
mut client_ticks: ResMut<ClientTicks>,
players: Query<(Entity, &Player, &Transform)>,
) {
for event in server_events.iter() {
match event {
ServerEvent::ClientConnected(id, _) => {
println!("Player {} connected.", id);
visualizer.add_client(*id);
// Initialize other players for this new client
for (entity, player, transform) in players.iter() {
let translation: [f32; 3] = transform.translation.into();
let message = bincode::serialize(&ServerMessages::PlayerCreate {
id: player.id,
entity,
translation,
})
.unwrap();
server.send_message(*id, ServerChannel::ServerMessages.id(), message);
}
// Spawn new player
let transform = Transform::from_xyz(0., 0.51, 0.);
let player_entity = commands
.spawn_bundle(PbrBundle {
mesh: meshes.add(Mesh::from(shape::Capsule::default())),
material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()),
transform,
..Default::default()
})
.insert(RigidBody::Dynamic)
.insert(LockedAxes::ROTATION_LOCKED | LockedAxes::TRANSLATION_LOCKED_Y)
.insert(Collider::capsule_y(0.5, 0.5))
.insert(PlayerInput::default())
.insert(Velocity::default())
.insert(Player {
id: *id,
location: Vec2::new(10., 10.),
})
.id();
lobby.players.insert(*id, player_entity);
let translation: [f32; 3] = transform.translation.into();
let message = bincode::serialize(&ServerMessages::PlayerCreate {
id: *id,
entity: player_entity,
translation,
})
.unwrap();
server.broadcast_message(ServerChannel::ServerMessages.id(), message);
}
ServerEvent::ClientDisconnected(id) => {
println!("Player {} disconnected.", id);
visualizer.remove_client(*id);
client_ticks.0.remove(id);
if let Some(player_entity) = lobby.players.remove(id) {
commands.entity(player_entity).despawn();
}
let message =
bincode::serialize(&ServerMessages::PlayerRemove { id: *id }).unwrap();
server.broadcast_message(ServerChannel::ServerMessages.id(), message);
}
}
}
for client_id in server.clients_id().into_iter() {
while let Some(message) = server.receive_message(client_id, ClientChannel::Command.id()) {
let command: PlayerCommand = bincode::deserialize(&message).unwrap();
match command {
PlayerCommand::BasicAttack { mut cast_at } => {
println!(
"Received basic attack from client {}: {:?}",
client_id, cast_at
);
if let Some(player_entity) = lobby.players.get(&client_id) {
if let Ok((_, _, player_transform)) = players.get(*player_entity) {
cast_at[1] = player_transform.translation[1];
let projectile_entity =
spawn_projectile(&mut commands, cast_at, cast_at);
let message = ServerMessages::SpawnProjectile {
entity: projectile_entity,
location: cast_at,
direction: cast_at,
};
let message = bincode::serialize(&message).unwrap();
server.broadcast_message(ServerChannel::ServerMessages.id(), message);
}
} }
} }
} }
} }
fn filter_peers(peers: &Vec<String>, filter: &String) -> Vec<String> { while let Some(message) = server.receive_message(client_id, ClientChannel::Input.id()) {
let mut new_peers: Vec<String> = vec![]; let input: PlayerInput = bincode::deserialize(&message).unwrap();
client_ticks.0.insert(client_id, input.most_recent_tick);
for p in peers { if let Some(player_entity) = lobby.players.get(&client_id) {
if p != filter { commands.entity(*player_entity).insert(input);
new_peers.push(String::from(p)); }
}
} }
} }
new_peers fn update_projectiles_system(
mut commands: Commands,
mut projectiles: Query<(Entity, &mut Projectile)>,
time: Res<Time>,
) {
for (entity, mut projectile) in projectiles.iter_mut() {
projectile.duration.tick(time.delta());
if projectile.duration.finished() {
commands.entity(entity).despawn();
}
}
}
fn update_visulizer_system(
mut egui_context: ResMut<EguiContext>,
mut visualizer: ResMut<RenetServerVisualizer<200>>,
server: Res<RenetServer>,
) {
visualizer.update(&server);
visualizer.show_window(egui_context.ctx_mut());
}
#[allow(clippy::type_complexity)]
fn server_network_sync(
mut tick: ResMut<NetworkTick>,
mut server: ResMut<RenetServer>,
networked_entities: Query<(Entity, &Transform), Or<(With<Player>, With<Projectile>)>>,
) {
let mut frame = NetworkFrame::default();
for (entity, transform) in networked_entities.iter() {
frame.entities.entities.push(entity);
frame
.entities
.translations
.push(transform.translation.into());
}
frame.tick = tick.0;
tick.0 += 1;
let sync_message = bincode::serialize(&frame).unwrap();
server.broadcast_message(ServerChannel::NetworkFrame.id(), sync_message);
}
fn move_players_system(mut query: Query<(&mut Velocity, &PlayerInput)>) {
for (mut velocity, input) in query.iter_mut() {
let x = (input.right as i8 - input.left as i8) as f32;
let y = (input.down as i8 - input.up as i8) as f32;
let direction = Vec2::new(x, y).normalize_or_zero();
velocity.linvel.x = direction.x * PLAYER_MOVE_SPEED;
velocity.linvel.y = direction.y * PLAYER_MOVE_SPEED;
}
}
pub fn setup_simple_camera(mut commands: Commands) {
// camera
commands.spawn_bundle(Camera3dBundle {
transform: Transform::from_xyz(-5.5, 5.0, 5.5).looking_at(Vec3::ZERO, Vec3::Y),
..Default::default()
});
}
fn despawn_projectile_system(
mut commands: Commands,
mut collision_events: EventReader<CollisionEvent>,
projectile_query: Query<Option<&Projectile>>,
) {
for collision_event in collision_events.iter() {
if let CollisionEvent::Started(entity1, entity2, _) = collision_event {
if let Ok(Some(_)) = projectile_query.get(*entity1) {
commands.entity(*entity1).despawn();
}
if let Ok(Some(_)) = projectile_query.get(*entity2) {
commands.entity(*entity2).despawn();
}
}
}
}
fn projectile_on_removal_system(
mut server: ResMut<RenetServer>,
removed_projectiles: RemovedComponents<Projectile>,
) {
for entity in removed_projectiles.iter() {
let message = ServerMessages::DespawnProjectile { entity };
let message = bincode::serialize(&message).unwrap();
server.broadcast_message(ServerChannel::ServerMessages.id(), message);
}
} }

11
shared/Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "daggmask-shared"
version = "0.1.0"
edition = "2021"
[dependencies]
bevy = "0.8.0"
bevy_renet = "0.0.5"
bevy_egui = "0.15.0"
bevy_rapier2d = "0.16.0"
serde = { version = "1.0", features = [ "derive" ] }

170
shared/src/lib.rs Normal file
View File

@ -0,0 +1,170 @@
use std::time::Duration;
use bevy::prelude::*;
use bevy_renet::renet::{
ChannelConfig, ReliableChannelConfig, RenetConnectionConfig, UnreliableChannelConfig,
NETCODE_KEY_BYTES,
};
use bevy_rapier2d::geometry::Collider;
use bevy_rapier2d::prelude::*;
use serde::{Deserialize, Serialize};
pub const PRIVATE_KEY: &[u8; NETCODE_KEY_BYTES] = b"en grisars katt hund hemlis key."; // 32-bytes
pub const PROTOCOL_ID: u64 = 7;
#[derive(Debug, Component)]
pub struct Player {
pub id: u64,
pub location: Vec2,
}
#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, Component)]
pub struct PlayerInput {
pub most_recent_tick: Option<u32>,
pub up: bool,
pub down: bool,
pub left: bool,
pub right: bool,
}
#[derive(Debug, Serialize, Deserialize, Component)]
pub enum PlayerCommand {
BasicAttack { cast_at: Vec2 },
}
pub enum ClientChannel {
Input,
Command,
}
pub enum ServerChannel {
ServerMessages,
NetworkFrame,
}
#[derive(Debug, Serialize, Deserialize, Component)]
pub enum ServerMessages {
PlayerCreate {
entity: Entity,
id: u64,
translation: [f32; 3],
},
PlayerRemove {
id: u64,
},
SpawnProjectile {
entity: Entity,
location: Vec2,
direction: Vec2,
},
DespawnProjectile {
entity: Entity,
},
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct NetworkedEntities {
pub entities: Vec<Entity>,
pub translations: Vec<[f32; 3]>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct NetworkFrame {
pub tick: u32,
pub entities: NetworkedEntities,
}
impl ClientChannel {
pub fn id(&self) -> u8 {
match self {
Self::Input => 0,
Self::Command => 1,
}
}
pub fn channels_config() -> Vec<ChannelConfig> {
vec![
ReliableChannelConfig {
channel_id: Self::Input.id(),
message_resend_time: Duration::ZERO,
..Default::default()
}
.into(),
ReliableChannelConfig {
channel_id: Self::Command.id(),
message_resend_time: Duration::ZERO,
..Default::default()
}
.into(),
]
}
}
impl ServerChannel {
pub fn id(&self) -> u8 {
match self {
Self::NetworkFrame => 0,
Self::ServerMessages => 1,
}
}
pub fn channels_config() -> Vec<ChannelConfig> {
vec![
UnreliableChannelConfig {
channel_id: Self::NetworkFrame.id(),
..Default::default()
}
.into(),
ReliableChannelConfig {
channel_id: Self::ServerMessages.id(),
message_resend_time: Duration::from_millis(200),
..Default::default()
}
.into(),
]
}
}
pub fn client_connection_config() -> RenetConnectionConfig {
RenetConnectionConfig {
send_channels_config: ClientChannel::channels_config(),
receive_channels_config: ServerChannel::channels_config(),
..Default::default()
}
}
pub fn server_connection_config() -> RenetConnectionConfig {
RenetConnectionConfig {
send_channels_config: ServerChannel::channels_config(),
receive_channels_config: ClientChannel::channels_config(),
..Default::default()
}
}
/// set up the level
pub fn setup_level(mut _commands: Commands) {
info!("bygger level...");
}
pub fn spawn_projectile(commands: &mut Commands, location: Vec2, direction: Vec2) -> Entity {
commands
.spawn()
.insert(Collider::ball(0.1))
.insert(Velocity::linear(direction * 10.))
.insert(ActiveEvents::COLLISION_EVENTS)
.insert(Projectile {
duration: Timer::from_seconds(1.5, false),
location,
direction,
})
.id()
}
#[derive(Debug, Component)]
pub struct Projectile {
pub duration: Timer,
pub location: Vec2,
pub direction: Vec2,
}