mirror of
https://github.com/eliasstepanik/voxel-simulation.git
synced 2026-01-24 03:58:37 +00:00
Synced Object with Auto remove + Player Sync
This commit is contained in:
parent
86aa2c77be
commit
9eabdf1be9
@ -6,7 +6,7 @@ use bevy_render::camera::{Exposure, PhysicalCameraParameters, Projection};
|
|||||||
use bevy_window::CursorGrabMode;
|
use bevy_window::CursorGrabMode;
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use random_word::Lang;
|
use random_word::Lang;
|
||||||
use crate::module_bindings::{set_name, spawn_entity, DbVector3};
|
use crate::module_bindings::{set_name, set_position, spawn_entity, DbVector3};
|
||||||
use crate::plugins::network::systems::database::DbConnectionResource;
|
use crate::plugins::network::systems::database::DbConnectionResource;
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
@ -28,7 +28,10 @@ impl Default for CameraController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn setup(mut commands: Commands) {
|
pub fn setup(mut commands: Commands,
|
||||||
|
db_resource: Res<DbConnectionResource>,) {
|
||||||
|
|
||||||
|
|
||||||
commands.spawn((
|
commands.spawn((
|
||||||
Transform::from_xyz(0.0, 0.0, 10.0), // initial f32
|
Transform::from_xyz(0.0, 0.0, 10.0), // initial f32
|
||||||
GlobalTransform::default(),
|
GlobalTransform::default(),
|
||||||
@ -45,6 +48,7 @@ pub fn setup(mut commands: Commands) {
|
|||||||
sensitivity_iso: 100.0,
|
sensitivity_iso: 100.0,
|
||||||
sensor_height: 0.01866,
|
sensor_height: 0.01866,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,6 +163,13 @@ pub fn camera_controller_system(
|
|||||||
let distance = controller.speed as f64 * delta_seconds;
|
let distance = controller.speed as f64 * delta_seconds;
|
||||||
transform.translation += direction * distance as f32;
|
transform.translation += direction * distance as f32;
|
||||||
|
|
||||||
|
ctx.0.reducers.set_position(DbVector3{
|
||||||
|
x: transform.translation.x,
|
||||||
|
y: transform.translation.y,
|
||||||
|
z: transform.translation.z,
|
||||||
|
}).expect("TODO: panic message");
|
||||||
|
|
||||||
|
|
||||||
// =========================
|
// =========================
|
||||||
// 4) Lock/Unlock Mouse (L)
|
// 4) Lock/Unlock Mouse (L)
|
||||||
// =========================
|
// =========================
|
||||||
|
|||||||
@ -1,12 +1,6 @@
|
|||||||
use bevy::app::{App, Plugin, Startup};
|
use bevy::app::{App, Plugin};
|
||||||
use bevy::color::palettes::basic::{GREEN, YELLOW};
|
|
||||||
use bevy::color::palettes::css::RED;
|
|
||||||
use bevy::prelude::*;
|
|
||||||
use crate::plugins::environment::systems::environment_system::*;
|
|
||||||
pub struct EnvironmentPlugin;
|
pub struct EnvironmentPlugin;
|
||||||
impl Plugin for EnvironmentPlugin {
|
impl Plugin for EnvironmentPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_systems(Startup, init);
|
|
||||||
app.add_systems(Update, sync_entities_system);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,93 +1,9 @@
|
|||||||
|
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use spacetimedb_sdk::Table;
|
|
||||||
use crate::module_bindings::{entity_table, DbVector3, EntityTableAccess};
|
|
||||||
use crate::plugins::network::systems::database::DbConnectionResource;
|
|
||||||
|
|
||||||
#[derive(Component)]
|
|
||||||
pub struct EntityDto {
|
|
||||||
|
|
||||||
pub entity_id: u32,
|
|
||||||
pub position: DbVector3,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init(mut commands: Commands,
|
|
||||||
ctx: Res<DbConnectionResource>,
|
|
||||||
mut meshes: ResMut<Assets<Mesh>>,
|
|
||||||
mut materials: ResMut<Assets<StandardMaterial>>,) {
|
|
||||||
|
|
||||||
|
|
||||||
|
pub fn setup(mut commands: Commands) {
|
||||||
let debug_material = materials.add(StandardMaterial { ..default() });
|
|
||||||
|
|
||||||
for entity in ctx.0.db.entity().iter() {
|
|
||||||
commands.spawn((
|
|
||||||
Mesh3d(meshes.add(Cuboid::default()),),
|
|
||||||
MeshMaterial3d(debug_material.clone ()),
|
|
||||||
Transform::from_xyz(
|
|
||||||
entity.position.x,
|
|
||||||
entity.position.y,
|
|
||||||
entity.position.z,
|
|
||||||
),
|
|
||||||
EntityDto{
|
|
||||||
entity_id: entity.entity_id,
|
|
||||||
position: entity.position,
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub fn sync_entities_system(
|
|
||||||
mut commands: Commands,
|
|
||||||
db_resource: Res<DbConnectionResource>,
|
|
||||||
mut query: Query<(Entity, &mut Transform, &mut EntityDto)>,
|
|
||||||
mut meshes: ResMut<Assets<Mesh>>,
|
|
||||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
||||||
) {
|
|
||||||
let ctx = &db_resource.0.db;
|
|
||||||
|
|
||||||
// For each entity record in DB, see if it exists in ECS:
|
|
||||||
for db_entity in ctx.entity().iter() {
|
|
||||||
// Try to find a matching entity in ECS by comparing `entity_id`
|
|
||||||
if let Some((entity, mut transform, mut dto)) = query
|
|
||||||
.iter_mut()
|
|
||||||
.find(|(_, _, dto)| dto.entity_id == db_entity.entity_id)
|
|
||||||
{
|
|
||||||
// It already exists. Perhaps update ECS data to match DB:
|
|
||||||
dto.position = db_entity.position;
|
|
||||||
transform.translation = Vec3::new(
|
|
||||||
dto.position.x,
|
|
||||||
dto.position.y,
|
|
||||||
dto.position.z,
|
|
||||||
);
|
|
||||||
// ...do any other sync logic
|
|
||||||
} else {
|
|
||||||
|
|
||||||
|
|
||||||
let debug_material = materials.add(StandardMaterial { ..default() });
|
|
||||||
// Not found in ECS, so spawn a new entity
|
|
||||||
commands.spawn((
|
|
||||||
EntityDto {
|
|
||||||
entity_id: db_entity.entity_id,
|
|
||||||
position: db_entity.position.clone(),
|
|
||||||
},
|
|
||||||
// Create an initial transform using DB data
|
|
||||||
Transform::from_xyz(
|
|
||||||
db_entity.position.x,
|
|
||||||
db_entity.position.y,
|
|
||||||
db_entity.position.z,
|
|
||||||
),
|
|
||||||
GlobalTransform::default(),
|
|
||||||
Mesh3d(meshes.add(Cuboid::default()),),
|
|
||||||
MeshMaterial3d(debug_material.clone ()),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(time: Res<Time>,) {
|
pub fn update(time: Res<Time>,) {
|
||||||
|
|||||||
@ -4,10 +4,13 @@ use bevy::color::palettes::css::RED;
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use crate::plugins::environment::systems::environment_system::*;
|
use crate::plugins::environment::systems::environment_system::*;
|
||||||
use crate::plugins::network::systems::database::setup_database;
|
use crate::plugins::network::systems::database::setup_database;
|
||||||
|
use crate::plugins::network::systems::entities::*;
|
||||||
|
|
||||||
pub struct NetworkPlugin;
|
pub struct NetworkPlugin;
|
||||||
impl Plugin for NetworkPlugin {
|
impl Plugin for NetworkPlugin {
|
||||||
fn build(&self, app: &mut App) {
|
fn build(&self, app: &mut App) {
|
||||||
app.add_systems(PreStartup, setup_database);
|
app.add_systems(PreStartup, setup_database);
|
||||||
|
/*app.add_systems(Startup, init);
|
||||||
|
app.add_systems(Update, sync_entities_system);*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ pub fn on_connected(_ctx: &DbConnection, _identity: Identity, token: &str) {
|
|||||||
if let Err(e) = creds_store().save(token) {
|
if let Err(e) = creds_store().save(token) {
|
||||||
eprintln!("Failed to save credentials: {:?}", e);
|
eprintln!("Failed to save credentials: {:?}", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Our `on_connect_error` callback: print the error, then exit the process.
|
/// Our `on_connect_error` callback: print the error, then exit the process.
|
||||||
|
|||||||
119
client/src/plugins/network/systems/entities.rs
Normal file
119
client/src/plugins/network/systems/entities.rs
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
use std::collections::HashSet;
|
||||||
|
use bevy::math::Vec3;
|
||||||
|
use bevy::pbr::{MeshMaterial3d, StandardMaterial};
|
||||||
|
use bevy::prelude::{default, Commands, Component, Cuboid, DespawnRecursiveExt, Entity, GlobalTransform, Mesh, Query, Res, ResMut, Sphere, Transform};
|
||||||
|
use bevy_asset::Assets;
|
||||||
|
use bevy_render::mesh::Mesh3d;
|
||||||
|
use spacetimedb_sdk::Table;
|
||||||
|
use crate::module_bindings::{DbVector3, EntityTableAccess, EntityType};
|
||||||
|
use crate::plugins::network::systems::database::DbConnectionResource;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct EntityDto {
|
||||||
|
|
||||||
|
pub entity_id: u32,
|
||||||
|
pub position: DbVector3,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init(mut commands: Commands,
|
||||||
|
ctx: Res<DbConnectionResource>,
|
||||||
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
|
mut materials: ResMut<Assets<StandardMaterial>>,) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let debug_material = materials.add(StandardMaterial { ..default() });
|
||||||
|
|
||||||
|
for entity in ctx.0.db.entity().iter() {
|
||||||
|
commands.spawn((
|
||||||
|
Mesh3d(meshes.add(Cuboid::default()),),
|
||||||
|
MeshMaterial3d(debug_material.clone ()),
|
||||||
|
Transform::from_xyz(
|
||||||
|
entity.position.x,
|
||||||
|
entity.position.y,
|
||||||
|
entity.position.z,
|
||||||
|
),
|
||||||
|
EntityDto{
|
||||||
|
entity_id: entity.entity_id,
|
||||||
|
position: entity.position,
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// System that syncs DB entities with the Bevy ECS
|
||||||
|
pub fn sync_entities_system(
|
||||||
|
mut commands: Commands,
|
||||||
|
db_resource: Res<DbConnectionResource>,
|
||||||
|
|
||||||
|
// We need the Entity handle for potential despawning,
|
||||||
|
// plus mutable references if we want to update Transform/EntityDto
|
||||||
|
mut query: Query<(Entity, &mut Transform, &mut EntityDto)>,
|
||||||
|
|
||||||
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||||
|
) {
|
||||||
|
// --- 1) Collect DB entities and build a set of IDs ---
|
||||||
|
let db_entities = db_resource.0.db.entity();
|
||||||
|
let db_ids: HashSet<u32> = db_entities.iter().map(|e| e.entity_id).collect();
|
||||||
|
|
||||||
|
// --- 2) For each DB entity, update or spawn in ECS ---
|
||||||
|
for db_entity in db_entities.iter() {
|
||||||
|
|
||||||
|
// Try to find a matching ECS entity by entity_id
|
||||||
|
if let Some((_, mut transform, mut dto)) =
|
||||||
|
query.iter_mut().find(|(_, _, dto)| dto.entity_id == db_entity.entity_id)
|
||||||
|
{
|
||||||
|
// Update fields
|
||||||
|
dto.position = db_entity.position.clone();
|
||||||
|
transform.translation = Vec3::new(
|
||||||
|
db_entity.position.x,
|
||||||
|
db_entity.position.y,
|
||||||
|
db_entity.position.z,
|
||||||
|
);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Not found in ECS, so spawn a new entity
|
||||||
|
let debug_material = materials.add(StandardMaterial {
|
||||||
|
// fill out any fields you want
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pick a mesh based on the entity type
|
||||||
|
let entity_type = match db_entity.entity_type {
|
||||||
|
EntityType::Sphere => Mesh3d(meshes.add(Sphere::default())),
|
||||||
|
EntityType::Cube => Mesh3d(meshes.add(Cuboid::default())),
|
||||||
|
EntityType::Custom => todo!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
commands.spawn((
|
||||||
|
EntityDto {
|
||||||
|
entity_id: db_entity.entity_id,
|
||||||
|
position: db_entity.position.clone(),
|
||||||
|
},
|
||||||
|
Transform::from_xyz(
|
||||||
|
db_entity.position.x,
|
||||||
|
db_entity.position.y,
|
||||||
|
db_entity.position.z,
|
||||||
|
),
|
||||||
|
GlobalTransform::default(),
|
||||||
|
entity_type,
|
||||||
|
MeshMaterial3d(debug_material),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 3) Despawn any ECS entity that doesn't exist in the DB anymore ---
|
||||||
|
for (entity, _, dto) in query.iter_mut() {
|
||||||
|
if !db_ids.contains(&dto.entity_id) {
|
||||||
|
// This ECS entity no longer matches anything in the DB => remove it
|
||||||
|
commands.entity(entity).despawn_recursive();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
pub mod database;
|
pub mod database;
|
||||||
mod connection;
|
mod connection;
|
||||||
mod callbacks;
|
mod callbacks;
|
||||||
mod subscriptions;
|
mod subscriptions;
|
||||||
|
pub mod entities;
|
||||||
@ -1,7 +1,9 @@
|
|||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
use spacetimedb::{reducer, ReducerContext, SpacetimeType, Table};
|
use spacetimedb::{reducer, ReducerContext, Table};
|
||||||
use crate::types::player::{player, Player};
|
use crate::types::entity::{entity, entity__TableHandle, Entity, EntityType};
|
||||||
|
use crate::types::player::{player, player__TableHandle, Player};
|
||||||
|
use crate::types::vec3::DbVector3;
|
||||||
|
|
||||||
#[spacetimedb::table(name = config, public)]
|
#[spacetimedb::table(name = config, public)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
@ -26,10 +28,21 @@ pub fn client_connected(ctx: &ReducerContext) {
|
|||||||
} else {
|
} else {
|
||||||
// If this is a new player, create a `player` row for the `Identity`,
|
// If this is a new player, create a `player` row for the `Identity`,
|
||||||
// which is online, but hasn't set a name.
|
// which is online, but hasn't set a name.
|
||||||
|
let entity = ctx.db.entity().try_insert(Entity{
|
||||||
|
entity_id: 0,
|
||||||
|
position: DbVector3 {
|
||||||
|
x: 0.0,
|
||||||
|
y: 0.0,
|
||||||
|
z: 10.0,
|
||||||
|
},
|
||||||
|
entity_type: EntityType::Sphere,
|
||||||
|
}).expect("TODO: panic message");
|
||||||
|
|
||||||
ctx.db.player().insert(Player {
|
ctx.db.player().insert(Player {
|
||||||
name: None,
|
name: None,
|
||||||
identity: ctx.sender,
|
identity: ctx.sender,
|
||||||
online: true,
|
online: true,
|
||||||
|
entity_id: entity.entity_id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -37,8 +50,20 @@ pub fn client_connected(ctx: &ReducerContext) {
|
|||||||
#[reducer(client_disconnected)]
|
#[reducer(client_disconnected)]
|
||||||
// Called when a client disconnects from SpacetimeDB
|
// Called when a client disconnects from SpacetimeDB
|
||||||
pub fn identity_disconnected(ctx: &ReducerContext) {
|
pub fn identity_disconnected(ctx: &ReducerContext) {
|
||||||
if let Some(player) = ctx.db.player().identity().find(ctx.sender) {
|
|
||||||
ctx.db.player().identity().update(Player { online: false, ..player });
|
let entity: &entity__TableHandle = ctx.db.entity();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if let Some(player_iter) = ctx.db.player().identity().find(ctx.sender) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
ctx.db.player().identity().update(Player { online: false, ..player_iter });
|
||||||
|
ctx.db.entity().iter().find(|e| e.entity_id == player_iter.entity_id).iter().for_each(|e| {
|
||||||
|
entity.delete(e.clone());
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
// This branch should be unreachable,
|
// This branch should be unreachable,
|
||||||
// as it doesn't make sense for a client to disconnect without connecting first.
|
// as it doesn't make sense for a client to disconnect without connecting first.
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
use spacetimedb::{Identity, ReducerContext, Table};
|
use spacetimedb::{Identity, ReducerContext, SpacetimeType, Table};
|
||||||
use crate::types::vec3::{DbVector3};
|
use crate::types::vec3::{DbVector3};
|
||||||
|
|
||||||
#[spacetimedb::table(name = entity, public)]
|
#[spacetimedb::table(name = entity, public)]
|
||||||
@ -8,18 +8,28 @@ pub struct Entity {
|
|||||||
#[auto_inc]
|
#[auto_inc]
|
||||||
#[primary_key]
|
#[primary_key]
|
||||||
pub entity_id: u32,
|
pub entity_id: u32,
|
||||||
|
|
||||||
|
|
||||||
pub position: DbVector3,
|
pub position: DbVector3,
|
||||||
|
pub entity_type: EntityType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(SpacetimeType, Clone, Debug)]
|
||||||
|
pub enum EntityType {
|
||||||
|
Cube,
|
||||||
|
Sphere,
|
||||||
|
Custom
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#[spacetimedb::reducer]
|
#[spacetimedb::reducer]
|
||||||
pub fn spawn_entity(ctx: &ReducerContext, position: DbVector3) -> Result<(), String> {
|
pub fn spawn_entity(ctx: &ReducerContext, position: DbVector3) -> Result<(), String> {
|
||||||
|
|
||||||
ctx.db.entity().try_insert(Entity {
|
ctx.db.entity().try_insert(Entity {
|
||||||
entity_id: 0,
|
entity_id: 0,
|
||||||
position,
|
position,
|
||||||
|
entity_type: EntityType::Cube,
|
||||||
}).expect("TODO: panic message");
|
}).expect("TODO: panic message");
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
pub mod vec3;
|
pub mod vec3;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
mod entity;
|
pub mod entity;
|
||||||
@ -1,10 +1,16 @@
|
|||||||
use spacetimedb::{reducer, Identity, ReducerContext};
|
use spacetimedb::{reducer, Identity, ReducerContext, Table};
|
||||||
|
use crate::types::entity::{entity, Entity};
|
||||||
|
use crate::types::vec3::DbVector3;
|
||||||
|
|
||||||
#[spacetimedb::table(name = player, public)]
|
#[spacetimedb::table(name = player, public)]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Player {
|
pub struct Player {
|
||||||
#[primary_key]
|
#[primary_key]
|
||||||
pub identity: Identity,
|
pub identity: Identity,
|
||||||
|
|
||||||
|
#[index(btree)]
|
||||||
|
pub entity_id: u32,
|
||||||
|
|
||||||
pub name: Option<String>,
|
pub name: Option<String>,
|
||||||
pub online: bool,
|
pub online: bool,
|
||||||
}
|
}
|
||||||
@ -21,6 +27,20 @@ pub fn set_name(ctx: &ReducerContext, name: String) -> Result<(), String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[reducer]
|
||||||
|
/// Clients invoke this reducer to set their user names.
|
||||||
|
pub fn set_position(ctx: &ReducerContext, position: DbVector3) -> Result<(), String> {
|
||||||
|
if let Some(entity) = ctx.db.entity().iter().find(|e| e.entity_id == ctx.db.player().identity().find(ctx.sender).unwrap().entity_id) {
|
||||||
|
ctx.db.entity().entity_id()
|
||||||
|
.update(Entity{
|
||||||
|
position,
|
||||||
|
..entity
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Takes a name and checks if it's acceptable as a user's name.
|
/// Takes a name and checks if it's acceptable as a user's name.
|
||||||
fn validate_name(name: String) -> Result<String, String> {
|
fn validate_name(name: String) -> Result<String, String> {
|
||||||
if name.is_empty() {
|
if name.is_empty() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user