目次

倉庫番

src/main.rs

use bevy::prelude::*;
use bevy::time::FixedTimestep;

const ARENA_WIDTH: i32 = 10;
const ARENA_HEIGHT: i32 = 10;
const TILE_SIZE: Vec2 = Vec2::new(30.0, 30.0);
const SCREEN_SIZE: Vec2 = Vec2::new(
    TILE_SIZE.x * ARENA_WIDTH as f32,
    TILE_SIZE.y * ARENA_HEIGHT as f32,
);
const PLAYER_COLOR: Color = Color::AQUAMARINE;
const CARGO_COLOR: Color = Color::BEIGE;
const GOAL_COLOR: Color = Color::CRIMSON;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins.set(WindowPlugin {
            window: WindowDescriptor {
                title: "SOKOBAN".to_string(),
                width: SCREEN_SIZE.x,
                height: SCREEN_SIZE.y,
                ..default()
            },
            ..default()
        }))
        .insert_resource(ClearColor(Color::BLUE))
        .add_startup_system(setup)
        .add_system(bevy::window::close_on_esc)
        .add_system_set(
            SystemSet::new()
                .with_run_criteria(FixedTimestep::step(1.0 / 60.0)) // BUG: any other FPS of 60 may not handle keyboard input as expected
                .with_system(move_player_and_cargo)
                .with_system(position_translation.after(move_player_and_cargo))
                .with_system(check_level_complete.after(move_player_and_cargo))
                .with_system(complete_level.after(check_level_complete)),
        )
        .add_event::<LevelCompleteEvent>()
        .run();
}

#[derive(Component, Debug, PartialEq, Eq, Clone, Copy)]
struct Position {
    x: i32,
    y: i32,
    z: i32,
}

#[derive(Component)]
struct Cargo;

#[derive(Component)]
struct Player;

#[derive(Component)]
struct Goal;

#[derive(Default)]
struct LevelCompleteEvent;

fn setup(mut commands: Commands) {
    commands.spawn(Camera2dBundle::default());

    // spawn Player
    commands
        .spawn(SpriteBundle {
            sprite: Sprite {
                color: PLAYER_COLOR,
                ..default()
            },
            transform: Transform {
                scale: Vec3::new(TILE_SIZE.x, TILE_SIZE.y, 0.0),
                ..default()
            },
            ..default()
        })
        .insert(Player)
        .insert(Position {
            x: ARENA_WIDTH / 2,
            y: 0,
            z: 3,
        });

    // spawn Cargo
    for (x, y) in (0..ARENA_WIDTH)
        .step_by(2)
        .zip(std::iter::repeat(ARENA_HEIGHT - 3))
        .chain(
            (1..ARENA_WIDTH)
                .step_by(2)
                .zip(std::iter::repeat(ARENA_HEIGHT - 4)),
        )
    {
        commands
            .spawn(SpriteBundle {
                sprite: Sprite {
                    color: CARGO_COLOR,
                    ..default()
                },
                transform: Transform {
                    scale: Vec3::new(TILE_SIZE.x * 0.8, TILE_SIZE.y * 0.8, 0.0),
                    ..default()
                },
                ..default()
            })
            .insert(Cargo)
            .insert(Position { x, y, z: 2 });
    }

    // spawn Goal
    for x in 0..ARENA_WIDTH {
        commands
            .spawn(SpriteBundle {
                sprite: Sprite {
                    color: GOAL_COLOR,
                    ..default()
                },
                transform: Transform {
                    scale: Vec3::new(TILE_SIZE.x * 0.9, TILE_SIZE.y * 0.9, 0.0),
                    ..default()
                },
                ..default()
            })
            .insert(Goal)
            .insert(Position {
                x,
                y: ARENA_HEIGHT - 1,
                z: 1,
            });
    }
}

fn position_translation(mut query: Query<(&Position, &mut Transform)>) {
    for (p, mut t) in query.iter_mut() {
        t.translation = Vec3::new(
            p.x as f32 * TILE_SIZE.x - (SCREEN_SIZE.x / 2.0) + (TILE_SIZE.x / 2.0),
            p.y as f32 * TILE_SIZE.y - (SCREEN_SIZE.y / 2.0) + (TILE_SIZE.y / 2.0),
            p.z as f32,
        );
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum Direction {
    Up,
    Down,
    Left,
    Right,
    None,
}

fn move_player_and_cargo(
    keyboard_input: Res<Input<KeyCode>>,
    mut player_positions: Query<&mut Position, (With<Player>, Without<Cargo>)>,
    mut cargo_positions: Query<&mut Position, (With<Cargo>, Without<Player>)>,
) {
    let direction = if keyboard_input.just_pressed(KeyCode::Up) {
        println!("up");
        Direction::Up
    } else if keyboard_input.just_pressed(KeyCode::Down) {
        Direction::Down
    } else if keyboard_input.just_pressed(KeyCode::Left) {
        Direction::Left
    } else if keyboard_input.just_pressed(KeyCode::Right) {
        Direction::Right
    } else {
        Direction::None
    };

    let mut player_position = player_positions.single_mut();

    let next_position = |p: &Position, direction, step| match direction {
        Direction::Up => Position {
            x: p.x,
            y: p.y + step,
            z: p.z,
        },
        Direction::Down => Position {
            x: p.x,
            y: p.y - step,
            z: p.z,
        },
        Direction::Left => Position {
            x: p.x - step,
            y: p.y,
            z: p.z,
        },
        Direction::Right => Position {
            x: p.x + step,
            y: p.y,
            z: p.z,
        },
        _ => *p,
    };

    let inside_arene =
        |p: &Position| p.x >= 0 && p.y >= 0 && p.x < ARENA_WIDTH && p.y < ARENA_HEIGHT;

    // try to push cargo and update player and cargo position if it possible
    let new_player_position = next_position(&player_position, direction, 1);
    let new_cargo_position = next_position(&player_position, direction, 2);
    let new_cargo_position_empty = cargo_positions
        .iter()
        .all(|cp| !(cp.x == new_cargo_position.x && cp.y == new_cargo_position.y));

    if let Some(mut cargo_forward) = cargo_positions
        .iter_mut()
        .find(|cp| cp.x == new_player_position.x && cp.y == new_player_position.y)
    {
        if new_cargo_position_empty && inside_arene(&new_cargo_position) {
            *cargo_forward = new_cargo_position;
            *player_position = new_player_position;
        }
    } else {
        if inside_arene(&new_player_position) {
            *player_position = new_player_position;
        }
    }
}

fn check_level_complete(
    debug_input: Res<Input<KeyCode>>,
    cargo_positions: Query<&Position, (With<Cargo>, Without<Goal>)>,
    goal_positions: Query<&Position, (With<Goal>, Without<Cargo>)>,
    mut level_complete_writer: EventWriter<LevelCompleteEvent>,
) {
    if debug_input.just_pressed(KeyCode::D) {
        level_complete_writer.send_default();
        return;
    }

    for cargo in cargo_positions.iter() {
        let maybe_on_goal = goal_positions
            .iter()
            .find(|gp| gp.x == cargo.x && gp.y == cargo.y);
        if maybe_on_goal.is_none() {
            return;
        }
    }
    level_complete_writer.send_default();
}

fn complete_level(
    mut commands: Commands,
    events: EventReader<LevelCompleteEvent>,
    asset_server: Res<AssetServer>,
) {
    if !events.is_empty() {
        events.clear();

        println!("Complete");

        let font_size = 50.0;

        // show screen message
        commands.spawn(
            TextBundle::from_section(
                "COMPLETE!",
                TextStyle {
                    font: asset_server.load("fonts/RubikGemstones-Regular.ttf"),
                    font_size,
                    color: Color::PINK,
                },
            )
            .with_text_alignment(TextAlignment::CENTER) // what does this effect?
            .with_style(Style {
                position_type: PositionType::Absolute,
                position: UiRect {
                    // hard-code position to display text center of screen
                    top: Val::Px(SCREEN_SIZE.y / 2.0 - 60.0 / 2.0),
                    left: Val::Px(SCREEN_SIZE.x / 2.0 - font_size * 2.5),
                    ..default()
                },
                ..default()
            }),
        );
    }
}

Cargo.toml

[package]
name = "sokoban"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
bevy = { version = "0.9.1", features = [ "dynamic" ] }

[profile.dev]
opt-level = 1

[profile.dev.package."*"]
opt-level = 3