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
GamePayloadEventcontaining decoded payload - Appropriate effect will get this event and match it with
Payload::UseItemvariant, and then emitUseItemPayloadEventcontaining position and item id - Another effect will get this event and return
EmitEventCommandcontainingUseEvent - Effect in lever script will get this event, check that its action matches
Actions::Leverand returnSetEntityAttributeCommandto switch the lever (change its id) - Engine will process this command and emit
ChangedEntityEventcontaining 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?