TL;DR
I started creating a new game engine and now I’m sharing a proof of concept with you to get feedback. If my idea turns out to make sense I’ll continue working on it and hopefully after a few years we will finally eradicate that crap called TFS.
Introduction
First of all, I want to thank you guys for creating and contributing to this thread, thanks to it I discovered Rust and started learning it, and after reading a few chapters of this great book I just fell in love with it. No wonder it's the most loved language for 6 years now, it’s simply the best programming language on the planet.
So, inspired by the thread I mentioned above, I now finally started creating this modern engine. And here are the goals I want to achieve:
Now, let me explain the structure of the game world: it’s a hash map of tiles. Every tile contains a vector of entities, and an entity can be anything: player, monster, item, NPC, and so on. There’s no inheritance here, but composition instead - every entity can have attributes. So for example, you can use
as long as
System
The main concept of the engine architecture is to keep its core as minimal as possible, and put all business logic in systems instead. To create a system, create a new Rust file anywhere inside systems directory and write:
and now, you can put other things inside these curly brackets. Here’s a list of "building blocks" that systems can contain:
Attribute
Attribute is a struct that implements
Attributes are used to insert some data to entities. You can also create an attribute without any fields inside and use it as a flag.
Event
Event is a struct that implements
Events are obviously used to handle something that happened. You can react to events using effects, or emit them using tasks. The engine can also emit some basic events, for example when a tile or an entity was updated.
Command
Implements
Commands are (or at least should be) the only way to change game state. You can return them from effects and the engine will process them in loop.
Task
Task is a block of asynchronous code that is actually a stream.
Task should be used if your system needs to run some asynchronous operations. You can „return” multiple values from it and await some things in the meantime. To „return” a value, use
There’s one tricky thing about tasks - the syntax above automatically adds it to tasker under the hood, so you can’t use it inside effects. I’ll think about how to solve it in a better way, but for now you can create a task inside effect manually, for example:
and then you can return
There are also other ways to create a stream, for example into_stream or unfold.
Effect
Effect is a function that accepts an event and returns vectors of commands and tasks.
There is a big flaw here - both
And how all these blocks work together? It’s basically a mix of: CQRS, event sourcing and ECS. If you want more, check out this great video that inspired me.
First of all, engine core is multithreaded. One core is used for game simulation, and all other cores are managed by tokio (it can move spawned tasks between threads if needed). First, there is a tasker (which runs in tokio task) that waits for new tasks and processes them asynchronously, and when a task yields an event, it then wraps it in
So for example, what will happen if you use a lever in the client?
Proof of concept
I created a proof of concept that allows you to login, walk, switch a stone switch and use a lever. It has a few things hardcoded (map loader, login/game login packets, registering systems), and some parts of the code could be better, for example there’s quite a lot of cloning which I’ll have to solve somehow, and there are multiple compiler warnings, mostly about unhandled results, but who cares - for now it’s enough to demonstrate the idea. And here's how it looks:
View attachment skyless_poc.mov
If you want to test it yourself, here's the code.
Besides these two scripts for handling switch and lever that I mentioned above, there’s also a tick system. It’s disabled by default, I included it just to show you an example of how we can use tasks to yield multiple events. In this case it will yield
So now it's time for your feedback. What do you think about this architecture? Maybe you can see some other use cases that will be hard or even impossible to implement using systems in their current shape? Let me know, so I can tweak it before it grows. And last, what do you think about the name?
I started creating a new game engine and now I’m sharing a proof of concept with you to get feedback. If my idea turns out to make sense I’ll continue working on it and hopefully after a few years we will finally eradicate that crap called TFS.
Introduction
First of all, I want to thank you guys for creating and contributing to this thread, thanks to it I discovered Rust and started learning it, and after reading a few chapters of this great book I just fell in love with it. No wonder it's the most loved language for 6 years now, it’s simply the best programming language on the planet.
So, inspired by the thread I mentioned above, I now finally started creating this modern engine. And here are the goals I want to achieve:
- modern technologies
- modular architecture
- easily customizable
- mechanics as close to Cipsoft as possible (as default, but easily customizable, as everything)
- support for as many protocol versions as possible
- no unsafe code
- unit/integration/e2e tests
Now, let me explain the structure of the game world: it’s a hash map of tiles. Every tile contains a vector of entities, and an entity can be anything: player, monster, item, NPC, and so on. There’s no inheritance here, but composition instead - every entity can have attributes. So for example, you can use
entity!
macro to easily create an item with action id:
Code:
entity![Item(1), Action(1)]
Item
and Action
attributes exist, of course.System
The main concept of the engine architecture is to keep its core as minimal as possible, and put all business logic in systems instead. To create a system, create a new Rust file anywhere inside systems directory and write:
Code:
use crate::core::prelude::*;
system! {}
Attribute
Attribute is a struct that implements
Attribute
trait. But all this implementation code will be added automatically by the #[attribute]
macro, so all you need is:
Code:
#[attribute]
pub struct Description(String);
#[attribute]
macro will under the hood create new getter method on Entity
struct (which returns Option
type). For example in this case it would create .description()
method that allows you to easily get access to the attribute. You can create attributes with any data you want, and then add them to entities, without modifying engine core. There are no methods for modifying attributes, though, because only commands should be able to change game state.Event
Event is a struct that implements
Event
trait. Again, it’s easy:
Code:
#[event]
pub struct UseEvent {
source: Option<Entity>,
target: Entity,
}
Command
Implements
Command
trait.
Code:
#[command]
pub struct EmitEventCommand(pub EventType);
Task
Task is a block of asynchronous code that is actually a stream.
Code:
task! {
// do something
}
yield
keyword. Notice that you can only yield Option<Event>
. Yielding None
can be useful if you have a task that only awaits something and there’s no need to return any value.There’s one tricky thing about tasks - the syntax above automatically adds it to tasker under the hood, so you can’t use it inside effects. I’ll think about how to solve it in a better way, but for now you can create a task inside effect manually, for example:
Code:
let task = Box::pin(stream! {
// do something
}) as TaskType;
task
variable from effect.There are also other ways to create a stream, for example into_stream or unfold.
Effect
Effect is a function that accepts an event and returns vectors of commands and tasks.
Code:
#[effect(TickEvent)]
fn handle_tick(event: EventType, attributes: GameAttributesType, world: WorldType) -> EffectResultType {
// do something
Some((Vec::new(), Vec::new()))
}
attributes
and world
arguments passed to effect are of type Arc<Mutex<T>>
, so it’s actually possible to directly change game state here. This is something that definitely has to be solved in the future.And how all these blocks work together? It’s basically a mix of: CQRS, event sourcing and ECS. If you want more, check out this great video that inspired me.
First of all, engine core is multithreaded. One core is used for game simulation, and all other cores are managed by tokio (it can move spawned tasks between threads if needed). First, there is a tasker (which runs in tokio task) that waits for new tasks and processes them asynchronously, and when a task yields an event, it then wraps it in
EmitEventCommand
and sends to the engine. The engine waits in loop for new commands and processes them, usually changing game state. Then it can also emit events to notify everyone that the game state has changed. There’s a special command I mentioned above, EmitEventCommand
, which will call all listeners for specific event that are registered. And listeners are just these functions with #[effect(…)]
attribute, that are registered at server startup.So for example, what will happen if you use a lever in the client?
- Task that runs tcp server will get and decode the packet and emit
GamePayloadEvent
containing decoded payload - Appropriate effect will get this event and match it with
Payload::UseItem
variant, and then emitUseItemPayloadEvent
containing position and item id - Another effect will get this event and return
EmitEventCommand
containingUseEvent
- Effect in lever script will get this event, check that its action matches
Actions::Lever
and returnSetEntityAttributeCommand
to switch the lever (change its id) - Engine will process this command and emit
ChangedEntityEvent
containing position and attribute name - Another effect will get this event, check that the attribute that has changed was item, and then send a packet to the client to notify it about the item update
Proof of concept
I created a proof of concept that allows you to login, walk, switch a stone switch and use a lever. It has a few things hardcoded (map loader, login/game login packets, registering systems), and some parts of the code could be better, for example there’s quite a lot of cloning which I’ll have to solve somehow, and there are multiple compiler warnings, mostly about unhandled results, but who cares - for now it’s enough to demonstrate the idea. And here's how it looks:
View attachment skyless_poc.mov
If you want to test it yourself, here's the code.
Besides these two scripts for handling switch and lever that I mentioned above, there’s also a tick system. It’s disabled by default, I included it just to show you an example of how we can use tasks to yield multiple events. In this case it will yield
TickEvent
every second, which could later be used to implement monsters or NPC „thinking”. And of course the whole network code was implemented as systems, so you can also check it.So now it's time for your feedback. What do you think about this architecture? Maybe you can see some other use cases that will be hard or even impossible to implement using systems in their current shape? Let me know, so I can tweak it before it grows. And last, what do you think about the name?