Загрузка данных
use std::collections::{HashMap, HashSet};
use bevy::{asset::RenderAssetUsages, prelude::*};
use bevy_rapier3d::prelude::{Collider, KinematicCharacterController, RigidBody};
use crate::core::{
app_state::AppState,
systems::camera::{
controller::{CameraFixed, CameraMode, CameraRig},
focus::CameraFocusable,
},
utils::attachment::Attached,
};
#[derive(Resource, Default)]
pub struct World {
chunks: HashMap<IVec2, Entity>,
ground_material: Option<Handle<StandardMaterial>>,
}
const CHUNK_SIZE: i32 = 16;
const TILE_GAP: f32 = 0.025;
#[derive(Component)]
pub(crate) struct TerrainChunk;
// =================
pub struct WorldTaskManager;
/*
1. Происходи инициализация загрузки мира
1.1 Поиск файлов мира и их загрузка
1.2 Прогрузка начальной области (территории, объектов и сущностей)
1.3 Прочая прогрузка (как игрок и т.д.)
2. Выгрузка мира
2.1 Сохранение мира в файл путём парсинга (сериализации данных в тот или иной вид)
*/
// TESTING
#[derive(Component)]
#[require(Name = auto_name())]
pub struct Player;
fn auto_name() -> Name {
Name::new("Player")
}
// /TESTING
impl WorldTaskManager {
/// Функция для загрузки данных мира
pub(crate) fn setup(
mut world: ResMut<World>,
mut _next_state: ResMut<NextState<AppState>>,
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
mut fix: ResMut<CameraFixed>,
mut camera_rig: ResMut<CameraRig>,
settings: Res<crate::core::utils::settings::Settings>,
_asset_server: Res<AssetServer>,
) {
world.ground_material = Some(materials.add(StandardMaterial {
base_color: Color::WHITE,
perceptual_roughness: 1.0,
..default()
}));
let player = commands
.spawn((
Mesh3d(meshes.add(Capsule3d::new(0.3, 0.8))),
MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))),
Transform::from_xyz(0.0, 0.7, 0.0),
RigidBody::KinematicPositionBased,
Collider::capsule_y(0.4, 0.3),
KinematicCharacterController {
up: Vec3::Y,
..default()
},
Player,
CameraFocusable,
))
.id();
fix.set_entity_to_fixed(&player);
camera_rig.mode = CameraMode::Fixed;
// camera_rig.view_direction = Vec3::Y;
camera_rig.zoom = 12.0;
commands.spawn((
Name::new("Light"),
PointLight {
radius: 64.,
..default()
},
Transform::from_xyz(3.0, 8.0, 5.0),
Attached::with_offset(player, Transform::from_xyz(0.0, 0.0, -2.0)),
));
Self::update_chunks_inner(
&mut commands,
&mut world,
&mut meshes,
IVec2::ZERO,
settings.rendering_distance.max(0),
);
}
/// Синхронизирует область загруженных чанков с позицией игрока.
pub(crate) fn update_chunks(
mut commands: Commands,
mut world: ResMut<World>,
mut meshes: ResMut<Assets<Mesh>>,
settings: Res<crate::core::utils::settings::Settings>,
player: Query<&Transform, With<Player>>,
) {
let Ok(player) = player.single() else {
return;
};
let tile_position = player.translation.xz().floor().as_ivec2();
let center = tile_position.div_euclid(IVec2::splat(CHUNK_SIZE));
let radius = settings.rendering_distance.max(0);
Self::update_chunks_inner(&mut commands, &mut world, &mut meshes, center, radius);
}
/// Создаёт недостающие чанки и удаляет вышедшие за радиус загрузки.
fn update_chunks_inner(
commands: &mut Commands,
world: &mut World,
meshes: &mut Assets<Mesh>,
center: IVec2,
radius: i32,
) {
let desired: HashSet<_> = (-radius..=radius)
.flat_map(|x| (-radius..=radius).map(move |z| center + IVec2::new(x, z)))
.collect();
for position in world.chunks.keys().copied().collect::<Vec<_>>() {
if !desired.contains(&position) {
if let Some(entity) = world.chunks.remove(&position) {
commands.entity(entity).despawn();
}
}
}
let Some(material) = world.ground_material.clone() else {
return;
};
for position in desired {
if world.chunks.contains_key(&position) {
continue;
}
let mesh = meshes.add(Self::build_chunk_mesh(position));
let entity = commands
.spawn((
Name::new(format!("Terrain [{}, {}]", position.x, position.y)),
TerrainChunk,
Mesh3d(mesh),
MeshMaterial3d(material.clone()),
))
.id();
commands.entity(entity).with_children(|parent| {
parent.spawn((
Name::new("Terrain Collider"),
RigidBody::Fixed,
Collider::cuboid(8.0, 0.1, 8.0),
Transform::from_xyz(
(position.x * CHUNK_SIZE) as f32 + 8.0,
-0.1,
(position.y * CHUNK_SIZE) as f32 + 8.0,
),
));
});
world.chunks.insert(position, entity);
}
}
pub(crate) fn teardown(
mut commands: Commands,
entities: Query<Entity, Or<(With<Player>, With<TerrainChunk>, With<Attached>)>>,
mut world: ResMut<World>,
) {
for entity in &entities {
commands.entity(entity).despawn();
}
*world = World::default();
}
/// Собирает все клетки чанка в один mesh, не создавая сущность на каждую клетку.
fn build_chunk_mesh(chunk: IVec2) -> Mesh {
let mut positions = Vec::with_capacity((CHUNK_SIZE * CHUNK_SIZE * 4) as usize);
let mut normals = Vec::with_capacity(positions.capacity());
let mut colors = Vec::with_capacity(positions.capacity());
let mut indices = Vec::with_capacity((CHUNK_SIZE * CHUNK_SIZE * 6) as usize);
for z in 0..CHUNK_SIZE {
for x in 0..CHUNK_SIZE {
let base = positions.len() as u32;
let x0 = (chunk.x * CHUNK_SIZE + x) as f32 + TILE_GAP;
let z0 = (chunk.y * CHUNK_SIZE + z) as f32 + TILE_GAP;
let x1 = x0 + 1.0 - TILE_GAP * 2.0;
let z1 = z0 + 1.0 - TILE_GAP * 2.0;
positions.extend_from_slice(&[
[x0, 0.0, z0],
[x1, 0.0, z0],
[x1, 0.0, z1],
[x0, 0.0, z1],
]);
normals.extend_from_slice(&[[0.0, 1.0, 0.0]; 4]);
let shade = if (x + z + chunk.x + chunk.y).rem_euclid(2) == 0 {
0.30
} else {
0.36
};
colors.extend_from_slice(&[[shade, 0.40, 0.27, 1.0]; 4]);
indices.extend_from_slice(&[base, base + 2, base + 1, base, base + 3, base + 2]);
}
}
Mesh::new(
bevy::mesh::PrimitiveTopology::TriangleList,
RenderAssetUsages::default(),
)
.with_inserted_indices(bevy::mesh::Indices::U32(indices))
.with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions)
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals)
.with_inserted_attribute(Mesh::ATTRIBUTE_COLOR, colors)
}
}