I have two entities: a sprite and a label. The label should always appear below the sprite and should remain easy to read. So, the sprite and label positions are coupled, but their rotations are not.
What is the best way to implement this? My implementation works, but it feels wrong.
My concerns are:
struct Antis not aBundlebecause it stores both parent and child data which cannot be spawned as a single entity.- A root, spatial bundle is used to establish relationship between two siblings, but
Positionis not associated with it so that queries expecting to work onPosition+AntOrientationwork against a single entity. - A system is used to sync updates to
Positionacross the sprite and label siblings because their parent's transform is not updated. - Label is always rendered with a fixed offset from sprite. The naive position system would overwrite this offset. So, an invisible LabelContainer wraps label to preserve the offset.
#[derive(Component)]
pub struct Position { pub x: isize, pub y: isize }
#[derive(Component)]
pub struct AntOrientation { facing: Facing, angle: Angle }
#[derive(Component)]
pub struct AntName(pub String);
struct Ant {
position: Position,
orientation: AntOrientation,
name: AntName,
sprite_bundle: SpriteBundle,
label_bundle: Text2dBundle,
}
pub fn setup_ants_startup_system(
mut commands: Commands,
asset_server: Res<AssetServer>,
world_map: ResMut<WorldMap>,
) {
let ants = world_map.initial_state.ants.iter().map(|ant_save_state| {
Ant::new(
ant_save_state.position,
ant_save_state.orientation,
ant_save_state.name.0.as_str(),
&asset_server,
)
}).collect::<Vec<_>>();
for ant in ants {
commands
// Wrap label and ant with common parent to allow a system to easily associate their position.
// Don't mess with parent transform to avoid rotating label.
.spawn(SpatialBundle::default())
.with_children(|ant_label_container| {
ant_label_container.spawn(( ant.sprite_bundle, ant.position, ant.orientation ));
// Put the label in its own container so offset on label itself isn't overwritten when updating position
ant_label_container.spawn((
SpatialBundle {
transform: Transform {
translation: Vec3::new(
ant.position.x as f32,
-ant.position.y as f32,
1.0,
),
..default()
},
..default()
},
LabelContainer,
))
.with_children(|label_container| {
label_container.spawn(ant.label_bundle);
});
});
}
}
pub fn update_position_system(
mut query: Query<(&mut Transform, Position, Option<&Parent>), (Without<LabelContainer>, Changed<Position>)>,
mut label_container_query: Query<(&mut Transform, &Parent), With<LabelContainer>>,
) {
for (mut transform, position, parent) in query.iter_mut() {
transform.translation.x = position.x as f32;
transform.translation.y = -position.y as f32;
// If entity has a parent container, check for sibling labels and update their position.
if let Some(parent) = parent {
label_container_query.iter_mut().for_each(
|(mut label_container_transform, label_container_parent)| {
if label_container_parent == parent {
label_container_transform.translation.x = transform.translation.x;
label_container_transform.translation.y = transform.translation.y;
}
},
);
}
}
}