Skip to content

Server Side Entity Module System (SSEMS)

DragonFighter603 edited this page Jun 8, 2023 · 8 revisions

The SSEMS consists of Entities, which may have one of each type of Module. The modules must be unique per entity, but may be generically differentiated.

Each Module has event functions, such as start or update, in which the core game logic takes place.

To satisfy the borrow checker, this is implemented in a way that these event functions do not immediately have access to &mut self. Rather, they receive a mutable reference to the Engine and current EntityId, with which a (mut) borrow to the current Module can be obtained.

1 - Create a Module and register an entity

server.rs

impl ServerMod for TestModServer {
    fn start(&mut self, engine: &mut Engine) {
        log!("server test start");
        let id = engine.new_entity();
        engine.mut_entity(&id).add_module(MyModule { counter: 0 });
    }
}

pub struct MyModule {
    counter: usize
}

impl Module for MyModule {
    
}

2 - Create Client Handle for entity

To interact with the clients, an Entity can create a ClientHandle, which is a client side correspondent of this entity. The entity and handle can call functions remotely on the other side by using the Messenger Module server side and a ClientMessenger client side.

To "spawn" an entity on a client, you need to add the cliient's id side messenger: messenger.add_client(user). This allows for granular control on who to send data to to save bandwidth.

1.1 Create and register your client handle client side

client.rs

impl ClientMod for TestModClient {
    fn register_handlers(&self, handlers: &mut HashMap<Id, fn() -> Box<dyn ClientHandle>>) {
        handlers.insert(type_to_id::<MyClientHandle>(), || Box::new(MyClientHandle { }));
    }
}

pub(crate) struct MyClientHandle {}

impl ClientEntity for MyClientHandle {}

impl ClientHandle for MyClientHandle {
    fn owning_layer(&self) -> TypeId {
        // replace the () with your actual layer to be able to render anything.
        // refer to `https://github.com/DragonFighter603/aeonetica/wiki/Client-side-Renderer` to find out more
        type_to_id::<()>()
    }

    fn start(&mut self, messenger: &mut ClientMessenger, renderer: Nullable<&mut Renderer>, store: &mut DataStore) {
        // This log should appear once the client starts,
        // at the same time as the server log `user joined: {user}`
        log!("client handle created!");
    }}
}

1.2 Connect your clients server side

server.rs

impl Module for MyModule {
    fn start(id: &EntityId, engine: &mut Engine) where Self: Sized {
        // ...
        // Add a messenger and tell it which ClientHandle it corresponds to
        let mut entity = engine.mut_entity(id);
        entity.add_module(Messenger::new::<MyClientHandle>());
        let mut messenger = engine.mut_module_of(id);

        // Add a ConnectionListener to get notifided when a user joins or leaves
        entity.add_module(ConnectionListener::new(
            |id, engine, user| {
                log!("user joined: {user}");
                let mut messenger = engine.mut_module_of(id);
                messenger.add_client(*user);
            },
            |id, engine, user| {
                log!("user left: {user}");
                let mut messenger = engine.mut_module_of(id);
                messenger.remove_client(user);
            })
        );
    }
}

Note that although you need a ConenctionListener somewhere to be aware of clients, you dont need one for every Messenger and most can probably be added/removed based on proximity or similar factors.

3 - Communicate with client

Communication happens via remote function calls. Both client and entity can generically register functions via a an id based on the hash of std::any::type_name::<T>().

Server side modules can call
messenger.call_client_fn(MyClientHandle::my_client_function, <arg>, <sendmode>);
or alternatively
messenger.call_client_fn_for(MyClientHandle::my_client_function, <ClientId>, <arg>, <sendmode>);
to send to a specific client. Note that even call_client_fn_for requires the client to be added to the messenger, otherwise there will be no corresponding Handle to receive the message.

There are two message send modes:

  • SendMode::Safe - uses TCP, is reliable and good for event based systems
  • SendMode::Quick - uses UDP, is lossy but quicker, good for continous updates, like position etc.

To send data, it has to implement SerBin and SeBin.
Generally, you should try to keep your networking to a minimum and not send every frame.

The following code requires you to append and insert into existing functions

2.1 Create the functions to be called

server.rs

// create the function that we want to call from client side inside the server module
impl MyModule {
    pub(crate) fn receive_client_msg(id: &EntityId, engine: &mut Engine,client_id: &ClientId, msg: String){
        log!("received client msg: {msg} from {client_id}")
    }
}

client.rs

// create the function that we want to call from server side inside the client module
impl MyClientHandle {
    pub(crate) fn receive_server_msg(&mut self, messenger: &mut ClientMessenger, renderer: Nullable<&mut Renderer>, store: &mut DataStore, msg: String){
        log!("received server msg: {msg}")
    }
}

2.2 Register the functions on the receiving side

server.rs

impl Module for MyModule {
    fn start(id: &EntityId, engine: &mut Engine) where Self: Sized {
        // ...
        let mut entity = engine.mut_entity(id);
        entity.add_module(Messenger::new::<MyClientHandle>());
        let mut messenger = engine.mut_module_of(id);
        // NEW:
        messenger.register_receiver(MyModule::receive_client_msg);

        // ...
        entity.add_module(...);
    }
}

client.rs

impl ClientHandle for MyClientHandle {
    fn start(&mut self, messenger: &mut ClientMessenger, store: &mut DataStore) {
        // ...
        // NEW:
        messenger.register_receiver(MyClientHandle::receive_server_msg);
    }
}

2.2 Call the functions at specific events

server.rs

// extend existing start method
impl Module for MyModule {
    fn start(id: &EntityId, engine: &mut Engine) where Self: Sized {
        // ...
        entity.add_module(ConnectionListener::new(
            |id, engine, user| {
                // ...
                // NEW:
                messenger.call_client_fn(MyClientHandle::receive_server_msg, format!("user joined: {user}"), SendMode::Safe);
            },
            |id, engine, user| {
                // ...
                // NEW:
                messenger.call_client_fn(MyClientHandle::receive_server_msg, format!("user left: {user}"), SendMode::Safe);
            })
        );
    }
}

client.rs

// add start function to existing implementation block
impl ClientHandle for MyClientHandle {
    fn start(&mut self, messenger: &mut ClientMessenger, store: &mut DataStore) {
        // ...
        // NEW:
        messenger.call_server_fn(MyModule::receive_client_msg, "Hello from client server call function".to_string(), SendMode::Safe);
    }
}

The respective messages should now be logged henever a client joins or leaves

3. Event & Scheduling System

The server side scheduling system facilitates scheduling of repeating or delayed tasks, aswell as reacting to certain events.

The system uses the unstable generator functions (yield syntax). Due to its rather quirky and unintuitive/difficult usage and lifetime rules, the yield is wrapped in the yield_task!(<engine>, Waiter); macro. Internally it is releasing and reaquiring the engine. The syntax or implementation might be subject to change as this unstable feature evolves.

server.rs

struct SampleEvent;

impl Event for SampleEvent;

impl Module for MyModule {
    fn start(id: &EntityId, engine: &mut Engine) where Self: Sized {
        // MyModule::start used as an example, can obviously be used anywhere

        // queuing tasks with time based intervals
        engine.queue_task(|mut e: &mut Engine| {
            for i in 1..11 {
                yield_task!(e, WaitFor::ticks(20));
                log!("waited {i} seconds...")
            }
            engine.fire_event::<SampleEvent>();
        });

        // queuing a task with an event based trigger
        engine.queue_task(|mut e: &mut Engine| {
            yield_task!(e, WaitFor::event::<SampleEvent>());
            log!("event fired!")
        }
    }
}