#Guidance on Parent/Child Relationships for Sprite w/ Label

10 messages · Page 1 of 1 (latest)

jade hare
#

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 Ant is not a Bundle because 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 Position is not associated with it so that queries expecting to work on Position + AntOrientation work against a single entity.
  • A system is used to sync updates to Position across 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;
                    }
                },
            );
        }
    }
}
#

Guidance on Parent/Child Relationships for Sprite w/ Label

#

There exists this GitHub issue which would address most of my concerns: https://github.com/bevyengine/bevy/issues/1780

If this issue were resolved then I would attach the label to the sprite as a child, prevent the label from rotating with the sprite, and would not need a wrapping SpatialBundle nor wrapping LabelContainer

It's not clear to me if my approach is ""standard"" until then?

GitHub

What problem does this solve or what need does it fill? An example use case is a tank with a child turret entity. The turret should inherit location and scale from the tank, but should rotate indep...

#

Another approach I looked into was creating a "label system" which spawned labels but did not represent them as children of other entities. This seemed much more complex because of the need to tie the lifetime of the label to its related entity. I found this approach somewhat appealing because of how awkward it feels to work with parent/child hierarchies in ECS, but I couldn't see myself building an entire application that avoided parent/child hierarchies for loosely related items. I wasn't sure how to easily and efficiently remove my label when its related sprite entity was despawned.

#

I think I might play around with the PostUpdate reset globaltransform rotation approach (Oh, apparently this suggestion in the GitHub issue is out of date)

jade hare
#

Okay I think I am just going to apply the inverse to specific components when their parent changes

#

Something like this (this works but trying to simplify still)

#
pub fn render_orientation(
    mut query: Query<(&mut Transform, Ref<AntOrientation>, &Children), Without<LabelContainer>>,
    mut label_container_query: Query<&mut Transform, With<LabelContainer>>,
) {
    for (mut transform, orientation, children) in query.iter_mut() {
        if orientation.is_changed() {
            let x_flip = if orientation.get_facing() == Facing::Left { -1.0 } else { 1.0 };
            transform.scale = Vec3::new(x_flip, 1.0, 1.0);
            transform.rotation = Quat::from_rotation_z(orientation.get_angle().as_radians());

            for &child in children.iter() {
                if let Ok(mut label_container_transform) = label_container_query.get_mut(child) {
                    if orientation.get_facing() == Facing::Left {
                        label_container_transform.scale = Vec3::new(x_flip, 1.0, 1.0);
                        label_container_transform.rotation = transform.rotation;
                    } else {
                        label_container_transform.rotation = transform.rotation.inverse();
                    }
                }
            }
        }
    }
}
jade hare
#

Ehhh

#

I might actually go back and try my hand at manually doing a sibling relationship rather than having parent/child