Synced Object with Auto remove + Player Sync

This commit is contained in:
Elias Stepanik 2025-04-06 19:19:32 +02:00
parent 86aa2c77be
commit 9eabdf1be9
11 changed files with 206 additions and 106 deletions

View File

@ -6,7 +6,7 @@ use bevy_render::camera::{Exposure, PhysicalCameraParameters, Projection};
use bevy_window::CursorGrabMode;
use rand::Rng;
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;
#[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((
Transform::from_xyz(0.0, 0.0, 10.0), // initial f32
GlobalTransform::default(),
@ -45,6 +48,7 @@ pub fn setup(mut commands: Commands) {
sensitivity_iso: 100.0,
sensor_height: 0.01866,
}),
));
}
@ -159,6 +163,13 @@ pub fn camera_controller_system(
let distance = controller.speed as f64 * delta_seconds;
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)
// =========================

View File

@ -1,12 +1,6 @@
use bevy::app::{App, Plugin, Startup};
use bevy::color::palettes::basic::{GREEN, YELLOW};
use bevy::color::palettes::css::RED;
use bevy::prelude::*;
use crate::plugins::environment::systems::environment_system::*;
use bevy::app::{App, Plugin};
pub struct EnvironmentPlugin;
impl Plugin for EnvironmentPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Startup, init);
app.add_systems(Update, sync_entities_system);
}
}

View File

@ -1,93 +1,9 @@
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>>,) {
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 setup(mut commands: Commands) {
}
pub fn update(time: Res<Time>,) {

View File

@ -4,10 +4,13 @@ use bevy::color::palettes::css::RED;
use bevy::prelude::*;
use crate::plugins::environment::systems::environment_system::*;
use crate::plugins::network::systems::database::setup_database;
use crate::plugins::network::systems::entities::*;
pub struct NetworkPlugin;
impl Plugin for NetworkPlugin {
fn build(&self, app: &mut App) {
app.add_systems(PreStartup, setup_database);
/*app.add_systems(Startup, init);
app.add_systems(Update, sync_entities_system);*/
}
}

View File

@ -11,6 +11,7 @@ pub fn on_connected(_ctx: &DbConnection, _identity: Identity, token: &str) {
if let Err(e) = creds_store().save(token) {
eprintln!("Failed to save credentials: {:?}", e);
}
}
/// Our `on_connect_error` callback: print the error, then exit the process.

View 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();
}
}
}

View File

@ -1,4 +1,5 @@
pub mod database;
mod connection;
mod callbacks;
mod subscriptions;
mod subscriptions;
pub mod entities;

View File

@ -1,7 +1,9 @@
mod types;
use spacetimedb::{reducer, ReducerContext, SpacetimeType, Table};
use crate::types::player::{player, Player};
use spacetimedb::{reducer, ReducerContext, Table};
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)]
pub struct Config {
@ -26,10 +28,21 @@ pub fn client_connected(ctx: &ReducerContext) {
} else {
// If this is a new player, create a `player` row for the `Identity`,
// 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 {
name: None,
identity: ctx.sender,
online: true,
entity_id: entity.entity_id,
});
}
}
@ -37,8 +50,20 @@ pub fn client_connected(ctx: &ReducerContext) {
#[reducer(client_disconnected)]
// Called when a client disconnects from SpacetimeDB
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 {
// This branch should be unreachable,
// as it doesn't make sense for a client to disconnect without connecting first.

View File

@ -1,5 +1,5 @@
use spacetimedb::{Identity, ReducerContext, Table};
use spacetimedb::{Identity, ReducerContext, SpacetimeType, Table};
use crate::types::vec3::{DbVector3};
#[spacetimedb::table(name = entity, public)]
@ -8,18 +8,28 @@ pub struct Entity {
#[auto_inc]
#[primary_key]
pub entity_id: u32,
pub position: DbVector3,
pub entity_type: EntityType,
}
#[derive(SpacetimeType, Clone, Debug)]
pub enum EntityType {
Cube,
Sphere,
Custom
}
#[spacetimedb::reducer]
pub fn spawn_entity(ctx: &ReducerContext, position: DbVector3) -> Result<(), String> {
ctx.db.entity().try_insert(Entity {
entity_id: 0,
position,
entity_type: EntityType::Cube,
}).expect("TODO: panic message");

View File

@ -1,3 +1,3 @@
pub mod vec3;
pub mod player;
mod entity;
pub mod entity;

View File

@ -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)]
#[derive(Debug, Clone)]
pub struct Player {
#[primary_key]
pub identity: Identity,
#[index(btree)]
pub entity_id: u32,
pub name: Option<String>,
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.
fn validate_name(name: String) -> Result<String, String> {
if name.is_empty() {