Compare commits

...

22 Commits

Author SHA1 Message Date
hheik 4f634306da Merge branch 'feat/gas-dispersion' 2025-08-17 10:54:46 +03:00
hheik 8ff232f21b feat: maximum density after disperse (experimental) 2023-01-04 22:33:04 +02:00
hheik 183475c60f wip: tweaking disperse system (random disperse, stabilisation) 2022-12-31 22:47:38 +02:00
hheik 76deff3f2f feat: first iteration of gas dispersion 2022-12-31 20:14:25 +02:00
hheik c3ea9f4513 updated texel and chunk structures to allow easier simulation 2022-12-27 18:14:49 +02:00
hheik c42859ded8 formatting 2022-12-27 16:24:05 +02:00
hheik c9d7e17a60 feat: simulation boundaries 2022-12-27 16:07:29 +02:00
hheik 6c1b68d1fd feat: property-based simulation 2022-12-26 22:49:19 +02:00
hheik 67512ff926 feat: gas simulation 2022-12-26 00:29:55 +02:00
hheik 4a4d40e0fa feat: liquids stabilise a bit better 2022-12-25 18:58:38 +02:00
hheik ab07b0c06a disabled collision visualizer and inspector plugins 2022-12-25 18:38:10 +02:00
hheik 4fea9d4220 feat: basic liquid simulation 2022-12-25 18:31:52 +02:00
hheik 5f2b2b9d06 feat: added TerrainStages and moved terrain debug systems to DebugPlugin 2022-12-22 19:41:51 +02:00
hheik 7edca9d472 wip: formatting and some debugging stuff 2022-12-19 16:00:40 +02:00
hheik bed39b9a5f feat: fixed window starting size 2022-12-18 17:17:09 +02:00
hheik 3460df2e18 feat: separate debug plugin for debugging systems 2022-12-18 11:04:58 +02:00
hheik 50544b06bf feat: First iteration of character controller overhaul 2022-12-18 06:17:17 +02:00
hheik 50367637b1 wip: fixing physics 2022-12-16 16:42:08 +02:00
hheik 352fca7093 wip: wild physics 2022-12-16 05:25:09 +02:00
hheik 97c8808281 fix: rapier2d version 2022-12-15 21:55:06 +02:00
hheik b969971ea0 feat: configured ccd and sleep for player rigidbody 2022-12-14 01:52:34 +02:00
hheik 56ffabdf26 formatting 2022-12-13 02:46:24 +02:00
16 changed files with 1292 additions and 428 deletions

5
Cargo.lock generated
View File

@ -644,7 +644,9 @@ checksum = "c36f4d3af0cda50c07e2010d0351ab79594681116edd280592ca394db73ef32b"
[[package]] [[package]]
name = "bevy_rapier2d" name = "bevy_rapier2d"
version = "0.18.0" version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fe53a4fefd2d8bade79f2e5260bd206643bfcc951a3c5f86b3b7a0273eb19e0"
dependencies = [ dependencies = [
"bevy", "bevy",
"bitflags", "bitflags",
@ -2082,6 +2084,7 @@ dependencies = [
"bevy-inspector-egui", "bevy-inspector-egui",
"bevy_prototype_debug_lines", "bevy_prototype_debug_lines",
"bevy_rapier2d", "bevy_rapier2d",
"fastrand",
"lazy_static", "lazy_static",
"noise", "noise",
] ]

View File

@ -9,7 +9,8 @@ edition = "2021"
bevy = { version = "0.9.0", features = ["dynamic"] } bevy = { version = "0.9.0", features = ["dynamic"] }
bevy-inspector-egui = "0.14.0" bevy-inspector-egui = "0.14.0"
bevy_prototype_debug_lines = "0.9.0" bevy_prototype_debug_lines = "0.9.0"
bevy_rapier2d = { path = "../bevy_rapier/bevy_rapier2d" } bevy_rapier2d = "0.19.0"
fastrand = "1.8.0"
lazy_static = "1.4.0" lazy_static = "1.4.0"
noise = "0.8.2" noise = "0.8.2"

View File

@ -1,53 +1,46 @@
use bevy::{input::mouse::MouseWheel, prelude::*}; use bevy::prelude::*;
use bevy_inspector_egui::*;
use bevy_prototype_debug_lines::DebugLinesPlugin;
use bevy_rapier2d::prelude::*; use bevy_rapier2d::prelude::*;
use crate::{ use crate::{
terrain2d::{Chunk2D, Terrain2D, Terrain2DPlugin, TerrainGen2D}, terrain2d::*,
util::Vector2I, util::{frame_counter::FrameCounterPlugin, Vector2I},
}; };
use self::{ use self::{
camera::{CameraFollow, GameCameraPlugin, WORLD_WIDTH}, camera::GameCameraPlugin, debug::DebugPlugin, kinematic::KinematicPlugin, player::PlayerPlugin,
kinematic::KinematicPlugin,
player::PlayerPlugin,
}; };
pub mod camera; pub mod camera;
pub mod debug;
pub mod kinematic; pub mod kinematic;
pub mod player; pub mod player;
pub fn init() { pub fn init() {
App::new() App::new()
.add_plugins(DefaultPlugins) .add_plugins(DefaultPlugins)
.add_plugin(DebugLinesPlugin::default()) .add_plugin(FrameCounterPlugin)
.add_plugin(RapierPhysicsPlugin::<NoUserData>::default()) .add_plugin(RapierPhysicsPlugin::<NoUserData>::default())
.add_plugin(RapierDebugRenderPlugin::default()) .add_plugin(Terrain2DPlugin)
.add_plugin(WorldInspectorPlugin::new()) .add_plugin(DebugPlugin)
.add_plugin(KinematicPlugin) .add_plugin(KinematicPlugin)
.add_plugin(GameCameraPlugin) .add_plugin(GameCameraPlugin)
.add_plugin(Terrain2DPlugin)
.add_plugin(PlayerPlugin) .add_plugin(PlayerPlugin)
.add_startup_system(setup_debug_terrain) .add_startup_system(setup_terrain)
.add_startup_system(setup_window)
.run(); .run();
} }
fn debug_controls( fn setup_window(mut windows: ResMut<Windows>) {
mut query: Query<&mut Transform, With<CameraFollow>>, if let Some(window) = windows.get_primary_mut() {
mut events: EventReader<MouseWheel>, window.set_resolution(1280.0, 720.0);
) { window.set_title("Kuilu".to_string());
for event in events.iter() {
for mut transform in query.iter_mut() {
transform.translation += Vec3::new(0.0, event.y, 0.0) * 30.0;
}
} }
} }
fn setup_debug_terrain(mut commands: Commands, mut terrain: ResMut<Terrain2D>) { fn setup_terrain(mut commands: Commands, mut terrain: ResMut<Terrain2D>) {
let terrain_gen = TerrainGen2D::new(432678); let terrain_gen = TerrainGen2D::new(432678);
for y in 0..(WORLD_WIDTH / Chunk2D::SIZE_Y as i32) { for y in 0..(Terrain2D::WORLD_HEIGHT / Chunk2D::SIZE_Y as i32) {
for x in 0..(WORLD_WIDTH / Chunk2D::SIZE_X as i32) { for x in 0..(Terrain2D::WORLD_WIDTH / Chunk2D::SIZE_X as i32) {
let position = Vector2I { x, y }; let position = Vector2I { x, y };
terrain.add_chunk(position, terrain_gen.gen_chunk(&position)); terrain.add_chunk(position, terrain_gen.gen_chunk(&position));
} }
@ -64,6 +57,6 @@ fn setup_debug_terrain(mut commands: Commands, mut terrain: ResMut<Terrain2D>) {
.spawn(Name::new("Right wall")) .spawn(Name::new("Right wall"))
.insert(Collider::halfspace(Vec2::NEG_X).unwrap()) .insert(Collider::halfspace(Vec2::NEG_X).unwrap())
.insert(TransformBundle::from_transform( .insert(TransformBundle::from_transform(
Transform::from_translation(Vec3::new(WORLD_WIDTH as f32, 0.0, 0.0)), Transform::from_translation(Vec3::new(Terrain2D::WORLD_WIDTH as f32, 0.0, 0.0)),
)); ));
} }

View File

@ -4,9 +4,10 @@ use bevy::{
}; };
use bevy_inspector_egui::{Inspectable, RegisterInspectable}; use bevy_inspector_egui::{Inspectable, RegisterInspectable};
use crate::util::{move_towards_vec3, vec3_lerp}; use crate::{
terrain2d::Terrain2D,
pub const WORLD_WIDTH: i32 = 512; util::{move_towards_vec3, vec3_lerp},
};
pub struct GameCameraPlugin; pub struct GameCameraPlugin;
@ -48,7 +49,7 @@ fn camera_setup(mut commands: Commands) {
Name::new("Camera"), Name::new("Camera"),
Camera2dBundle { Camera2dBundle {
projection: OrthographicProjection { projection: OrthographicProjection {
scaling_mode: ScalingMode::FixedHorizontal(WORLD_WIDTH as f32), scaling_mode: ScalingMode::FixedHorizontal(Terrain2D::WORLD_WIDTH as f32),
window_origin: WindowOrigin::Center, window_origin: WindowOrigin::Center,
scale: 1.0 / 2.0, scale: 1.0 / 2.0,
..default() ..default()
@ -77,10 +78,9 @@ fn camera_system(
None => return, None => return,
}; };
// let offset = Vec3::new(WORLD_WIDTH as f32 / 2.0, 0.0, 999.9);
for (mut camera_transform, projection) in camera_query.iter_mut() { for (mut camera_transform, projection) in camera_query.iter_mut() {
let left_limit = 0.0; let left_limit = 0.0;
let right_limit = WORLD_WIDTH as f32; let right_limit = Terrain2D::WORLD_WIDTH as f32;
let offset = Vec3::new(0.0, 0.0, 999.9); let offset = Vec3::new(0.0, 0.0, 999.9);
match follow.movement { match follow.movement {
FollowMovement::Instant => { FollowMovement::Instant => {
@ -101,6 +101,7 @@ fn camera_system(
); );
} }
} }
// horizontal boundaries
let camera_x = camera_transform.translation.x; let camera_x = camera_transform.translation.x;
camera_transform.translation += Vec3::new( camera_transform.translation += Vec3::new(
(left_limit - (projection.left * projection.scale + camera_x)).max(0.0), (left_limit - (projection.left * projection.scale + camera_x)).max(0.0),

17
src/game/debug.rs Normal file
View File

@ -0,0 +1,17 @@
use bevy::prelude::*;
use bevy_prototype_debug_lines::DebugLinesPlugin;
pub mod terrain;
use terrain::TerrainDebugPlugin;
pub struct DebugPlugin;
impl Plugin for DebugPlugin {
fn build(&self, app: &mut App) {
app.add_plugin(DebugLinesPlugin::default())
// .add_plugin(bevy_rapier2d::prelude::RapierDebugRenderPlugin::default())
// .add_plugin(bevy_inspector_egui::WorldInspectorPlugin::new())
.add_plugin(TerrainDebugPlugin);
}
}

220
src/game/debug/terrain.rs Normal file
View File

@ -0,0 +1,220 @@
use std::collections::HashMap;
use crate::{game::camera::GameCamera, terrain2d::*, util::Vector2I};
use bevy::{input::mouse::MouseWheel, prelude::*, render::camera::RenderTarget};
use bevy_prototype_debug_lines::DebugLines;
pub struct TerrainDebugPlugin;
impl Plugin for TerrainDebugPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(TerrainBrush2D::default())
// .add_system_to_stage(TerrainStages::EventHandler, dirty_rect_visualizer)
// .add_system_to_stage(CoreStage::Last, chunk_debugger)
.add_system(debug_painter);
}
}
#[derive(Resource)]
struct TerrainBrush2D {
pub radius: i32,
pub tile: TexelID,
}
impl Default for TerrainBrush2D {
fn default() -> Self {
TerrainBrush2D {
radius: 40,
tile: 8,
}
}
}
// REM: Dirty and hopefully temporary
fn debug_painter(
mut terrain: ResMut<Terrain2D>,
mut debug_draw: ResMut<DebugLines>,
mut brush: ResMut<TerrainBrush2D>,
windows: Res<Windows>,
mouse_input: Res<Input<MouseButton>>,
key_input: Res<Input<KeyCode>>,
mut mouse_wheel: EventReader<MouseWheel>,
camera_query: Query<(&Camera, &GlobalTransform), With<GameCamera>>,
) {
// let allow_painting = key_input.pressed(KeyCode::LControl);
let allow_painting = true;
// Change brush
for event in mouse_wheel.iter() {
if allow_painting {
brush.radius = (brush.radius + event.y.round() as i32).clamp(1, 128);
}
}
if !allow_painting {
return;
}
// https://bevy-cheatbook.github.io/cookbook/cursor2world.html#2d-games
// get the camera info and transform
// assuming there is exactly one main camera entity, so query::single() is OK
let (camera, camera_transform) = camera_query.single();
// get the window that the camera is displaying to (or the primary window)
let window = if let RenderTarget::Window(id) = camera.target {
windows.get(id).unwrap()
} else {
windows.get_primary().unwrap()
};
// check if the cursor is inside the window and get its position
let world_pos = if let Some(screen_pos) = window.cursor_position() {
// get the size of the window
let window_size = Vec2::new(window.width() as f32, window.height() as f32);
// convert screen position [0..resolution] to ndc [-1..1] (gpu coordinates)
let ndc = (screen_pos / window_size) * 2.0 - Vec2::ONE;
// matrix for undoing the projection and camera transform
let ndc_to_world = camera_transform.compute_matrix() * camera.projection_matrix().inverse();
// use it to convert ndc to world-space coordinates
let world_pos = ndc_to_world.project_point3(ndc.extend(-1.0));
// reduce it to a 2D value
world_pos.truncate()
} else {
return;
};
for (index, key) in vec![
KeyCode::Key1,
KeyCode::Key2,
KeyCode::Key3,
KeyCode::Key4,
KeyCode::Key5,
KeyCode::Key6,
KeyCode::Key7,
KeyCode::Key8,
KeyCode::Key9,
]
.iter()
.enumerate()
{
if key_input.just_pressed(*key) {
brush.tile = index as u8 + 1;
}
}
let origin = Vector2I::from(world_pos);
let radius = brush.radius;
let id = match (
mouse_input.pressed(MouseButton::Left),
mouse_input.pressed(MouseButton::Right),
) {
(true, false) => brush.tile,
(_, _) => 0,
};
let color = TexelBehaviour2D::from_id(&brush.tile)
.map_or(Color::rgba(0.0, 0.0, 0.0, 0.0), |tb| tb.color);
for y in origin.y - (radius - 1)..origin.y + radius {
for x in origin.x - (radius - 1)..origin.x + radius {
let dx = (x - origin.x).abs();
let dy = (y - origin.y).abs();
let draw = dx * dx + dy * dy <= (radius - 1) * (radius - 1);
if draw {
let pos: Vector2I = Vector2I { x, y };
debug_draw.line_colored(
Vec3::from(pos) + Vec3::new(0.45, 0.45, 1.0),
Vec3::from(pos) + Vec3::new(0.55, 0.55, 1.0),
0.0,
color,
);
if mouse_input.pressed(MouseButton::Left) || mouse_input.pressed(MouseButton::Right)
{
terrain.set_texel(&pos, Texel2D { id, ..default() }, None)
}
}
}
}
}
/**
Visualize dirty rects
*/
fn dirty_rect_visualizer(terrain: Res<Terrain2D>, mut debug_draw: ResMut<DebugLines>) {
for (chunk_index, chunk) in terrain.chunk_iter() {
let rect = if let Some(rect) = chunk.dirty_rect {
rect
} else {
continue;
};
let offset = Vec3::from(chunk_index_to_global(chunk_index));
let min = offset + Vec3::from(rect.min);
let max = offset + Vec3::from(rect.max + Vector2I::ONE);
draw_box(&mut debug_draw, min, max, Color::RED, 0.0);
}
}
fn chunk_debugger(terrain: Res<Terrain2D>, mut debug_draw: ResMut<DebugLines>) {
for (chunk_index, chunk) in terrain.chunk_iter() {
println!("chunk contents: {chunk_index:?}");
let offset = Vec3::from(chunk_index_to_global(chunk_index));
let min = offset + Vec3::ZERO;
let max = offset + Vec3::from(Chunk2D::SIZE);
draw_box(
&mut debug_draw,
min,
max,
Color::rgba(0.5, 0.0, 0.5, 0.5),
0.0,
);
let mut tile_counter: HashMap<TexelID, (u32, u32)> = HashMap::new();
for y in 0..Chunk2D::SIZE_Y as i32 {
for x in 0..Chunk2D::SIZE_X as i32 {
let local = Vector2I::new(x, y);
let global = local_to_global(&local, chunk_index);
if let (Some(texel), _) = terrain.get_texel_behaviour(&global) {
if !tile_counter.contains_key(&texel.id) {
tile_counter.insert(texel.id, (0, 0));
}
let (old_count, old_density) = tile_counter[&texel.id].clone();
tile_counter.insert(
texel.id,
(old_count + 1, old_density + texel.density as u32),
);
}
}
}
let mut counts: Vec<(u8, String, u32, u32)> = vec![];
for (id, (count, total_density)) in tile_counter.iter() {
let name =
TexelBehaviour2D::from_id(id).map_or("unknown".to_string(), |b| b.name.to_string());
counts.push((*id, name, *count, *total_density));
}
counts.sort_unstable_by_key(|c| c.0);
for (id, name, count, total_density) in counts {
println!(
"\tmaterial: {name:<24}id: {id:<8}count: {count:<8}total_density: {total_density:<8}"
);
}
}
}
pub fn draw_box(debug_draw: &mut DebugLines, min: Vec3, max: Vec3, color: Color, duration: f32) {
let points = vec![
Vec3::new(min.x, min.y, min.z),
Vec3::new(max.x, min.y, min.z),
Vec3::new(max.x, max.y, min.z),
Vec3::new(min.x, max.y, min.z),
];
for i in 0..points.len() {
debug_draw.line_colored(points[i], points[(i + 1) % points.len()], duration, color);
}
}

View File

@ -18,31 +18,23 @@ impl Plugin for KinematicPlugin {
#[derive(Bundle)] #[derive(Bundle)]
pub struct KinematicBundle { pub struct KinematicBundle {
pub kinematic: KinematicState, pub state: KinematicState,
pub properties: KinematicProperties,
pub rigidbody: RigidBody, pub rigidbody: RigidBody,
pub velocity: Velocity,
pub gravity_scale: GravityScale,
pub collider: Collider,
pub locked_axes: LockedAxes,
pub events: ActiveEvents, pub events: ActiveEvents,
pub collisions: ActiveCollisionTypes, pub collisions: ActiveCollisionTypes,
pub properties: KinematicProperties,
pub transform: TransformBundle, pub transform: TransformBundle,
} }
impl Default for KinematicBundle { impl Default for KinematicBundle {
fn default() -> Self { fn default() -> Self {
KinematicBundle { KinematicBundle {
kinematic: KinematicState::default(), state: KinematicState::default(),
rigidbody: RigidBody::Dynamic, properties: KinematicProperties::default(),
gravity_scale: GravityScale(3.0), rigidbody: RigidBody::KinematicPositionBased,
locked_axes: LockedAxes::ROTATION_LOCKED,
events: ActiveEvents::COLLISION_EVENTS, events: ActiveEvents::COLLISION_EVENTS,
collisions: ActiveCollisionTypes::all(), collisions: ActiveCollisionTypes::all(),
collider: Collider::default(),
properties: KinematicProperties::default(),
transform: TransformBundle::default(), transform: TransformBundle::default(),
velocity: Velocity::default(),
} }
} }
} }
@ -50,27 +42,14 @@ impl Default for KinematicBundle {
#[derive(Component, Reflect, Default)] #[derive(Component, Reflect, Default)]
#[reflect(Component)] #[reflect(Component)]
pub struct KinematicState { pub struct KinematicState {
on_ground: bool, #[reflect(ignore)]
did_jump: bool, pub last_move: Option<MoveShapeOutput>,
air_jump_counter: u8, pub did_jump: bool,
} }
impl KinematicState { impl KinematicState {
#[inline]
pub fn on_ground(&self) -> bool {
self.on_ground
}
#[inline]
pub fn did_jump(&self) -> bool {
self.did_jump
}
#[inline]
pub fn air_jump_counter(&self) -> u8 {
self.air_jump_counter
}
pub fn can_jump(&self) -> bool { pub fn can_jump(&self) -> bool {
self.on_ground && !self.did_jump self.last_move.as_ref().map_or(false, |last| last.grounded) && !self.did_jump
} }
} }
@ -84,20 +63,20 @@ pub struct KinematicProperties {
pub air_acceleration: f32, pub air_acceleration: f32,
pub air_friction: f32, pub air_friction: f32,
pub jump_height: f32, pub jump_height: f32,
pub air_jumps: u8, pub gravity: Option<f32>,
} }
impl Default for KinematicProperties { impl Default for KinematicProperties {
fn default() -> Self { fn default() -> Self {
Self { Self {
ground_speed: 100.0, ground_speed: 75.0,
ground_acceleration: 20.0, ground_acceleration: 20.0,
ground_friction: 30.0, ground_friction: 30.0,
air_speed: 100.0, air_speed: 75.0,
air_acceleration: 10.0, air_acceleration: 10.0,
air_friction: 10.0, air_friction: 10.0,
jump_height: 150.0, jump_height: 100.0,
air_jumps: 1, gravity: Some(1.0),
} }
} }
} }
@ -110,40 +89,38 @@ pub struct KinematicInput {
} }
fn kinematic_movement( fn kinematic_movement(
time: Res<Time>,
mut query: Query<( mut query: Query<(
Entity, Entity,
&mut Velocity,
&mut KinematicState, &mut KinematicState,
&mut Transform,
&KinematicProperties, &KinematicProperties,
&GlobalTransform, &GlobalTransform,
Option<&KinematicInput>, Option<&KinematicInput>,
Option<&GravityScale>, Option<&CollisionGroups>,
Option<&Collider>,
)>, )>,
rapier_context: Res<RapierContext>, shape_query: Query<&Collider, Without<Sensor>>,
child_query: Query<&Children>,
mut rapier_context: ResMut<RapierContext>,
) { ) {
let dt = rapier_context.integration_parameters.dt;
for ( for (
entity, entity,
mut velocity,
mut kinematic_state, mut kinematic_state,
mut transform,
props, props,
global_transform, global_transform,
input, input,
gravity, collision_groups,
collider,
) in query.iter_mut() ) in query.iter_mut()
{ {
let default = &KinematicInput::default(); let default = &KinematicInput::default();
let input = input.unwrap_or(default); let input = input.unwrap_or(default);
let has_gravity = if let Some(gravity) = gravity { let (speed, acceleration, friction) = if kinematic_state
gravity.0.abs() > f32::EPSILON .last_move
} else { .as_ref()
false .map_or(false, |last| last.grounded)
}; {
let (speed, acceleration, friction) = if kinematic_state.on_ground {
( (
props.ground_speed, props.ground_speed,
props.ground_acceleration, props.ground_acceleration,
@ -154,10 +131,21 @@ fn kinematic_movement(
}; };
const GRAVITY_DIR: Vec2 = Vec2::NEG_Y; const GRAVITY_DIR: Vec2 = Vec2::NEG_Y;
const GRAVITY_COEFFICIENT: f32 = 2.0;
let current_velocity = velocity.linvel; let current_velocity = kinematic_state
let target_velocity = .last_move
input.movement * speed + current_velocity.project_onto_normalized(GRAVITY_DIR); .as_ref()
.map_or(Vec2::ZERO, |last| {
if last.grounded {
last.effective_translation
.reject_from_normalized(GRAVITY_DIR)
} else {
last.effective_translation
}
})
/ dt;
let target_velocity = input.movement * speed;
let angle_lerp = if current_velocity.length_squared() > 0.01 { let angle_lerp = if current_velocity.length_squared() > 0.01 {
let result = inverse_lerp( let result = inverse_lerp(
@ -178,42 +166,104 @@ fn kinematic_movement(
let delta_interpolation = angle_lerp.clamp(0.0, 1.0); let delta_interpolation = angle_lerp.clamp(0.0, 1.0);
let velocity_change_speed = lerp(acceleration, friction, delta_interpolation) * speed; let velocity_change_speed = lerp(acceleration, friction, delta_interpolation) * speed;
velocity.linvel = move_towards_vec2( let mut velocity = if let Some(gravity) = props.gravity {
current_velocity, // Also apply gravity
target_velocity, move_towards_vec2(
velocity_change_speed * time.delta_seconds(), current_velocity,
); target_velocity.reject_from_normalized(GRAVITY_DIR)
+ current_velocity.project_onto_normalized(GRAVITY_DIR),
velocity_change_speed * dt,
) + GRAVITY_DIR * GRAVITY_COEFFICIENT * gravity
} else {
move_towards_vec2(
current_velocity,
target_velocity,
velocity_change_speed * dt,
)
};
if input.want_jump && kinematic_state.can_jump() { if input.want_jump && kinematic_state.can_jump() {
velocity.linvel = Vec2 { velocity = Vec2 {
y: props.jump_height, y: props.jump_height,
..velocity.linvel ..velocity
}; };
kinematic_state.did_jump = true; kinematic_state.did_jump = true;
} }
if has_gravity { let shape = if let Ok(shape) = shape_query.get(entity) {
// Reset any possible jump snapping and stuff after the peak of jump Some(shape)
if velocity.linvel.y <= 0.0 { } else if let Ok(children) = child_query.get(entity) {
kinematic_state.did_jump = false; children
.iter()
.find_map(|child| shape_query.get(*child).ok())
} else {
None
};
// move
kinematic_state.last_move = if let Some(shape) = shape {
let (_scale, rotation, translation) = global_transform.to_scale_rotation_translation();
let move_options = &MoveShapeOptions {
up: Vec2::Y,
autostep: Some(CharacterAutostep {
min_width: CharacterLength::Absolute(0.5),
max_height: CharacterLength::Absolute(2.1),
include_dynamic_bodies: false,
}),
slide: true,
max_slope_climb_angle: (50.0_f32).to_radians(),
min_slope_slide_angle: (50.0_f32).to_radians(),
snap_to_ground: Some(CharacterLength::Absolute(5.0)),
// snap_to_ground: props.gravity.map_or(None, |_| {
// if velocity.y <= 0.0 {
// Some(CharacterLength::Absolute(5.0))
// } else {
// None
// }
// }),
offset: CharacterLength::Absolute(0.01),
..MoveShapeOptions::default()
};
let mut filter = QueryFilter::new();
let predicate = |coll_entity| coll_entity != entity;
filter.predicate = Some(&predicate);
if let Some(collision_groups) = collision_groups {
filter.groups(InteractionGroups::new(
bevy_rapier2d::rapier::geometry::Group::from_bits_truncate(
collision_groups.memberships.bits(),
),
bevy_rapier2d::rapier::geometry::Group::from_bits_truncate(
collision_groups.filters.bits(),
),
));
} }
if let Some(collider) = collider {
let (_, rot, pos) = global_transform.to_scale_rotation_translation(); let last_move: MoveShapeOutput = rapier_context.move_shape(
let angle = rot.to_euler(EulerRot::YXZ).2; velocity * dt,
let mut shape = collider.clone(); shape,
shape.set_scale(Vec2::ONE * 0.9, 1); translation.truncate(),
if let Some((_coll_entity, _hit)) = rapier_context.cast_shape( rotation.to_euler(EulerRot::ZYX).0,
Vec2::new(pos.x, pos.y), shape.raw.0.mass_properties(1.0).mass(),
angle, move_options,
Vec2::NEG_Y, filter,
&shape, |_coll: CharacterCollision| (),
2.0, );
QueryFilter::new().exclude_collider(entity),
) { // Apply movement
kinematic_state.on_ground = true; transform.translation += last_move.effective_translation.extend(0.0);
} else {
kinematic_state.on_ground = false; Some(last_move)
} } else {
None
};
if props.gravity.is_some() {
// Reset any possible jump snapping and stuff after the peak of jump
if velocity.y <= 0.0 {
kinematic_state.did_jump = false;
} }
} }
} }

View File

@ -38,8 +38,9 @@ pub fn player_system(
let movement = Vec2 { let movement = Vec2 {
x: input_to_axis(input.pressed(KeyCode::A), input.pressed(KeyCode::D)), x: input_to_axis(input.pressed(KeyCode::A), input.pressed(KeyCode::D)),
// y: input_to_axis(input.pressed(KeyCode::S), input.pressed(KeyCode::W)), // x: -1.0,
y: 0.0, y: input_to_axis(input.pressed(KeyCode::S), input.pressed(KeyCode::W)),
// y: 0.0,
}; };
kinematic_input.movement = movement; kinematic_input.movement = movement;
@ -59,10 +60,13 @@ fn input_to_axis(negative: bool, positive: bool) -> f32 {
pub fn player_spawn(mut commands: Commands) { pub fn player_spawn(mut commands: Commands) {
let kinematic = KinematicBundle { let kinematic = KinematicBundle {
collider: Collider::round_cuboid(4.0, 8.0, 1.0),
transform: TransformBundle::from_transform(Transform::from_translation(Vec3::new( transform: TransformBundle::from_transform(Transform::from_translation(Vec3::new(
256.0, 128.0, 0.0, 256.0, 128.0, 0.0,
))), ))),
properties: KinematicProperties {
gravity: None,
..default()
},
..default() ..default()
}; };
@ -72,18 +76,21 @@ pub fn player_spawn(mut commands: Commands) {
.insert(SpriteBundle { .insert(SpriteBundle {
sprite: Sprite { sprite: Sprite {
color: Color::rgb(0.75, 0.25, 0.25), color: Color::rgb(0.75, 0.25, 0.25),
custom_size: Some(Vec2 { x: 8.0, y: 16.0 }), custom_size: Some(Vec2 { x: 6.0, y: 12.0 }),
..default() ..default()
}, },
..default() ..default()
}) })
.insert(Collider::cuboid(3.0, 6.0))
.insert(PlayerBundle { .insert(PlayerBundle {
kinematic, kinematic,
..default() ..default()
}) })
.insert(KinematicInput::default()) .insert(KinematicInput::default())
.insert(Ccd::enabled())
.insert(Sleeping::disabled())
.insert(CameraFollow { .insert(CameraFollow {
priority: 1, priority: 1,
movement: FollowMovement::Smooth(18.0), movement: FollowMovement::Instant,
}); });
} }

View File

@ -3,196 +3,349 @@ use std::collections::{
HashMap, HashMap,
}; };
use bevy::{input::mouse::MouseWheel, prelude::*, render::camera::RenderTarget}; use bevy::ecs::prelude::SystemStage;
use bevy_prototype_debug_lines::DebugLines; use bevy::prelude::*;
use bevy_rapier2d::prelude::*; use bevy_rapier2d::prelude::*;
mod chunk2d; mod chunk2d;
mod terrain_gen2d; mod terrain_gen2d;
mod texel2d; mod texel2d;
mod texel_behaviour2d;
pub use chunk2d::*; pub use chunk2d::*;
pub use terrain_gen2d::*; pub use terrain_gen2d::*;
pub use texel2d::*; pub use texel2d::*;
pub use texel_behaviour2d::*;
use crate::{ use crate::util::{frame_counter::FrameCounter, math::*, Vector2I};
game::camera::GameCamera,
util::{math::*, Vector2I},
};
pub struct Terrain2DPlugin; pub struct Terrain2DPlugin;
impl Plugin for Terrain2DPlugin { impl Plugin for Terrain2DPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
// Add terrain stages
app.add_stage_before(
CoreStage::Update,
TerrainStages::Simulation,
SystemStage::parallel(),
);
// After update, but before rapier
app.add_stage_before(
PhysicsStages::SyncBackend,
TerrainStages::EventHandler,
SystemStage::parallel(),
)
.add_stage_after(
TerrainStages::EventHandler,
TerrainStages::ChunkSync,
SystemStage::parallel(),
);
app.register_type::<TerrainChunk2D>() app.register_type::<TerrainChunk2D>()
.insert_resource(Terrain2D::new()) .insert_resource(Terrain2D::new(
.insert_resource(TerrainBrush2D::default()) Some(Terrain2D::WORLD_HEIGHT),
Some(0),
Some(0),
Some(Terrain2D::WORLD_WIDTH),
))
.add_event::<TerrainEvent2D>() .add_event::<TerrainEvent2D>()
.add_system(debug_painter) .add_system_to_stage(TerrainStages::Simulation, terrain_simulation)
.add_system_to_stage(TerrainStages::EventHandler, emit_terrain_events)
.add_system_to_stage( .add_system_to_stage(
CoreStage::PostUpdate, TerrainStages::EventHandler,
dirty_rect_visualizer.before(emit_terrain_events), // TODO: Figure out why .after() creates a lagspike for the first frame
)
.add_system_to_stage(
CoreStage::PostUpdate,
chunk_spawner.before(emit_terrain_events), chunk_spawner.before(emit_terrain_events),
) )
.add_system_to_stage( .add_system_to_stage(TerrainStages::ChunkSync, chunk_sprite_sync)
CoreStage::PostUpdate, .add_system_to_stage(CoreStage::PostUpdate, chunk_collision_sync);
chunk_sprite_sync.after(chunk_spawner),
)
.add_system_to_stage(
CoreStage::PostUpdate,
chunk_collision_sync.after(chunk_spawner),
)
.add_system_to_stage(CoreStage::PostUpdate, emit_terrain_events);
} }
} }
#[derive(Resource)] #[derive(StageLabel)]
struct TerrainBrush2D { pub enum TerrainStages {
pub radius: i32, /// Terrain simulation stage. Should run before update.
pub tile: TexelID, Simulation,
/// The stage that Handles collected events and creates new chunk entities as needed. Should run after update.
EventHandler,
/// Chunk sync systems (e.g. collsion and sprite) run in this stage.
ChunkSync,
} }
impl Default for TerrainBrush2D { fn terrain_simulation(
fn default() -> Self {
TerrainBrush2D { radius: 7, tile: 3 }
}
}
// REM: Dirty and hopefully temporary
fn debug_painter(
mut terrain: ResMut<Terrain2D>, mut terrain: ResMut<Terrain2D>,
mut debug_draw: ResMut<DebugLines>, frame_counter: Res<FrameCounter>,
mut brush: ResMut<TerrainBrush2D>, mut debug_draw: ResMut<bevy_prototype_debug_lines::DebugLines>,
windows: Res<Windows>,
mouse_input: Res<Input<MouseButton>>,
key_input: Res<Input<KeyCode>>,
mut mouse_wheel: EventReader<MouseWheel>,
camera_query: Query<(&Camera, &GlobalTransform), With<GameCamera>>,
) { ) {
let allow_painting = key_input.pressed(KeyCode::LControl); let simulation_frame = (frame_counter.frame % u8::MAX as u64) as u8 + 1;
// Change brush let indices = terrain
for event in mouse_wheel.iter() { .chunk_iter()
if allow_painting { .map(|(chunk_index, _)| *chunk_index)
brush.radius = (brush.radius + event.y.round() as i32).clamp(1, 128); .collect::<Vec<Chunk2DIndex>>()
} .clone();
}
if !allow_painting { for chunk_index in indices.iter() {
return; // Mark few chunks dirty in interval. Should help activate stale chunks
} if let Some(chunk) = terrain.index_to_chunk_mut(&chunk_index) {
let interval = 1;
if frame_counter.frame % interval == 0 {
let i = ((frame_counter.frame / interval) % 100) as i32;
if (chunk_index.y % 10) * 10 + (chunk_index.x % 10) == i {
chunk.mark_all_dirty();
}
}
};
// https://bevy-cheatbook.github.io/cookbook/cursor2world.html#2d-games if let Some(rect) = &terrain
// get the camera info and transform .index_to_chunk(&chunk_index)
// assuming there is exactly one main camera entity, so query::single() is OK .map_or(None, |chunk| chunk.dirty_rect.clone())
let (camera, camera_transform) = camera_query.single(); {
if let Some(chunk) = terrain.index_to_chunk_mut(&chunk_index) {
chunk.mark_clean();
} else {
continue;
};
// get the window that the camera is displaying to (or the primary window) // Texel simulation
let window = if let RenderTarget::Window(id) = camera.target { let mut y_range: Vec<_> = (rect.min.y..rect.max.y + 1).collect();
windows.get(id).unwrap() let mut x_range: Vec<_> = (rect.min.x..rect.max.x + 1).collect();
} else { if frame_counter.frame % 2 == 0 {
windows.get_primary().unwrap() y_range.reverse();
}; }
if frame_counter.frame / 2 % 2 == 0 {
x_range.reverse();
}
// check if the cursor is inside the window and get its position for y in y_range.iter() {
let world_pos = if let Some(screen_pos) = window.cursor_position() { for x in x_range.iter() {
// get the size of the window let local = Vector2I::new(*x, *y);
let window_size = Vec2::new(window.width() as f32, window.height() as f32); let global = local_to_global(&local, &chunk_index);
// convert screen position [0..resolution] to ndc [-1..1] (gpu coordinates) if terrain
let ndc = (screen_pos / window_size) * 2.0 - Vec2::ONE; .get_latest_simulation(&global)
.map_or(true, |frame| frame == simulation_frame)
{
continue;
};
// matrix for undoing the projection and camera transform simulate_texel(global, &mut terrain, &frame_counter);
let ndc_to_world = camera_transform.compute_matrix() * camera.projection_matrix().inverse(); }
}
// use it to convert ndc to world-space coordinates // Gas dispersion
let world_pos = ndc_to_world.project_point3(ndc.extend(-1.0)); let alternate_dispersion = frame_counter.frame % 2 == 0;
let alternate = if alternate_dispersion { 1 } else { 0 };
let y_range =
((rect.min.y - alternate)..rect.max.y + 1 + alternate).collect::<Vec<_>>();
let x_range =
((rect.min.x - alternate)..rect.max.x + 1 + alternate).collect::<Vec<_>>();
const DISPERSION_WIDTH: usize = 2;
const DISPERSION_HEIGHT: usize = 2;
for y_arr in y_range.chunks(DISPERSION_HEIGHT) {
for x_arr in x_range.chunks(DISPERSION_WIDTH) {
let mut global_positions = vec![];
for y in y_arr.iter() {
for x in x_arr.iter() {
let local = Vector2I::new(*x, *y);
let global = local_to_global(&local, &chunk_index);
global_positions.push(global);
}
}
// reduce it to a 2D value // Distribute gas
world_pos.truncate() disperse_gas(
} else { global_positions,
return; &mut terrain,
}; &frame_counter,
&mut debug_draw,
if key_input.just_pressed(KeyCode::Key1) { )
brush.tile = 1;
}
if key_input.just_pressed(KeyCode::Key2) {
brush.tile = 2;
}
if key_input.just_pressed(KeyCode::Key3) {
brush.tile = 3;
}
let colors = vec![
Color::rgba(1.0, 0.25, 0.25, 1.0),
Color::rgba(0.25, 1.0, 0.25, 1.0),
Color::rgba(0.25, 0.25, 1.0, 1.0),
];
let origin = Vector2I::from(world_pos);
let radius = brush.radius;
let color = colors[brush.tile as usize % colors.len()];
let id = match (
mouse_input.pressed(MouseButton::Left),
mouse_input.pressed(MouseButton::Right),
) {
(true, false) => brush.tile,
(_, _) => 0,
};
for y in origin.y - (radius - 1)..origin.y + radius {
for x in origin.x - (radius - 1)..origin.x + radius {
let dx = (x - origin.x).abs();
let dy = (y - origin.y).abs();
if dx * dx + dy * dy <= (radius - 1) * (radius - 1) {
let pos: Vector2I = Vector2I { x, y };
debug_draw.line_colored(
Vec3::from(pos) + Vec3::new(0.45, 0.45, 0.0),
Vec3::from(pos) + Vec3::new(0.55, 0.55, 0.0),
0.0,
color,
);
if mouse_input.pressed(MouseButton::Left) || mouse_input.pressed(MouseButton::Right)
{
terrain.set_texel(&pos, id)
} }
} }
} }
} }
} }
/** // TODO: Don't update if the result of dispersion is similar to before
Visualize dirty rects fn disperse_gas(
*/ global_positions: Vec<Vector2I>,
fn dirty_rect_visualizer(terrain: Res<Terrain2D>, mut debug_draw: ResMut<DebugLines>) { terrain: &mut Terrain2D,
for (chunk_index, chunk) in terrain.chunk_iter() { frame_counter: &FrameCounter,
let rect = if let Some(rect) = chunk.dirty_rect { debug_draw: &mut bevy_prototype_debug_lines::DebugLines,
rect ) {
use u32 as Capacity;
use u8 as Min;
use u8 as Max;
let mut total_densities: HashMap<TexelID, (Capacity, Min, Max)> = HashMap::new();
let mut valid_globals = vec![];
for global in global_positions.iter() {
let (texel, behaviour) = terrain.get_texel_behaviour(global);
if behaviour.clone().map_or(true, |b| b.form == TexelForm::Gas) {
valid_globals.push(*global);
}
match (texel, behaviour) {
(Some(texel), Some(behaviour)) => {
if behaviour.form == TexelForm::Gas {
if let Some((old_density, old_min, old_max)) = total_densities.get(&texel.id) {
total_densities.insert(
texel.id,
(
texel.density as u32 + *old_density,
texel.density.min(*old_min),
texel.density.max(*old_max),
),
);
} else {
total_densities.insert(
texel.id,
(texel.density as u32, texel.density, texel.density),
);
}
}
}
(_, _) => (),
}
}
let mut total_densities: Vec<(TexelID, Capacity, Min, Max)> = total_densities
.iter()
.map(|(t, (d, min, max))| (*t, *d, *min, *max))
.collect();
if total_densities.len() == 0 {
return;
}
total_densities.sort_unstable_by_key(|(_, density, _, _)| *density);
total_densities.reverse();
const TILE_CAPACITY: u32 = u8::MAX as u32;
let free_slots = valid_globals.len() as u32
- total_densities
.iter()
.map(|(_, v, _, _)| (*v / (TILE_CAPACITY + 1)) + 1)
.sum::<u32>();
// Stop if the gas is already close to a stable state
const STABLE_TRESHOLD: u8 = 3;
if total_densities.iter().all(|(_, _, min, max)| {
if u8::abs_diff(*min, *max) > STABLE_TRESHOLD {
return false;
}
free_slots > 0 && *max <= STABLE_TRESHOLD
}) {
// // DEBUG: draw box for stabilized area
// let mut min = valid_globals.first().unwrap().clone();
// let mut max = valid_globals.first().unwrap().clone();
// for global in valid_globals.iter() {
// min = Vector2I::min(&min, global);
// max = Vector2I::max(&max, global);
// }
// max = max + Vector2I::ONE;
// crate::game::debug::terrain::draw_box(
// debug_draw,
// Vec3::from(min),
// Vec3::from(max),
// Color::CYAN,
// 0.0,
// );
return;
}
// Allocate slots
let mut slots: Vec<(TexelID, u32)> = vec![];
for (id, density, _, _) in total_densities.iter() {
let min_slots = (density / (TILE_CAPACITY + 1)) + 1;
slots.push((*id, min_slots));
}
for i in 0..free_slots as usize {
let len = slots.len();
slots[i % len].1 += 1;
}
// Disperse into given slots
let mut texels: Vec<Texel2D> = vec![];
for (id, total_density, _, _) in total_densities.iter() {
let slots = slots.iter().find(|s| s.0 == *id).unwrap().1;
let mut density_left = *total_density;
for i in 0..slots {
let density = if i < (slots - 1) {
(total_density / slots).min(density_left)
} else {
density_left
}
.min(u8::MAX as u32);
if density > 0 {
texels.push(Texel2D {
id: *id,
density: density as u8,
});
density_left -= density;
}
}
}
// Apply changes
if texels.len() > valid_globals.len() {
panic!("disperse_gas() - valid_globals is shorter than texels");
}
fastrand::shuffle(&mut valid_globals);
for i in 0..valid_globals.len() {
let global = valid_globals[i];
if i < texels.len() {
let texel = texels[i];
terrain.set_texel(&global, texel, None);
} else { } else {
continue; terrain.set_texel(&global, Texel2D::default(), None)
}; }
}
}
let color = Color::RED; fn simulate_texel(global: Vector2I, terrain: &mut Terrain2D, frame_counter: &FrameCounter) {
let (_, behaviour) = match terrain.get_texel_behaviour(&global) {
(Some(texel), Some(behaviour)) => (texel, behaviour),
(_, _) => return,
};
let points = vec![ let simulation_frame = (frame_counter.frame % u8::MAX as u64) as u8 + 1;
Vec3::new(rect.min.x as f32, rect.min.y as f32, 0.0),
Vec3::new((rect.max.x + 1) as f32, rect.min.y as f32, 0.0), // Gravity
Vec3::new((rect.max.x + 1) as f32, (rect.max.y + 1) as f32, 0.0), if let Some(gravity) = behaviour.gravity {
Vec3::new(rect.min.x as f32, (rect.max.y + 1) as f32, 0.0), let grav_offset = Vector2I::from(gravity);
]; let grav_pos = global + grav_offset;
for i in 0..points.len() {
let offset = Vec3::from(chunk_index_to_global(chunk_index)); if behaviour.form != TexelForm::Gas || gravity.abs() > fastrand::u8(0..u8::MAX) {
debug_draw.line_colored( // Try falling
offset + points[i], {
offset + points[(i + 1) % points.len()], let (_, other_behaviour) = terrain.get_texel_behaviour(&grav_pos);
0.0, if TexelBehaviour2D::can_displace(&behaviour, &other_behaviour) {
color, terrain.swap_texels(&global, &grav_pos, Some(simulation_frame));
); return;
}
if terrain.can_transfer_density(&global, &grav_pos) {
terrain.transfer_density(&global, &grav_pos, gravity, Some(simulation_frame))
}
}
// Try "sliding"
let mut dirs = vec![Vector2I::RIGHT, Vector2I::LEFT];
if ((frame_counter.frame / 73) % 2) as i32 == global.y % 2 {
dirs.reverse();
}
for dir in dirs.iter() {
let slide_pos = match behaviour.form {
TexelForm::Solid => grav_pos + *dir,
TexelForm::Liquid | TexelForm::Gas => global + *dir,
};
let (_, other_behaviour) = terrain.get_texel_behaviour(&slide_pos);
if TexelBehaviour2D::can_displace(&behaviour, &other_behaviour) {
terrain.swap_texels(&global, &slide_pos, Some(simulation_frame));
return;
}
if terrain.can_transfer_density(&global, &grav_pos) {
terrain.transfer_density(&global, &grav_pos, gravity, Some(simulation_frame))
}
}
} }
} }
} }
@ -207,7 +360,6 @@ fn emit_terrain_events(
for (chunk_index, chunk) in terrain.chunk_iter_mut() { for (chunk_index, chunk) in terrain.chunk_iter_mut() {
if let Some(rect) = &chunk.dirty_rect { if let Some(rect) = &chunk.dirty_rect {
terrain_events.send(TerrainEvent2D::TexelsUpdated(*chunk_index, *rect)); terrain_events.send(TerrainEvent2D::TexelsUpdated(*chunk_index, *rect));
chunk.mark_clean();
} }
} }
} }
@ -222,13 +374,29 @@ pub enum TerrainEvent2D {
pub struct Terrain2D { pub struct Terrain2D {
chunk_map: HashMap<Chunk2DIndex, Chunk2D>, chunk_map: HashMap<Chunk2DIndex, Chunk2D>,
events: Vec<TerrainEvent2D>, events: Vec<TerrainEvent2D>,
pub top_boundary: Option<i32>,
pub bottom_boundary: Option<i32>,
pub left_boundary: Option<i32>,
pub right_boundary: Option<i32>,
} }
impl Terrain2D { impl Terrain2D {
pub fn new() -> Terrain2D { pub const WORLD_WIDTH: i32 = 512;
Terrain2D { pub const WORLD_HEIGHT: i32 = Self::WORLD_WIDTH * 2;
pub fn new(
top_boundary: Option<i32>,
bottom_boundary: Option<i32>,
left_boundary: Option<i32>,
right_boundary: Option<i32>,
) -> Self {
Self {
chunk_map: HashMap::new(), chunk_map: HashMap::new(),
events: Vec::new(), events: Vec::new(),
top_boundary,
bottom_boundary,
left_boundary,
right_boundary,
} }
} }
@ -280,16 +448,161 @@ impl Terrain2D {
} }
} }
pub fn set_texel(&mut self, global: &Vector2I, id: TexelID) { pub fn mark_dirty(&mut self, global: &Vector2I) {
let index = global_to_chunk_index(global); let index = global_to_chunk_index(global);
match self.index_to_chunk_mut(&index) { if let Some(chunk) = self.index_to_chunk_mut(&index) {
Some(chunk) => chunk.set_texel(&global_to_local(global), id), chunk.mark_dirty(&global_to_local(global));
None => { }
let mut chunk = Chunk2D::new(); }
chunk.set_texel(&global_to_local(global), id);
self.add_chunk(index, chunk); pub fn is_within_boundaries(&self, global: &Vector2I) -> bool {
if let Some(top) = self.top_boundary {
if global.y >= top {
return false;
} }
} }
if let Some(bottom) = self.bottom_boundary {
if global.y < bottom {
return false;
}
}
if let Some(left) = self.left_boundary {
if global.x < left {
return false;
}
}
if let Some(right) = self.right_boundary {
if global.x >= right {
return false;
}
}
return true;
}
pub fn get_texel(&self, global: &Vector2I) -> Option<Texel2D> {
self.global_to_chunk(global)
.map_or(None, |chunk| chunk.get_texel(&global_to_local(global)))
}
pub fn get_latest_simulation(&self, global: &Vector2I) -> Option<u8> {
self.global_to_chunk(global).map_or(None, |chunk| {
chunk.get_latest_simulation(&global_to_local(global))
})
}
pub fn get_texel_behaviour(
&self,
global: &Vector2I,
) -> (Option<Texel2D>, Option<TexelBehaviour2D>) {
let texel = self.get_texel(global);
(
texel,
if self.is_within_boundaries(global) {
texel.map_or(None, |t| TexelBehaviour2D::from_id(&t.id))
} else {
Some(TexelBehaviour2D::OUT_OF_BOUNDS)
},
)
}
pub fn set_texel(
&mut self,
global: &Vector2I,
new_texel: Texel2D,
simulation_frame: Option<u8>,
) {
if !self.is_within_boundaries(global) {
return;
}
let index = global_to_chunk_index(global);
let changed = match self.index_to_chunk_mut(&index) {
Some(chunk) => chunk.set_texel(&global_to_local(global), new_texel, simulation_frame),
None => {
let mut chunk = Chunk2D::new();
let changed =
chunk.set_texel(&global_to_local(global), new_texel, simulation_frame);
self.add_chunk(index, chunk);
changed
}
};
if changed {
self.mark_dirty(&(*global + Vector2I::UP));
self.mark_dirty(&(*global + Vector2I::RIGHT));
self.mark_dirty(&(*global + Vector2I::DOWN));
self.mark_dirty(&(*global + Vector2I::LEFT));
}
}
pub fn swap_texels(
&mut self,
from_global: &Vector2I,
to_global: &Vector2I,
simulation_frame: Option<u8>,
) {
let from = self.get_texel(from_global).unwrap_or_default();
let to = self.get_texel(to_global).unwrap_or_default();
self.set_texel(to_global, from, simulation_frame);
// REM: The displaced texel is also marked as simulated
self.set_texel(from_global, to, simulation_frame);
}
fn can_transfer_density(&self, from_global: &Vector2I, to_global: &Vector2I) -> bool {
let from = self.get_texel(from_global).unwrap_or_default();
let to = self.get_texel(to_global).unwrap_or_default();
if from.id != to.id {
return false;
}
let behaviour = if let Some(behaviour) = from.behaviour() {
behaviour
} else {
return false;
};
behaviour.form == TexelForm::Gas
}
fn transfer_density(
&mut self,
from_global: &Vector2I,
to_global: &Vector2I,
gravity: TexelGravity,
simulation_frame: Option<u8>,
) {
let from = self.get_texel(from_global).unwrap_or_default();
let to = self.get_texel(to_global).unwrap_or_default();
let max_transfer = gravity.abs();
// DEBUG: Test this out, another property?
const MAX_TARGET_DENSITY: u8 = 25;
let transfer = (u8::MAX - to.density)
.min(max_transfer)
.min(from.density)
.min(MAX_TARGET_DENSITY.max(to.density) - to.density);
if transfer == 0 {
return;
}
if from.density - transfer == 0 {
self.set_texel(&from_global, Texel2D::default(), simulation_frame);
} else {
self.set_texel(
&from_global,
Texel2D {
density: from.density - transfer,
..from
},
simulation_frame,
);
}
self.set_texel(
&to_global,
Texel2D {
density: to.density + transfer,
..to
},
simulation_frame,
);
} }
} }
@ -311,10 +624,21 @@ pub fn texel_index_to_local(i: usize) -> Vector2I {
} }
} }
pub fn global_to_local(position: &Vector2I) -> Vector2I { pub fn texel_index_to_global(i: usize, chunk_index: &Chunk2DIndex) -> Vector2I {
Vector2I { Vector2I {
x: wrapping_remainder(position.x, Chunk2D::SIZE.x), x: i as i32 % Chunk2D::SIZE.x,
y: wrapping_remainder(position.y, Chunk2D::SIZE.y), y: i as i32 / Chunk2D::SIZE.y,
} + chunk_index_to_global(chunk_index)
}
pub fn local_to_global(local: &Vector2I, chunk_index: &Chunk2DIndex) -> Vector2I {
chunk_index_to_global(chunk_index) + *local
}
pub fn global_to_local(global: &Vector2I) -> Vector2I {
Vector2I {
x: wrapping_remainder(global.x, Chunk2D::SIZE.x),
y: wrapping_remainder(global.y, Chunk2D::SIZE.y),
} }
} }

View File

@ -1,31 +1,15 @@
use std::collections::{HashMap, VecDeque}; use std::collections::VecDeque;
use super::{ use super::*;
local_to_texel_index, texel_index_to_local, Terrain2D, TerrainEvent2D, Texel2D, TexelID, use crate::util::{CollisionLayers, Segment2I, Vector2I};
NEIGHBOUR_INDEX_MAP, use bevy::render::{render_resource::Extent3d, texture::ImageSampler};
};
use crate::util::{Segment2I, Vector2I};
use bevy::{
prelude::*,
render::{render_resource::Extent3d, texture::ImageSampler},
};
use bevy_rapier2d::prelude::*;
use lazy_static::lazy_static; use lazy_static::lazy_static;
type Island = VecDeque<Segment2I>; type Island = VecDeque<Segment2I>;
pub type Chunk2DIndex = Vector2I;
pub type NeighbourMask = u8;
lazy_static! { lazy_static! {
pub static ref COLOR_MAP: HashMap<TexelID, [u8; 4]> = {
let mut map = HashMap::new();
map.insert(0, [0x00, 0x00, 0x00, 0x00]);
// map.insert(0, [0x03, 0x03, 0x03, 0xff]);
// map.insert(1, [0x47, 0x8e, 0x48, 0xff]);
map.insert(1, [0x9e, 0x7f, 0x63, 0xff]);
map.insert(2, [0x38, 0x32, 0x2d, 0xff]);
map.insert(3, [0x1e, 0x1e, 0x1e, 0xff]);
map
};
/// Marching Square case dictionary. /// Marching Square case dictionary.
/// ///
/// Key is a bitmask of neighbouring tiles (up, right, down, left - least significant bit first). /// Key is a bitmask of neighbouring tiles (up, right, down, left - least significant bit first).
@ -60,6 +44,14 @@ lazy_static! {
/* down */ Segment2I { from: Vector2I::RIGHT, to: Vector2I::ZERO }, /* down */ Segment2I { from: Vector2I::RIGHT, to: Vector2I::ZERO },
/* left */ Segment2I { from: Vector2I::ZERO, to: Vector2I::UP }, /* left */ Segment2I { from: Vector2I::ZERO, to: Vector2I::UP },
]; ];
static ref NEIGHBOUR_INDEX_MAP: HashMap<Vector2I, u8> = {
let mut map = HashMap::new();
for i in 0..Chunk2D::NEIGHBOUR_OFFSET_VECTORS.len() {
map.insert(Chunk2D::NEIGHBOUR_OFFSET_VECTORS[i], i as u8);
}
map
};
} }
#[derive(Reflect, Component, Default)] #[derive(Reflect, Component, Default)]
@ -90,17 +82,28 @@ pub struct ChunkColliderBundle {
pub transform: TransformBundle, pub transform: TransformBundle,
} }
pub type Chunk2DIndex = Vector2I;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub struct ChunkRect { pub struct ChunkRect {
pub min: Vector2I, pub min: Vector2I,
pub max: Vector2I, pub max: Vector2I,
} }
impl ChunkRect {
pub fn include_point(&self, point: Vector2I) -> Self {
ChunkRect {
min: Vector2I::min(&self.min, &point),
max: Vector2I::max(&self.max, &point),
}
}
}
pub struct Chunk2D { pub struct Chunk2D {
pub texels: [Texel2D; (Self::SIZE_X * Self::SIZE_Y) as usize], pub texels: [Texel2D; Self::SIZE_X * Self::SIZE_Y],
// TODO: handle multiple dirty rects /// bitmask of empty/non-empty neighbours, see NEIGHBOUR_OFFSET_VECTORS for the order
pub neighbour_mask: [NeighbourMask; Self::SIZE_X * Self::SIZE_Y],
/// Used in simulation step so that texels won't be updated twice. Value of 0 is always updated.
pub simulation_frames: [u8; Self::SIZE_X * Self::SIZE_Y],
// TODO: handle multiple dirty rects?
pub dirty_rect: Option<ChunkRect>, pub dirty_rect: Option<ChunkRect>,
} }
@ -111,65 +114,22 @@ impl Chunk2D {
x: Self::SIZE_X as i32, x: Self::SIZE_X as i32,
y: Self::SIZE_Y as i32, y: Self::SIZE_Y as i32,
}; };
pub const NEIGHBOUR_OFFSET_VECTORS: [Vector2I; 4] = [
Vector2I { x: 0, y: 1 },
Vector2I { x: 1, y: 0 },
Vector2I { x: 0, y: -1 },
Vector2I { x: -1, y: 0 },
];
pub fn new() -> Chunk2D { pub fn new() -> Chunk2D {
Chunk2D { Chunk2D {
texels: Self::new_texel_array(), texels: [Texel2D::default(); Self::SIZE_X * Self::SIZE_Y],
neighbour_mask: [0; Self::SIZE_X * Self::SIZE_Y],
simulation_frames: [0; Self::SIZE_X * Self::SIZE_Y],
dirty_rect: None, dirty_rect: None,
} }
} }
pub fn new_full() -> Chunk2D {
let mut chunk = Chunk2D {
texels: Self::new_texel_array(),
dirty_rect: None,
};
for y in 0..Self::SIZE_Y {
for x in 0..Self::SIZE_X {
chunk.set_texel(&Vector2I::new(x as i32, y as i32), 1);
}
}
chunk
}
pub fn new_half() -> Chunk2D {
let mut chunk = Chunk2D {
texels: Self::new_texel_array(),
dirty_rect: None,
};
for y in 0..Self::SIZE_Y {
for x in 0..Self::SIZE_X {
if x <= Self::SIZE_Y - y {
chunk.set_texel(&Vector2I::new(x as i32, y as i32), 1);
}
}
}
chunk
}
pub fn new_circle() -> Chunk2D {
let mut chunk = Chunk2D {
texels: Self::new_texel_array(),
dirty_rect: None,
};
let origin = Self::SIZE / 2;
let radius = Self::SIZE_X as i32 / 2;
for y in 0..Self::SIZE_Y {
for x in 0..Self::SIZE_X {
let dx = (x as i32 - origin.x).abs();
let dy = (y as i32 - origin.y).abs();
if dx * dx + dy * dy <= (radius - 1) * (radius - 1) {
chunk.set_texel(&Vector2I::new(x as i32, y as i32), 1);
}
}
}
chunk
}
pub fn new_texel_array() -> [Texel2D; Self::SIZE_X * Self::SIZE_Y] {
[Texel2D::default(); Self::SIZE_X * Self::SIZE_Y]
}
pub fn xy_vec() -> Vec<Vector2I> { pub fn xy_vec() -> Vec<Vector2I> {
let mut result = Vec::with_capacity(Self::SIZE_X * Self::SIZE_Y); let mut result = Vec::with_capacity(Self::SIZE_X * Self::SIZE_Y);
for y in 0..Self::SIZE_Y { for y in 0..Self::SIZE_Y {
@ -186,18 +146,13 @@ impl Chunk2D {
pub fn mark_all_dirty(&mut self) { pub fn mark_all_dirty(&mut self) {
self.dirty_rect = Some(ChunkRect { self.dirty_rect = Some(ChunkRect {
min: Vector2I::ZERO, min: Vector2I::ZERO,
max: Self::SIZE, max: Self::SIZE - Vector2I::ONE,
}); });
} }
pub fn mark_dirty(&mut self, position: &Vector2I) { pub fn mark_dirty(&mut self, position: &Vector2I) {
match &self.dirty_rect { match &self.dirty_rect {
Some(rect) => { Some(rect) => self.dirty_rect = Some(rect.include_point(*position)),
self.dirty_rect = Some(ChunkRect {
min: Vector2I::min(&rect.min, position),
max: Vector2I::max(&rect.max, position),
})
}
None => { None => {
self.dirty_rect = Some(ChunkRect { self.dirty_rect = Some(ChunkRect {
min: *position, min: *position,
@ -215,58 +170,69 @@ impl Chunk2D {
local_to_texel_index(position).map(|i| self.texels[i]) local_to_texel_index(position).map(|i| self.texels[i])
} }
pub fn get_latest_simulation(&self, position: &Vector2I) -> Option<u8> {
local_to_texel_index(position).map(|i| self.simulation_frames[i])
}
pub fn get_texel_mut(&mut self, position: &Vector2I) -> Option<&mut Texel2D> { pub fn get_texel_mut(&mut self, position: &Vector2I) -> Option<&mut Texel2D> {
local_to_texel_index(position).map(|i| &mut self.texels[i]) local_to_texel_index(position).map(|i| &mut self.texels[i])
} }
pub fn set_texel(&mut self, position: &Vector2I, id: TexelID) { pub fn set_texel(
&mut self,
position: &Vector2I,
new_texel: Texel2D,
simulation_frame: Option<u8>,
) -> bool {
let i = local_to_texel_index(position).expect("Texel index out of range"); let i = local_to_texel_index(position).expect("Texel index out of range");
if self.texels[i].id != id { if self.texels[i] == new_texel {
self.mark_dirty(position); return false;
}
self.mark_dirty(position);
let update_neighbours = self.texels[i].has_collision() != new_texel.has_collision();
self.texels[i] = new_texel;
// Update simulation frame
if let Some(simulation_frame) = simulation_frame {
self.simulation_frames[i] = simulation_frame;
} }
let update_neighbours = self.texels[i].is_empty()
!= (Texel2D {
id,
..self.texels[i]
})
.is_empty();
self.texels[i].id = id;
// Update neighbour mask // Update neighbour mask
if update_neighbours { if update_neighbours {
for offset in Texel2D::NEIGHBOUR_OFFSET_VECTORS { for offset in Self::NEIGHBOUR_OFFSET_VECTORS {
// Flip neighbour's bit // Flip neighbour's bit
match self.get_texel_mut(&(*position + offset)) { match local_to_texel_index(&(*position + offset)) {
Some(mut neighbour) => { Some(index) => {
neighbour.neighbour_mask ^= 1 << NEIGHBOUR_INDEX_MAP[&-offset]; self.neighbour_mask[index] ^= 1 << NEIGHBOUR_INDEX_MAP[&-offset];
} }
None => (), None => (),
} }
} }
} }
true
} }
pub fn create_texture_data(&self) -> Vec<u8> { pub fn create_texture_data(&self) -> Vec<u8> {
let mut image_data = Vec::with_capacity(Chunk2D::SIZE_X * Chunk2D::SIZE_Y * 4); let mut image_data = Vec::with_capacity(Chunk2D::SIZE_X * Chunk2D::SIZE_Y * 4);
let fallback: [u8; 4] = [0x00, 0x00, 0x00, 0x00];
for y in (0..Chunk2D::SIZE_Y).rev() { for y in (0..Chunk2D::SIZE_Y).rev() {
for x in 0..Chunk2D::SIZE_X { for x in 0..Chunk2D::SIZE_X {
image_data.append( let texel = &self.get_texel(&Vector2I::new(x as i32, y as i32)).unwrap();
&mut COLOR_MAP let behaviour = texel.behaviour();
.get( let mut color =
&self behaviour.map_or(Color::rgba_u8(0, 0, 0, 0), |behaviour| behaviour.color);
.get_texel(&Vector2I::new(x as i32, y as i32)) color.set_a(color.a() * ((texel.density as f32) / 256.0));
.unwrap() let color_data = color.as_rgba_u32();
.id, let mut color_data: Vec<u8> = vec![
) ((color_data >> 0) & 0xff) as u8,
.unwrap_or(&fallback) ((color_data >> 8) & 0xff) as u8,
.to_vec() ((color_data >> 16) & 0xff) as u8,
.clone(), ((color_data >> 24) & 0xff) as u8,
); ];
image_data.append(&mut color_data);
} }
} }
image_data image_data
} }
// TODO: Don't create collision for falling texels, it's pretty annoying that a stream of small grains blocks movement
pub fn create_collision_data(&self) -> Vec<Vec<Vec2>> { pub fn create_collision_data(&self) -> Vec<Vec<Vec2>> {
let mut islands: Vec<Island> = Vec::new(); let mut islands: Vec<Island> = Vec::new();
for i in 0..self.texels.len() { for i in 0..self.texels.len() {
@ -284,8 +250,9 @@ impl Chunk2D {
| if local.x == 0 { 1 << 3 } else { 0 }; | if local.x == 0 { 1 << 3 } else { 0 };
let mut sides: Vec<Segment2I>; let mut sides: Vec<Segment2I>;
if self.texels[i].is_empty() { let has_collision = TexelBehaviour2D::has_collision(&self.texels[i].id);
sides = MST_CASE_MAP[self.texels[i].neighbour_mask as usize] if !has_collision {
sides = MST_CASE_MAP[self.neighbour_mask[i] as usize]
.iter() .iter()
.clone() .clone()
.map(|side| Segment2I { .map(|side| Segment2I {
@ -293,7 +260,7 @@ impl Chunk2D {
to: side.to + local, to: side.to + local,
}) })
.collect(); .collect();
} else if !self.texels[i].is_empty() && edge_mask != 0 { } else if has_collision && edge_mask != 0 {
sides = Vec::with_capacity(Chunk2D::SIZE_X * 2 + Chunk2D::SIZE_Y * 2); sides = Vec::with_capacity(Chunk2D::SIZE_X * 2 + Chunk2D::SIZE_Y * 2);
for i in 0..MST_EDGE_CASE_MAP.len() { for i in 0..MST_EDGE_CASE_MAP.len() {
if edge_mask & (1 << i) != 0 { if edge_mask & (1 << i) != 0 {
@ -434,7 +401,7 @@ pub fn chunk_spawner(
..default() ..default()
}, },
texture, texture,
transform: Transform::from_translation(Vec3::new(pos.x, pos.y, 0.0)), transform: Transform::from_translation(Vec3::new(pos.x, pos.y, 1.0)),
..default() ..default()
}, },
..default() ..default()
@ -471,9 +438,7 @@ pub fn chunk_spawner(
} }
} }
/** /// Update the chunk sprite as needed
Update the chunk sprite as needed
*/
pub fn chunk_sprite_sync( pub fn chunk_sprite_sync(
mut terrain_events: EventReader<TerrainEvent2D>, mut terrain_events: EventReader<TerrainEvent2D>,
mut images: ResMut<Assets<Image>>, mut images: ResMut<Assets<Image>>,
@ -530,9 +495,7 @@ pub fn chunk_sprite_sync(
} }
} }
/** /// Create and update colliders for chunk as needed
Create and update colliders for chunk as needed
*/
pub fn chunk_collision_sync( pub fn chunk_collision_sync(
mut terrain_events: EventReader<TerrainEvent2D>, mut terrain_events: EventReader<TerrainEvent2D>,
mut commands: Commands, mut commands: Commands,
@ -574,7 +537,9 @@ pub fn chunk_collision_sync(
} }
} }
// Kinda messy, partly due do how entity creatin is queued // let layer_membership = CollisionLayers::WORLD;
// REM: Kinda messy, partly due do how entity creation is timed
for (entity, chunk_component) in updated_chunks.iter() { for (entity, chunk_component) in updated_chunks.iter() {
let chunk = terrain.index_to_chunk(&chunk_component.index).unwrap(); let chunk = terrain.index_to_chunk(&chunk_component.index).unwrap();
let new_islands = chunk.create_collision_data(); let new_islands = chunk.create_collision_data();
@ -594,6 +559,7 @@ pub fn chunk_collision_sync(
builder builder
.spawn(Collider::polyline(island.clone(), None)) .spawn(Collider::polyline(island.clone(), None))
.insert(TransformBundle::default()) .insert(TransformBundle::default())
.insert(CollisionGroups::new(CollisionLayers::WORLD, Group::ALL))
.insert(Name::new(format!("Island #{}", index))); .insert(Name::new(format!("Island #{}", index)));
}); });
} }
@ -605,6 +571,7 @@ pub fn chunk_collision_sync(
builder builder
.spawn(Collider::polyline(island.clone(), None)) .spawn(Collider::polyline(island.clone(), None))
.insert(TransformBundle::default()) .insert(TransformBundle::default())
.insert(CollisionGroups::new(CollisionLayers::WORLD, Group::ALL))
.insert(Name::new(format!("Island #{}", index))); .insert(Name::new(format!("Island #{}", index)));
}); });
} }

View File

@ -1,6 +1,7 @@
use noise::{NoiseFn, PerlinSurflet}; use noise::{NoiseFn, PerlinSurflet};
use super::{chunk_index_to_global, Chunk2D, Chunk2DIndex}; use super::*;
use crate::util::{inverse_lerp, lerp};
pub struct TerrainGen2D { pub struct TerrainGen2D {
pub seed: u32, pub seed: u32,
@ -30,16 +31,16 @@ impl TerrainGen2D {
let mut id = 0; let mut id = 0;
if value > 0.35 { if value > 0.35 {
id = 1; id = 11;
} }
if value > 0.42 { if value > 0.42 {
id = 2; id = 12;
} }
if value > 0.9 { if value > 0.9 {
id = 3; id = 13;
} }
chunk.set_texel(&local, id); chunk.set_texel(&local, Texel2D { id, ..default() }, None);
} }
chunk chunk
} }

View File

@ -1,38 +1,32 @@
use lazy_static::lazy_static;
use std::collections::HashMap;
pub use u8 as TexelID; pub use u8 as TexelID;
pub use u8 as NeighbourMask;
use crate::util::Vector2I; use super::TexelBehaviour2D;
#[derive(Clone, Copy, Default)] #[derive(Clone, Copy, Debug, PartialEq)]
pub struct Texel2D { pub struct Texel2D {
/// Identifier for a set of properties
pub id: TexelID, pub id: TexelID,
/// bitmask of empty/non-empty neighbours, see NEIGHBOUR_OFFSET_VECTORS for the order /// Used by gas materials
pub neighbour_mask: NeighbourMask, pub density: u8,
} }
lazy_static! { impl Default for Texel2D {
pub static ref NEIGHBOUR_INDEX_MAP: HashMap<Vector2I, u8> = { fn default() -> Self {
let mut map = HashMap::new(); Self {
for i in 0..Texel2D::NEIGHBOUR_OFFSET_VECTORS.len() { id: TexelID::default(),
map.insert(Texel2D::NEIGHBOUR_OFFSET_VECTORS[i], i as u8); density: u8::MAX,
} }
map }
};
} }
impl Texel2D { impl Texel2D {
pub const EMPTY: TexelID = 0; pub const EMPTY: TexelID = 0;
pub const NEIGHBOUR_OFFSET_VECTORS: [Vector2I; 4] = [
Vector2I { x: 0, y: 1 },
Vector2I { x: 1, y: 0 },
Vector2I { x: 0, y: -1 },
Vector2I { x: -1, y: 0 },
];
pub fn is_empty(&self) -> bool { pub fn has_collision(&self) -> bool {
self.id == 0 TexelBehaviour2D::has_collision(&self.id)
}
pub fn behaviour(&self) -> Option<TexelBehaviour2D> {
TexelBehaviour2D::from_id(&self.id)
} }
} }

View File

@ -0,0 +1,255 @@
use crate::util::Vector2I;
use super::TexelID;
use bevy::prelude::*;
use lazy_static::lazy_static;
use std::{borrow::Cow, collections::HashMap};
lazy_static! {
static ref ID_MAP: HashMap<TexelID, TexelBehaviour2D> = {
let mut result = HashMap::new();
result.insert(
1,
TexelBehaviour2D {
name: Cow::Borrowed("loose sand"),
color: Color::rgb(0.61, 0.49, 0.38),
gravity: Some(TexelGravity::Down(200)),
has_collision: true,
..default()
},
);
result.insert(
2,
TexelBehaviour2D {
name: Cow::Borrowed("loose stone"),
color: Color::rgb(0.21, 0.19, 0.17),
gravity: Some(TexelGravity::Down(200)),
has_collision: true,
..default()
},
);
result.insert(
3,
TexelBehaviour2D {
name: Cow::Borrowed("loose sturdy stone"),
color: Color::rgb(0.11, 0.11, 0.11),
gravity: Some(TexelGravity::Down(200)),
has_collision: true,
..default()
},
);
result.insert(
4,
TexelBehaviour2D {
name: Cow::Borrowed("water"),
color: Color::rgba(0.0, 0.0, 1.0, 0.5),
form: TexelForm::Liquid,
gravity: Some(TexelGravity::Down(50)),
..default()
},
);
result.insert(
5,
TexelBehaviour2D {
name: Cow::Borrowed("oil"),
color: Color::rgba(0.5, 0.5, 0.25, 0.5),
form: TexelForm::Liquid,
gravity: Some(TexelGravity::Down(20)),
..default()
},
);
result.insert(
6,
TexelBehaviour2D {
name: Cow::Borrowed("light gas"),
color: Color::rgba(0.0, 1.0, 0.0, 0.5),
form: TexelForm::Gas,
gravity: Some(TexelGravity::Up(160)),
..default()
},
);
result.insert(
7,
TexelBehaviour2D {
name: Cow::Borrowed("heavy gas"),
color: Color::rgba(1.0, 0.5, 0.5, 0.5),
form: TexelForm::Gas,
gravity: Some(TexelGravity::Down(60)),
..default()
},
);
result.insert(
8,
TexelBehaviour2D {
name: Cow::Borrowed("oxygen"),
color: Color::rgba(0.5, 0.5, 0.5, 0.5),
form: TexelForm::Gas,
..default()
},
);
result.insert(
11,
TexelBehaviour2D {
name: Cow::Borrowed("sand"),
color: Color::rgb(0.61, 0.49, 0.38),
has_collision: true,
..default()
},
);
result.insert(
12,
TexelBehaviour2D {
name: Cow::Borrowed("stone"),
color: Color::rgb(0.21, 0.19, 0.17),
has_collision: true,
..default()
},
);
result.insert(
13,
TexelBehaviour2D {
name: Cow::Borrowed("sturdy stone"),
color: Color::rgb(0.11, 0.11, 0.11),
has_collision: true,
..default()
},
);
result
};
static ref FORM_DISPLACEMENT_PRIORITY: HashMap<TexelForm, u8> = {
let mut result = HashMap::new();
result.insert(TexelForm::Gas, 0);
result.insert(TexelForm::Liquid, 1);
result.insert(TexelForm::Solid, 2);
result
};
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum TexelForm {
#[default]
// Solid materials, when affected by gravity, create pyramid-like piles
Solid,
// Liquid materials, when affected by gravity, act like solids but also try to stabilise the surface level by traveling flat surfaces
Liquid,
// Gas materials act like liquids, but also have density/pressure that causes them to disperse
Gas,
}
impl TexelForm {
fn priority(&self) -> u8 {
FORM_DISPLACEMENT_PRIORITY
.get(self)
.cloned()
.unwrap_or_default()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TexelGravity {
Down(u8),
Up(u8),
}
impl From<TexelGravity> for Vector2I {
fn from(gravity: TexelGravity) -> Self {
match gravity {
TexelGravity::Down(_) => Vector2I::DOWN,
TexelGravity::Up(_) => Vector2I::UP,
}
}
}
impl TexelGravity {
pub fn abs(&self) -> u8 {
match self {
TexelGravity::Down(grav) => *grav,
TexelGravity::Up(grav) => *grav,
}
}
}
#[derive(Clone, Debug)]
pub struct TexelBehaviour2D {
pub name: Cow<'static, str>,
pub color: Color,
pub form: TexelForm,
pub has_collision: bool,
pub gravity: Option<TexelGravity>,
pub toughness: Option<f32>,
}
impl Default for TexelBehaviour2D {
fn default() -> Self {
TexelBehaviour2D {
name: Cow::Borrowed("Unnamed material"),
color: Color::PINK,
form: TexelForm::Solid,
has_collision: false,
gravity: None,
toughness: None,
}
}
}
impl TexelBehaviour2D {
pub const OUT_OF_BOUNDS: Self = TexelBehaviour2D {
name: Cow::Borrowed(":)"),
color: Color::BLACK,
has_collision: true,
form: TexelForm::Solid,
gravity: None,
toughness: None,
};
pub fn from_id(id: &TexelID) -> Option<Self> {
ID_MAP.get(id).cloned()
}
pub fn is_empty(id: &TexelID) -> bool {
ID_MAP.get(id).is_none()
}
pub fn has_collision(id: &TexelID) -> bool {
ID_MAP.get(id).map_or(false, |b| b.has_collision)
}
/// Can this type of material displace another?
pub fn can_displace(from: &TexelBehaviour2D, to: &Option<TexelBehaviour2D>) -> bool {
let to = if let Some(to) = to { to } else { return true };
match (from.form, to.form) {
(from_form, to_form) => {
if from_form.priority() != to_form.priority() {
return from_form.priority() > to_form.priority();
}
if let (Some(from_grav), Some(to_grav)) = (from.gravity, to.gravity) {
match (from_grav, to_grav) {
(TexelGravity::Down(from_grav), TexelGravity::Down(to_grav)) => {
from_grav > to_grav
}
(TexelGravity::Up(from_grav), TexelGravity::Up(to_grav)) => {
from_grav > to_grav
}
(_, _) => true,
}
} else {
// Solids can also be displaced, but only if the other material has gravity
to_form != TexelForm::Solid
}
}
}
}
}

View File

@ -1,10 +1,13 @@
use bevy::prelude::*; use bevy::prelude::*;
mod collision_layers;
pub mod frame_counter;
pub mod math; pub mod math;
mod segment2_i32; mod segment2_i32;
mod vector2; mod vector2;
mod vector2_i32; mod vector2_i32;
pub use collision_layers::*;
pub use segment2_i32::*; pub use segment2_i32::*;
pub use vector2::*; pub use vector2::*;
pub use vector2_i32::*; pub use vector2_i32::*;

View File

@ -0,0 +1,9 @@
use bevy_rapier2d::prelude::*;
pub struct CollisionLayers;
impl CollisionLayers {
pub const WORLD: Group = Group::GROUP_1;
pub const PLAYER: Group = Group::GROUP_2;
pub const ENEMY: Group = Group::GROUP_3;
}

19
src/util/frame_counter.rs Normal file
View File

@ -0,0 +1,19 @@
use bevy::prelude::*;
pub struct FrameCounterPlugin;
impl Plugin for FrameCounterPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(FrameCounter { frame: 0 })
.add_system_to_stage(CoreStage::First, frame_increment);
}
}
#[derive(Resource)]
pub struct FrameCounter {
pub frame: u64,
}
fn frame_increment(mut frame_counter: ResMut<FrameCounter>) {
frame_counter.frame += 1;
}