Skip to content
Snippets Groups Projects
bevy_scene.rs 9.16 KiB
Newer Older
use bevy::{
    math::{Vec3Swizzles, Vec4Swizzles},
    prelude::*,
};
use kayak_ui::prelude::{widgets::*, *};

const TILE_SIZE: Vec2 = Vec2::from_array([50.0, 50.0]);
const COLORS: &[Color] = &[Color::TEAL, Color::MAROON, Color::INDIGO];

// ! === Unnecessary Details Below === ! //
// Below this point are mainly implementation details. The main purpose of this example is to show how to know
// when to allow or disallow world interaction through `BevyContext` (see the `set_active_tile_target` function)

/// A resource used to control the color of the tiles
#[derive(Resource)]
struct ActiveColor {
    index: usize,
}

/// A component used to control the "Active Tile" that moves to the clicked positions
#[derive(Default, Component)]
struct ActiveTile {
    target: Vec2,
}

/// A component used to control the "Ghost Tile" that follows the user's cursor
#[derive(Component)]
struct GhostTile;

/// A component used to mark the "world camera" (differentiating it from other cameras possibly in the scene)
#[derive(Component)]
struct WorldCamera;

/// This is the system that sets the active tile's target position
///
/// To prevent the tile from being moved to a position under our UI, we can use the `BevyContext` resource
/// to filter out clicks that occur over the UI
fn set_active_tile_target(
    mut tile: Query<&mut ActiveTile>,
    cursor: Res<Input<MouseButton>>,
    event_context: Query<&EventDispatcher, With<GameUI>>,
    camera_transform: Query<&GlobalTransform, With<WorldCamera>>,
    windows: Res<Windows>,
) {
    if !cursor.just_pressed(MouseButton::Left) {
        // Only run this system when the mouse button is clicked
        return;
    }

    if event_context.single().contains_cursor() {
        // This is the important bit:
        // If the cursor is over a part of the UI, then we should not allow clicks to pass through to the world
        return;
    }

    // If you wanted to allow clicks through the UI as long as the cursor is not on a focusable widget (such as Buttons),
    // you could use `context.wants_cursor()` instead:
    //
    // ```
    // if context.wants_cursor() {
    //     return;
    // }
    // ```

    let world_pos = cursor_to_world(&windows, camera_transform.single());
    let tile_pos = world_to_tile(world_pos);
    let mut tile = tile.single_mut();
    tile.target = tile_pos;
}

/// A system that moves the active tile to its target position
fn move_active_tile(mut tile: Query<(&mut Transform, &ActiveTile)>) {
    let (mut transform, tile) = tile.single_mut();
    let curr_pos = transform.translation.xy();
    let next_pos = curr_pos.lerp(tile.target, 0.1);
    transform.translation.x = next_pos.x;
    transform.translation.y = next_pos.y;
}

/// A system that moves the ghost tile to the cursor's position
fn move_ghost_tile(
    event_context: Query<&EventDispatcher, With<GameUI>>,
    mut tile: Query<&mut Transform, With<GhostTile>>,
    mut cursor_moved: EventReader<CursorMoved>,
    camera_transform: Query<&GlobalTransform, With<WorldCamera>>,
    windows: Res<Windows>,
) {
    for _ in cursor_moved.iter() {
        if !event_context.single().contains_cursor() {
            let world_pos = cursor_to_world(&windows, camera_transform.single());
            let tile_pos = world_to_tile(world_pos);
            let mut ghost = tile.single_mut();
            ghost.translation.x = tile_pos.x;
            ghost.translation.y = tile_pos.y;
        }
    }
}

/// A system that updates the tiles' color
fn on_color_change(
    mut active_tile: Query<&mut Sprite, (With<ActiveTile>, Without<GhostTile>)>,
    mut ghost_tile: Query<&mut Sprite, (With<GhostTile>, Without<ActiveTile>)>,
    active_color: Res<ActiveColor>,
) {
    if !active_color.is_changed() {
        return;
    }

    let mut active_tile = active_tile.single_mut();
    active_tile.color = COLORS[active_color.index];

    let mut ghost_tile = ghost_tile.single_mut();
    ghost_tile.color = ghost_color(COLORS[active_color.index]);
}

/// A system that sets up the world
fn world_setup(mut commands: Commands, active_color: Res<ActiveColor>) {
    commands.spawn((Camera2dBundle::default(), WorldCamera));
    commands
        .spawn(SpriteBundle {
            sprite: Sprite {
                color: COLORS[active_color.index],
                custom_size: Some(TILE_SIZE),
                ..Default::default()
            },
            ..Default::default()
        })
        .insert(ActiveTile::default());
    commands
        .spawn(SpriteBundle {
            sprite: Sprite {
                color: ghost_color(COLORS[active_color.index]),
                custom_size: Some(TILE_SIZE),
                ..Default::default()
            },
            ..Default::default()
        })
        .insert(GhostTile);
}

/// Get the world position of the cursor in 2D space
fn cursor_to_world(windows: &Windows, camera_transform: &GlobalTransform) -> Vec2 {
    let window = windows.get_primary().unwrap();
    let size = Vec2::new(window.width(), window.height());

    let mut pos = window.cursor_position().unwrap_or_default();
    pos -= size / 2.0;

    let point = camera_transform.compute_matrix() * pos.extend(0.0).extend(1.0);
    point.xy()
}

/// Converts a world coordinate to a rounded tile coordinate
fn world_to_tile(world_pos: Vec2) -> Vec2 {
    let extents = TILE_SIZE / 2.0;
    let world_pos = world_pos - extents;
    (world_pos / TILE_SIZE).ceil() * TILE_SIZE
}

/// Get the ghost tile color for a given color
fn ghost_color(color: Color) -> Color {
    let mut c = color;
    c.set_a(0.35);
    c
}

#[derive(Component)]
pub struct GameUI;

fn startup(
    mut commands: Commands,
    mut font_mapping: ResMut<FontMapping>,
    asset_server: Res<AssetServer>,
) {
    font_mapping.set_default(asset_server.load("roboto.kayak_font"));

    let mut widget_context = KayakRootContext::new();
    widget_context.add_plugin(KayakWidgetsContextPlugin);

    let handle_change_color = OnEvent::new(
        move |In((event_dispatcher_context, _, event, _)): In<(
            EventDispatcherContext,
            WidgetState,
            Event,
            Entity,
        )>,
              mut active_color: ResMut<ActiveColor>| {
            match event.event_type {
                EventType::Click(..) => {
                    active_color.index = (active_color.index + 1) % COLORS.len();
                }
                _ => {}
            }
            (event_dispatcher_context, event)
        },
    );

    let text_styles = KStyle {
        left: StyleProp::Value(Units::Stretch(1.0)),
        right: StyleProp::Value(Units::Stretch(1.0)),
        ..Default::default()
    };
    let button_styles = KStyle {
        min_width: StyleProp::Value(Units::Pixels(150.0)),
        width: StyleProp::Value(Units::Auto),
        height: StyleProp::Value(Units::Auto),
        left: StyleProp::Value(Units::Stretch(1.0)),
        right: StyleProp::Value(Units::Stretch(1.0)),
        top: StyleProp::Value(Units::Pixels(16.0)),
        bottom: StyleProp::Value(Units::Pixels(8.0)),
        padding: StyleProp::Value(Edge::axis(Units::Pixels(8.0), Units::Pixels(48.0))),
        ..Default::default()
    };

    let parent_id = None;
    rsx! {
        <KayakAppBundle>
            <WindowBundle
                window={KWindow {
                    draggable: true,
                    initial_position: Vec2::new(50.0, 50.0),
                    size: Vec2::new(300.0, 200.0),
                    title: "Square Mover: The Game".to_string(),
                    ..Default::default()
                }}
            >
                <TextWidgetBundle
                    styles={text_styles.clone()}
                    text={TextProps {
                        size: 13.0,
                        content: "You can check if the cursor is over the UI or on a focusable widget using the BevyContext resource.".to_string(),
                        ..Default::default()
                    }}
                />
                <KButtonBundle
                    button={KButton {
                        text: "Change Tile Color".into(),
                        ..Default::default()
                    }}
                    on_event={handle_change_color}
                    styles={button_styles}
                    styles={KStyle {
                        top: Units::Pixels(10.0).into(),
                        ..text_styles
                    }}
                    text={TextProps {
                        size: 11.0,
                        content: "Go ahead and click the button! The tile won't move.".to_string(),
                        ..Default::default()
                    }}
                />
            </WindowBundle>
        </KayakAppBundle>
    commands.spawn((UICameraBundle::new(widget_context), GameUI));
}

fn main() {
    App::new()
        .insert_resource(ClearColor(Color::rgb(0.0, 0.0, 0.0)))
        .insert_resource(ActiveColor { index: 0 })
        .add_plugins(DefaultPlugins)
        .add_plugin(KayakContextPlugin)
        .add_plugin(KayakWidgets)
        .add_startup_system(startup)
        .add_startup_system(world_setup)
        .add_system(move_ghost_tile)
        .add_system(set_active_tile_target)
        .add_system(move_active_tile)
        .add_system(on_color_change)
        .run()
}