DEV Community

Veyyr
Veyyr

Posted on

I made Functional DOD Framework for 3D Games to support older PCs that makes Rust easy for beginners and compiles in seconds.

Hi!

Someone saw a similar post in reddit, but I'm reworked it again to present the project.

What do you think? Is it really possible to combine Functional Programming (style) with ECS in Rust? And even turn it into a game framework based on Macroquad and Bevy ECS?

It seems impossible (although in Bevy systems are written with functions), but I did it... It was shocking to me...

The Result: 1300+ entities at 28% CPU load on a 2013 laptop. (but there are draw calls problems, see about this below of post).

X550CC

Why not Bevy?

Why I created a new Framework on Macroquad and use Bevy ECS instead of regular Bevy?

There are reasons:

  1. On my laptop Bevy compiles approximately 2.5 HOURS.
  2. Rust analyzer used ~4 GB RAM! (my laptop has 6 GB)
  3. When I finally ran the project, I got an error saying my video card doesn't support the graphics API. Even when I tried to enable OpenGL through code.

It was really frustrating for me. When I was learning Bevy, it was really easy for me (15 times easier than OOP!). But when I tried to run it, everything broke. I thought then that I would never be able to make games in Rust.

So I made Light Acorn. And it is more than a framework. It is a manifest for the "Lord of the Code".

Light Acorn — an Open Source project (MIT/MPL) designed for old hardware like my 2013 X550CC laptop (i3-3217u, GT 720m) running on antiX.

The Light Acorn Features

  • The Core: Instead of a complex scheduler, I use an self-written Kernel based on Macroquad async loop.
  • Zones & Locations architecture: You manually define the order of functions in containers.
  • Runtime Flexibility: You can add, remove, or reorder functions while the game is running WITHOUT: unsafe blocks, smart pointers, macros and you shouldn't linking your code with Python, Lua (REACORN-way). PS: Only vectors and syntactic sugar macros.
  • DOD: No Arc> or complex lifetimes for the user. Because the entire framework skeleton is built exclusively on vectors and loops.
  • Minimum entry threshold: you don't need to lifetime fighting 'a and 'b, to know complex macros and smart pointers for begin creating your games.
  • In one moment only one function is executed. This eliminates data races. Also, multiple Bevy Queries can be run in a single function.
  • Bevy ECS include but optional. That means functions are not required to change state.

For example, function here accepts arguments but is not required to use them:

    fn acorn_example_draw_circle(_world: &mut World, _context: &mut AcornContext) {
        draw_circle(
            screen_width()/2.0, 
            screen_height()/2.0, 
            60.0, 
            BLUE
        )
    }
Enter fullscreen mode Exit fullscreen mode

Also...

  • Developer has choice of architecture Light Acorn: predicatable monolith (ACORN) OR flexible change of the order of execution of functions (REACORN)
  • Game built-in tools for create apps: There are functions, structs, variables etc. which developer can use for creating. OR DELETE this tools and create own. Light Acorn doesn't stop developer to choose libraries or tools.

What are Zones, Locations?

  • Zone & Location is unique concept: The grouping of functions and their order is the basis of the engine. Developer control code's order, grouping functions by Zone (group of Locations) and Location (group of functions). Developer can create own Zones or Locations in Kernel: custom Zones with custom execution order.

Zone & Location architecture

The idea is very simple: functions are executed in order in an infinite loop. Zones, Locations are containers for functions.

Concept is simple:

Zone is when, Location is where, Function is time-marker.

Actually Light Acorn is macroquad with architecture: Where Zones, Locations are a convenient list of functions that can be easily modified in the Light Acorn API.

how-to-use

In the code it looks like that:

truecode

This is not a crate, It's a template for your projects.

Why?

Because:

  • You can edit the framework core to suit your needs.
  • You can connect other dependencies or update them.
  • You can see all Acorn kernel to learn how it works.
  • You can optimize the framework (for example, instead of f32 for coordinates, you can use u16).
  • You are Lord of your code.

The Stack

  • Rust.
  • Macroquad (main render).
  • Bevy ECS.
  • Tobj (to parse .obj files and load your 3D models in game!).

And it's really ALL! This explains why everything compiles so quickly.

The proof of simplicity

Example, you want to draw blue circle.

Create Acorn function and add there a simple Macroquad function:

    fn acorn_example_draw_circle(_world: &mut World, _context: &mut AcornContext) {
       draw_circle(
           screen_width()/2.0,
           screen_height()/2.0,
           60.0,
           BLUE
       )
    }
Enter fullscreen mode Exit fullscreen mode

Add function to Zone. Preferably in the Zone after function set_default_camera(); because this turns on 2D rendering:

    let after_2d_zone = Zone::default()
        .with_locations(vec![
            Location::from_fn_vec(vec![
                acorn_example_draw_circle
                // add own functions through comma 
            ]),
            // add own locations through comma 
        ]);
Enter fullscreen mode Exit fullscreen mode

See the result:

circle

It's ALL! Your function will be run every frame. Sometimes it seems to me that it’s even EASIER THAN REACT (although React doesn't try draw every frame which have not changed).

The confession

Actually, in Light Acorn functions are not like in Haskell. For example, acorn_example_draw_circle doesn't use arguments, but changes the rendering state.

Light Acorn is Functional Style Data-Oriented Framework OR Functional-Driven ECS.

I try to be honest.

The Another True

You might argue that storing functions in vectors isn't scalable, and that c*hanging the order of functions at runtime can lead to chaos* if used incorrectly.

What if a team of 30+ people requires 200-300+ functions? Do we really need to manually write every single function to make Zone bloat into a huge codebase?

So, the architecture is flexible but weak.

But show me a single programming paradigm or architectural approach that doesn't require human discipline to scale software?

  • OOP? But building a hierarchy requires planning and, therefore, discipline.
  • Regular Bevy (or ECS-like)? Yes, it's easy to write queries there, but Bevy is a crate that requires architecture to prevent Queries from becoming a giant main.rs. Designing and maintaining an architecture is a discipline.
  • Functional programming? A great example of abstraction for humans, but until monads were invented and pure functional programming is inherently terrible for performance.

If you offer something "better" than the Acorn approach, you will soon realize that you are offering the approach you are used to and use it everywhere like a golden hammer.

Everyone says that Unreal Engine and similar ones are scalable for AAA, but not because this is really true, but because everyone is used to it and can’t change their mindset.

Understand only that: the entire history of IT has been an attempt to hide from hardware, hiding behind the complexity of the concept. Now we're at a point where IT is returning to hardware.

And I'm not just talking empty words, but proposing a solution in Acorn: Lord-Minor architecture for controlling the order of REACORN's runtime changes.

lord-minor

A Lord-Function controls its Location and can change the order of functions there, remove them, or add them. In code is like here:

    let before_2d_zone = Zone::default()
        .with_locations(vec![
            // Lord-Location.
            Location::from_fn_vec(vec![
                //(press TAB to delete functions in Minor-Location)
                acorn_example_delete_function,
            ]),
            // Minor-Location
            Location::from_fn_vec(vec![
                acorn_example_greeting,
                acorn_game_draw_3d_assets, 
            ]),
        ]);
Enter fullscreen mode Exit fullscreen mode

In other words, each Lord-Function has its own territory. This is a hierarchy, and it requires discipline. So...

I invented a tool, not a developer discipline.

Also...

YOU are not required to use Lord-Minor architecture. You are Lord of your ideas.

The Acorn Problem

Despite everything, Light Acorn has a problem with draw calls.

In the first image of post you could see 26 FPS, although CPU loading is 28%. CPU says to GPU separately to draw each 3D models of Acorn. And my GT 720m is choking on draw calls.

So that you understand how weak my laptop is:

  • i3-3217u has maximum frequency 1.8 Ghz, 2 cores and 4 threads, TDP is 17 W.
  • 6 GB RAM DDR3 1600 MT/s (vs standart DDR5 ~4800 MT/s).
  • Old HDD 720 GB Toshiba with CMR, 5400 RPM.
  • GT 720m: 2 GB DDR3, ~192 CUDA cores, memory bus is 64 bit (even modern integrated video card is more powerful).

And so I have been using it for ~13 years.

I wrote in dicord directly to QUADS (macroquad, miniquad community) for help to implement instancing. I was told on Discord that this might require a miniquad and that it will be very difficult.

I am sincerely grateful to those two people who at least answered me and didn't set the "robot" reaction, thinking that I was a bot. Thank you!

I'm also grateful to the person who gave Light Acorn the first star despite my poor presentation of the project.

How you can help to Acorn

This project is Open Source and you can help to implement GPU instancing.

Docs, code comments are included in Light Acorn. You don't have to figure out how everything fits together on your own. Just clone the project and open main.rs file and docs.

To solve problem: I suggest to use GLSL #100 (if it's real and requires shaders) and link Macroquad with Miniquad.

EVEN if it will be very difficult, required drop to Miniquad, EVEN if I have to learn GLSL in depth and EVEN if we have to invent another architecture.

The problem in this code:

    pub fn acorn_game_draw_3d_assets(world: &mut World, context: &mut AcornContext) {
        let gl = acorn_get_gl_contex();

        let mut query = 
            world.query::<(&Entity3DTransform, &Entity3DModel)>();

        for (transform, mesh) in query.iter(world) {
            let model_matrix = acorn_generate_matrix(&transform);

            gl.push_model_matrix(model_matrix);

            /*
            You may change to if/else branching for safety
            But I use perfomance mode

            if let Some(mesh) = context.assets_3d.meshes.get(mesh.mesh_id) {
                draw_mesh(mesh);
            } else {
                println!("oops...")
            }
            */

            draw_mesh(&context.assets_3d.meshes[mesh.mesh_id]);

            gl.pop_model_matrix();
        }
    }
Enter fullscreen mode Exit fullscreen mode

The draw_mesh function initializes new draw call for each Meshes.

1000 Meshes = 1000 draw calls = 1000 sufferings of GT 720m.

One more thing...

About me

You might not believe it... but I'm 18 years old and I live in Kyrgyzstan. I started learning Rust just because it's hard 8 months ago.

Fun fact: I quit learning C++ because it wouldn't let me declare a function after void main() (also I don't like OOP due to unjustified complexity).

The Conclusion

I hope you read to the end. Even if the project doesn't solve your problems, I'll still be pleased to know I wrote this post for a reason.

For those who are willing to help or want to make simple 3D games in Rust, I've attached a link:

https://github.com/Veyyr3/Light_Acorn

And perhaps soon I will add Taffy to the stack so that this framework is not only for games, but also for applications.

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.