Design of ECS x RTS: How to organize units and orders in a scalable/modular way? #2659
-
(bit long but just because I try to explain the problem entirely and what I have so far) Pretext just to make sure we're on the same page for RTS'sSay you're doing an RTS in Bevy. You have units which can be controlled, like in Age of Empires or Warcraft or Starcraft etc. Each unit has a certain set of abilities. Abilities could be very basic things like Move or Attack (most buildings wouldn't have these but most other units would). It could also be more involved abilities, like casting a spell on an enemy for example. Using an ability gives an order to a unit. A unit can have many orders, which it will perform in sequence. The player can give a sequence of orders to a unit by holding shift while giving the orders, for example. How to model this in ECS/Bevy?What I've got to so far is something like this:
struct Orders(VecDeque<Order>); Where enum Order {
// A move order, including the point the unit should move to.
Move(Vec2),
// An attack order, including the unit entity to attack.
Attack(Entity),
// ... etc. for every kind of order.
// Could even have some kind of spell ability being cast in here like
SomeAreaOfAttackSpell(Vec2), // The Vec2 is the target of the spell on the ground.
} This fn perform_orders(mut query: Query<(&mut Orders, &mut Transform)>, time: Res<Time>) {
for (mut orders, mut transform) in query.iter_mut() {
match orders.front() {
Some(Order::Move(point)) => {
// Move the unit by using the point and the transform and the delta time
}
... // Etc. for other order variants.
}
// Remove the front order from orders if the order is complete.
}
} For now, this system only needs
I've thought hard about this but can't really get to a satisfiable design. I've thought about making each Am I making some obvious mistake or missing some viable design path? Or is there no better way than this? I'm guessing most RTS "solve" this by having a scripting layer on top which handles all of this but I'd prefer to avoid that (also because Bevy doesn't support any scripting out of the box and I like that). |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments 3 replies
-
I would consider using a Trait for I did this with “Conditions” Code: use bevy::prelude::*;
pub struct ConditionsPlugin;
/// This plugin is used for adding and removing conditions from entities.
/// Conditions need to mutate the creature(entity) once but then remain “on” the character. Events trigger adding and removing conditions
impl Plugin for ConditionsPlugin {
fn build(&self, app: &mut AppBuilder) {
app.add_event::<AddConditionEvent<Flatfooted>>()
.add_event::<RemoveConditionEvent>()
.add_system(add_flatfooted_event_reader.system())
.add_system(flatfooted_condition_event_writer.system())
.add_system(flatfooted.system())
.add_system(remove_flatfooted_event_reader.system());
}
}
pub trait ConditionFunctions {
fn add(&self, entity: Entity, commands: &mut Commands);
fn remove(&self, entity: Entity, commands: &mut Commands);
}
#[derive(Debug, Clone)]
struct Condition<T: ConditionFunctions> {
applied: bool,
condition: T,
}
/// Use this in a system to mark an entity that needs to be mutated by the condition.
struct NeedsCondition<T: ConditionFunctions> {
condition: Condition<T>,
}
struct AddConditionEvent<T: ConditionFunctions> {
condition: Condition<T>,
}
struct RemoveConditionEvent {
entities: Vec<Entity>,
}
/// ----------------------------------------------
/// # Flatfooted Condition Code
/// ----------------------------------------------
#[derive(Debug, Clone, Copy)]
struct Flatfooted;
impl ConditionFunctions for Flatfooted {
fn add(&self, entity: Entity, commands: &mut Commands) {
eprintln!("Flatfooted condition added to {:?}", entity);
}
fn remove(&self, entity: Entity, commands: &mut Commands) {
eprintln!("Flatfooted condition removed from {:?}", entity);
}
}
/// # Flatfooted System
/// This system mutates any actor with the Flatfooted condition and removes the NeedsFlatFooted condition if still present
fn flatfooted(mut query: Query<(Entity, &NeedsCondition<Flatfooted>)>, mut commands: Commands) {
for (entity, needs) in query.iter_mut() {
let mut flatfooted = needs.condition.clone();
if !flatfooted.applied {
flatfooted.condition.add(entity, &mut commands);
flatfooted.applied = true;
commands
.entity(entity)
.remove::<NeedsCondition<Flatfooted>>()
.insert(flatfooted);
eprintln!("{:?} has been mutated to be flatfooted.", entity);
}
}
}
/// Reader just to mark the entity as needing the flatfooted condition. Exmple in this case, just adds to all entities.
fn add_flatfooted_event_reader(
mut reader: EventReader<AddConditionEvent<Flatfooted>>,
mut query: Query<(Entity, Without<Condition<Flatfooted>>)>,
mut commands: Commands,
) {
for event in reader.iter() {
for (entity, _) in query.iter_mut() {
eprintln!("Adding Condtion<Flatfooted> to entity: {:?}", entity);
commands
.entity(entity)
.insert(NeedsCondition::<Flatfooted> {
condition: event.condition.clone(),
});
}
}
}
fn remove_flatfooted_event_reader(
mut reader: EventReader<RemoveConditionEvent>,
query: Query<&Condition<Flatfooted>>,
mut commands: Commands,
) {
for event in reader.iter() {
eprintln!(
"{:?} entities need flatfooted removed",
event.entities.len()
);
for &entity in event.entities.iter() {
eprintln!("Removing flatfooted from entity: {:?}", entity);
let component = query.get(entity);
match component {
Ok(component) => component.condition.remove(entity, &mut commands),
Err(err) => eprintln!(
"Error removing flatfooted from entity: {:?}; {:?}",
entity, err
),
};
commands.entity(entity).remove::<Condition<Flatfooted>>();
}
}
}
/// Event Writer system to trigger AddCondition<Flatfooted> (just keyboard for now). Just an example.
fn flatfooted_condition_event_writer(
mut writer: EventWriter<AddConditionEvent<Flatfooted>>,
mut writer2: EventWriter<RemoveConditionEvent>,
query: Query<Entity, With<Condition<Flatfooted>>>,
keys: Res<Input<KeyCode>>,
) {
if keys.just_pressed(bevy::input::prelude::KeyCode::A) {
let condition = Condition::<Flatfooted> {
applied: false,
condition: Flatfooted,
};
eprintln!("INPUT -- A was pressed. Firing AddConditionEvent with Flatfooted condition.");
let condition_event = AddConditionEvent::<Flatfooted> { condition };
writer.send(condition_event);
}
if keys.just_pressed(bevy::input::prelude::KeyCode::R) {
eprintln!("INPUT -- R was pressed. Firing RemoveConditionEvent<Flatfooted>");
let remove_event = RemoveConditionEvent {
entities: query.iter().collect(),
};
eprint!("{:?} ----", remove_event.entities);
writer2.send(remove_event);
}
} Sent from my iPhone. |
Beta Was this translation helpful? Give feedback.
-
So I saw the Conditions components more as markers and the trait functions (add/remove) are more of setup functions that do any changes that need to remain static. To setup the entity to be worked on by other systems with markers and such.
Dynamic calculation for things like ArmorClass will have to be Condition aware.
For the ArmorClass example (which FlatFooted does decrease AC by two!), I will likely make any AC system aware of conditions in general. If a condition that affects AC exists, the system would act accordingly. This allows for other conditions as well that could impact AC.
On mobile so sorry for anything typed/incoherent. :)
Get Outlook for iOS<https://aka.ms/o0ukef>
…________________________________
From: SorteKanin ***@***.***>
Sent: Sunday, August 15, 2021 9:11:23 AM
To: bevyengine/bevy ***@***.***>
Cc: Mirko Rainer ***@***.***>; Comment ***@***.***>
Subject: Re: [bevyengine/bevy] Design of ECS x RTS: How to organize units and orders in a scalable/modular way? (#2659)
This looks interesting, but I'm not sure how I could squeeze all the kinds of orders into a single trait interface. I mean, consider the trait:
pub trait Order {
fn perform(&self, entity: Entity, commands: &mut Commands);
}
Is this something like what you are proposing? I don't see how the Move order could modify the Transform of the entity inside that function.
Or is it that it should modify the transform inside the system that would be something like this?
fn move(mut query: Query<(Entity, &MustPerformOrder<Move>)>, mut commands: Commands) {}
Where the move system is the analog of your flatfooted system and MustPerformOrder is the analog of NeedsCondition. Then the above system could obviously just include the Transform in the query. Is this what you were thinking?
Tbh I'm not even sure I see how this works for conditions. Say an entity has an ArmorClass component (just an int) and the flatfooted condition should decrease the ArmorClass by 2. How would the flatfooted condition do that? Is the method then to add the ArmorClass component to the query of the flatfooted system?
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub<#2659 (reply in thread)>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/AMTS6CHKXYU3YKTL4Z6XR63T464HXANCNFSM5CGB3PDA>.
|
Beta Was this translation helpful? Give feedback.
-
@TheRawMeatball answered via Discord. [7:35 AM] TheRawMeatball: creating entity groups is one of the initial motivators of relations, but since it's not a thing yet I don't think it's actively useful [3:55 AM] TheRawMeatball: use bevy::ecs::system::EntityCommands;
use bevy::prelude::*;
trait Order: Send + Sync {
fn add_to(&self, entity: &mut EntityCommands);
fn remove_from(&self, entity: &mut EntityCommands);
}
impl<B: Bundle + Clone> Order for B {
fn add_to(&self, commands: &mut EntityCommands) {
commands.insert_bundle(self.clone());
}
fn remove_from(&self, commands: &mut EntityCommands) {
commands.remove_bundle::<B>();
}
}
struct OrderNode {
order: Box<dyn Order>,
next: Option<Entity>,
rc: u32,
}
struct Orderable {
current_order_node: Option<Entity>,
}
struct EntityCompletedOrder(Entity);
fn main() {
App::build()
.add_plugins(DefaultPlugins)
.add_system(order_dispatch_system)
.add_system(move_order_system)
.run();
}
fn order_dispatch_system(
mut commands: Commands,
mut orderables: Query<&mut Orderable>,
mut orders: Query<&mut OrderNode>,
mut order_completing_entities: EventReader<EntityCompletedOrder>,
) {
for entity in order_completing_entities.iter().map(|e| e.0) {
let mut orderable = orderables.get_mut(entity).unwrap();
let mut entity = commands.entity(entity);
let current_node_entity = orderable.current_order_node.unwrap();
let mut current_node = orders.get_mut(current_node_entity).unwrap();
current_node.rc -= 1;
let cnrc = current_node.rc;
current_node.order.remove_from(&mut entity);
if let Some(next) = current_node.next {
orderable.current_order_node = Some(next);
let mut next_node = orders.get_mut(next).unwrap();
next_node.rc += 1;
next_node.order.add_to(&mut entity);
if cnrc == 0 {
next_node.rc -= 1;
}
} else {
orderable.current_order_node = None;
}
if cnrc == 0 {
commands.entity(current_node_entity).despawn();
}
}
}
// each order is like this, with one or multiple systems defining how it runs, and a bundle which carries all the order-specific components.
struct MoveOrderBundle {
target: MoveTarget,
}
struct MoveTarget(Vec2);
fn move_order_system(
mut q: Query<(Entity, &mut Transform, &MoveTarget)>,
mut ew: EventWriter<EntityCompletedOrder>,
time: Res<Time>,
) {
const SPEED: f32 = 10.0;
for (e, mut transform, target) in q.iter_mut() {
let current_pos = transform.translation.truncate();
let diff = target.0 - current_pos;
let max_travel = time.delta_seconds() * SPEED;
if diff.length_squared() <= max_travel * max_travel {
transform.translation = target.0.extend(transform.translation.z);
ew.send(EntityCompletedOrder(e));
} else {
transform.translation += diff.normalize().extend(0.0) * max_travel;
}
}
} |
Beta Was this translation helpful? Give feedback.
-
After some further discussion, ended up with this (which doesn't share orders between units and uses a VecDeque instead of the "linked list" approach": use std::collections::VecDeque;
use bevy::ecs::system::EntityCommands;
use bevy::prelude::*;
trait Order: Send + Sync {
fn add_to(&self, entity: &mut EntityCommands);
fn remove_from(&self, entity: &mut EntityCommands);
}
impl<B: Bundle + Clone> Order for B {
fn add_to(&self, commands: &mut EntityCommands) {
commands.insert_bundle(self.clone());
}
fn remove_from(&self, commands: &mut EntityCommands) {
commands.remove_bundle::<B>();
}
}
struct Orderable {
queue: VecDeque<Box<dyn Order>>,
idle: bool,
}
impl Orderable {
pub fn push_order(&mut self, order: impl Order + 'static) {
self.queue.push_back(Box::new(order));
}
}
struct EntityCompletedOrder(Entity);
fn main() {
App::build()
.add_plugins(DefaultPlugins)
.add_system(order_dispatch_system.system())
.add_system(move_order_system.system())
.run();
}
fn order_dispatch_system(
mut commands: Commands,
mut orderables: Query<&mut Orderable>,
changed_orderables: Query<Entity, Changed<Orderable>>,
mut order_completing_entities: EventReader<EntityCompletedOrder>,
) {
for entity in order_completing_entities.iter().map(|e| e.0) {
let mut orderable = orderables.get_mut(entity).unwrap();
let mut entity = commands.entity(entity);
orderable.queue.pop_front().unwrap().remove_from(&mut entity);
if let Some(next) = orderable.queue.front_mut() {
next.add_to(&mut entity);
} else {
orderable.idle = true;
}
}
for entity in changed_orderables.iter() {
let mut orderable = orderables.get_mut(entity).unwrap();
if !orderable.idle {
continue;
}
let mut entity = commands.entity(entity);
orderable.queue.front_mut().unwrap().add_to(&mut entity);
orderable.idle = false;
}
}
// each order is like this, with one or multiple systems defining how it runs, and a bundle which carries all the order-specific components.
struct MoveOrderBundle {
target: MoveTarget,
}
struct MoveTarget(Vec2);
fn move_order_system(
mut q: Query<(Entity, &mut Transform, &MoveTarget)>,
mut ew: EventWriter<EntityCompletedOrder>,
time: Res<Time>,
) {
const SPEED: f32 = 10.0;
for (e, mut transform, target) in q.iter_mut() {
let current_pos = transform.translation.truncate();
let diff = target.0 - current_pos;
let max_travel = time.delta_seconds() * SPEED;
if diff.length_squared() <= max_travel * max_travel {
transform.translation = target.0.extend(transform.translation.z);
ew.send(EntityCompletedOrder(e));
} else {
transform.translation += diff.normalize().extend(0.0) * max_travel;
}
}
} There's still some edge cases (like how to handle a "Stop" order), see further discussion in the Discord thread. |
Beta Was this translation helpful? Give feedback.
After some further discussion, ended up with this (which doesn't share orders between units and uses a VecDeque instead of the "linked list" approach":