🗂

Rustのゲームエンジンbevyにて倉庫番を実装

2023/05/03に公開

Cargo.toml
[package]
name = "reversi"
version = "0.1.0"
edition = "2021"

[dependencies]
bevy = "0.10"
bevy_prototype_debug_lines = "0.10"
main.rs
pub mod components;
pub mod resources;
pub mod states;
mod systems;

use bevy::prelude::*;
use bevy_prototype_debug_lines::*;

use systems::*;
use resources::*;
use components::*;
use states::*;

fn main() {
    App::new()
        .init_resource::<Stage>()
        .init_resource::<ObjectMoveTimer>()
        .insert_resource(ClearColor(COLOR_BACK_GROUND))
        .add_state::<PlayerState>()

        .add_plugins(DefaultPlugins.set(WindowPlugin {
            primary_window: Some(Window {
                title: "soukoban!".into(),
                resolution: (WINDOW_WIDTH, WINDOW_HEIGHT).into(),
                ..default()
            }),
            ..default()
        }))
        .add_startup_system(spawn_camera)
        .add_startup_system(spawn_stage)

        .add_system(bevy::window::close_on_esc)
        .add_plugin(DebugLinesPlugin::default())
        .add_system(draw_grid)

        .add_system(
            input_direction
            .run_if(in_state(PlayerState::Stop))
        )
        .add_system(
            reflect_position
            .in_schedule(OnExit(PlayerState::Stop))
        )

        .add_system(
            object_movement
            .run_if(in_state(PlayerState::Move))
        )
        .add_system(
            debug_print_stage
            .in_schedule(OnExit(PlayerState::Move))
        )

        .run();
}
components.rs
use super::resources::*;

use bevy::prelude::*;

pub const X_BOTTOM: f32 = -250.0;
pub const Y_BOTTOM: f32 = -250.0;

pub const WINDOW_WIDTH: f32 = 500.0;
pub const WINDOW_HEIGHT: f32 = 500.0;

pub const GRID_SIZE: f32 = WINDOW_WIDTH / GRID_X_LENGTH as f32;

#[derive(Component)]
pub struct Player {}

#[derive(Component)]
pub struct Box {}

#[derive(Component, Debug)]
pub struct Position {
    pub x: i8,
    pub y: i8,
}

impl Position {
    pub fn translation(&self)->Vec3{
        let x: f32 = self.x as f32 * GRID_SIZE + GRID_SIZE/2.0 + X_BOTTOM;
        let y: f32 = self.y as f32 * GRID_SIZE + GRID_SIZE/2.0 + Y_BOTTOM;

        return Vec3::new(x, y, 0.0);
    }
}

#[derive(Component)]
pub struct MoveDirection {
    pub vec3: Vec3,
}
resources.rs
use std::usize;

use bevy::prelude::*;

pub const GRID_X_LENGTH: i8 = 8;
pub const GRID_Y_LENGTH: i8 = 8;

pub const COLOR_PLAYER: Color = Color::rgb(0.8, 0.4, 0.1);
pub const COLOR_BOX: Color = Color::rgb(0.1, 0.4, 0.8);
pub const COLOR_WALL: Color = Color::rgb(0.2, 0.2, 0.2);
pub const COLOR_BACK_GROUND: Color = Color::rgb(0.6, 0.6, 0.6);

pub const OBJECT_MOVE_TIME: f32 = 0.5;

pub const STAGE: &str = "
########:
#B     #:
#  B ###:
#   P  #:
###B#  #:
#  B  ##:
#      #:
########:
";

#[derive(Copy, Clone, PartialEq, Debug)]
pub enum Object {
    Wall,
    Player,
    Box,
    Empty,
}

impl Object {
    pub fn color(&self)->Color{
        match self {
            Object::Player => COLOR_PLAYER,
            Object::Wall => COLOR_WALL,
            Object::Box => COLOR_BOX,
            Object::Empty => panic!(),
        }
    }
}

#[derive(Resource)]
pub struct Stage {
   pub squares:[[Object; GRID_X_LENGTH as usize]; GRID_Y_LENGTH as usize]
}

impl Default for Stage {
    fn default() -> Self {
        let mut squares =
            [[Object::Empty; GRID_X_LENGTH as usize]; GRID_Y_LENGTH as usize];

        let mut i_y:isize = (GRID_Y_LENGTH - 1) as isize;
        let mut i_x:usize = 0;

        for char in STAGE.chars() {
            match char {
                '\n' => continue,
                ' ' => squares[i_y as usize][i_x] = Object::Empty,
                ':' => {
                    i_y -= 1;
                    i_x = 0;

                    continue;
                },
                '#' => squares[i_y as usize][i_x] = Object::Wall ,
                'P' => squares[i_y as usize][i_x] = Object::Player,
                'B' => squares[i_y as usize][i_x] = Object::Box,
                _ =>{
                    panic!();
                },
            }

            i_x += 1;
        }

        Stage {
            squares: squares
        }
    }

}

impl Stage {
    pub fn swap (&mut self, (x1, y1): (usize, usize), (x2, y2): (usize, usize)){
        let buff = self.squares[y1][x1];
        self.squares[y1][x1] = self.squares[y2][x2];
        self.squares[y2][x2] = buff;
    }
}

#[derive(Resource)]
pub struct ObjectMoveTimer {
    pub timer: Timer,
}

impl Default for ObjectMoveTimer {
    fn default() -> Self {
        ObjectMoveTimer {
            timer: Timer::from_seconds(OBJECT_MOVE_TIME, TimerMode::Repeating),
        }
    }
}
states.rs
use bevy::prelude::*;

#[derive(States, Debug, Clone, Copy, Eq, PartialEq, Hash, Default)]
pub enum PlayerState {
    Move,
    #[default]
    Stop,
}
systems.rs
use super::resources::*;
use super::components::*;

use bevy::prelude::*;
use bevy::window::PrimaryWindow;
use bevy_prototype_debug_lines::*;

use super::states::*;

pub const OBJECT_MOVE_SPEED: f32 = GRID_SIZE / OBJECT_MOVE_TIME;

pub fn spawn_camera(mut commands: Commands){
    commands.spawn(Camera2dBundle::default());
}

pub fn draw_grid(
    window_query: Query<&Window, With<PrimaryWindow>>,
    mut lines: ResMut<DebugLines>
) {
    let window = window_query.get_single().unwrap();
    let half_win_width = 0.5 * window.width();
    let half_win_height = 0.5 * window.height();
    let x_space = window.width() / GRID_X_LENGTH as f32;
    let y_space = window.height() / GRID_Y_LENGTH as f32;

    let mut i = -1. * half_win_height;
    while i < half_win_height {
        lines.line(
            Vec3::new(-1. * half_win_width, i, 0.0),
            Vec3::new(half_win_width, i, 0.0),
            0.0,
        );
        i += y_space;
    }

    i = -1. * half_win_width;
    while i < half_win_width {
        lines.line(
            Vec3::new(i, -1. * half_win_height, 0.0),
            Vec3::new(i, half_win_height, 0.0),
            0.0,
        );
        i += x_space;
    }

    lines.line(
        Vec3::new(0., -1. * half_win_height, 0.0),
        Vec3::new(0., half_win_height, 0.0),
        0.0,
    );
}

pub fn spawn_stage (
    mut commands: Commands,
    stage: Res<Stage>
){
    for i_y in 0..GRID_Y_LENGTH {
        for i_x in 0..GRID_X_LENGTH {
            let positon = Position { x: i_x, y: i_y };
            let squares = stage.squares[i_y as usize][i_x as usize];

            if squares == Object::Empty {
                continue;
            }

            if squares == Object::Player {
                commands.spawn((
                    SpriteBundle {
                        sprite: Sprite {
                            color: squares.color(),
                            ..default()
                        },
                        transform: Transform {
                            scale: Vec3::new(GRID_SIZE, GRID_SIZE, 10.0),
                            translation: positon.translation(),
                            ..default()
                        },
                        ..default()
                    },
                    positon,
                    Player{},
                ));
            } else if squares == Object::Wall {
                commands.spawn((
                    SpriteBundle {
                        sprite: Sprite {
                            color: squares.color(),
                            ..default()
                        },
                        transform: Transform {
                            scale: Vec3::new(GRID_SIZE, GRID_SIZE, 10.0),
                            translation: positon.translation(),
                            ..default()
                        },
                        ..default()
                    },
                ));
            } else if squares == Object::Box {
                commands.spawn((
                    SpriteBundle {
                        sprite: Sprite {
                            color: squares.color(),
                            ..default()
                        },
                        transform: Transform {
                            scale: Vec3::new(GRID_SIZE, GRID_SIZE, 10.0),
                            translation: positon.translation(),
                            ..default()
                        },
                        ..default()
                    },
                    positon,
                    Box {},
                ));
            }
        }
    }
}

pub fn input_direction (
    mut commands: Commands,
    mut player_query: Query<(Entity, &Position), With<Player>>,
    mut box_query: Query<(Entity, &Position), (With<Box>, Without<Player>)>,
    keyboard_input: Res<Input<KeyCode>>,
    mut stage: ResMut<Stage>,
){

    if let Ok((player_entity, player_posision)) = player_query.get_single_mut() {

        let direction = if keyboard_input.pressed(KeyCode::Left){
            Vec3::new(-1.0, 0.0, 0.0)
        } else if keyboard_input.pressed(KeyCode::Right){
            Vec3::new(1.0, 0.0, 0.0)
        } else if keyboard_input.pressed(KeyCode::Up){
            Vec3::new(0.0, 1.0, 0.0)
        } else if keyboard_input.pressed(KeyCode::Down){
            Vec3::new(0.0, -1.0, 0.0)
        } else {
            Vec3::ZERO
        };

        if direction == Vec3::ZERO {
            return;
        }

        // 移動に伴い座標の値が変化する座標を保存
        let mut swap_position_list: Vec<Position> = Vec::new();

        swap_position_list.push(Position { x: player_posision.x, y: player_posision.y });

        // 移動先に障害物にないか調査
        let mut i_x = (player_posision.x as isize + direction.x as isize) as usize;
        let mut i_y = (player_posision.y as isize + direction.y as isize) as usize;

        while stage.squares[i_y][i_x] != Object::Empty && stage.squares[i_y][i_x] != Object::Wall {
            swap_position_list.push(Position { x: i_x as i8, y: i_y as i8});
            
            i_x = (i_x as isize + direction.x as isize) as usize;
            i_y = (i_y as isize + direction.y as isize) as usize;
        }
        swap_position_list.push(Position { x: i_x as i8, y: i_y as i8});

        // 移動先に壁があった場合は移動をしない
        if stage.squares[i_y][i_x] == Object::Wall {
            return;
        }

        // 移動に併せて配列stageの中身を変更
        for index in (1..swap_position_list.len()).rev() {
            stage.swap(
                (swap_position_list[index].x as usize, swap_position_list[index].y as usize), 
                (swap_position_list[index-1].x as usize, swap_position_list[index-1].y as usize)
            );
        }

        for (box_entity, box_position) in box_query.iter_mut() {
            if swap_position_list.iter().any(|position| 
                position.x == box_position.x && position.y == box_position.y)
            {
                commands.entity(box_entity).insert(MoveDirection{
                    vec3: direction
                });
            }
        }

        commands.entity(player_entity)
            .insert(MoveDirection{
                vec3: direction
            });

        commands.insert_resource(NextState(Some(PlayerState::Move)));

    }
}

pub fn reflect_position (
    mut object_query: Query<(&mut Position, &MoveDirection)>,
) {
    for (mut position, move_direction) in object_query.iter_mut() {
        position.x = (position.x as isize + move_direction.vec3.x as isize) as i8;
        position.y = (position.y as isize + move_direction.vec3.y as isize) as i8;
    }
}

pub fn object_movement (
    mut commands: Commands,
    mut object_move_timer: ResMut<ObjectMoveTimer>,
    mut object_query: Query<(Entity, &Position, &mut Transform, &MoveDirection)>,
    time: Res<Time>,
) {
    for (entity, posision, mut transform, move_direction) in object_query.iter_mut(){
        transform.translation += move_direction.vec3 * OBJECT_MOVE_SPEED * time.delta_seconds();

        if object_move_timer.timer.finished() {
           transform.translation = posision.translation();

            commands.entity(entity)
                .remove::<MoveDirection>();

            commands.insert_resource(NextState(Some(PlayerState::Stop)));
        }
    }
    object_move_timer.timer.tick(time.delta());
}

pub fn debug_print_stage (
    stage: Res<Stage>,
) {

    for i_y in (0..GRID_Y_LENGTH).rev() {
        for i_x in 0..GRID_X_LENGTH {
            let c = match stage.squares[i_y as usize][i_x as usize] {
                Object::Player => 'P', 
                Object::Box => 'B', 
                Object::Wall => '#', 
                Object::Empty => ' ', 
            };
            print!("{}", c);
        }
        print!("\n");
    } 

}

Discussion