Skip to content

Commit

Permalink
feat!: add layer entity for Entity layers, changing the hierarchy (#257)
Browse files Browse the repository at this point in the history
Closes #224 

Ever since layer metadata has been accessible for tile-based layer
entities via a component, it has become a pain point that entity layers
do not have a layer entity associated with them. This PR makes it so
they do. Now, `LdtkEntity`/`EntityInstance`s spawned by the plugin will
be children of a layer entity w/ a `LayerMetadata` component, which in
turn is a child of the level entity. The behavior of the `Worldly`
component has been adjusted accordingly.

The scheduling of `worldly_adoption` has also changed. In the most
recent iteration of the schedule, the main constraint for scheduling
this system was that it should be after `Update` to have minimal frame
delay in the odd case that a user adds it to entities manually in
`Update`. Putting it in `PostUpdate` still satisfies this constraint,
and also allows us to schedule it after transform propagation. The
purpose of this system is for worldly entities to maintain their global
transform after they are re-homed, so I think it's important for the
global transform to be calculated before trying to give them a new
parent. It shouldn't affect physics since they wouldn't have a global
transform until `PostUpdate` anyway.
  • Loading branch information
Trouv authored Oct 28, 2023
1 parent ab21e2c commit ee20a53
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 105 deletions.
14 changes: 8 additions & 6 deletions src/components/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ pub struct IntGridCell {
pub value: i32,
}

/// [Component] that indicates that an ldtk entity should be a child of the world, not the level.
/// [Component] that indicates that an ldtk entity should be a child of the world, not their layer.
///
/// By default, [LdtkEntity]s are children of the level they spawn in.
/// By default, [LdtkEntity]s are children of the layer they spawn in.
/// This can be a problem if that entity is supposed to travel across multiple levels, since they
/// will despawn the moment the level they were born in despawns.
///
Expand Down Expand Up @@ -340,13 +340,15 @@ pub(crate) struct EntityInstanceBundle {
/// Each level is its own entity, with the [`LdtkWorldBundle`] as its parent.
/// Each level has a [`LevelIid`] component.
///
/// All non-Entity layers (IntGrid, Tile, and AutoTile) will also spawn as their own entities.
/// All layers will also spawn as their own entities.
/// Each layer's parent will be the level entity.
/// Each layer will have a [`LayerMetadata`] component, and are bevy_ecs_tilemap TileMaps.
/// Each layer will have a [`LayerMetadata`] component.
///
/// AutoTile, Tile, and IntGrid layer entities are bevy_ecs_tilemap TileMaps.
/// Each tile in these layers will have the layer entity as its parent.
///
/// For Entity layers, all LDtk entities in the level are spawned as children to the level entity,
/// unless marked by a [`Worldly`] component.
/// For Entity layers, all LDtk entities will have the layer entity as their parent by default.
/// However, this behavior can be changed by marking them with the [`Worldly`] component.
#[derive(Clone, Default, Bundle)]
pub struct LdtkWorldBundle {
pub ldtk_handle: Handle<LdtkProject>,
Expand Down
124 changes: 65 additions & 59 deletions src/level.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,56 +270,38 @@ pub fn spawn_level(
}

for layer_instance in layer_instances.iter().rev() {
let layer_offset = Vec2::new(
layer_instance.px_total_offset_x as f32,
-layer_instance.px_total_offset_y as f32,
);

match layer_instance.layer_instance_type {
Type::Entities => {
commands.entity(ldtk_entity).with_children(|commands| {
for entity_instance in &layer_instance.entity_instances {
let transform = calculate_transform_from_entity_instance(
entity_instance,
entity_definition_map,
*level.px_hei(),
layer_z as f32,
);
// Note: entities do not seem to be affected visually by layer offsets in
// the editor, so no layer offset is added to the transform here.

let (tileset, tileset_definition) = match &entity_instance.tile {
Some(t) => (
tileset_map.get(&t.tileset_uid),
tileset_definition_map.get(&t.tileset_uid).copied(),
),
None => (None, None),
};

let predicted_worldly = Worldly::bundle_entity(
entity_instance,
layer_instance,
tileset,
tileset_definition,
asset_server,
texture_atlases,
);

if !worldly_set.contains(&predicted_worldly) {
let default_ldtk_entity: Box<dyn PhantomLdtkEntityTrait> =
Box::new(PhantomLdtkEntity::<EntityInstanceBundle>::new());
let mut entity_commands = commands.spawn_empty();
let layer_entity = commands
.spawn(SpatialBundle::from_transform(Transform::from_translation(
layer_offset.extend(layer_z as f32),
)))
.insert(LayerMetadata::from(layer_instance))
.insert(Name::new(layer_instance.identifier.to_owned()))
.with_children(|commands| {
for entity_instance in &layer_instance.entity_instances {
let transform = calculate_transform_from_entity_instance(
entity_instance,
entity_definition_map,
*level.px_hei(),
);
// Note: entities do not seem to be affected visually by layer offsets in
// the editor, so no layer offset is added to the transform here.

// insert Name before evaluating LdtkEntitys so that user-provided
// names aren't overwritten
entity_commands.insert((
EntityIid::new(entity_instance.iid.to_owned()),
Name::new(entity_instance.identifier.to_owned()),
));
let (tileset, tileset_definition) = match &entity_instance.tile {
Some(t) => (
tileset_map.get(&t.tileset_uid),
tileset_definition_map.get(&t.tileset_uid).copied(),
),
None => (None, None),
};

ldtk_map_get_or_default(
layer_instance.identifier.clone(),
entity_instance.identifier.clone(),
&default_ldtk_entity,
ldtk_entity_map,
)
.evaluate(
&mut entity_commands,
let predicted_worldly = Worldly::bundle_entity(
entity_instance,
layer_instance,
tileset,
Expand All @@ -328,13 +310,44 @@ pub fn spawn_level(
texture_atlases,
);

entity_commands.insert(SpatialBundle {
transform,
..default()
});
if !worldly_set.contains(&predicted_worldly) {
let default_ldtk_entity: Box<dyn PhantomLdtkEntityTrait> =
Box::new(PhantomLdtkEntity::<EntityInstanceBundle>::new());
let mut entity_commands = commands.spawn_empty();

// insert Name before evaluating LdtkEntitys so that user-provided
// names aren't overwritten
entity_commands.insert((
EntityIid::new(entity_instance.iid.to_owned()),
Name::new(entity_instance.identifier.to_owned()),
));

ldtk_map_get_or_default(
layer_instance.identifier.clone(),
entity_instance.identifier.clone(),
&default_ldtk_entity,
ldtk_entity_map,
)
.evaluate(
&mut entity_commands,
entity_instance,
layer_instance,
tileset,
tileset_definition,
asset_server,
texture_atlases,
);

entity_commands.insert(SpatialBundle {
transform,
..default()
});
}
}
}
});
})
.id();

commands.entity(ldtk_entity).add_child(layer_entity);
layer_z += 1;
}
_ => {
Expand Down Expand Up @@ -673,13 +686,6 @@ pub fn spawn_level(
-grid_tile_size_difference * tile_pivot_y,
);

// Layers in LDtk can also have a plain offset value.
// Not much calculation needs to be done here.
let layer_offset = Vec2::new(
layer_instance.px_total_offset_x as f32,
-layer_instance.px_total_offset_y as f32,
);

commands
.entity(layer_entity)
.insert(tilemap_bundle)
Expand Down
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@
//! Regardless of your choice, the spawned entities will have an appropriate [Transform].
//! They will also be spawned and despawned along with the levels they belong to, unless otherwise
//! specified with a [Worldly] component.
//! This is because, by default, the entities are spawned as children of the level entities.
//! This is because, by default, the entities are spawned as children of their layer entities,
//! which in turn are children of level entities.
//!
//! ### Worlds and Levels
//!
Expand Down
14 changes: 8 additions & 6 deletions src/plugin.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! Provides [LdtkPlugin] and its scheduling-related dependencies.
use crate::{app, assets, components, resources, systems};
use bevy::{app::MainScheduleOrder, ecs::schedule::ScheduleLabel, prelude::*};
use bevy::{
app::MainScheduleOrder, ecs::schedule::ScheduleLabel, prelude::*, transform::TransformSystem,
};

/// Schedule for processing this plugin's ECS API, inserted after [Update].
///
Expand Down Expand Up @@ -52,10 +54,6 @@ impl Plugin for LdtkPlugin {
PreUpdate,
(systems::process_ldtk_assets, systems::process_ldtk_levels),
)
.add_systems(
ProcessLdtkApi,
systems::worldly_adoption.in_set(ProcessApiSet::PreClean),
)
.add_systems(
ProcessLdtkApi,
(systems::apply_level_selection, systems::apply_level_set)
Expand All @@ -70,7 +68,11 @@ impl Plugin for LdtkPlugin {
)
.add_systems(
PostUpdate,
systems::detect_level_spawned_events.pipe(systems::fire_level_transformed_events),
(
systems::detect_level_spawned_events
.pipe(systems::fire_level_transformed_events),
systems::worldly_adoption.after(TransformSystem::TransformPropagate),
),
)
.register_type::<components::LevelIid>()
.register_type::<components::EntityIid>()
Expand Down
21 changes: 13 additions & 8 deletions src/systems.rs
Original file line number Diff line number Diff line change
Expand Up @@ -378,15 +378,20 @@ pub fn clean_respawn_entities(world: &mut World) {
/// Implements the functionality for `Worldly` components.
pub fn worldly_adoption(
mut commands: Commands,
mut worldly_query: Query<(&mut Transform, &Parent, Entity), Added<Worldly>>,
transform_query: Query<(&Transform, &Parent), Without<Worldly>>,
ancestors: Query<&Parent>,
worldly_query: Query<Entity, Added<Worldly>>,
) {
for (mut transform, parent, entity) in worldly_query.iter_mut() {
if let Ok((level_transform, level_parent)) = transform_query.get(parent.get()) {
// Find the entity's world-relative transform, so it doesn't move when its parent changes
*transform = level_transform.mul_transform(*transform);
// Make it a child of the world
commands.entity(level_parent.get()).add_child(entity);
for worldly_entity in worldly_query.iter() {
// world entity for this worldly entity is its third ancestor...
// - first ancestor is the layer entity
// - second ancestor is the level entity
// - third ancestor is the world entity
if let Some(world_entity) = ancestors.iter_ancestors(worldly_entity).nth(2) {
commands
.entity(worldly_entity)
.set_parent_in_place(world_entity);
} else {
commands.entity(worldly_entity).remove_parent_in_place();
}
}
}
Expand Down
39 changes: 14 additions & 25 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,17 @@ pub fn create_layer_definition_map(
layer_definitions.iter().map(|l| (l.uid, l)).collect()
}

/// Performs [EntityInstance] to [Transform] conversion
/// Performs [`EntityInstance`] to [`Transform`] conversion
///
/// The `entity_definition_map` should be a map of [EntityDefinition] uids to [EntityDefinition]s.
/// The `entity_definition_map` should be a map of [`EntityDefinition`] uids to [`EntityDefinition`]s.
///
/// Internally, this transform is used to place [EntityInstance]s as children of the level.
/// Internally, this transform is used to place [`EntityInstance`]s as children of their layer.
///
/// [`Transform`]: https://docs.rs/bevy/latest/bevy/prelude/struct.Transform.html
pub fn calculate_transform_from_entity_instance(
entity_instance: &EntityInstance,
entity_definition_map: &HashMap<i32, &EntityDefinition>,
level_height: i32,
z_value: f32,
) -> Transform {
let entity_definition = entity_definition_map.get(&entity_instance.def_uid).unwrap();

Expand All @@ -92,7 +93,7 @@ pub fn calculate_transform_from_entity_instance(
);
let scale = size.as_vec2() / def_size.as_vec2();

Transform::from_translation(translation.extend(z_value)).with_scale(scale.extend(1.))
Transform::from_translation(translation.extend(0.)).with_scale(scale.extend(1.))
}

fn ldtk_coord_conversion(coords: IVec2, height: i32) -> IVec2 {
Expand Down Expand Up @@ -459,12 +460,8 @@ mod tests {
pivot: Vec2::new(0., 0.),
..Default::default()
};
let result = calculate_transform_from_entity_instance(
&entity_instance,
&entity_definition_map,
320,
0.,
);
let result =
calculate_transform_from_entity_instance(&entity_instance, &entity_definition_map, 320);
assert_eq!(result, Transform::from_xyz(272., 48., 0.));

// difficult case
Expand All @@ -476,15 +473,11 @@ mod tests {
pivot: Vec2::new(1., 1.),
..Default::default()
};
let result = calculate_transform_from_entity_instance(
&entity_instance,
&entity_definition_map,
100,
2.,
);
let result =
calculate_transform_from_entity_instance(&entity_instance, &entity_definition_map, 100);
assert_eq!(
result,
Transform::from_xyz(25., 75., 2.).with_scale(Vec3::new(3., 2., 1.))
Transform::from_xyz(25., 75., 0.).with_scale(Vec3::new(3., 2., 1.))
);
}

Expand Down Expand Up @@ -513,15 +506,11 @@ mod tests {
}),
..Default::default()
};
let result = calculate_transform_from_entity_instance(
&entity_instance,
&entity_definition_map,
100,
2.,
);
let result =
calculate_transform_from_entity_instance(&entity_instance, &entity_definition_map, 100);
assert_eq!(
result,
Transform::from_xyz(32., 68., 2.).with_scale(Vec3::new(4., 2., 1.))
Transform::from_xyz(32., 68., 0.).with_scale(Vec3::new(4., 2., 1.))
);
}

Expand Down

0 comments on commit ee20a53

Please sign in to comment.