thombruce / verse

🚀 A universe in progress
Other
8 stars 0 forks source link

Refactor Modules / Organise Code #50

Closed thombruce closed 10 months ago

thombruce commented 11 months ago

I've recently moved a lot of code into several distinct domains:

This I think I loosely want to keep. The main thing is that World is separate from Player is separate from UI, as these should work independently of one another.

What I want to do also is start organising things like CONSTANTS, Components and systems into separate files. I shouldn't have to open up orbit.rs to see what Components I have concerning orbits in the same place as I modify its systems and plugins. I want CONSTANTS and Components to be separated, so that they are more easily found and managed.

The astronomy module is a good place to start thinking about this, as it has a number of important concerns now as well as a lot of separate components and bundles. My thinking is that maybe we split the astronomy module into folders like components/ (containing components and bundles), systems/ and... I dunno about entities/ but we'll see, and we might have a separate constants.rs in the base module folder...

This way, code is separated first by domain, then by utility. And both the orbit systems and the planetary_system systems are borrowing components from a shared components/ folder, rather than sharing and swapping them with one another directly.

It seems cleaner, it seems more like a pattern I'm familiar with (coming from OOP and in particular Ruby on Rails, which has always had a very clear separation of purpose to its MVC architecture).

thombruce commented 10 months ago

So I do wanna go for more of a separation of components and systems (possibly bundles too [entity templates]).

I'll provide a for instance...

We have this Bullet component:

#[derive(Component)]
pub struct Bullet {
    pub despawn_timer: Timer,
}

And we have this bullet_despawn_system in the same file (bullet.rs):

fn bullet_despawn_system(
    mut commands: Commands,
    time: Res<Time>,
    mut query: Query<(Entity, &mut Bullet)>,
) {
    for (entity, mut bullet) in query.iter_mut() {
        bullet.despawn_timer.tick(time.delta());
        if bullet.despawn_timer.finished() {
            commands.entity(entity).despawn();
        }
    }
}

What we ought to have instead is...

Here's what a bullet consists of now, for example:

        (
            SpriteBundle {
                transform: Transform {
                    translation: Vec3::new(transform.translation.x, transform.translation.y, 99.0)
                        + transform.rotation * (Vec3::Y * 35.0), // Ships radius * scaling factor + 5px padding
                    rotation: transform.rotation,
                    ..default()
                },
                texture: handles.bullet.clone(),
                ..default()
            },
            Bullet {
                despawn_timer: Timer::from_seconds(2.0, TimerMode::Once),
            },
            ForState {
                states: GameState::IN_GAME_STATE.to_vec(),
            },
            RigidBody::Dynamic,
            Collider::cuboid(2.5, 8.0),
            velocity,
            Sensor,
            ActiveEvents::COLLISION_EVENTS,
            AudioBundle {
                source: audios.gun.clone(),
                ..default()
            },
        )

All of that can be part of a BulletBundle with a ..default() implemented...

I'm thinking...

...on a per-plugin basis. And the root mod.rs file weaves them together.

Although, reminder, some components/systems should be reclaimed and made more general - see mention of DespawnTimer above.

thombruce commented 10 months ago

Consider working with change detection to optimise some systems that depend on other changes: https://bevy-cheatbook.github.io/programming/change-detection.html

Another thing I want to do in particular is bring all of my systems' handling into one place... The systems may be defined elsewhere, but I'd like a sort of systems overview file. The analog in my mind is Ruby on Rails' routes.rb which essentially describes the mapping between URLs and controller actions. I want some similar setup here, mapping each tick to the sequence of systems that are going to be executed. It's not a perfect analog, but you catch my drift: I want the systems to be executed from a single file or small collection of files, essentially so that I can easily reorder and organise them into sets or chains as needed. A systems super-system.

thombruce commented 10 months ago

A problem with the approach suggested above:

Systems could no longer be isolated to their respective plugins.

Counter-argument: They already sort of aren't.

In an ideal world, any particular plugin could be toggled on or off without affecting others. But presently, a lot of plugins depend on the existence of ships (for example) or the player.

Take the camera's follow_player system for instance; this requires the existence of the player, without which (let me check)... okay, no, it no longer depends absolutely on the player's existence, but it is redundant without them.

There is a lot of implied dependency though.

It might be better to address some of this after introducing a different view of the world (ship interiors/planetary landings), as these will introduce alternative control schemes not just for the player, but also new systems with which the camera ought to interact. It would provide a fuller sense of how and when the player-dependent systems interact with the different player states.

Ideally, plugins would operate independently of any other plugin.

Also ideally, we would have a well-organises "super system" of systems... but I think this may be at odds with the above (unless we are able to say "If... SomePlugin is active" which... yup, is possible:

app.is_plugin_added();

^ Not a full snippet, but the method described returns a bool true or false if a given plugin is added, which is exactly what we want; it will help us decouple plugins from one another on the condition that the dependents aren't being used.

thombruce commented 10 months ago

Proof that I'm still not in a very ECS mindset...

All those systems that depend on the Player component? They can introduce their own component to serve that function, and that component can be attached to the Player entity (we might even create a PlayerBundle having all of them).

Take the follow_player system in the camera.rs module for instance: Instead of querying for an entity with the Player component, it could query for a CameraShouldFollow component or something like that (CameraFollows, CameraFocused). By default, the player entity would have this, but we could also introduce a gameplay element that allows for it to follow some other entity - you could imagine a Sims-mode or management-mode in which the camera follows a selected NPC to observe their AI behaviours.

For each system depending on component queries to do its job, consider what the function of that job is and create custom components for the task as appropriate. This is how we truly decouple the plugins from one another.

Think:

  1. Systems - acting on...
  2. Components - building...
  3. Entities

Separate each plugin/module into its Component and System concerns (Entities are a little more ambiguous, they're constructed within setup/spawn systems; where possible we should try to isolate plugin functionality and separate out the entity spawning logic to some other place, some gameplay or game world module instead).