Initial commit. Darkness plugin with basic lighting.

master
hheik 2023-08-30 02:28:36 +03:00
commit 2bc44d694d
28 changed files with 8272 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
/target
/graphs
/assets/levels/*/backups/

4297
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

23
Cargo.toml Normal file
View File

@ -0,0 +1,23 @@
[package]
name = "moonlit"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
bevy = "0.11.1"
bevy-inspector-egui = "0.19.0"
bevy_ecs_ldtk = { version = "0.8.0", features = ["derive", "atlas"] }
bevy_mod_debugdump = "0.8.0"
bevy_prototype_debug_lines = "0.11.1"
bevy_rapier2d = "0.22.0"
num-traits = "0.2.16"
# Enable a small amount of optimization in debug mode
[profile.dev]
opt-level = 1
# Enable high optimizations for dependencies (incl. Bevy), but not for our code:
[profile.dev.package."*"]
opt-level = 3

2172
assets/levels/world.ldtk Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,88 @@
#import bevy_pbr::mesh_vertex_output MeshVertexOutput
#import bevy_sprite::mesh2d_view_bindings globals
struct DarknessMaterial {
color: vec4<f32>,
};
struct PointLight {
position: vec2<f32>,
radius: f32,
}
struct SpotLight {
position: vec2<f32>,
radius: f32,
rotation: f32,
angle: f32,
padding1: u32,
padding2: u32,
padding3: u32,
}
@group(1) @binding(0)
var<uniform> material: DarknessMaterial;
@group(1) @binding(1)
var<uniform> point_light_count: i32;
@group(1) @binding(2)
var<uniform> point_lights: array<PointLight, 64>;
@group(1) @binding(3)
var<uniform> spot_light_count: i32;
@group(1) @binding(4)
var<uniform> spot_lights: array<SpotLight, 64>;
@fragment
fn fragment(
mesh: MeshVertexOutput,
) -> @location(0) vec4<f32> {
let color = material.color;
let pos = mesh.world_position.xy;
let t = globals.time;
let light_edge = 6.0;
let light_edge_mult = 0.8;
var bright_light = 0.0;
var dim_light = 0.0;
for (var i: i32 = 0; i < point_light_count; i++) {
if (point_lights[i].radius <= 0.0) {
continue;
}
let radius = point_lights[i].radius + sin(t * 5.0) * 0.2;
let dist = distance(pos, point_lights[i].position);
let edge_treshold = radius * light_edge_mult - light_edge;
let edge_dist = max(dist - edge_treshold, 0.0);
bright_light = bright_light + step(dist, edge_treshold);
bright_light = bright_light + 1.0 / edge_dist;
dim_light = dim_light + step(dist, radius);
dim_light = dim_light + 1.0 / max(dist - radius, 0.0);
}
for (var i: i32 = 0; i < spot_light_count; i++) {
if (spot_lights[i].radius <= 0.0 || spot_lights[i].angle <= 0.0) {
continue;
}
let spot = spot_lights[i];
let radius = spot.radius + sin(t * 5.0) * 0.2;
let diff = pos - spot.position;
let dist = length(diff);
let spot_dir = vec2<f32>(cos(spot.rotation), sin(spot.rotation));
let angle_diff = acos(dot(spot_dir, normalize(diff)));
let edge_treshold = radius * light_edge_mult - light_edge;
let edge_dist = max(dist - edge_treshold, 0.0);
let angle_mult = step(angle_diff, spot.angle / 2.0);
bright_light = bright_light + step(dist, edge_treshold) * angle_mult;
dim_light = dim_light + step(dist, radius) * angle_mult;
}
let edge1 = step(bright_light, 0.75);
let edge2 = step(dim_light, 0.75) * 0.5 + 0.5;
return vec4(color.rgb, color.a * edge1 * (edge2 * 0.5 + 0.5));
}

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 321 B

Binary file not shown.

BIN
assets/sprites/dungeon1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

BIN
assets/sprites/enemies.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 B

47
src/debug.rs Normal file
View File

@ -0,0 +1,47 @@
use bevy::prelude::*;
use bevy_prototype_debug_lines::DebugLinesPlugin;
pub struct DebugPlugin;
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
pub struct DebugSet;
impl Plugin for DebugPlugin {
fn build(&self, app: &mut App) {
app.configure_set(Last, DebugSet.run_if(is_debug_enabled));
app.insert_resource(DebugMode::off())
.add_plugins((
bevy_inspector_egui::quick::WorldInspectorPlugin::new().run_if(is_debug_enabled),
bevy_rapier2d::prelude::RapierDebugRenderPlugin::default(),
))
.add_plugins(DebugLinesPlugin::default())
.add_systems(Update, debug_toggle);
}
}
#[derive(Reflect, Resource, Default)]
#[reflect(Resource)]
pub struct DebugMode {
pub enabled: bool,
}
impl DebugMode {
pub fn on() -> Self {
Self { enabled: true }
}
pub fn off() -> Self {
Self { enabled: false }
}
}
fn is_debug_enabled(debug_mode: Res<DebugMode>) -> bool {
debug_mode.enabled
}
fn debug_toggle(input: Res<Input<KeyCode>>, mut debug_mode: ResMut<DebugMode>) {
if input.just_pressed(KeyCode::P) {
debug_mode.enabled = !debug_mode.enabled
}
}

40
src/game.rs Normal file
View File

@ -0,0 +1,40 @@
use crate::{debug, game_setup};
use bevy::prelude::*;
use bevy_ecs_ldtk::{LdtkWorldBundle, LevelSelection};
use self::darkness::SpotLight2D;
pub mod camera;
pub mod darkness;
pub mod ldtk;
pub fn init(app: &mut App) {
app.insert_resource(LevelSelection::Index(0))
.add_plugins((
game_setup::GameSetupPlugin,
bevy_rapier2d::prelude::RapierPhysicsPlugin::<bevy_rapier2d::prelude::NoUserData>::default(),
ldtk::LdtkHelperPlugin,
darkness::DarknessPlugin,
camera::GameCameraPlugin,
debug::DebugPlugin,
))
.add_systems(Startup, setup);
}
fn setup(mut commands: Commands, assets: Res<AssetServer>) {
commands.spawn(LdtkWorldBundle {
ldtk_handle: assets.load("levels/world.ldtk"),
..default()
});
commands.spawn((
Name::new("Spot light"),
SpatialBundle::from_transform(Transform::from_xyz(32.0, 31.0, 0.0).with_rotation(
Quat::from_euler(EulerRot::YXZ, 0.0, 0.0, f32::to_radians(-90.0)),
)),
SpotLight2D {
radius: 100.0,
angle: 1.0,
},
));
}

246
src/game/camera.rs Normal file
View File

@ -0,0 +1,246 @@
use crate::util::{move_towards_vec3, vec3_lerp};
use bevy::input::mouse::{MouseScrollUnit, MouseWheel};
use bevy::prelude::*;
use bevy::render::camera::ScalingMode;
use bevy::{input::mouse::MouseMotion, transform::TransformSystem};
use bevy_ecs_ldtk::prelude::*;
use super::darkness::{PointLight2D, SpotLight2D};
pub struct GameCameraPlugin;
impl Plugin for GameCameraPlugin {
fn build(&self, app: &mut App) {
app.configure_set(
PostUpdate,
CameraSystem.before(TransformSystem::TransformPropagate),
)
.register_type::<CameraFollow>()
.register_type::<GameCamera>()
.add_systems(Startup, camera_setup)
.add_systems(Update, control_light)
.add_systems(
PostUpdate,
(follow_detach_system, follow_system, free_system).in_set(CameraSystem),
)
.add_systems(
PostUpdate,
room_restraint
.after(CameraSystem)
.before(TransformSystem::TransformPropagate),
);
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, SystemSet)]
pub struct CameraSystem;
#[derive(Clone, Copy, PartialEq, Reflect)]
pub enum FollowMovement {
Instant,
Linear(f32),
Smooth(f32),
}
impl Default for FollowMovement {
fn default() -> Self {
Self::Instant
}
}
#[derive(Default, Reflect, PartialEq)]
pub enum CameraMode {
#[default]
Free,
Follow,
}
#[derive(Default, Component, Reflect)]
#[reflect(Component)]
pub struct GameCamera {
pub mode: CameraMode,
}
#[derive(Default, Component, Reflect)]
#[reflect(Component)]
pub struct CameraFollow {
pub target: Option<Entity>,
pub movement: FollowMovement,
}
#[derive(Default, Component)]
pub struct CameraRoomRestraint;
fn camera_setup(mut commands: Commands) {
commands.spawn((
Name::new("Game Camera"),
Camera2dBundle {
projection: OrthographicProjection {
scaling_mode: ScalingMode::AutoMax {
max_width: 256.0,
max_height: 160.0,
},
..default()
},
camera_2d: Camera2d {
clear_color: bevy::core_pipeline::clear_color::ClearColorConfig::Custom(
Color::rgb(0.0, 0.0, 0.0),
),
},
transform: Transform::from_xyz(0.0, 0.0, 999.9),
..default()
},
GameCamera::default(),
Visibility::default(),
ComputedVisibility::default(),
CameraRoomRestraint,
PointLight2D { radius: 30.0 },
));
}
fn follow_detach_system(
mouse_input: Res<Input<MouseButton>>,
mut mouse_events: EventReader<MouseMotion>,
mut camera_query: Query<&mut GameCamera>,
) {
let raw_mouse_motion: Vec2 = mouse_events.iter().map(|e| e.delta).sum();
for mut camera in camera_query.iter_mut() {
if camera.mode != CameraMode::Free
&& mouse_input.pressed(MouseButton::Middle)
&& raw_mouse_motion != Vec2::ZERO
{
camera.mode = CameraMode::Free
}
}
}
fn free_system(
mut free_query: Query<(&mut Transform, &GameCamera, &OrthographicProjection)>,
mut mouse_events: EventReader<MouseMotion>,
mouse_input: Res<Input<MouseButton>>,
) {
let raw_mouse_motion: Vec2 = mouse_events.iter().map(|e| e.delta).sum();
for (mut transform, camera, projection) in free_query.iter_mut() {
if camera.mode == CameraMode::Free {
let mouse_motion = raw_mouse_motion * projection.scale * Vec2::new(-1.0, 1.0);
if mouse_input.pressed(MouseButton::Middle) {
input_movement(&mut transform, mouse_motion)
}
}
}
}
fn follow_system(
time: Res<Time>,
follow_query: Query<(Entity, &CameraFollow, Option<&GameCamera>)>,
mut transform_query: Query<&mut Transform>,
) {
for (entity, follow, camera) in follow_query.iter() {
if camera.map_or(true, |cam| cam.mode == CameraMode::Follow) {
if let Some(target_entity) = follow.target {
let target = transform_query
.get(target_entity)
.ok()
.map_or(Vec3::ZERO, |transform| transform.translation);
let target = Vec3 { z: 999.9, ..target };
if let Ok(mut camera_transform) = transform_query.get_mut(entity) {
follow_movement(
&mut camera_transform,
&follow.movement,
target,
time.delta_seconds(),
)
}
}
}
}
}
fn room_restraint(
mut camera_query: Query<(&mut Transform, &OrthographicProjection), With<CameraRoomRestraint>>,
level_query: Query<(&GlobalTransform, &Handle<LdtkLevel>), Without<OrthographicProjection>>,
level_selection: Res<LevelSelection>,
ldtk_levels: Res<Assets<LdtkLevel>>,
) {
for (mut camera_transform, projection) in camera_query.iter_mut() {
for (level_transform, level_handle) in &level_query {
if let Some(ldtk_level) = ldtk_levels.get(level_handle) {
let level = &ldtk_level.level;
if level_selection.is_match(&0, level) {
let top = camera_transform.translation.y + projection.area.max.y;
let bottom = camera_transform.translation.y + projection.area.min.y;
let left = camera_transform.translation.x + projection.area.min.x;
let right = camera_transform.translation.x + projection.area.max.x;
let top_limit = level_transform.translation().y + level.px_hei as f32;
let bottom_limit = level_transform.translation().y;
let left_limit = level_transform.translation().x;
let right_limit = level_transform.translation().x + level.px_wid as f32;
let top_move = (top_limit - top).min(0.0);
let bottom_move = (bottom_limit - bottom).max(0.0);
let left_move = (left_limit - left).max(0.0);
let right_move = (right_limit - right).min(0.0);
// Move camera back to room
camera_transform.translation.x += left_move + right_move;
camera_transform.translation.y += top_move + bottom_move;
// Center camera on room if room is too small
if top - bottom > level.px_hei as f32 {
camera_transform.translation.y = (top_limit + bottom_limit) / 2.0;
}
if right - left > level.px_wid as f32 {
camera_transform.translation.x = (right_limit + left_limit) / 2.0;
}
}
}
}
}
}
fn follow_movement(
transform: &mut Transform,
movement: &FollowMovement,
target: Vec3,
delta_time: f32,
) {
match movement {
FollowMovement::Instant => {
transform.translation = target;
}
FollowMovement::Linear(speed) => {
transform.translation =
move_towards_vec3(transform.translation, target, speed * delta_time);
}
FollowMovement::Smooth(speed) => {
transform.translation =
vec3_lerp(transform.translation, target, (speed * delta_time).min(1.0));
}
}
}
fn input_movement(transform: &mut Transform, movement: Vec2) {
transform.translation += movement.extend(0.0);
}
fn control_light(
mut mouse_scroll_events: EventReader<MouseWheel>,
mut point_light_query: Query<&mut PointLight2D>,
mut spot_light_query: Query<&mut SpotLight2D>,
) {
let mut scrolling = 0.0;
for event in mouse_scroll_events.iter() {
match event.unit {
MouseScrollUnit::Line => scrolling += event.y * 0.05,
MouseScrollUnit::Pixel => scrolling += event.y * 0.025,
}
}
for mut point_light in point_light_query.iter_mut() {
point_light.radius *= 1.0 + scrolling;
}
for mut spot_light in spot_light_query.iter_mut() {
spot_light.radius *= 1.0 + scrolling;
}
}

172
src/game/darkness.rs Normal file
View File

@ -0,0 +1,172 @@
use bevy::{
prelude::*,
render::{mesh::Indices, render_resource::ShaderType},
sprite::{Material2dPlugin, Mesh2dHandle},
};
use bevy_ecs_ldtk::{LdtkLevel, LevelEvent};
mod material;
pub use material::*;
// Needs to be the same as in darkness.wgsl
pub const MAX_POINT_LIGHTS: usize = 64;
pub const MAX_SPOT_LIGHTS: usize = 64;
pub struct DarknessPlugin;
impl Plugin for DarknessPlugin {
fn build(&self, app: &mut App) {
app.register_type::<PointLight2D>()
.register_type::<SpotLight2D>()
.register_asset_reflect::<DarknessMaterial>()
.add_plugins(Material2dPlugin::<DarknessMaterial>::default())
.add_systems(Update, add_to_level)
.add_systems(Last, prepare_lights);
}
}
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub struct PointLight2D {
pub radius: f32,
}
#[derive(Copy, Clone, Debug, Default, Reflect, ShaderType)]
pub(crate) struct GpuPointLight2D {
pub(crate) position: Vec2,
pub(crate) radius: f32,
}
#[derive(Component, Reflect, Default, Debug)]
#[reflect(Component)]
pub struct SpotLight2D {
pub radius: f32,
pub angle: f32,
}
#[derive(Copy, Clone, Debug, Default, Reflect, ShaderType)]
pub(crate) struct GpuSpotLight2D {
pub(crate) position: Vec2,
pub(crate) radius: f32,
pub(crate) rotation: f32,
pub(crate) angle: f32,
padding1: u32,
padding2: u32,
padding3: u32,
}
fn add_to_level(
mut commands: Commands,
mut level_events: EventReader<LevelEvent>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<DarknessMaterial>>,
level_query: Query<(Entity, &Handle<LdtkLevel>)>,
levels: Res<Assets<LdtkLevel>>,
) {
for event in level_events.iter() {
match event {
LevelEvent::Spawned(iid) => {
for (entity, ldtk_level) in level_query
.iter()
.map(|(entity, handle)| {
(
entity,
levels.get(handle).expect(
format!("LDTK level '{}' should have been loaded", iid).as_str(),
),
)
})
.filter(|(_, ldtk_level)| ldtk_level.level.iid == *iid)
{
let size = Vec2::new(
ldtk_level.level.px_wid as f32,
ldtk_level.level.px_hei as f32,
);
let mut plane =
Mesh::new(bevy::render::render_resource::PrimitiveTopology::TriangleStrip);
// 2 -------- 3
// | |
// | |
// 0 -------- 1
let indices = vec![0, 1, 2, 3];
let positions = vec![
[0.0, 0.0, 0.0],
[size.x, 0.0, 0.0],
[0.0, size.y, 0.0],
[size.x, size.y, 0.0],
];
let normals = vec![
[0.0, 0.0, 1.0],
[0.0, 0.0, 1.0],
[0.0, 0.0, 1.0],
[0.0, 0.0, 1.0],
];
let uvs = vec![[0.0, 1.0], [1.0, 1.0], [0.0, 0.0], [1.0, 0.0]];
plane.set_indices(Some(Indices::U32(indices)));
plane.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
plane.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
plane.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
commands.entity(entity).with_children(|builder| {
builder.spawn((
Name::new("Darkness plane"),
DarknessMeshBundle {
transform: Transform::from_xyz(0.0, 0.0, 100.0),
mesh: Mesh2dHandle(meshes.add(plane)),
material: materials.add(DarknessMaterial::default()),
..default()
},
));
});
}
}
_ => (),
}
}
}
fn prepare_lights(
mut materials: ResMut<Assets<DarknessMaterial>>,
material_query: Query<&Handle<DarknessMaterial>>,
point_light_query: Query<(&GlobalTransform, &PointLight2D)>,
spot_light_query: Query<(&GlobalTransform, &SpotLight2D)>,
) {
let point_lights: Vec<(_, _)> = point_light_query.iter().collect();
let spot_lights: Vec<(_, _)> = spot_light_query.iter().collect();
for handle in &material_query {
let material = match materials.get_mut(handle) {
Some(material) => material,
None => continue,
};
material.point_light_count = point_lights.len() as i32;
point_lights
.iter()
.enumerate()
.for_each(|(i, (transform, light))| {
material.point_lights[i] = GpuPointLight2D {
position: transform.translation().truncate(),
radius: light.radius,
}
});
material.spot_light_count = spot_lights.len() as i32;
spot_lights
.iter()
.enumerate()
.for_each(|(i, (transform, light))| {
material.spot_lights[i] = GpuSpotLight2D {
position: transform.translation().truncate(),
radius: light.radius,
rotation: f32::atan2(transform.right().y, transform.right().x),
angle: light.angle,
padding1: 0,
padding2: 0,
padding3: 0,
};
});
}
}

View File

@ -0,0 +1,55 @@
use bevy::{
prelude::*,
reflect::TypeUuid,
render::render_resource::{AsBindGroup, ShaderRef},
sprite::{Material2d, MaterialMesh2dBundle},
};
use super::*;
pub type DarknessMeshBundle = MaterialMesh2dBundle<DarknessMaterial>;
#[derive(AsBindGroup, Reflect, Debug, Clone, TypeUuid)]
#[reflect(Default, Debug)]
#[uuid = "7229bcd8-c4f8-4067-a4c2-98b04e203fd5"]
pub struct DarknessMaterial {
#[uniform(0)]
pub color: Color,
#[uniform(1)]
pub(crate) point_light_count: i32,
#[uniform(2)]
pub(crate) point_lights: [GpuPointLight2D; MAX_POINT_LIGHTS],
#[uniform(3)]
pub(crate) spot_light_count: i32,
#[uniform(4)]
pub(crate) spot_lights: [GpuSpotLight2D; MAX_SPOT_LIGHTS],
}
impl Default for DarknessMaterial {
fn default() -> Self {
Self {
color: Color::Rgba {
red: 0.0,
green: 0.0,
blue: 0.0,
alpha: 0.75,
},
point_light_count: 0,
point_lights: [GpuPointLight2D::default(); MAX_POINT_LIGHTS],
spot_light_count: 0,
spot_lights: [GpuSpotLight2D::default(); MAX_SPOT_LIGHTS],
}
}
}
impl DarknessMaterial {
pub fn new(color: Color) -> Self {
Self { color, ..default() }
}
}
impl Material2d for DarknessMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/darkness.wgsl".into()
}
}

305
src/game/ldtk.rs Normal file
View File

@ -0,0 +1,305 @@
use bevy::ecs::prelude::*;
use bevy::prelude::*;
use bevy::utils::HashMap;
use bevy_ecs_ldtk::prelude::*;
use bevy_rapier2d::prelude::*;
use std::collections::HashSet;
use crate::util::Vector2I;
pub struct LdtkHelperPlugin;
impl From<IVec2> for Vector2I {
fn from(value: IVec2) -> Self {
Self {
x: value.x,
y: value.y,
}
}
}
impl Plugin for LdtkHelperPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(LdtkPlugin)
.insert_resource(LdtkSettings {
level_spawn_behavior: LevelSpawnBehavior::UseWorldTranslation {
load_level_neighbors: false,
},
set_clear_color: SetClearColor::FromLevelBackground,
..default()
})
.add_event::<EntityInstanceAdded>()
.register_ldtk_int_cell::<WallBundle>(1)
.insert_resource(WordlyInstances::default())
.add_systems(Update, entity_instance_events)
.add_systems(PostUpdate, (entity_namer, unique_handler));
}
}
#[derive(Event)]
pub struct EntityInstanceAdded {
pub entity: Entity,
pub instance: EntityInstance,
}
#[derive(Resource, Default)]
pub struct WordlyInstances {
pub def_uid_map: HashMap<i32, Entity>,
}
#[derive(Bundle, LdtkIntCell, Default, Clone, Debug)]
pub struct WallBundle {
wall: Wall,
}
#[derive(Component, Reflect, Default, Clone, Copy, Debug)]
#[reflect(Component)]
pub struct Wall;
fn entity_instance_events(
query: Query<(Entity, &EntityInstance), Added<EntityInstance>>,
worldly_instances: Res<WordlyInstances>,
mut events: EventWriter<EntityInstanceAdded>,
mut commands: Commands,
) {
for (entity, instance) in query.iter() {
// Spawn the entity if it's not in the unique instances list (or if the old one is deleted)
// TODO: Detect deleted entities safely: https://github.com/bevyengine/bevy/issues/3845
if worldly_instances
.def_uid_map
.get(&instance.def_uid)
.map_or(true, |ent| commands.get_entity(*ent).is_none())
{
events.send(EntityInstanceAdded {
entity,
instance: instance.clone(),
});
}
}
}
fn entity_namer(
mut commands: Commands,
mut events: EventReader<EntityInstanceAdded>,
nameless_query: Query<(), Without<Name>>,
) {
for event in events.iter() {
if nameless_query.contains(event.entity) {
commands
.entity(event.entity)
.insert(Name::new(event.instance.identifier.clone()));
}
}
}
fn unique_handler(
query: Query<(Entity, &EntityInstance), Added<Worldly>>,
mut worldly_instances: ResMut<WordlyInstances>,
) {
for (entity, instance) in query.iter() {
worldly_instances
.def_uid_map
.insert(instance.def_uid, entity);
}
}
pub trait FieldValueGetter {
fn find_string(&self, identifier: &str) -> Option<String>;
fn find_i32(&self, identifier: &str) -> Option<i32>;
}
impl FieldValueGetter for EntityInstance {
fn find_string(&self, identifier: &str) -> Option<String> {
match self
.field_instances
.iter()
.find(|fi| fi.identifier.as_str() == identifier)?
.value
.clone()
{
FieldValue::String(value) => value,
_ => None,
}
}
fn find_i32(&self, identifier: &str) -> Option<i32> {
match self
.field_instances
.iter()
.find(|fi| fi.identifier.as_str() == identifier)?
.value
{
FieldValue::Int(value) => value,
_ => None,
}
}
}
/// Wall collider system from bevy_ecs_ldtk example.
///
/// Spawns colliders for the walls of a level
///
/// You could just insert a ColliderBundle in to the WallBundle,
/// but this spawns a different collider for EVERY wall tile.
/// This approach leads to bad performance.
///
/// Instead, by flagging the wall tiles and spawning the collisions later,
/// we can minimize the amount of colliding entities.
///
/// The algorithm used here is a nice compromise between simplicity, speed,
/// and a small number of rectangle colliders.
/// In basic terms, it will:
/// 1. consider where the walls are
/// 2. combine wall tiles into flat "plates" in each individual row
/// 3. combine the plates into rectangles across multiple rows wherever possible
/// 4. spawn colliders for each rectangle
pub fn wall_setup(
mut commands: Commands,
wall_query: Query<(&GridCoords, &Parent), Added<Wall>>,
parent_query: Query<&Parent, Without<Wall>>,
level_query: Query<(Entity, &Handle<LdtkLevel>)>,
levels: Res<Assets<LdtkLevel>>,
) {
/// Represents a wide wall that is 1 tile tall
/// Used to spawn wall collisions
#[derive(Clone, Eq, PartialEq, Debug, Default, Hash)]
struct Plate {
left: i32,
right: i32,
}
/// A simple rectangle type representing a wall of any size
struct Rect {
left: i32,
right: i32,
top: i32,
bottom: i32,
}
// Consider where the walls are
// storing them as GridCoords in a HashSet for quick, easy lookup
//
// The key of this map will be the entity of the level the wall belongs to.
// This has two consequences in the resulting collision entities:
// 1. it forces the walls to be split along level boundaries
// 2. it lets us easily add the collision entities as children of the appropriate level entity
let mut level_to_wall_locations: HashMap<Entity, HashSet<GridCoords>> = HashMap::new();
wall_query.for_each(|(&grid_coords, parent)| {
// An intgrid tile's direct parent will be a layer entity, not the level entity
// To get the level entity, you need the tile's grandparent.
// This is where parent_query comes in.
if let Ok(grandparent) = parent_query.get(parent.get()) {
level_to_wall_locations
.entry(grandparent.get())
.or_default()
.insert(grid_coords);
}
});
if !wall_query.is_empty() {
level_query.for_each(|(level_entity, level_handle)| {
if let Some(level_walls) = level_to_wall_locations.get(&level_entity) {
let level = levels
.get(level_handle)
.expect("Level should be loaded by this point");
let LayerInstance {
c_wid: width,
c_hei: height,
grid_size,
..
} = level
.level
.layer_instances
.clone()
.expect("Level asset should have layers")[0];
// combine wall tiles into flat "plates" in each individual row
let mut plate_stack: Vec<Vec<Plate>> = Vec::new();
for y in 0..height {
let mut row_plates: Vec<Plate> = Vec::new();
let mut plate_start = None;
// + 1 to the width so the algorithm "terminates" plates that touch the right edge
for x in 0..width + 1 {
match (plate_start, level_walls.contains(&GridCoords { x, y })) {
(Some(s), false) => {
row_plates.push(Plate {
left: s,
right: x - 1,
});
plate_start = None;
}
(None, true) => plate_start = Some(x),
_ => (),
}
}
plate_stack.push(row_plates);
}
// combine "plates" into rectangles across multiple rows
let mut rect_builder: HashMap<Plate, Rect> = HashMap::new();
let mut prev_row: Vec<Plate> = Vec::new();
let mut wall_rects: Vec<Rect> = Vec::new();
// an extra empty row so the algorithm "finishes" the rects that touch the top edge
plate_stack.push(Vec::new());
for (y, current_row) in plate_stack.into_iter().enumerate() {
for prev_plate in &prev_row {
if !current_row.contains(prev_plate) {
// remove the finished rect so that the same plate in the future starts a new rect
if let Some(rect) = rect_builder.remove(prev_plate) {
wall_rects.push(rect);
}
}
}
for plate in &current_row {
rect_builder
.entry(plate.clone())
.and_modify(|e| e.top += 1)
.or_insert(Rect {
bottom: y as i32,
top: y as i32,
left: plate.left,
right: plate.right,
});
}
prev_row = current_row;
}
commands.entity(level_entity).with_children(|level| {
// Spawn colliders for every rectangle..
// Making the collider a child of the level serves two purposes:
// 1. Adjusts the transforms to be relative to the level for free
// 2. the colliders will be despawned automatically when levels unload
for wall_rect in wall_rects {
level
.spawn_empty()
.insert(Collider::cuboid(
(wall_rect.right as f32 - wall_rect.left as f32 + 1.)
* grid_size as f32
/ 2.,
(wall_rect.top as f32 - wall_rect.bottom as f32 + 1.)
* grid_size as f32
/ 2.,
))
.insert(RigidBody::Fixed)
.insert(Friction::new(1.0))
.insert(Transform::from_xyz(
(wall_rect.left + wall_rect.right + 1) as f32 * grid_size as f32
/ 2.,
(wall_rect.bottom + wall_rect.top + 1) as f32 * grid_size as f32
/ 2.,
0.,
))
.insert(GlobalTransform::default());
}
});
}
});
}
}

21
src/game_setup.rs Normal file
View File

@ -0,0 +1,21 @@
use bevy::{prelude::*, window::WindowResolution};
pub struct GameSetupPlugin;
impl Plugin for GameSetupPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(ClearColor(Color::BLACK)).add_plugins(
DefaultPlugins
.set(WindowPlugin {
primary_window: Some(Window {
resolution: WindowResolution::new(512.0 * 2.0, 320.0 * 2.0),
title: "Moonlit".to_string(),
resizable: false,
..default()
}),
..default()
})
.set(ImagePlugin::default_nearest()),
);
}
}

14
src/main.rs Normal file
View File

@ -0,0 +1,14 @@
use bevy::prelude::*;
use util::create_app_graphs;
pub mod debug;
pub mod game;
pub mod game_setup;
pub mod util;
fn main() {
let mut app = App::new();
game::init(&mut app);
create_app_graphs(&mut app);
app.run();
}

180
src/util.rs Normal file
View File

@ -0,0 +1,180 @@
use bevy::{ecs::schedule::ScheduleLabel, prelude::*};
use bevy_mod_debugdump::{render_graph, render_graph_dot, schedule_graph, schedule_graph_dot};
use core::{fmt, ops};
use std::{borrow::Borrow, fs, path::Path};
mod basis;
mod transform_f64;
mod vector2;
mod vector2_i32;
mod vector3;
mod vector3_i32;
pub use basis::*;
pub use transform_f64::*;
pub use vector2::*;
pub use vector2_i32::*;
pub use vector3::*;
pub use vector3_i32::*;
pub trait VectorComponent:
Sized
+ Copy
+ PartialOrd
+ Reflect
+ fmt::Display
+ ops::Add<Output = Self>
+ ops::Neg<Output = Self>
+ ops::Sub<Output = Self>
+ ops::Mul<Output = Self>
+ ops::Div<Output = Self>
+ num_traits::identities::Zero
+ num_traits::identities::One
+ num_traits::sign::Signed
{
fn min(self, b: Self) -> Self;
fn max(self, b: Self) -> Self;
}
impl<T> VectorComponent for T
where
T: Sized
+ Copy
+ PartialOrd
+ Reflect
+ fmt::Display
+ ops::Neg<Output = T>
+ ops::Add<Output = T>
+ ops::Sub<Output = T>
+ ops::Mul<Output = T>
+ ops::Div<Output = T>
+ num_traits::identities::Zero
+ num_traits::identities::One
+ num_traits::sign::Signed,
{
fn min(self, b: Self) -> Self {
if self < b {
self
} else {
b
}
}
fn max(self, b: Self) -> Self {
if self > b {
self
} else {
b
}
}
}
pub fn lerp<
T: Copy
+ ops::Add<Output = T>
+ ops::Sub<Output = T>
+ ops::Mul<Output = T>
+ num_traits::identities::One,
>(
a: T,
b: T,
t: T,
) -> T {
a * (T::one() - t) + b * t
}
pub fn inverse_lerp<T: Copy + ops::Sub<Output = T> + ops::Div<Output = T>>(
a: T,
b: T,
value: T,
) -> T {
(value - a) / (b - a)
}
pub fn vec2_lerp(a: Vec2, b: Vec2, t: f32) -> Vec2 {
Vec2 {
x: lerp(a.x, b.x, t),
y: lerp(a.y, b.y, t),
}
}
pub fn vec3_lerp(a: Vec3, b: Vec3, t: f32) -> Vec3 {
Vec3 {
x: lerp(a.x, b.x, t),
y: lerp(a.y, b.y, t),
z: lerp(a.z, b.z, t),
}
}
pub fn move_towards_vec2(from: Vec2, to: Vec2, amount: f32) -> Vec2 {
let diff = to - from;
let length = diff.length();
if length <= f32::EPSILON {
return from;
}
from + diff.normalize() * length.min(amount)
}
pub fn move_towards_vec3(from: Vec3, to: Vec3, amount: f32) -> Vec3 {
let diff = to - from;
let length = diff.length();
if length <= f32::EPSILON {
return from;
}
from + diff.normalize() * length.min(amount)
}
pub fn loop_value(from: f32, to: f32, value: f32) -> f32 {
let range = to - from;
if !range.is_normal() {
return from;
}
value - inverse_lerp(from, to, value).floor() * range
}
pub fn create_app_graphs(app: &mut App) {
let schedules: Vec<Box<dyn ScheduleLabel>> = app
.world
.borrow()
.resource::<Schedules>()
.iter()
.map(|(label, _)| label.dyn_clone())
.collect();
println!("Writing {} schedule graphs", schedules.len());
schedules.iter().for_each(|schedule_label| {
let path = Path::new("graphs").join(format!("schedule_{schedule_label:?}.dot"));
write_schedule_graph(app, &path, &schedule_label.to_owned());
println!("\t- {}", path.to_string_lossy())
});
println!("Writing render graph");
{
let path = Path::new("graphs").join("render.dot");
write_render_graph(app, &path);
println!("\t- {}", path.to_string_lossy());
}
}
fn write_schedule_graph(app: &mut App, path: &Path, schedule: &dyn ScheduleLabel) {
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(
path,
schedule_graph_dot(
app,
schedule.dyn_clone(),
&schedule_graph::settings::Settings::default(),
),
)
.unwrap();
}
fn write_render_graph(app: &mut App, path: &Path) {
fs::create_dir_all(path.parent().unwrap()).unwrap();
fs::write(
path,
render_graph_dot(app, &render_graph::settings::Settings::default()),
)
.unwrap();
}

42
src/util/basis.rs Normal file
View File

@ -0,0 +1,42 @@
use bevy::prelude::*;
use super::{Vector3, VectorComponent};
#[derive(PartialEq, Eq, Hash, Clone, Copy, Debug, Reflect)]
pub struct Basis<T: VectorComponent> {
pub x: Vector3<T>,
pub y: Vector3<T>,
pub z: Vector3<T>,
}
impl<T: VectorComponent> Default for Basis<T> {
fn default() -> Self {
Self {
x: Vector3::x(),
y: Vector3::y(),
z: Vector3::z(),
}
}
}
impl<T: VectorComponent> Basis<T> {
pub fn identity() -> Self {
Self::default()
}
pub fn inverse(&self) -> Self {
Self {
x: self.x * -T::one(),
y: self.y * -T::one(),
z: self.z * -T::one(),
}
}
pub fn transform(&self, point: Vector3<T>) -> Vector3<T> {
self.x * point.x + self.y * point.y + self.z * point.z
}
pub fn inverse_transform(&self, point: Vector3<T>) -> Vector3<T> {
self.inverse().transform(point)
}
}

34
src/util/transform_f64.rs Normal file
View File

@ -0,0 +1,34 @@
use bevy::prelude::*;
mod basis_f64;
mod vector3_f64;
pub use basis_f64::*;
pub use vector3_f64::*;
pub struct DoublePrecisionPlugin;
impl Plugin for DoublePrecisionPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(WorldOffset::default())
.register_type::<TranslationD>()
.add_systems(PostUpdate, translation_system);
}
}
#[derive(Resource, Reflect, Default)]
#[reflect(Resource)]
pub struct WorldOffset(pub Vector3D);
#[derive(Component, Reflect, Default)]
#[reflect(Component)]
pub struct TranslationD(pub Vector3D);
fn translation_system(
mut query: Query<(&mut Transform, &TranslationD)>,
world_offset: Res<WorldOffset>,
) {
for (mut real_transform, translation) in query.iter_mut() {
real_transform.translation = Vec3::from(translation.0 - world_offset.0);
}
}

View File

@ -0,0 +1,23 @@
use crate::util::Basis;
use super::Vector3D;
pub type BasisD = Basis<f64>;
impl BasisD {
pub fn from_euler(yaw: f64, pitch: f64, roll: f64) -> Self {
let basis = Self::identity();
let basis = basis.rotate_by_axis(&basis.y, yaw);
let basis = basis.rotate_by_axis(&basis.x, pitch);
let basis = basis.rotate_by_axis(&basis.z, roll);
basis
}
pub fn rotate_by_axis(&self, axis: &Vector3D, angle: f64) -> Self {
Self {
x: self.x.rotate_around_axis(axis, angle),
y: self.y.rotate_around_axis(axis, angle),
z: self.z.rotate_around_axis(axis, angle),
}
}
}

View File

@ -0,0 +1,74 @@
use bevy::prelude::*;
use crate::util::Vector3;
pub type Vector3D = Vector3<f64>;
impl Vector3D {
pub const ZERO: Self = Self {
x: 0.0,
y: 0.0,
z: 0.0,
};
pub const ONE: Self = Self {
x: 1.0,
y: 1.0,
z: 1.0,
};
pub const UP: Self = Self {
x: 0.0,
y: 1.0,
z: 0.0,
};
pub const DOWN: Self = Self {
x: 0.0,
y: -1.0,
z: 0.0,
};
pub const LEFT: Self = Self {
x: -1.0,
y: 0.0,
z: 0.0,
};
pub const RIGHT: Self = Self {
x: 1.0,
y: 0.0,
z: 0.0,
};
pub const FORWARD: Self = Self {
x: 0.0,
y: 0.0,
z: -1.0,
};
pub const BACK: Self = Self {
x: 0.0,
y: 0.0,
z: 1.0,
};
pub fn rotate_around_axis(&self, axis: &Self, angle: f64) -> Self {
*self * angle.cos()
+ axis.cross(self) * angle.sin()
+ *axis * axis.dot(self) * (1.0 - angle.cos())
}
}
impl From<Vec3> for Vector3D {
fn from(vec: Vec3) -> Self {
Self {
x: vec.x as f64,
y: vec.y as f64,
z: vec.y as f64,
}
}
}
impl From<Vector3D> for Vec3 {
fn from(vec: Vector3D) -> Self {
Vec3 {
x: vec.x as f32,
y: vec.y as f32,
z: vec.z as f32,
}
}
}

152
src/util/vector2.rs Normal file
View File

@ -0,0 +1,152 @@
use bevy::reflect::Reflect;
use core::{fmt, ops};
use super::VectorComponent;
#[derive(PartialEq, Eq, Hash, Clone, Copy, Default, Debug, Reflect)]
pub struct Vector2<T: VectorComponent> {
pub x: T,
pub y: T,
}
impl<T: VectorComponent> Vector2<T> {
pub fn x() -> Self {
Self {
x: T::one(),
y: T::zero(),
}
}
pub fn y() -> Self {
Self {
x: T::zero(),
y: T::one(),
}
}
pub fn zero() -> Self {
Self {
x: T::zero(),
y: T::zero(),
}
}
pub fn one() -> Self {
Self {
x: T::one(),
y: T::one(),
}
}
pub fn up() -> Self {
Self {
x: T::zero(),
y: T::one(),
}
}
pub fn down() -> Self {
Self {
x: T::zero(),
y: -T::one(),
}
}
pub fn left() -> Self {
Self {
x: -T::one(),
y: T::zero(),
}
}
pub fn right() -> Self {
Self {
x: T::one(),
y: T::zero(),
}
}
pub fn new(x: T, y: T) -> Vector2<T> {
Vector2 { x, y }
}
pub fn min(&self, other: &Vector2<T>) -> Vector2<T> {
Vector2 {
x: VectorComponent::min(self.x, other.x),
y: VectorComponent::min(self.y, other.y),
}
}
pub fn max(&self, other: &Vector2<T>) -> Vector2<T> {
Vector2 {
x: VectorComponent::max(self.x, other.x),
y: VectorComponent::max(self.y, other.y),
}
}
}
impl<T: VectorComponent> fmt::Display for Vector2<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
impl<T: VectorComponent> ops::Add<Vector2<T>> for Vector2<T> {
type Output = Vector2<T>;
fn add(self, rhs: Vector2<T>) -> Self::Output {
Vector2 {
x: self.x + rhs.x,
y: self.y + rhs.y,
}
}
}
impl<T: VectorComponent> ops::Neg for Vector2<T> {
type Output = Vector2<T>;
fn neg(self) -> Self::Output {
Vector2 {
x: -self.x,
y: -self.y,
}
}
}
impl<T: VectorComponent> ops::Sub<Vector2<T>> for Vector2<T> {
type Output = Vector2<T>;
fn sub(self, rhs: Vector2<T>) -> Self::Output {
self + (-rhs)
}
}
impl<T: VectorComponent> ops::Mul<Vector2<T>> for Vector2<T> {
type Output = Vector2<T>;
fn mul(self, rhs: Vector2<T>) -> Self::Output {
Vector2 {
x: self.x * rhs.x,
y: self.y * rhs.y,
}
}
}
impl<T: VectorComponent> ops::Mul<T> for Vector2<T> {
type Output = Vector2<T>;
fn mul(self, rhs: T) -> Self::Output {
Vector2 {
x: self.x * rhs,
y: self.y * rhs,
}
}
}
impl<T: VectorComponent> ops::Div<Vector2<T>> for Vector2<T> {
type Output = Vector2<T>;
fn div(self, rhs: Vector2<T>) -> Self::Output {
Vector2 {
x: self.x / rhs.x,
y: self.y / rhs.y,
}
}
}
impl<T: VectorComponent> ops::Div<T> for Vector2<T> {
type Output = Vector2<T>;
fn div(self, rhs: T) -> Self::Output {
Vector2 {
x: self.x / rhs,
y: self.y / rhs,
}
}
}

46
src/util/vector2_i32.rs Normal file
View File

@ -0,0 +1,46 @@
use bevy::prelude::*;
use super::Vector2;
pub type Vector2I = Vector2<i32>;
impl Vector2I {
pub const ZERO: Vector2I = Vector2I { x: 0, y: 0 };
pub const ONE: Vector2I = Vector2I { x: 1, y: 1 };
pub const UP: Vector2I = Vector2I { x: 0, y: 1 };
pub const DOWN: Vector2I = Vector2I { x: 0, y: -1 };
pub const LEFT: Vector2I = Vector2I { x: -1, y: 0 };
pub const RIGHT: Vector2I = Vector2I { x: 1, y: 0 };
pub fn angle(&self) -> f32 {
(self.y as f32).atan2(self.x as f32)
}
}
impl From<Vec2> for Vector2I {
fn from(vec: Vec2) -> Self {
Self {
x: vec.x as i32,
y: vec.y as i32,
}
}
}
impl From<Vector2I> for Vec2 {
fn from(vec: Vector2I) -> Self {
Vec2 {
x: vec.x as f32,
y: vec.y as f32,
}
}
}
impl From<Vector2I> for Vec3 {
fn from(vec: Vector2I) -> Self {
Vec3 {
x: vec.x as f32,
y: vec.y as f32,
z: 0.0,
}
}
}

202
src/util/vector3.rs Normal file
View File

@ -0,0 +1,202 @@
use bevy::reflect::Reflect;
use core::{fmt, ops};
use super::VectorComponent;
#[derive(PartialEq, Eq, Hash, Clone, Copy, Default, Debug, Reflect)]
pub struct Vector3<T: VectorComponent> {
pub x: T,
pub y: T,
pub z: T,
}
impl<T: VectorComponent> Vector3<T> {
pub fn x() -> Self {
Self {
x: T::one(),
y: T::zero(),
z: T::zero(),
}
}
pub fn y() -> Self {
Self {
x: T::zero(),
y: T::one(),
z: T::zero(),
}
}
pub fn z() -> Self {
Self {
x: T::zero(),
y: T::zero(),
z: T::one(),
}
}
pub fn zero() -> Self {
Self {
x: T::zero(),
y: T::zero(),
z: T::zero(),
}
}
pub fn one() -> Self {
Self {
x: T::one(),
y: T::one(),
z: T::one(),
}
}
pub fn up() -> Self {
Self {
x: T::zero(),
y: T::one(),
z: T::zero(),
}
}
pub fn down() -> Self {
Self {
x: T::zero(),
y: -T::one(),
z: T::zero(),
}
}
pub fn left() -> Self {
Self {
x: -T::one(),
y: T::zero(),
z: T::zero(),
}
}
pub fn right() -> Self {
Self {
x: T::one(),
y: T::zero(),
z: T::zero(),
}
}
pub fn forward() -> Self {
Self {
x: T::zero(),
y: T::zero(),
z: -T::one(),
}
}
pub fn back() -> Self {
Self {
x: T::zero(),
y: T::zero(),
z: T::one(),
}
}
pub fn new(x: T, y: T, z: T) -> Self {
Vector3 { x, y, z }
}
pub fn min(&self, other: &Self) -> Self {
Vector3 {
x: VectorComponent::min(self.x, other.x),
y: VectorComponent::min(self.y, other.y),
z: VectorComponent::min(self.z, other.z),
}
}
pub fn max(&self, other: &Self) -> Self {
Vector3 {
x: VectorComponent::max(self.x, other.x),
y: VectorComponent::max(self.y, other.y),
z: VectorComponent::max(self.z, other.z),
}
}
pub fn dot(&self, other: &Self) -> T {
self.x * other.x + self.y * other.y + self.z * other.z
}
pub fn cross(&self, other: &Self) -> Self {
Self {
x: self.y * other.z - self.z * other.y,
y: self.z * other.x - self.x * other.z,
z: self.x * other.y - self.y * other.x,
}
}
}
impl<T: VectorComponent> fmt::Display for Vector3<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {}, {})", self.x, self.y, self.z)
}
}
impl<T: VectorComponent> ops::Add<Vector3<T>> for Vector3<T> {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Vector3 {
x: self.x + rhs.x,
y: self.y + rhs.y,
z: self.z + rhs.z,
}
}
}
impl<T: VectorComponent> ops::Neg for Vector3<T> {
type Output = Self;
fn neg(self) -> Self::Output {
Vector3 {
x: -self.x,
y: -self.y,
z: -self.z,
}
}
}
impl<T: VectorComponent> ops::Sub<Vector3<T>> for Vector3<T> {
type Output = Self;
fn sub(self, rhs: Self) -> Self::Output {
self + (-rhs)
}
}
impl<T: VectorComponent> ops::Mul<Vector3<T>> for Vector3<T> {
type Output = Self;
fn mul(self, rhs: Self) -> Self::Output {
Vector3 {
x: self.x * rhs.x,
y: self.y * rhs.y,
z: self.z * rhs.z,
}
}
}
impl<T: VectorComponent> ops::Mul<T> for Vector3<T> {
type Output = Self;
fn mul(self, rhs: T) -> Self::Output {
Vector3 {
x: self.x * rhs,
y: self.y * rhs,
z: self.z * rhs,
}
}
}
impl<T: VectorComponent> ops::Div<Vector3<T>> for Vector3<T> {
type Output = Self;
fn div(self, rhs: Vector3<T>) -> Self::Output {
Vector3 {
x: self.x / rhs.x,
y: self.y / rhs.y,
z: self.z / rhs.z,
}
}
}
impl<T: VectorComponent> ops::Div<T> for Vector3<T> {
type Output = Self;
fn div(self, rhs: T) -> Self::Output {
Vector3 {
x: self.x / rhs,
y: self.y / rhs,
z: self.z / rhs,
}
}
}

36
src/util/vector3_i32.rs Normal file
View File

@ -0,0 +1,36 @@
use bevy::prelude::*;
use super::Vector3;
pub type Vector3I = Vector3<i32>;
impl Vector3I {
pub const ZERO: Self = Self { x: 0, y: 0, z: 0 };
pub const ONE: Self = Self { x: 1, y: 1, z: 1 };
pub const UP: Self = Self { x: 0, y: 1, z: 0 };
pub const DOWN: Self = Self { x: 0, y: -1, z: 0 };
pub const LEFT: Self = Self { x: -1, y: 0, z: 0 };
pub const RIGHT: Self = Self { x: 1, y: 0, z: 0 };
pub const FORWARD: Self = Self { x: 0, y: 0, z: -1 };
pub const BACK: Self = Self { x: 0, y: 0, z: 1 };
}
impl From<Vec3> for Vector3I {
fn from(vec: Vec3) -> Self {
Self {
x: vec.x as i32,
y: vec.y as i32,
z: vec.y as i32,
}
}
}
impl From<Vector3I> for Vec3 {
fn from(vec: Vector3I) -> Self {
Vec3 {
x: vec.x as f32,
y: vec.y as f32,
z: vec.z as f32,
}
}
}