Bevy Study: Digital Extinction
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;
}