bevy_ecs_tilemap
bevy_ecs_tilemap copied to clipboard
Moving tiles to where another tile used to be fails to render properly
When moving tiles as in the move_tiles.rs example, if multiple tiles are moved simultaneously and one moves to the coordinates where another previously was, it's rendered as blank space.
Here's code to reproduce this issue, it's just a slightly modified move_tiles.rs example. Two tiles are moved around a 2x2 square. Both are visible at first, but after the first move only one is visible.
use bevy::prelude::*;
use bevy_ecs_tilemap::prelude::*;
mod helpers;
fn startup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2dBundle::default());
let texture_handle: Handle<Image> = asset_server.load("tiles.png");
let map_size = TilemapSize { x: 2, y: 2 };
// Create a tilemap entity a little early.
let tilemap_entity = commands.spawn_empty().id();
let mut tile_storage = TileStorage::empty(map_size);
// Spawn the elements of the tilemap.
let tile_pos = TilePos { x: 0, y: 0 };
let tile_entity = commands
.spawn(TileBundle {
position: tile_pos,
tilemap_id: TilemapId(tilemap_entity),
..Default::default()
})
.id();
tile_storage.set(&tile_pos, tile_entity);
let tile_pos = TilePos { x: 1, y: 0 };
let tile_entity = commands
.spawn(TileBundle {
position: tile_pos,
tilemap_id: TilemapId(tilemap_entity),
..Default::default()
})
.id();
tile_storage.set(&tile_pos, tile_entity);
let tile_size = TilemapTileSize { x: 16.0, y: 16.0 };
let grid_size = tile_size.into();
let map_type = TilemapType::default();
commands.entity(tilemap_entity).insert(TilemapBundle {
grid_size,
map_type,
size: map_size,
storage: tile_storage,
texture: TilemapTexture::Single(texture_handle),
tile_size,
transform: get_tilemap_center_transform(&map_size, &grid_size, &map_type, 0.0),
..Default::default()
});
}
fn swap_pos(keyboard_input: Res<ButtonInput<KeyCode>>, mut query: Query<&mut TilePos>) {
if keyboard_input.just_pressed(KeyCode::Space) {
for mut pos in query.iter_mut() {
if pos.x == 0 && pos.y == 0 {
pos.x = 1;
pos.y = 0;
} else if pos.x == 1 && pos.y == 0 {
pos.x = 1;
pos.y = 1;
} else if pos.x == 1 && pos.y == 1 {
pos.x = 0;
pos.y = 1;
} else {
pos.x = 0;
pos.y = 0;
}
}
}
}
fn main() {
App::new()
.add_plugins(
DefaultPlugins
.set(WindowPlugin {
primary_window: Some(Window {
title: String::from("Update tile positions without despawning."),
..Default::default()
}),
..default()
})
.set(ImagePlugin::default_nearest()),
)
.add_plugins(TilemapPlugin)
.add_systems(Startup, startup)
.add_systems(Update, helpers::camera::movement)
.add_systems(Update, swap_pos)
.run();
}
confirmed the behavior described here. haven't looked into cause yet though. Toggling visibility on the tile shows it is always in the expected location.
https://github.com/user-attachments/assets/8728d017-298a-4a4e-bdbd-413273eb60c8
I am running into the same issue, also with translating tile positions.
use bevy::dev_tools::fps_overlay::FpsOverlayPlugin;
use bevy::input::common_conditions::input_just_pressed;
use bevy::prelude::*;
use bevy::DefaultPlugins;
use bevy_ecs_tilemap::prelude::*;
use bevy_ecs_tilemap::TilemapPlugin;
#[derive(Component)]
struct Cell;
fn main() {
App::new()
.add_plugins((
FpsOverlayPlugin::default(),
DefaultPlugins.set(ImagePlugin::default_nearest()),
TilemapPlugin
))
.add_systems(Startup, startup)
.add_systems(Update, move_cells.run_if(input_just_pressed(KeyCode::Space)))
.run();
}
fn move_cells(mut cells: Query<&mut TilePos>) {
for mut pos in cells.iter_mut() {
pos.x += 1;
}
}
fn startup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2d);
let texture_handle: Handle<Image> = asset_server.load("tiles.png");
let map_size = TilemapSize { x: 32, y: 32 };
let tilemap_entity = commands.spawn_empty().id();
let mut tile_storage = TileStorage::empty(map_size);
for x in 0..2 {
for y in 0..2 {
let tile_pos = TilePos { x, y };
let tile_entity = commands
.spawn(TileBundle {
position: tile_pos,
tilemap_id: TilemapId(tilemap_entity),
..Default::default()
})
.id();
tile_storage.set(&tile_pos, tile_entity);
}
}
let tile_pos = TilePos { x: 10, y: 10 };
let tile_entity = commands
.spawn((TileBundle {
position: tile_pos,
tilemap_id: TilemapId(tilemap_entity),
..Default::default()
}, Cell))
.id();
tile_storage.set(&tile_pos, tile_entity);
let tile_size = TilemapTileSize { x: 16.0, y: 16.0 };
let grid_size = tile_size.into();
let map_type = TilemapType::default();
commands.entity(tilemap_entity).insert(TilemapBundle {
grid_size,
map_type,
size: map_size,
storage: tile_storage,
texture: TilemapTexture::Single(texture_handle.clone()),
tile_size,
transform: get_tilemap_center_transform(&map_size, &grid_size, &map_type, 0.0),
..Default::default()
});
}
I am not seeing an easy way to fix this. There seem to be several layers of code in bevy_ecs_tilemap that assume a single tile per position.
But by the end of the system there is still only one tile per cell. It seems like the tile map is being overly aggressive when it updates the storage of where a cell is or there is an issue in the rendering. As @ChristopherBiscardi showed in the video, the cells are in the correct place and exist, they just don't seem to be visible.
It looks like maybe it has to do something with the change detection in the extract. Removing the whole Or<...> block seems sort of fix it, though it seems to leave the initial one behind. 🤔 My domain of knowledge in this aspect is rather small though, so I'm not 100% why the change detection is not catching swapping of tile positions.
Alright I found the issue: the tile data was getting removed in the render's prepare. There was a check in the extracted tiles query that was removing the tile at the old_position when the new and old did not line up. When swapping tiles it would overlap and remove the old position when it was already updated in a previous iteration.