====== 倉庫番 ====== {{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