Proposal and discussion on a default structure for amethyst projects

#1

I was recently thinking on how to structure an amethyst project that let us work better with amethyst 3rd party packages and integrate them easily into our projects, something similar to rails initializers.

The problem

Right now you need to write a lot of boiler plate just to get your barebone app working, I know that is good to know how to write it, but for me this structure (where all the initialization is in the main.rs) is not something maintanable, at least for medium/big projects. Your main file will eventually have hundred or thousand of lines of code just to setup the systems used.

And also another problem is that some systems (well this was talked a lot in other posts too) is that there’s no convenient way to know what systems depend on others, etc.

The solution

Encourage a good practice to put the initializers in their respective file under the folder initializers that can look something like

mod.rs

renderer.rs

This is just a tentative approach and I think it could better worked so we can use this in the future to integrate with the editor and let the configuration accours there, but we need an extra step for that to work and its to come with a convention that the systems can load a ron file to setup their properties (some of the core systems already do something similar).

But in the mean while we can to address this issue in a primitive way and let the amethyst-cli tool to fetch the initializers from the repo (if is present in the library repo), copy that default initializer (if is not already present in the initializer folder) and edit the mod.rs file to include that mod and call the init.

There are some issues still with this approach, maybe those 3rd party package need that you setup some core system and need to be initialized after them, so we may need an extra manifest file for the amethyst assets that could resolve this issue, we can use the same approach that cocoapods/homebrew use maybe and setup and repo where amethyst-tools read to get the amethyst 3rd party packages and so this cli will be responsible to update the cargo.toml, setup the initializers and maybe setup the ron configuration file for those initializers.

Closing notes

I’m not saying that we all must follow this kind of structure or force it, but what I want to say is we need to encourage, because IMHO the great success of rails/unity/etc all those tools has good practices that they promote, so it’s easy (at least for rails) to jump into a project and know more or less where to start looking to work with an existing project, unity is kinda similar but their structure isn’t too strict as rails.

Disclaimer

I’m not aware if this was already discused (I’m not sure if I already talked somewhere about this a while ago) by the core team and if they have some work on this or at least an RFC about this.

And sorry if my proposal isn’t well elaborated or too vague.

5 Likes

(Joël Lupien) #2

I agree that we need a better way to declare systems.
However, I feel like splitting the initialization of systems in different files is not ideal either.
You end up not knowing exactly what system is initialized where, and in which order, which could lead to people forgetting to add systems or adding them in the wrong order.

2 Likes

#3

From my perspective could work, and that’s also I bring it to the forum, to see/discuss what others think about it and try to find a solution, because sooner or later IMHO we need this feature, because unity is so popular just for having easy ways to install 3rd party code and use them, right now it’s a bit trickier to do this, so I think maybe the people involved in specs/nitric could help here, because the setup occurs in the ECS library.

0 Likes

(Ezro) #4

I agree with @jojolepro that you end up not knowing exactly what systems are initialized where, in which order, etc. In my mind, this proposal is effectively a cosmetic convenience layer which I don’t think solves the underlying problem.

In viewing this ~same discussion from a different perspective, I’ve been trying to rationalize how an editor may work and I’ve identified that the bootstrap has a few parts to it:

  • Bundles to use (e.g.,TransformBundle, InputBundle, RenderBundle, custom bundles)
  • Systems to use
  • Starting state
  • Logger

These dependencies then need to be instantiated in an order that has meaning to the user (i.e., movement system before collision, rendering last, etc.).

This problem, from my perspective, is one of how to templatize the bootstrapping process so that the dependencies can be dependency injected into Amethyst in a way that allows for the application to be built dynamically.

I don’t have a good solution for how to handle that bootstrapping, but my immediate thought was dynamic code generation using templates. For instance, a declarative contract to Amethyst could say:

{
    "variables": [
        {
            "name": "draw_flat_2d",
            "type": "DrawFlat2D",
            "args": [
                {
                    "name": "new",
                    "values": [
                        ""
                    ]
                }
            ]
        },
        {
            "name": "draw_debug_lines",
            "type": "DrawDebugLines",
            "args": [
                {
                    "name": "<PosColorNorm>",
                    "values": [
                        ""
                    ]
                },
                {
                    "name": "new",
                    "values": [
                        ""
                    ]
                }
            ]
        },
        {
            "name": "stage",
            "type": "Stage",
            "args": [
                {
                    "name": "with_backbuffer",
                    "values": [
                        ""
                    ]
                },
                {
                    "name": "clear_target",
                    "values": [
                        "[0.1, 0.1, 0.1, 1.0], 1.0"
                    ]
                },
                {
                    "name": "with_pass",
                    "values": [
                        "{variables.draw_flat_2d}"
                    ]
                },
                {
                    "name": "with_pass",
                    "values": [
                        "{variables.draw_debug_lines}"
                    ]
                }
            ]
        },
        {
            "name": "pipe",
            "type": "Pipeline",
            "args": [
                {
                    "name": "with_stage",
                    "values": [
                        "{variables.stage}"
                    ]
                }
            ]
        },
        {
            "name": "display_config",
            "type": "DisplayConfig",
            "args": [
                {
                    "name": "load",
                    "values": [
                        "C:\\game\\resources\\display_config.ron"
                    ]
                }
            ]
        }
    ],
    "states": [{
        "name": "start",
        "TODO": "How to reflect states in a dynamic way?"
    }],
    "gameDataBuilder": [
        {
            "type": "Bundle",
            "bundle": "TransformBundle",
            "args": [
                {
                    "name": "new",
                    "values": [
                        ""
                    ]
                }
            ],
            "try": true
        },
        {
            "type": "System",
            "system": "CustomSystem",
            "name": "custom_system",
            "dependencies": [],
            "try": false
        },
        {
            "type": "Bundle",
            "bundle": "RenderBundle",
            "args": [
                {
                    "name": "new",
                    "values": [
                        "{variables.pipe}",
                        "Some({variables.display_config})"
                    ]
                },
                {
                    "name": "with_sprite_sheet_processor",
                    "values": [
                        ""
                    ]
                },
                {
                    "name": "with_sprite_visibility_sorting",
                    "values": [
                        "&[]"
                    ]
                }
            ],
            "try": true
        }
    ],
    "application": {
        "type": "CoreApplication",
        "args": [
            {
                "name": "build",
                "values": [
                    "C:\\game\\resources",
                    "{states.start}"
                ],
                "try": true
            },
            {
                "name": "build",
                "values": [
                    "{gameDataBuilder}"
                ],
                "try": true
            },
            {
                "name": "run",
                "values": [],
                "try": false
            }
        ]
    }
}

This example uses json, but ron or any other format could be used.

Which would then be translated to the following Rust code:

fn main() {
    let draw_flat_2d = DrawFlat2D::new();
    let draw_debug_lines = DrawDebugLines::<PosColorNorm>::new();
    let stage = Stage::with_backbuffer()
            .clear_target([0.1, 0.1, 0.1, 1.0], 1.0)
            .with_pass(draw_flat_2d)
            .with_pass(draw_debug_lines);
    let pipe = Pipeline::build().with_stage(stage);
    let display_config = DisplayConfig::load("C:\\game\\resources\\display_config.ron");
    let game_data = GameDataBuilder::default()
        .with_bundle(TransformBundle::new())?
        .with(CustomSystem, "custom_system", &[])
        .with_bundle(
            RenderBundle::new(pipe, Some(display_config))
                .with_sprite_sheet_processor()
                .with_sprite_visibility_sorting(&[]),
        )?
        .with_bundle(editor_sync_bundle)?;
    let mut game = Application::build(root, Start)?.build(game_data)?;
    game.run();
}

This is a very naive, incorrect, and overall less-than-ideal implementation, but I think the premise is strong.

The reason, in my opinion, that other engines have a lower barrier for entry is because the vast majority of the bootstrapping is handled for you by the engine, and afforded to you as a user from within an editor; to drive this point home, here’s an analogous new project workflow in Unity:

  • A new project is created and a completely fresh scene is created for you with predefined objects, such as the camera. As a user you don’t have to configure where the camera is in their renderer. Additionally, their renderer is completely opaque to the user; you don’t have to set up what kind of pipeline to use, etc.

I’m very interested to hear what other people think about this, and especially how we can solve this problem to have our “scenes” / projects reflect a more declarative nature with the ultimate goal of shifting the heavy lifting to a control plane (i.e., Editor).

0 Likes

(Khionu Sybiern) #5

This was actually raised as an issue by @Xaeroxe, and he suggested we refactor the engine into a binary which loads in the user’s code as libraries. That could be something we still do. Have the bootstrapping as a separate module that will import the other modules.

0 Likes

(Fletcher) #6

A lot of this touches on the UX of Amethyst. Right now, it is high touch, high friction, in that it is both laborious and difficult to get started. Our goal is to be low touch, low friction. Unity does this really well; examples outside of the gaming space include Ruby on Rails and MongoDB.

From a technical standpoint, both are mediocre at best, and were really bad when they launched. But they made laborious, difficult things trivial: building RESTful APIs and tossing and retrieving JSON.

I highly encourage everyone to read this article: https://www.defmacro.org/2017/01/18/why-rethinkdb-failed.html

As far as Amethyst specifically, I think the guiding principles should be:

  1. Reasonable defaults that can be overridden by the user
  2. Convention over configuration

We don’t want our users to have to make 500 choices before they can even start making a game. People only have so much mental energy to expend. For a gaming engine, we want to help them focus on making their game, not what they should name their directories and such.

3 Likes

#7

Yes, you are right, but what I wanted to tell is that we need to think this for when amethyst get a stable version, so we need to have this in mind and develop the engine to be friendly with approach like this, because one of the amethyst goals is to have an editor in a near/not so far future. In the current state of amethyst is hard to have a tool like this to support 3rd party libraries with an easy integration.

Also in the regard of the folder structure, that’s why we have amethyst-cli I think we can encourage a structure that we think is a good practice just for the sake of readability and organization, I know that if we try to ship a very polish/full featured engine from the start we will lose the race, but that doesn’t mean that we cannot think about it and have it in our roadmap with clear goals.

#offtopic: Maybe we can (if there’s not yet, or let me know if I’m not aware) have a roadmap visible in the main amethyst repo or webpage, it doesn’t need to go into details of everything but can have the general goals so people that isn’t following day to day amethyst development can have a quick glimpse about the future of the project

1 Like

(Fletcher) #8

I am working on that as we speak. =) The PR and Issue cleanup I did a few days ago was step 1. Collecting these user stories is step 2.

I also agree about the need for a better general experience in getting started and using the engine. Items to improve that will be a high priority on the roadmap.

1 Like