Skip to content

A set of Rust crates design to help reduce boilerplate for MMO servers

Notifications You must be signed in to change notification settings

RecursivePineapple/chimera-ecs

Repository files navigation

Note: this project is not FOSS despite being open source. I have to decide on the terms of use first, as I don't want to support scam MMOs. If you would like to use this for whatever reason, feel free to message me and I'll likely give you a FOSS license.

Chimera ECS

Chimera is a collection of mostly-separate Rust crates that help replace the boilerplate involved in making an MMO. It is essentially a network-exposed Entity Component System. Chimera manages all client/server syncing along with all entity-to-entity messaging. Entities are essentially just state machines that expose pure functions & impure functions.

Entities can only communicate using messages and act on other entities via effects. Effects are invoked asynchronously to prevent dead locks. Entities create their in-game representation by constructing a node tree, which is sent to the clients by the chimera-networking crate. The chimera-reify-gdext crate can then convert this node tree into the proper game objects, which can communicate with the server over message dispatches.

Simple Entity Example

#[derive(Debug)]
pub struct A;

impl StaticallyTypedEntity for A {
    const TYPE: &'static str = "A";
}

impl A {
    pub fn new(_: &EntityList) -> Arc<RwLock<Self>> {
        Arc::new(RwLock::new(Self))
    }
}

#[entity(singleton)]
impl Entity for A {
    fn get_id(&self) -> chimera_ecs::prelude::Id {
        Self::get_singleton_id()
    }

    fn get_type(&self) -> CowStr {
        CowStr::Borrowed(Self::TYPE)
    }

    fn on_pushed(&mut self) -> Effect {
        // BMessage is automatically generated by the #[entity] macro
        // If B wasn't a singleton, we'd have to specify the entity Id
        // Message factories have one method per #[action] fn
        BMessage::say_hello(false)
    }

    fn on_popped(&mut self) -> Effect {
        BMessage::say_goodbye(true)
    }

    // Actions & queries must return an Effect or ()
    // The shim code is automatically generated by the #[entity] macro
    // Queries must return something that's Serializable & Deserializable for the query to be exposed to the network
    // Actions & queries can specify #[action(no_serialize)] or #[query(no_serialize)] to make the action/query local only (within the process)
    #[query]
    fn get_name(&self) -> Effect<String> {
        Effect::value("Entity A".to_owned())
    }

    // Action params must also be Serializable & Deserializable for them to be network-callable
    #[action]
    fn tell(&mut self, message: String) {
        println!(message);
    }
}

#[derive(Debug)]
pub struct B {
    said_hello: bool,
    waved: bool,
    dispatch: Arc<EntityDispatch>,
}

impl StaticallyTypedEntity for B {
    const TYPE: &'static str = "B";
}

impl B {
    pub fn new(e: &EntityList) -> Arc<RwLock<Self>> {
        Arc::new(RwLock::new(Self {
            said_hello: false,
            waved: false,
            dispatch: e.open_dispatch(Self::get_singleton_id()),
        }))
    }
}

#[entity(singleton)]
impl Entity for B {
    fn get_id(&self) -> chimera_ecs::prelude::Id {
        Self::get_singleton_id()
    }

    fn get_type(&self) -> CowStr {
        CowStr::Borrowed(Self::TYPE)
    }

    #[action]
    fn say_hello(&mut self, waved: bool) -> Effect {
        self.said_hello = true;
        self.waved = waved;
        if waved {
            // AQuery is also generated by the #[entity] macro
            // Query factories have one method per #[query] fn
            AQuery::get_name().and_then(|name: String| AMessage::tell(format!("Hello, {name}!")))
        } else {
            Effect::none()
        }
    }

    #[action]
    fn say_goodbye(&mut self, waved: bool) -> Effect {
        self.said_hello = false;
        self.waved = waved;
        if waved {
            AQuery::get_name().and_then(|name: String| AMessage::tell(format!("Goodbye, {name}!")))
        } else {
            Effect::none()
        }
    }

    #[action(dispatchable)]
    fn bar(&mut self, foo: i32) {
        // the game client can call dispatch.send to send this message to the server
    }

    fn render(&self) -> Result<Option<Node>> {
        Ok(Some(Node {
            scene: "res://B.tscn".to_owned(),
            props: str_hash_map!({
                // data is automatically sent (only deltas are sent over the network)
                // props that change frequently have a special Variable prop
                // A value prop can be anything that implements serde::Serialize
                has_said_hello => self.said_hello.as_value()?,
                dispatch => self.dispatch.as_dispatch()?,
            }),
            ..Default::default()
        }))
    }
}

Current status

Chimera can currently act as a single-node game server. The sharding crate is currently in progress.

Chimera currently only has a pre-made godot 4 node reifier. Support for unreal engine and unity is planned, but those crates will be worked on after sharding is usable.

About

A set of Rust crates design to help reduce boilerplate for MMO servers

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published