====== 倉庫番 ======
{{youtube:sokoban.png?400|}}
===== 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::()
.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>,
mut player_positions: Query<&mut Position, (With, Without)>,
mut cargo_positions: Query<&mut Position, (With, Without)>,
) {
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>,
cargo_positions: Query<&Position, (With, Without)>,
goal_positions: Query<&Position, (With, Without)>,
mut level_complete_writer: EventWriter,
) {
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,
asset_server: Res,
) {
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