Tainted \\ Coders

Bevy Study: Digital Extinction

Last updated:

Digital Extinction is a 3D real-time strategy (RTS) game. It is set in the near future when humans and AI fight over their existence.

Its an open source RTS focused on macro level gameplay. Not a lot of testing.

https://github.com/DigitalExtinction/Game/blob/main/DESIGN.md

Code organization

src/main.rs is the only file in the src folder.

All other dependencies are broken up into folders under ./crates.

Crates are broken out into the following modules:

  • behaviour
  • camera
  • combat
  • conf
  • connector
  • construction
  • controller
  • core
  • gui
  • index
  • loader
  • lobby
  • lobby_client
  • lobby_model
  • log
  • map
  • menu
  • movement
  • net
  • objects
  • pathing
  • signs
  • spawner
  • terrain
  • tools
  • uom

Crates contain an src folder with at least lib.rs and supporting files.

lib.rs defines a plugin which is then added to the app. Each supporting file usually also defines a plugin which is then added to the plugin in lib.rs.

impl PluginGroup for CameraPluginGroup {
    fn build(self) -> PluginGroupBuilder {
        PluginGroupBuilder::start::<Self>()
            .add(CameraPlugin)
            .add(DistancePlugin)
    }
}

Uses getters mostly over raw property access:

// Send this event to change target location of freshly manufactured units.
pub struct ChangeDeliveryLocationEvent {
    factory: Entity,
    position: Vec2,
}

impl ChangeDeliveryLocationEvent {
    pub fn new(factory: Entity, position: Vec2) -> Self {
        Self { factory, position }
    }

    fn factory(&self) -> Entity {
        self.factory
    }

    fn position(&self) -> Vec2 {
        self.position
    }
}

Input

Input mostly handled through events. Events like MouseDragged fire other events such as UpdateSelectionBoxEvent.

impl Plugin for HandlersPlugin {
    fn build(&self, app: &mut App) {
        app.add_system(
            right_click_handler
                .in_base_set(GameSet::Input)
                .run_if(in_state(GameState::Playing))
                .run_if(on_click(MouseButton::Right))
                .after(PointerSet::Update)
                .after(MouseSet::Buttons)
                .before(CommandsSet::SendSelected)
                .before(CommandsSet::DeliveryLocation)
                .before(CommandsSet::Attack),
        // ...
        )
    }
}

Debugging

Uses debug_assert! to panic on test builds.

Cargo

Uses workspaces all dependencies are defined under [workspace.dependencies]:

[workspace]
members = ["crates/*"]

[workspace.package]
version = "0.1.0-dev"

edition = "2021"
authors = ["Martin Indra <martin.indra@mgn.cz>"]
repository = "https://github.com/DigitalExtinction/Game"
keywords = ["DigitalExtinction", "gamedev", "game", "bevy", "3d"]
homepage = "https://de-game.org/"
license = "GPL-3.0"
categories = ["games"]

[workspace.dependencies]
# DE
de_behaviour = { path = "crates/behaviour", version = "0.1.0-dev" }
de_camera = { path = "crates/camera", version = "0.1.0-dev" }
de_combat = { path = "crates/combat", version = "0.1.0-dev" }
de_conf = { path = "crates/conf", version = "0.1.0-dev" }
...
# Other
ahash = "0.7.6"
anyhow = "1.0"
approx = "0.5.1"
assert_cmd = "2.0.10"
async-compat = "0.2.1"
async-std = "1.11"
async-tar = "0.4.2"
...

Uses a profile for testing releases that flags debugging:

[profile.lto]
inherits = "release"
lto = true

[profile.testing]
inherits = "release"
opt-level = 2
debug = true
debug-assertions = true
overflow-checks = true

[profile.testing.package."*"]
opt-level = 3

App definition

app.insert_resource(Msaa::Sample4)
    .add_plugins(
        DefaultPlugins
            .set(WindowPlugin {
                primary_window: Some(Window {
                    title: "Digital Extinction".to_string(),
                    mode: WindowMode::BorderlessFullscreen,
                    ..Default::default()
                }),
                ..default()
            })
            .disable::<LogPlugin>(),
    )
    .add_plugin(LogDiagnosticsPlugin::default())
    .add_plugin(FrameTimeDiagnosticsPlugin::default())
    .add_plugin(GamePlugin)
    .add_plugins(ConfigPluginGroup)
    .add_plugins(GuiPluginGroup)
    .add_plugins(LobbyClientPluginGroup)
    .add_plugins(MenuPluginGroup)
    .add_plugins(CorePluginGroup)
    .add_plugins(ObjectsPluginGroup)
    .add_plugins(TerrainPluginGroup)
    .add_plugins(LoaderPluginGroup)
    .add_plugins(IndexPluginGroup)
    .add_plugins(PathingPluginGroup)
    .add_plugins(SignsPluginGroup)
    .add_plugins(SpawnerPluginGroup)
    .add_plugins(MovementPluginGroup)
    .add_plugins(ControllerPluginGroup)
    .add_plugins(CameraPluginGroup)
    .add_plugins(BehaviourPluginGroup)
    .add_plugins(CombatPluginGroup)
    .add_plugins(ConstructionPluginGroup);

Grabbing the cursor

impl Plugin for GamePlugin {
    fn build(&self, app: &mut App) {
        app.add_state_with_set::<AppState>();

        #[cfg(not(target_os = "macos"))]
        {
            app.add_system(cursor_grab_system.in_schedule(OnEnter(AppState::AppLoading)));
        }
    }
}

#[cfg(not(target_os = "macos"))]
fn cursor_grab_system(mut window_query: Query<&mut Window, With<PrimaryWindow>>) {
    let mut window = window_query.single_mut();
    window.cursor.grab_mode = CursorGrabMode::Confined;
}