4 Commits
Author SHA1 Message Date
mitchellandCopilot db5946a7de add a shader for a newline track
Co-authored-by: Copilot <copilot@github.com>
2026-05-02 22:38:12 -05:00
mitchell f509833262 start to a shader?? 2026-03-17 20:00:07 -05:00
mitchell db19b9c416 ui 2026-02-09 17:16:17 -06:00
mitchell 25896a5a92 add aeronet 2026-02-09 15:34:40 -06:00
18 changed files with 1524 additions and 812 deletions
-8
View File
@@ -1,8 +0,0 @@
# for Linux
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
# for Windows
[target.x86_64-pc-windows-msvc]
linker = "rust-lld.exe"
Generated
+846 -364
View File
File diff suppressed because it is too large Load Diff
+8 -12
View File
@@ -4,15 +4,11 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
avian2d = "0.6.1" aeronet = "0.19.0"
bevy = { version = "0.18.0", features = ["dynamic_linking"] } aeronet_replicon = { version = "0.19.0", features = ["client", "server"] }
avian2d = "0.5.0"
bevy = { version = "0.18.1", features = ["debug"] }
# Enable a small amount of optimization in the dev profile. bevy-inspector-egui = "0.36.0"
[profile.dev] bevy_replicon = "0.38.2"
opt-level = 1 component = "0.1.1"
rust-analyzer = "0.0.1"
# Enable a large amount of optimization in the dev profile for dependencies.
[profile.dev.package."*"]
opt-level = 3
Binary file not shown.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

+1
View File
@@ -0,0 +1 @@
/home/mitchell/Pictures/time_travel_logo.png
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 34 B

+1
View File
@@ -0,0 +1 @@
/home/mitchell/Pictures/player.png

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 34 B

+61
View File
@@ -0,0 +1,61 @@
struct Material {
p0: vec2<f32>,
c0: vec2<f32>,
c1: vec2<f32>,
p1: vec2<f32>,
width: f32,
color: vec4<f32>,
};
@group(2) @binding(0)
var<uniform> mat: Material;
fn bezier(p0: vec2<f32>, c0: vec2<f32>, c1: vec2<f32>, p1: vec2<f32>, t: f32) -> vec2<f32> {
let u = 1.0 - t;
return
u*u*u*p0 +
3.0*u*u*t*c0 +
3.0*u*t*t*c1 +
t*t*t*p1;
}
fn distance_to_bezier(p: vec2<f32>) -> f32 {
var min_d = 1e9;
let steps = 32;
var prev = mat.p0;
for (var i = 1; i <= steps; i++) {
let t = f32(i) / f32(steps);
let cur = bezier(mat.p0, mat.c0, mat.c1, mat.p1, t);
// distance to segment
let v = cur - prev;
let w = p - prev;
let t_seg = clamp(dot(w, v) / dot(v, v), 0.0, 1.0);
let proj = prev + t_seg * v;
min_d = min(min_d, distance(p, proj));
prev = cur;
}
return min_d;
}
@fragment
fn fragment(
@location(0) world_pos: vec3<f32>,
) -> @location(0) vec4<f32> {
let d = distance_to_bezier(world_pos.xy);
let half_w = mat.width * 0.5;
let aa = fwidth(d);
let alpha = smoothstep(half_w + aa, half_w - aa, d);
if (alpha <= 0.001) {
discard;
}
return vec4(mat.color.rgb, mat.color.a * alpha);
}
+29
View File
@@ -0,0 +1,29 @@
struct Material {
color_a: vec4<f32>,
color_b: vec4<f32>,
border_color: vec4<f32>,
time: f32,
speed: f32,
border_size: f32,
};
@group(2) @binding(0)
var<uniform> mat: Material;
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
let uv = in.uv;
// animate
let x = uv.x + params.time * params.speed;
let g = fract(x);
let fill = mix(params.color_a, params.color_b, g);
// borders
let top = step(uv.y, params.border_size);
let bottom = step(1.0 - params.border_size, uv.y);
let border_mask = max(top, bottom);
return mix(fill, params.border_color, border_mask);
}
+188
View File
@@ -0,0 +1,188 @@
const PI: f32 = 3.14159265358979323846264338327950288;
struct Uniforms {
/// The size of the canvas
size: vec2<f32>,
/// The length of the first horizontal segment
len_start: f32,
/// The length of the last horizontal segment
len_end: f32,
/// The inner half thickness
thickness: f32,
/// The thickness of the border (added to the inner thickness for total thickness)
border: f32,
/// Current time
time: f32,
/// The period of the pulse effect (in seconds)
pulse_period: f32,
/// The speed at which the pulse travels along the path (in units per second)
pulse_speed: f32,
/// The width of the pulse effect (in units)
pulse_width: f32,
/// The color of the border
border_color: vec4<f32>,
/// The color of the base of the track
color_base: vec4<f32>,
/// The color of the pulsing
color_accent: vec4<f32>,
};
@group(#{MATERIAL_BIND_GROUP}) @binding(0)
var<uniform> U: Uniforms;
// Distance to segment
fn sdSegment(p: vec2<f32>, a: vec2<f32>, b: vec2<f32>) -> f32 {
let pa = p - a;
let ba = b - a;
let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
return length(pa - ba * h);
}
fn sdArc(
p: vec2<f32>,
c: vec2<f32>,
r: f32,
a0: f32,
a1: f32
) -> f32 {
let d = p - c;
var ang = atan2(d.y, d.x);
var s = a0;
var e = a1;
if (e < s) { e += 2.0 * PI; }
if (ang < s) { ang += 2.0 * PI; }
let on_arc = (ang >= s) && (ang <= e);
let circle = abs(length(d) - r);
let p0 = c + r * vec2<f32>(cos(s), sin(s));
let p1 = c + r * vec2<f32>(cos(e), sin(e));
let ends = min(length(p - p0), length(p - p1));
return select(ends, circle, on_arc);
}
// Compute SDF and normalized distance along path
fn trackSDF(p: vec2<f32>) -> vec2<f32> {
let top_offset = U.thickness + U.border;
let bottom_offset = U.size.y - top_offset;
let midline = U.size.y / 2.0;
let drop = midline - top_offset;
let radius = drop / 2.0;
// Construct points
let p0 = vec2<f32>(U.size.x - U.len_start, top_offset);
let p1 = vec2<f32>(U.size.x - radius, top_offset);
let arc0_center = vec2<f32>(p1.x, drop / 2.0 + top_offset);
let p2 = vec2<f32>(p1.x, midline);
let p3 = vec2<f32>(radius, midline);
let arc1_center = vec2<f32>(p3.x, drop / 2.0 + midline);
let p4 = vec2<f32>(radius, bottom_offset);
let p5 = vec2<f32>(U.len_end, bottom_offset);
// Total length
let L0 = length(p1 - p0);
let L1 = PI * length(p2 - p1);
let L2 = length(p3 - p2);
let L3 = PI * length(p4 - p3);
let L4 = length(p5 - p4);
let total = L0 + L1 + L2 + L3 + L4;
var best_d = 1e9;
var best_t = 0.0;
// Helper macro-like inline pattern
// segment i with accumulated offset
var accum = 0.0;
// p0->p1
{
let d = sdSegment(p, p0, p1);
let ba = p1 - p0;
let h = clamp(dot(p - p0, ba) / dot(ba, ba), 0.0, 1.0);
let t = (accum + h * L0) / total;
if (d < best_d) { best_d = d; best_t = t; }
accum = accum + L0;
}
// ARC p1->p2
{
let d = sdArc(p, arc0_center, radius, PI * 3.0 / 2.0, PI / 2.0);
let ba = p2 - p1;
let h = clamp(dot(p - p1, ba) / dot(ba, ba), 0.0, 1.0);
let t = (accum + h * L1) / total;
if (d < best_d) { best_d = d; best_t = t; }
accum = accum + L1;
}
// p2->p3 (full width)
{
let d = sdSegment(p, p2, p3);
let ba = p3 - p2;
let h = clamp(dot(p - p2, ba) / dot(ba, ba), 0.0, 1.0);
let t = (accum + h * L2) / total;
if (d < best_d) { best_d = d; best_t = t; }
accum = accum + L2;
}
// ARC p3->p4
{
let d = sdArc(p, arc1_center, radius, PI / 2.0, PI * 3.0 / 2.0);
let ba = p4 - p3;
let h = clamp(dot(p - p3, ba) / dot(ba, ba), 0.0, 1.0);
let t = (accum + h * L3) / total;
if (d < best_d) { best_d = d; best_t = t; }
accum = accum + L3;
}
// p4->p5
{
let d = sdSegment(p, p4, p5);
let ba = p5 - p4;
let h = clamp(dot(p - p4, ba) / dot(ba, ba), 0.0, 1.0);
let t = (accum + h * L4) / total;
if (d < best_d) { best_d = d; best_t = t; }
}
return vec2<f32>(best_d, best_t);
}
@fragment
fn fs_main(@builtin(position) frag_coord: vec4<f32>) -> @location(0) vec4<f32> {
let p = frag_coord.xy;
let res = trackSDF(p);
let d = res.x;
let t = res.y;
// bounces back and forth between 0.0 and 1.0
let t_wrap = abs(((t + U.time) % 2.0) - 1.0);
// bounces back and forth between 0.0 and U.pulse_period
let t_wrap_pulse = abs(((t - U.pulse_speed * U.time) % (U.pulse_period * 2.0)) - U.pulse_period);
let inner = U.thickness;
let outer = U.thickness + U.border;
let aa = 1.0;
let inner_a = smoothstep(inner + aa, inner - aa, d);
let outer_a = smoothstep(outer + aa, outer - aa, d);
let border_mask = clamp(outer_a - inner_a, 0.0, 1.0);
// let color_mix = smoothstep(-0.1, 0.0, -abs(t - t_wrap_pulse));
let color_mix = smoothstep(-U.pulse_width, 0.0, -abs(t - t_wrap));
let inner_color = mix(U.color_base, U.color_accent, color_mix);
// let inner_color = mix(U.color_base, U.color_accent, d);
let color = inner_color * inner_a + U.border_color * border_mask;
let alpha = max(inner_a, outer_a);
return vec4<f32>(color.rgb, alpha);
// return vec4<f32>(d / 3000.0, t / 3000.0, 0.0, 1.0);
// return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}
+29
View File
@@ -0,0 +1,29 @@
struct Uniforms {
resolution: vec2<f32>, // viewport size (pixels)
origin: vec2<f32>, // start point (pixels)
len0: f32, // first horizontal segment (to the right)
len2: f32, // last horizontal segment (to the right)
drop: f32, // vertical drop between segments
thickness: f32, // inner half-width
border: f32, // border thickness
border_color: vec4<f32>,
color_start: vec4<f32>,
color_end: vec4<f32>,
};
@group(0) @binding(0)
var<uniform> U: Uniforms;
struct VertexOutput {
@builtin(position) position : vec4<f32>,
@location(0) uv : vec2<f32>,
};
@fragment
fn fragment(mesh: VertexOutput) -> @location(0) vec4<f32> {
let u = clamp(mesh.uv.x, 0.0, 1.0);
return vec4<f32>(mesh.position.x / 1000.0, mesh.position.y / 1000.0, 0.0, 1.0);
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

+9 -24
View File
@@ -1,9 +1,6 @@
use avian2d::{math::*, prelude::*}; use avian2d::{math::*, prelude::*};
use bevy::prelude::*; use bevy::prelude::*;
const PLAYER_SPEED: f32 = 300.0;
pub struct CharacterControllerPlugin; pub struct CharacterControllerPlugin;
impl Plugin for CharacterControllerPlugin { impl Plugin for CharacterControllerPlugin {
@@ -29,10 +26,6 @@ pub struct MaxSpeed(Scalar);
#[derive(Component, Default, Copy, Clone)] #[derive(Component, Default, Copy, Clone)]
pub struct MaxAcceleration(Scalar); pub struct MaxAcceleration(Scalar);
/// Whether inputs are enabled for a CharacterController.
#[derive(Component, Default, Copy, Clone)]
pub struct InputEnabled(pub bool);
/// A bundle that contains the components needed for a basic /// A bundle that contains the components needed for a basic
/// kinematic character controller. /// kinematic character controller.
#[derive(Bundle)] #[derive(Bundle)]
@@ -42,18 +35,16 @@ pub struct CharacterControllerBundle {
collider: Collider, collider: Collider,
speed: MaxSpeed, speed: MaxSpeed,
acceleration: MaxAcceleration, acceleration: MaxAcceleration,
enabled: InputEnabled,
} }
impl CharacterControllerBundle { impl CharacterControllerBundle {
pub fn new(collider: Collider) -> Self { pub fn new(collider: Collider) -> Self {
Self { Self {
character_controller: CharacterController, character_controller: CharacterController,
body: RigidBody::Dynamic, body: RigidBody::Kinematic,
collider, collider,
speed: MaxSpeed(PLAYER_SPEED), speed: MaxSpeed(300.),
acceleration: MaxAcceleration(5000.), acceleration: MaxAcceleration(5000.),
enabled: InputEnabled(true),
} }
} }
} }
@@ -102,24 +93,21 @@ fn gamepad_input(mut movement_writer: MessageWriter<MovementAction>, gamepads: Q
fn movement( fn movement(
time: Res<Time>, time: Res<Time>,
mut movement_reader: MessageReader<MovementAction>, mut movement_reader: MessageReader<MovementAction>,
mut controllers: Query<(&MaxSpeed, &MaxAcceleration, &mut LinearVelocity, &InputEnabled)>, mut controllers: Query<(&MaxSpeed, &MaxAcceleration, &mut LinearVelocity)>,
) { ) {
// Precision is adjusted so that the example works with // Precision is adjusted so that the example works with
// both the `f32` and `f64` features. Otherwise you don't need this. // both the `f32` and `f64` features. Otherwise you don't need this.
let delta_time = time.delta_secs_f64().adjust_precision(); let delta_time = time.delta_secs_f64().adjust_precision();
for (max_speed, max_acceleration, mut linear_velocity, enabled) in &mut controllers { for (max_speed, max_acceleration, mut linear_velocity) in &mut controllers {
if enabled.0 { while movement_reader.len() > 1 {
// let len = movement_reader.len(); warn!("Extra movement message. Ignoring");
// while len > 1 { movement_reader.read();
// warn!("Extra movement message. Ignoring"); }
// println!("{}", len);
// let _ = movement_reader.read().take(len - 1).count();
// }
let target = movement_reader let target = movement_reader
.read() .read()
.last() .next()
.map(|ma| ma.0) .map(|ma| ma.0)
.unwrap_or_default() .unwrap_or_default()
* max_speed.0; * max_speed.0;
@@ -131,8 +119,5 @@ fn movement(
delta = delta.normalize() * max_acceleration; delta = delta.normalize() * max_acceleration;
} }
**linear_velocity += delta; **linear_velocity += delta;
} else {
**linear_velocity = Vec2::ZERO;
}
} }
} }
+161 -23
View File
@@ -1,34 +1,96 @@
use crate::avian::{CharacterControllerBundle, CharacterControllerPlugin}; use crate::{
use crate::world::{MainCamera, Player, WorldPlugin}; avian::{CharacterControllerBundle, CharacterControllerPlugin},
use avian2d::parry::simba::simd::SimdComplexField; shaders::BezierMaterial,
use avian2d::{
prelude::*,
PhysicsPlugins,
}; };
use bevy::tasks::futures_lite::StreamExt; use avian2d::{
use bevy::{camera::ScalingMode, color::palettes::css::GREEN, prelude::*}; PhysicsPlugins,
pub mod avian; math::Vector,
pub mod world; prelude::{Collider, Gravity, RigidBody},
};
use bevy::{
camera::ScalingMode,
color::palettes::css::{GREEN, RED, WHITE},
input_focus::InputFocus,
prelude::*,
sprite_render::Material2dPlugin,
};
use crate::shaders::track::{TrackMaterial, setup_track, update_track_time};
use bevy_inspector_egui::{bevy_egui::EguiPlugin, quick::WorldInspectorPlugin};
pub mod avian;
pub mod shaders;
const NORMAL_BUTTON: Color = Color::srgb(0.15, 0.15, 0.15);
const HOVERED_BUTTON: Color = Color::srgb(0.25, 0.25, 0.25);
const PRESSED_BUTTON: Color = Color::srgb(0.35, 0.75, 0.35);
fn main() { fn main() {
App::new() App::new()
.add_plugins(( .add_plugins((
// DefaultPlugins, DefaultPlugins,
DefaultPlugins.set(ImagePlugin::default_nearest()),
// Add physics plugins and specify a units-per-meter scaling factor, 1 meter = 20 pixels. // Add physics plugins and specify a units-per-meter scaling factor, 1 meter = 20 pixels.
// The unit allows the engine to tune its parameters for the scale of the world, improving stability. // The unit allows the engine to tune its parameters for the scale of the world, improving stability.
PhysicsPlugins::default().with_length_unit(20.0), PhysicsPlugins::default().with_length_unit(20.0),
Material2dPlugin::<BezierMaterial>::default(),
Material2dPlugin::<TrackMaterial>::default(),
CharacterControllerPlugin, CharacterControllerPlugin,
WorldPlugin,
)) ))
.add_systems(Startup, setup) .add_plugins(EguiPlugin::default())
.add_systems(Update, debug_border) .add_plugins(WorldInspectorPlugin::new())
.init_resource::<InputFocus>()
// .add_systems(Startup, (setup, setup_ui))
.add_systems(Startup, (setup, setup_ui, setup_track))
.add_systems(Update, (debug_border, do_menu_interactions, move_bezier, update_track_time))
.insert_resource(Gravity(Vector::ZERO))
.run(); .run();
} }
fn do_menu_interactions(
mut input_focus: ResMut<InputFocus>,
mut interaction_query: Query<
(
Entity,
&Interaction,
&mut BackgroundColor,
&mut BorderColor,
&mut Button,
&Children,
),
Changed<Interaction>,
>,
mut text_query: Query<&mut Text>,
) {
for (entity, interaction, mut color, mut border_color, mut button, children) in
&mut interaction_query
{
let mut text = text_query.get_mut(children[0]).unwrap();
match *interaction {
Interaction::Pressed => {
input_focus.set(entity);
**text = "Press".to_string();
*color = PRESSED_BUTTON.into();
*border_color = BorderColor::all(RED);
// The accessibility system's only update the button's state when the `Button` component is marked as changed.
button.set_changed();
}
Interaction::Hovered => {
input_focus.set(entity);
**text = "Hover".to_string();
*color = HOVERED_BUTTON.into();
*border_color = BorderColor::all(Color::WHITE);
button.set_changed();
}
Interaction::None => {
input_focus.clear();
**text = "Button".to_string();
*color = NORMAL_BUTTON.into();
*border_color = BorderColor::all(Color::BLACK);
}
}
}
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
let mut projection = OrthographicProjection::default_2d(); let mut projection = OrthographicProjection::default_2d();
@@ -36,16 +98,13 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
min_width: 1920., min_width: 1920.,
min_height: 1080., min_height: 1080.,
}; };
commands.spawn((Camera2d, MainCamera, Projection::Orthographic(projection))); commands.spawn((Camera2d, Projection::Orthographic(projection)));
// player // player
commands.spawn(( commands.spawn((
Sprite::from_image(asset_server.load("south.png")), Sprite::from_image(asset_server.load("player.png")),
Transform::from_xyz(0.0, 0.0, 0.0),
Player, CharacterControllerBundle::new(Collider::capsule(15., 27.5)),
Transform::from_xyz(0.0, 0.0, 2.0).with_scale(Vec3::splat(4.0)),
CharacterControllerBundle::new(Collider::rectangle(14.0, 30.0)),
LockedAxes::ROTATION_LOCKED,
)); ));
// A cube to move around // A cube to move around
@@ -55,14 +114,93 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
custom_size: Some(Vec2::new(30.0, 30.0)), custom_size: Some(Vec2::new(30.0, 30.0)),
..default() ..default()
}, },
Transform::from_xyz(50.0, -100.0, 1.0), Transform::from_xyz(50.0, -100.0, 0.0),
RigidBody::Dynamic, RigidBody::Dynamic,
Collider::rectangle(30.0, 30.0), Collider::rectangle(30.0, 30.0),
)); ));
}
fn setup_ui(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<BezierMaterial>>,
) {
commands.spawn((
Node {
width: percent(100),
height: percent(100),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
children![(
Button,
Node {
width: px(150),
height: px(65),
border: UiRect::all(px(5)),
// horizontally center child text
justify_content: JustifyContent::Center,
// vertically center child text
align_items: AlignItems::Center,
border_radius: BorderRadius::MAX,
..default()
},
BorderColor::all(Color::WHITE),
BackgroundColor(Color::BLACK),
children![(
Text::new("Button"),
TextFont {
font: asset_server.load("FiraSans-Bold.ttf"),
font_size: 33.0,
..default()
},
TextColor(Color::srgb(0.9, 0.9, 0.9)),
TextShadow::default(),
)]
)],
));
// Curve points (world-space)
let p0 = Vec2::new(-200.0, -100.0);
let p1 = Vec2::new(200.0, 100.0);
let c0 = p0 + Vec2::new(150.0, 0.0);
let c1 = p1 + Vec2::new(-150.0, 0.0);
// Bounding quad (must contain entire curve + width)
let center = (p0 + p1) * 0.5;
let size = Vec2::new(500.0, 300.0);
commands.spawn((
Mesh2d(meshes.add(Rectangle::new(size.x, size.y))),
MeshMaterial2d(materials.add(BezierMaterial {
p0,
c0,
c1,
p1,
width: 12.0,
color: WHITE.into(),
})),
Transform::from_translation(center.extend(0.0)),
));
}
fn move_bezier(mut materials: ResMut<Assets<BezierMaterial>>, windows: Query<&Window>) {
let Ok(window) = windows.single() else {
return;
};
let Some(mouse_pos) = window.cursor_position() else {
return;
};
for (_, mat) in materials.iter_mut() {
mat.p0 = mouse_pos;
}
// pos: Vec2, in logical pixels
// (0, 0) is bottom-left of the window
// println!("Mouse window position: {:?}", mouse_pos);
} }
fn debug_border(mut gizmos: Gizmos) { fn debug_border(mut gizmos: Gizmos) {
View File
+66
View File
@@ -0,0 +1,66 @@
use bevy::prelude::*;
use bevy::render::render_resource::*;
use bevy::shader::ShaderRef;
use bevy::sprite_render::{AlphaMode2d, Material2d};
pub mod track;
// This is the struct that will be passed to your shader
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
pub struct BezierMaterial {
#[uniform(0)]
pub p0: Vec2,
#[uniform(0)]
pub c0: Vec2,
#[uniform(0)]
pub c1: Vec2,
#[uniform(0)]
pub p1: Vec2,
#[uniform(0)]
pub width: f32,
#[uniform(0)]
pub color: LinearRgba,
}
/// The Material2d trait is very configurable, but comes with sensible defaults for all methods.
/// You only need to implement functions for features that need non-default behavior. See the Material2d api docs for details!
impl Material2d for BezierMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/bezier.wgsl".into()
}
fn alpha_mode(&self) -> AlphaMode2d {
AlphaMode2d::Blend
}
}
// This is the struct that will be passed to your shader
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
pub struct FlowMaterial {
#[uniform(0)]
pub color_a: LinearRgba,
#[uniform(0)]
pub color_b: LinearRgba,
#[uniform(0)]
pub border_color: LinearRgba,
#[uniform(0)]
pub time: f32,
#[uniform(0)]
pub speed: f32,
#[uniform(0)]
pub border_size: f32,
}
/// The Material2d trait is very configurable, but comes with sensible defaults for all methods.
/// You only need to implement functions for features that need non-default behavior. See the Material2d api docs for details!
impl Material2d for FlowMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/flow.wgsl".into()
}
fn alpha_mode(&self) -> AlphaMode2d {
AlphaMode2d::Blend
}
}
+113
View File
@@ -0,0 +1,113 @@
use bevy::prelude::*;
use bevy::render::render_resource::*;
use bevy::shader::ShaderRef;
use bevy::sprite_render::{AlphaMode2d, Material2d};
/// The Uniform data passed to the track shader.
#[repr(C)]
#[derive(Clone, Copy, ShaderType)]
pub struct TrackUniform {
/// The size of the canvas
pub size: Vec2,
/// The length of the first horizontal segment
pub len_start: f32,
/// The length of the last horizontal segment
pub len_end: f32,
/// The inner half thickness
pub thickness: f32,
/// The thickness of the border (added to the inner thickness for total thickness)
pub border: f32,
/// Current time
pub time: f32,
/// The period of the pulse effect (in seconds)
pub pulse_period: f32,
/// The speed at which the pulse travels along the path (in units per second)
pub pulse_speed: f32,
/// The width of the pulse effect (in units)
pub pulse_width: f32,
/// The color of the border
pub border_color: LinearRgba,
/// The color of the base of the track
pub color_base: LinearRgba,
/// The color of the pulsing
pub color_accent: LinearRgba,
}
#[derive(AsBindGroup, Asset, TypePath, Clone)]
pub struct TrackMaterial {
#[uniform(0)]
pub data: TrackUniform,
}
impl Material2d for TrackMaterial {
fn fragment_shader() -> ShaderRef {
"shaders/track.wgsl".into()
}
fn alpha_mode(&self) -> AlphaMode2d {
AlphaMode2d::Blend
}
}
pub fn setup_track(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<TrackMaterial>>,
// windows: Query<&Window>,
) {
// let window = windows.single().unwrap();
let width = 1920.0 / 2.0;
let height = 1080.0 / 2.0;
// let width = 1920.0;
// let height = 1080.0;
let material = materials.add(TrackMaterial {
data: TrackUniform {
size: Vec2::new(width, height),
len_start: 400.0,
len_end: 300.0,
thickness: 8.0,
border: 2.0,
time: 0.0,
pulse_period: 2.0,
pulse_speed: 0.5,
pulse_width: 0.5,
border_color: LinearRgba::BLACK,
color_base: LinearRgba::rgb(0.0, 0.9, 1.0),
color_accent: LinearRgba::rgb(0.8, 0.0, 0.0),
},
});
// commands.spawn(Camera2d);
// println!("Window: {}, {}", window.width(), window.height());
commands.spawn((
Mesh2d(meshes.add(Rectangle::new(width * 2.0, height * 2.0))),
MeshMaterial2d(material),
Transform::from_translation(Vec3::new(
// window.width() * 0.5,
// window.height() * 0.5,
0.0,
0.0,
0.0,
)),
Name::new("Snake")
));
}
pub fn update_track_time(
time: Res<Time>,
mut materials: ResMut<Assets<TrackMaterial>>,
) {
for (_, mat) in materials.iter_mut() {
mat.data.time = time.elapsed_secs();
}
}
-368
View File
@@ -1,368 +0,0 @@
use avian2d::math::Vector;
use avian2d::prelude::{Collider, CollidingEntities, CollisionEventsEnabled, CollisionStart, Gravity, RigidBody, Sensor};
use bevy::camera::Viewport;
use bevy::prelude::*;
use bevy::window::PrimaryWindow;
use crate::avian::InputEnabled;
const ROOM_WIDTH: f32 = 1920.0;
const ROOM_HEIGHT: f32 = 1080.0;
const WALL_THICKNESS: f32 = 20.0;
const DOOR_WIDTH: f32 = 120.0;
const DOOR_DEPTH: f32 = 40.0;
const SPAWN_DISTANCE: f32 = 80.0;
const FADE_DURATION: f32 = 0.35;
const TARGET_ASPECT: f32 = ROOM_WIDTH / ROOM_HEIGHT;
const HALF_W: f32 = ROOM_WIDTH / 2.0;
const HALF_H: f32 = ROOM_HEIGHT / 2.0;
pub struct WorldPlugin;
impl Plugin for WorldPlugin {
fn build(&self, app: &mut App) {
app
.add_systems(Startup, setup)
.add_systems(
Update,
(
door_detection.run_if(in_state(GameState::Playing)),
transition_system.run_if(in_state(GameState::Transitioning)),
update_viewport,
),
)
.init_state::<GameState>()
.insert_resource(CurrentRoom(IVec2::ZERO))
.insert_resource(Gravity(Vector::ZERO))
.insert_resource(ClearColor(Color::BLACK));
}
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
spawn_room(&mut commands, IVec2::new(0, 0), vec![Direction::North, Direction::East], &asset_server);
spawn_room(&mut commands, IVec2::new(1, 0), vec![Direction::West], &asset_server);
spawn_room(&mut commands, IVec2::new(0, 1), vec![Direction::South], &asset_server);
commands.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
position_type: PositionType::Absolute,
..default()
},
BackgroundColor(Color::srgba(0.0, 0.0, 0.0, 0.0)),
FadeOverlay,
));
}
#[derive(States, Default, Clone, Eq, PartialEq, Hash, Debug)]
enum GameState {
#[default]
Playing,
Transitioning,
}
#[derive(Resource)]
struct CurrentRoom(IVec2);
#[derive(Resource)]
struct RoomTransition {
target_room: IVec2,
target_player_position: Vec2,
timer: Timer,
phase: TransitionPhase,
}
#[derive(Clone, Copy, PartialEq)]
enum TransitionPhase {
FadeOut,
FadeIn,
}
#[derive(Component)]
pub struct Player;
#[derive(Component)]
pub struct MainCamera;
#[derive(Component)]
struct FadeOverlay;
#[derive(Component)]
struct Door {
room: IVec2,
travel_direction: Direction,
}
#[derive(Clone, Copy, PartialEq)]
enum Direction {
North,
South,
East,
West,
}
fn spawn_room(commands: &mut Commands, room: IVec2, door_directions: Vec<Direction>, asset_server: &Res<AssetServer>) {
let center = room_center(room);
commands.spawn((
Sprite {
image: asset_server.load("img.png"),
custom_size: Some(Vec2::new(ROOM_WIDTH, ROOM_HEIGHT)),
image_mode: SpriteImageMode::Tiled {
tile_x: true,
tile_y: true,
stretch_value: 1.0
},
..default()
},
// Sprite::from_color(color, Vec2::new(ROOM_WIDTH, ROOM_HEIGHT)),
Transform::from_translation(center),
));
spawn_doors(commands, room, door_directions);
spawn_room_walls(commands, center);
}
fn spawn_room_walls(commands: &mut Commands, center: Vec3) {
// LEFT
commands.spawn((
RigidBody::Static,
Sprite::from_color(Color::srgb(0.5, 0.5, 0.5), Vec2::new(WALL_THICKNESS, ROOM_HEIGHT)),
Collider::rectangle(WALL_THICKNESS, ROOM_HEIGHT),
Transform::from_xyz(
center.x - HALF_W + WALL_THICKNESS / 2.0,
center.y,
10.0,
),
));
// RIGHT
commands.spawn((
RigidBody::Static,
Sprite::from_color(Color::srgb(0.5, 0.5, 0.5), Vec2::new(WALL_THICKNESS, ROOM_HEIGHT)),
Collider::rectangle(WALL_THICKNESS, ROOM_HEIGHT),
Transform::from_xyz(
center.x + HALF_W - WALL_THICKNESS / 2.0,
center.y,
10.0,
),
));
// TOP
commands.spawn((
RigidBody::Static,
Sprite::from_color(Color::srgb(0.5, 0.5, 0.5), Vec2::new(ROOM_WIDTH, WALL_THICKNESS)),
Collider::rectangle(ROOM_WIDTH, WALL_THICKNESS),
Transform::from_xyz(
center.x,
center.y + HALF_H - WALL_THICKNESS / 2.0,
10.0,
),
));
// BOTTOM
commands.spawn((
RigidBody::Static,
Sprite::from_color(Color::srgb(0.5, 0.5, 0.5), Vec2::new(ROOM_WIDTH, WALL_THICKNESS)),
Collider::rectangle(ROOM_WIDTH, WALL_THICKNESS),
Transform::from_xyz(
center.x,
center.y - HALF_H + WALL_THICKNESS / 2.0,
10.0,
),
));
}
fn spawn_doors(commands: &mut Commands, room: IVec2, door_directions: Vec<Direction>) {
let room_center = room_center(room);
const HALF_DEPTH: f32 = DOOR_DEPTH / 2.0;
for direction in door_directions {
let transform: Transform;
let size: Vec2;
match direction {
Direction::North => {
transform = Transform::from_xyz(room_center.x, room_center.y + HALF_H - HALF_DEPTH, 1.0);
size = Vec2::new(DOOR_WIDTH, DOOR_DEPTH);
},
Direction::East => {
transform = Transform::from_xyz(room_center.x + HALF_W - HALF_DEPTH, room_center.y, 1.0);
size = Vec2::new(DOOR_DEPTH, DOOR_WIDTH);
},
Direction::South => {
transform = Transform::from_xyz(room_center.x, room_center.y - HALF_H + HALF_DEPTH, 1.0);
size = Vec2::new(DOOR_WIDTH, DOOR_DEPTH);
},
Direction::West => {
transform = Transform::from_xyz(room_center.x - HALF_W + HALF_DEPTH, room_center.y, 1.0);
size = Vec2::new(DOOR_DEPTH, DOOR_WIDTH);
}
}
commands.spawn((
Sprite::from_color(Color::srgb(0.1, 0.1, 0.1), size),
Collider::rectangle(size.x, size.y),
Sensor,
CollisionEventsEnabled,
CollidingEntities::default(),
Door {
room: IVec2::from(room),
travel_direction: direction,
},
transform,
GlobalTransform::default(),
));
}
}
fn door_detection(
mut commands: Commands,
mut next_state: ResMut<NextState<GameState>>,
player: Single<(Entity, &Transform), With<Player>>,
doors: Query<&Door>,
mut started: MessageReader<CollisionStart>,
) {
for event in started.read() {
println!("CollisionStart: {event:?}");
let door = doors.get(event.collider2);
if event.collider1 == player.0 && door.is_ok() {
commands.insert_resource(RoomTransition {
target_room: door_target_room(door.unwrap()),
target_player_position: door_target_position(door.unwrap()),
timer: Timer::from_seconds(
FADE_DURATION,
TimerMode::Once,
),
phase: TransitionPhase::FadeOut,
});
commands.entity(player.0).insert(InputEnabled(false));
next_state.set(GameState::Transitioning);
}
}
}
fn door_target_room(door: &Door) -> IVec2 {
match door.travel_direction {
Direction::North => IVec2::new(door.room.x, door.room.y + 1),
Direction::East => IVec2::new(door.room.x + 1, door.room.y),
Direction::South => IVec2::new(door.room.x, door.room.y - 1),
Direction::West => IVec2::new(door.room.x - 1, door.room.y),
}
}
fn door_target_position(door: &Door) -> Vec2 {
match door.travel_direction {
Direction::North => Vec2::new(0.0, -HALF_H + WALL_THICKNESS + DOOR_DEPTH + SPAWN_DISTANCE),
Direction::East => Vec2::new(-HALF_W + WALL_THICKNESS + DOOR_DEPTH + SPAWN_DISTANCE, 0.0),
Direction::South => Vec2::new(0.0, HALF_H - WALL_THICKNESS - DOOR_DEPTH - SPAWN_DISTANCE),
Direction::West => Vec2::new(HALF_W - WALL_THICKNESS - DOOR_DEPTH - SPAWN_DISTANCE, 0.0),
}
}
fn transition_system(
mut commands: Commands,
time: Res<Time>,
mut transition: ResMut<RoomTransition>,
mut next_state: ResMut<NextState<GameState>>,
mut current_room: ResMut<CurrentRoom>,
mut player: Single<(Entity, &mut Transform), With<Player>>,
mut camera: Query<&mut Transform, (With<MainCamera>, Without<Player>)>,
mut overlay: Query<&mut BackgroundColor, With<FadeOverlay>>,
) {
transition.timer.tick(time.delta());
let mut overlay_color = overlay.single_mut().unwrap();
match transition.phase {
TransitionPhase::FadeOut => {
let alpha = transition.timer.fraction();
overlay_color.0 = Color::srgba(0.0, 0.0, 0.0, alpha);
if transition.timer.is_finished() {
current_room.0 = transition.target_room;
let room_center =
room_center(transition.target_room);
player.1.translation =
room_center
+ transition.target_player_position.extend(10.0);
camera.single_mut().unwrap().translation =
room_center + Vec3::new(0.0, 0.0, 999.0);
transition.phase = TransitionPhase::FadeIn;
transition.timer.reset();
}
}
TransitionPhase::FadeIn => {
let alpha = 1.0 - transition.timer.fraction();
overlay_color.0 = Color::srgba(0.0, 0.0, 0.0, alpha);
if transition.timer.is_finished() {
commands.entity(player.0).insert(InputEnabled(true));
next_state.set(GameState::Playing);
}
}
}
}
fn update_viewport(
window: Single<&Window, With<PrimaryWindow>>,
mut camera: Single<&mut Camera>,
) {
let window_width = window.physical_width();
let window_height = window.physical_height();
let window_aspect = window_width as f32 / window_height as f32;
let (viewport_width, viewport_height) = if window_aspect > TARGET_ASPECT {
// Ultrawide: black bars left/right
let h = window_height;
let w = (h as f32 * TARGET_ASPECT) as u32;
(w, h)
} else {
// Too tall: black bars top/bottom
let w = window_width;
let h = (w as f32 / TARGET_ASPECT) as u32;
(w, h)
};
let x = (window_width - viewport_width) / 2;
let y = (window_height - viewport_height) / 2;
camera.viewport = Some(Viewport {
physical_position: UVec2::new(x, y),
physical_size: UVec2::new(viewport_width, viewport_height),
..default()
});
}
fn room_center(room: IVec2) -> Vec3 {
Vec3::new(
room.x as f32 * ROOM_WIDTH,
room.y as f32 * ROOM_HEIGHT,
0.0,
)
}