ゲームエンジンBevyのセットアップ方法
この記事ではゲームエンジンBevy
のセットアップ方法について書かれています。
実装したコードは以下のURLから見ることもできます。
バージョン
Bevy
のバージョンは0.14.2
を使用しています。
このバージョン以外で実装を行うと動作しない可能性があります。
bevy = "0.14.2"
セットアップ
リポジトリ
をクローンしてから、cargo run
を実行することで遊ぶことができます。
実装内容
このプロジェクトは主にゲームステートの遷移についての記述がされています。
ゲームステートはMainmenu, Ingame, Pause, Gameover, Gameclear
の5つで、これらのステートを行き来するコードが書かれています。
各ステートの遷移方法は以下の通り。
Mainmenu
ゲームを起動した際に初めに遷移するステート。
画面をクリックすることでIngame
ステートに遷移します。
Ingame
ゲームを遊ぶためのステート。
画面左下のポーズボタンをクリックするとPause
ステートに遷移します。
キーA
を押すことでGameover
ステートに遷移します。
キーD
を押すことでGameclear
ステートに遷移します。
また10秒間経過でGameover
ステートに遷移します。
Pause
ゲームを一時停止した時に遷移するステート。
画面左下のポーズボタンをクリックするとIngame
ステートに遷移します。
Gameover
ゲームにクリアできなかった時に遷移するステート。
キーR
を押すことでIngame
ステートに遷移します。
キーB
を押すことでMainmenu
ステートに遷移します。
Gameclear
ゲームをクリアした時に遷移するステート。
キーR
を押すことでIngame
ステートに遷移します。
キーB
を押すことでMainmenu
ステートに遷移します。
ファイル構造
ファイル構造は以下の通り。
bevy-setup
├── Cargo.lock
├── Cargo.toml
├── README.md
├── assets
│ ├── fonts
│ │ └── misaki_gothic.ttf
│ ├── images
│ │ ├── mainmenu.png
│ │ └── pausebutton.png
│ └── sounds
│ └── bgm.ogg
└── src
├── gameclear.rs
├── gameover.rs
├── ingame
│ ├── key.rs
│ ├── mod.rs
│ ├── pausebutton.rs
│ ├── scoreboard.rs
│ ├── text.rs
│ └── timer.rs
├── main.rs
└── mainmenu.rs
各ファイルの記述内容は以下のとおり。
ファイル名 | 記述内容 |
---|---|
main.rs |
プロジェクトの環境変数やセットアップコードなど |
mainmenu.rs |
メインメニューのセットアップ、処理など |
gameover.rs |
ゲームオーバーのセットアップ、処理など |
gameclear.rs |
ゲームクリアのセットアップ、処理など |
ingame/mod.rs |
ゲーム内の各パーツをまとめているファイル |
ingame/key.rs |
ゲーム内のキーボード処理 |
ingame/pausebutton.rs |
ゲーム内のポーズボタンのセットアップ、処理 |
ingame/scoreboard.rs |
ゲーム内のスコアボードのセットアップ、処理 |
ingame/text.rs |
ゲーム内のテキストのセットアップ、処理 |
ingame/timer.rs |
ゲーム内のタイマーなセットアップ、処理 |
各ファイルの記述と処理内容
main.rs
ここには環境変数、定数、ステート、全ステート共通のセットアップなどが書かれています。
use bevy::prelude::*;
mod mainmenu;
mod ingame;
mod gameover;
mod gameclear;
const GAMETITLE: &str = "Bevyセットアップ";
const WINDOW_SIZE: Vec2 = Vec2::new(640.0, 480.0);
const BACKGROUND_COLOR: Color = Color::srgb(0.9, 0.9, 0.9);
const CURSOR_RANGE: f32 = 10.0;
const GAMETIME_LIMIT: f32 = 10.0;
const PATH_FONT: &str = "fonts/misaki_gothic.ttf";
const PATH_IMAGE_MAINMENU: &str = "images/mainmenu.png";
const PATH_IMAGE_PAUSEBUTTON: &str = "images/pausebutton.png";
const PATH_SOUND_BGM: &str = "sounds/bgm.ogg";
#[derive(States, Default, Debug, Clone, PartialEq, Eq, Hash)]
enum AppState {
#[default]
Mainmenu,
Ingame,
Pause,
Gameover,
Gameclear,
}
#[derive(Resource, Deref, DerefMut, Debug)]
struct Config {
setup_ingame: bool,
}
#[derive(Resource, Deref, DerefMut, Debug)]
struct Score(pub usize);
#[derive(Resource)]
struct GameTimer(Timer);
fn main() {
App::new()
.add_plugins(DefaultPlugins
.set(WindowPlugin {
primary_window: Some(Window {
resolution: WINDOW_SIZE.into(),
title: GAMETITLE.to_string(),
..Default::default()
}),
..Default::default()
})
)
.init_state::<AppState>()
.insert_resource(ClearColor(BACKGROUND_COLOR))
.insert_resource(Time::<Fixed>::from_seconds(1.0 / 60.0))
.insert_resource(Config { setup_ingame: true })
.insert_resource(Score(0))
.insert_resource(GameTimer(
Timer::from_seconds(GAMETIME_LIMIT, TimerMode::Once)
))
.add_systems(Startup, setup)
.add_plugins(mainmenu::MainmenuPlugin)
.add_plugins(ingame::IngamePlugin)
.add_plugins(gameover::GameoverPlugin)
.add_plugins(gameclear::GameclearPlugin)
.run();
}
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
) {
println!("main: setup");
// camera
commands.spawn(Camera2dBundle::default());
// bgm
let bgm_sound = asset_server.load(PATH_SOUND_BGM);
commands.spawn(
AudioBundle {
source: bgm_sound,
settings: PlaybackSettings::LOOP.with_spatial(true),
}
)
.insert(Name::new("bgm"));
}
mainmenu.rs
ここにはメインメニューのゲームタイトル、クリックスタート、背景画像のセットアップや、
ステート遷移させるコードなどが書かれています。
use bevy::{
prelude::*,
sprite::{MaterialMesh2dBundle, Mesh2dHandle},
};
use crate::{
GAMETITLE,
WINDOW_SIZE,
PATH_FONT,
PATH_IMAGE_MAINMENU,
AppState,
};
const GAMETITLE_FONT_SIZE: f32 = 32.0;
const GAMETITLE_COLOR: Color = Color::srgb(0.1, 0.1, 0.1);
const CLICKSTART_TEXT: &str = "クリックしてスタート";
const CLICKSTART_FONT_SIZE: f32 = 20.0;
const CLICKSTART_COLOR: Color = Color::srgb(0.2, 0.2, 0.2);
const TEXT_PADDING: f32 = 40.0;
const BOARD_SIZE: Vec2 = Vec2::new(280.0, 210.0);
const BOARD_COLOR: Color = Color::srgb(0.9, 0.9, 0.9);
#[derive(Component)]
struct Mainmenu;
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
println!("mainmenu: setup");
// game title
let top = Val::Px(WINDOW_SIZE.y / 2.0 - GAMETITLE_FONT_SIZE / 2.0 - TEXT_PADDING);
commands.spawn((
TextBundle::from_section(
GAMETITLE,
TextStyle {
font: asset_server.load(PATH_FONT),
font_size: GAMETITLE_FONT_SIZE,
color: GAMETITLE_COLOR,
}
)
.with_style(Style {
position_type: PositionType::Relative,
justify_self: JustifySelf::Center,
top,
..Default::default()
}),
Mainmenu,
))
.insert(Name::new("gametitle"));
// click start
let top = Val::Px(WINDOW_SIZE.y / 2.0 - CLICKSTART_FONT_SIZE / 2.0 + TEXT_PADDING);
commands.spawn((
TextBundle::from_section(
CLICKSTART_TEXT,
TextStyle {
font: asset_server.load(PATH_FONT),
font_size: CLICKSTART_FONT_SIZE,
color: CLICKSTART_COLOR,
}
)
.with_style(Style {
position_type: PositionType::Relative,
justify_self: JustifySelf::Center,
top,
..Default::default()
}),
Mainmenu,
))
.insert(Name::new("clickstart"));
// board
commands.spawn((
MaterialMesh2dBundle {
mesh: Mesh2dHandle(meshes.add(Rectangle::new(BOARD_SIZE.x, BOARD_SIZE.y))),
material: materials.add(BOARD_COLOR),
..Default::default()
},
Mainmenu,
))
.insert(Name::new("board"));
// image
commands.spawn((
SpriteBundle {
texture: asset_server.load(PATH_IMAGE_MAINMENU),
transform: Transform::from_xyz(0.0, 0.0, -10.0),
..default()
},
Mainmenu,
))
.insert(Name::new("image"));
}
fn update(
mouse_event: Res<ButtonInput<MouseButton>>,
mainmenu_query: Query<Entity, With<Mainmenu>>,
mut commands: Commands,
mut next_state: ResMut<NextState<AppState>>,
) {
if mouse_event.just_pressed(MouseButton::Left) {
println!("mainmenu: clicked");
println!("mainmenu: despawned");
for mainmenu_entity in mainmenu_query.iter() {
commands.entity(mainmenu_entity).despawn();
}
println!("mainmenu: moved state to Ingame from Mainmenu");
next_state.set(AppState::Ingame);
}
}
pub struct MainmenuPlugin;
impl Plugin for MainmenuPlugin {
fn build(&self, app: &mut App) {
app
.add_systems(OnEnter(AppState::Mainmenu), setup)
.add_systems(Update, update.run_if(in_state(AppState::Mainmenu)));
}
}
gameover.rs
ここにはゲームオーバーのテキスト、スコア、リトライ、タイトルに戻るセットアップや、
ステートの遷移するコードが書かれています。
use bevy::prelude::*;
use crate::{
WINDOW_SIZE,
PATH_FONT,
AppState,
Config,
Score,
};
const GAMEOVER_TEXT: &str = "ゲームオーバー";
const GAMEOVER_FONT_SIZE: f32 = 32.0;
const SCORE_TEXT: &str = "スコア: ";
const RETRY_TEXT: &str = "リトライ: Key[R]";
const BACKTOTITLE_TEXT: &str = "タイトルに戻る: Key[B]";
const TEXT_COLOR: Color = Color::srgb(0.1, 0.1, 0.1);
const TEXT_FONT_SIZE: f32 = 20.0;
const TEXT_PADDING: f32 = 40.0;
#[derive(Component)]
struct Gameover;
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
score: Res<Score>,
) {
println!("gameover: setup");
// gameover
let top = Val::Px(WINDOW_SIZE.y / 2.0 - GAMEOVER_FONT_SIZE / 2.0 - TEXT_PADDING * 1.5);
commands.spawn((
TextBundle::from_section(
GAMEOVER_TEXT,
TextStyle {
font: asset_server.load(PATH_FONT),
font_size: GAMEOVER_FONT_SIZE,
color: TEXT_COLOR,
}
)
.with_style(Style {
position_type: PositionType::Relative,
justify_self: JustifySelf::Center,
top,
..Default::default()
}),
Gameover,
))
.insert(Name::new("gameover"));
// score
let top = Val::Px(WINDOW_SIZE.y / 2.0 - TEXT_FONT_SIZE / 2.0 - TEXT_PADDING * 0.5);
commands.spawn((
TextBundle::from_section(
format!("{}{}", SCORE_TEXT, **score),
TextStyle {
font: asset_server.load(PATH_FONT),
font_size: TEXT_FONT_SIZE,
color: TEXT_COLOR,
}
)
.with_style(Style {
position_type: PositionType::Relative,
justify_self: JustifySelf::Center,
top,
..Default::default()
}),
Gameover,
))
.insert(Name::new("score"));
// retry
let top = Val::Px(WINDOW_SIZE.y / 2.0 - TEXT_FONT_SIZE / 2.0 + TEXT_PADDING * 0.5);
commands.spawn((
TextBundle::from_section(
RETRY_TEXT,
TextStyle {
font: asset_server.load(PATH_FONT),
font_size: TEXT_FONT_SIZE,
color: TEXT_COLOR,
}
)
.with_style(Style {
position_type: PositionType::Relative,
justify_self: JustifySelf::Center,
top,
..Default::default()
}),
Gameover,
))
.insert(Name::new("retry"));
// back to title
let top = Val::Px(WINDOW_SIZE.y / 2.0 - TEXT_FONT_SIZE / 2.0 + TEXT_PADDING * 1.5);
commands.spawn((
TextBundle::from_section(
BACKTOTITLE_TEXT,
TextStyle {
font: asset_server.load(PATH_FONT),
font_size: TEXT_FONT_SIZE,
color: TEXT_COLOR,
}
)
.with_style(Style {
position_type: PositionType::Relative,
justify_self: JustifySelf::Center,
top,
..Default::default()
}),
Gameover,
))
.insert(Name::new("backtotitle"));
}
fn update(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut config: ResMut<Config>,
mut next_state: ResMut<NextState<AppState>>,
) {
let mut closure = |key: &KeyCode, app_state: AppState| {
println!("gameover: {:?} just pressed", key);
println!("gameover: config setup ingame is true");
config.setup_ingame = true;
println!("gameover: moved state to {:?} from Gameover", app_state);
next_state.set(app_state);
};
for key in keyboard_input.get_just_pressed() {
match key {
KeyCode::KeyR => closure(key, AppState::Ingame),
KeyCode::KeyB => closure(key, AppState::Mainmenu),
_ => {},
}
}
}
fn despawn_gameover(
mut commands: Commands,
query: Query<Entity, With<Gameover>>,
) {
println!("gameover: despawned");
for entity in query.iter() {
commands.entity(entity).despawn();
}
}
pub struct GameoverPlugin;
impl Plugin for GameoverPlugin {
fn build(&self, app: &mut App) {
app
.add_systems(OnEnter(AppState::Gameover), setup)
.add_systems(Update, update.run_if(in_state(AppState::Gameover)))
.add_systems(OnExit(AppState::Gameover), despawn_gameover);
}
}
gameclear.rs
ここにはゲームオーバーの処理とほぼ同じコードが書かれています。
use bevy::prelude::*;
use crate::{
WINDOW_SIZE,
PATH_FONT,
AppState,
Config,
Score,
GameTimer,
};
const GAMECLEAR_TEXT: &str = "ゲームクリア";
const GAMECLEAR_FONT_SIZE: f32 = 32.0;
const SCORE_TEXT: &str = "スコア: ";
const TIMER_TEXT: &str = "タイム: ";
const RETRY_TEXT: &str = "リトライ: Key[R]";
const BACKTOTITLE_TEXT: &str = "タイトルに戻る: Key[B]";
const TEXT_COLOR: Color = Color::srgb(0.1, 0.1, 0.1);
const TEXT_FONT_SIZE: f32 = 20.0;
const TEXT_PADDING: f32 = 40.0;
#[derive(Component)]
struct Gameclear;
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
score: Res<Score>,
timer: Res<GameTimer>,
) {
println!("gameclear: setup");
// gameover
let top = WINDOW_SIZE.y / 2.0 - GAMECLEAR_FONT_SIZE / 2.0 - TEXT_PADDING * 2.0;
commands.spawn((
TextBundle::from_section(
GAMECLEAR_TEXT,
TextStyle {
font: asset_server.load(PATH_FONT),
font_size: GAMECLEAR_FONT_SIZE,
color: TEXT_COLOR,
}
)
.with_style(Style {
position_type: PositionType::Relative,
justify_self: JustifySelf::Center,
top: Val::Px(top),
..Default::default()
}),
Gameclear,
))
.insert(Name::new("gameclear"));
// score
let top = WINDOW_SIZE.y / 2.0 - TEXT_FONT_SIZE / 2.0 - TEXT_PADDING;
commands.spawn((
TextBundle::from_section(
format!("{}{}", SCORE_TEXT, **score),
TextStyle {
font: asset_server.load(PATH_FONT),
font_size: TEXT_FONT_SIZE,
color: TEXT_COLOR,
}
)
.with_style(Style {
position_type: PositionType::Relative,
justify_self: JustifySelf::Center,
top: Val::Px(top),
..Default::default()
}),
Gameclear,
))
.insert(Name::new("score"));
// timer
let top = WINDOW_SIZE.y / 2.0 - TEXT_FONT_SIZE / 2.0;
commands.spawn((
TextBundle::from_section(
format!("{}{}", TIMER_TEXT, timer.0.remaining_secs().round().to_string()),
TextStyle {
font: asset_server.load(PATH_FONT),
font_size: TEXT_FONT_SIZE,
color: TEXT_COLOR,
}
)
.with_style(Style {
position_type: PositionType::Relative,
justify_self: JustifySelf::Center,
top: Val::Px(top),
..Default::default()
}),
Gameclear,
))
.insert(Name::new("timer"));
// retry
let top = WINDOW_SIZE.y / 2.0 - TEXT_FONT_SIZE / 2.0 + TEXT_PADDING;
commands.spawn((
TextBundle::from_section(
RETRY_TEXT,
TextStyle {
font: asset_server.load(PATH_FONT),
font_size: TEXT_FONT_SIZE,
color: TEXT_COLOR,
}
)
.with_style(Style {
position_type: PositionType::Relative,
justify_self: JustifySelf::Center,
top: Val::Px(top),
..Default::default()
}),
Gameclear,
))
.insert(Name::new("retry"));
// back to title
let top = WINDOW_SIZE.y / 2.0 - TEXT_FONT_SIZE / 2.0 + TEXT_PADDING * 2.0;
commands.spawn((
TextBundle::from_section(
BACKTOTITLE_TEXT,
TextStyle {
font: asset_server.load(PATH_FONT),
font_size: TEXT_FONT_SIZE,
color: TEXT_COLOR,
}
)
.with_style(Style {
position_type: PositionType::Relative,
justify_self: JustifySelf::Center,
top: Val::Px(top),
..Default::default()
}),
Gameclear,
))
.insert(Name::new("backtotitle"));
}
fn update(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut config: ResMut<Config>,
mut next_state: ResMut<NextState<AppState>>,
) {
let mut closure = |key: &KeyCode, app_state: AppState| {
println!("gameclear: {:?} just pressed", key);
println!("gameclear: config setup ingame is true");
config.setup_ingame = true;
println!("gameclear: moved state to {:?} from Gameclear", app_state);
next_state.set(app_state);
};
for key in keyboard_input.get_just_pressed() {
match key {
KeyCode::KeyR => closure(key, AppState::Ingame),
KeyCode::KeyB => closure(key, AppState::Mainmenu),
_ => {},
}
}
}
fn despawn_gameclear(
mut commands: Commands,
query: Query<Entity, With<Gameclear>>,
) {
println!("gameclear: despawned");
for entity in query.iter() {
commands.entity(entity).despawn();
}
}
pub struct GameclearPlugin;
impl Plugin for GameclearPlugin {
fn build(&self, app: &mut App) {
app
.add_systems(OnEnter(AppState::Gameclear), setup)
.add_systems(Update, update.run_if(in_state(AppState::Gameclear)))
.add_systems(OnExit(AppState::Gameclear), despawn_gameclear);
}
}
ingame/mod.rs
ここにはゲームの各パーツをまとめるコードが書かれています。
use bevy::prelude::*;
mod key;
mod pausebutton;
mod scoreboard;
mod text;
mod timer;
pub struct IngamePlugin;
impl Plugin for IngamePlugin {
fn build(&self, app: &mut App) {
app
.add_plugins(key::KeyPlugin)
.add_plugins(pausebutton::PausebuttonPlugin)
.add_plugins(scoreboard::ScoreBoardPlugin)
.add_plugins(text::TextPlugin)
.add_plugins(timer::TimerPlugin);
}
}
ingame/key.rs
ここにはキーボード操作でステートを遷移先を決めるようなコードが書かれています。
use bevy::prelude::*;
use crate::AppState;
fn update(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut next_state: ResMut<NextState<AppState>>,
) {
let mut closure = |key: &KeyCode, app_state: AppState| {
println!("key: {:?} just pressed", key);
println!("key: moved state to {:?} from Ingame", app_state);
next_state.set(app_state);
};
for key in keyboard_input.get_just_pressed() {
match key {
KeyCode::KeyA => closure(key, AppState::Gameover),
KeyCode::KeyD => closure(key, AppState::Gameclear),
_ => {},
}
}
}
pub struct KeyPlugin;
impl Plugin for KeyPlugin {
fn build(&self, app: &mut App) {
app
.add_systems(Update, update.run_if(in_state(AppState::Ingame)));
}
}
ingame/pausebutton.rs
ここにはポーズボタンのセットアップや、
クリックした時のトグル、
ステートの遷移、
遷移した時のコンポーネントの削除するコードなどが書かれています。
use bevy::{
prelude::*,
window::PrimaryWindow,
};
use crate::{
WINDOW_SIZE,
CURSOR_RANGE,
PATH_IMAGE_PAUSEBUTTON,
AppState,
Config,
};
const IMAGE_SIZE: u32 = 64;
const SIZE: f32 = 32.0;
#[derive(Component)]
pub struct PauseButton {
first: usize,
last: usize,
}
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
config: Res<Config>,
) {
if !config.setup_ingame { return }
println!("pausebutton: setup");
let layout = TextureAtlasLayout::from_grid(UVec2::splat(IMAGE_SIZE), 2, 1, None, None);
let texture_atlas_layout = texture_atlas_layouts.add(layout);
let animation_indices = PauseButton { first: 0, last: 1, };
let pos = Vec3::new(
WINDOW_SIZE.x / 2.0 - SIZE,
-WINDOW_SIZE.y / 2.0 + SIZE,
10.0
);
commands.spawn((
SpriteBundle {
sprite: Sprite {
custom_size: Some(Vec2::splat(SIZE)),
..Default::default()
},
texture: asset_server.load(PATH_IMAGE_PAUSEBUTTON),
transform: Transform::from_translation(pos),
..Default::default()
},
TextureAtlas {
layout: texture_atlas_layout,
index: animation_indices.first,
},
animation_indices,
))
.insert(Name::new("pausebutton"));
}
fn update(
mouse_event: Res<ButtonInput<MouseButton>>,
window_query: Query<&Window, With<PrimaryWindow>>,
mut query: Query<(&Transform, &PauseButton, &mut TextureAtlas), With<PauseButton>>,
mut config: ResMut<Config>,
mut next_state: ResMut<NextState<AppState>>,
) {
if !mouse_event.just_pressed(MouseButton::Left) { return; }
let window = window_query.single();
let mut cursor_pos = window.cursor_position().unwrap();
let Ok((transform, prop, mut atlas)) = query.get_single_mut() else { return; };
let pausebutton_pos = transform.translation.truncate();
// get cursor position
cursor_pos = Vec2::new(
cursor_pos.x - WINDOW_SIZE.x / 2.0,
-cursor_pos.y + WINDOW_SIZE.y / 2.0
);
let distance = cursor_pos.distance(pausebutton_pos);
if distance < SIZE - CURSOR_RANGE {
println!("pausebutton: clicked");
if atlas.index == prop.first {
println!("pausebutton: toggled");
atlas.index = prop.last;
println!("pausebutton: moved state to Pause from Ingame");
next_state.set(AppState::Pause);
} else {
println!("pausebutton: change config.setup_ingame to false");
config.setup_ingame = false;
println!("pausebutton: toggled");
atlas.index = prop.first;
println!("pausebutton: moved state to Ingame from Pause");
next_state.set(AppState::Ingame);
}
}
}
fn despawn_pausebutton(
mut commands: Commands,
query: Query<Entity, With<PauseButton>>,
) {
let entity = query.single();
println!("pausebutton: despawned");
commands.entity(entity).despawn();
}
pub struct PausebuttonPlugin;
impl Plugin for PausebuttonPlugin {
fn build(&self, app: &mut App) {
app
.add_systems(OnEnter(AppState::Ingame), setup)
.add_systems(Update, update.run_if(in_state(AppState::Ingame)))
.add_systems(Update, update.run_if(in_state(AppState::Pause)))
.add_systems(OnEnter(AppState::Gameover), despawn_pausebutton)
.add_systems(OnEnter(AppState::Gameclear), despawn_pausebutton);
}
}
ingame/scoreboard.rs
ここにはスコア、タイマーが書かれたスコアボードのセットアップ、
値の更新、
ステート遷移した時のコンポーネントの削除するコードなどが書かれています。
use bevy::prelude::*;
use crate::{
PATH_FONT,
AppState,
Config,
Score,
GameTimer,
};
const SCORE_TEXT: &str = "スコア: ";
const TIMER_TEXT: &str = " | タイム: ";
const TEXT_FONT_SIZE: f32 = 20.0;
const TEXT_COLOR: Color = Color::srgb(0.1, 0.1, 0.1);
const TEXT_PADDING: Val = Val::Px(5.0);
#[derive(Component)]
struct ScoreboardUi;
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
config: Res<Config>,
) {
if !config.setup_ingame { return }
commands.spawn((
TextBundle::from_sections([
TextSection::new(
SCORE_TEXT,
TextStyle {
font: asset_server.load(PATH_FONT),
font_size: TEXT_FONT_SIZE,
color: TEXT_COLOR,
},
),
TextSection::from_style(TextStyle {
font: asset_server.load(PATH_FONT),
font_size: TEXT_FONT_SIZE,
color: TEXT_COLOR,
}),
TextSection::new(
TIMER_TEXT,
TextStyle {
font: asset_server.load(PATH_FONT),
font_size: TEXT_FONT_SIZE,
color: TEXT_COLOR,
},
),
TextSection::from_style(TextStyle {
font: asset_server.load(PATH_FONT),
font_size: TEXT_FONT_SIZE,
color: TEXT_COLOR,
}),
])
.with_style(Style {
position_type: PositionType::Absolute,
top: TEXT_PADDING,
left: TEXT_PADDING,
..Default::default()
}),
ScoreboardUi,
));
}
fn update(
mut query: Query<&mut Text, With<ScoreboardUi>>,
score: Res<Score>,
timer: Res<GameTimer>,
) {
let mut text = query.single_mut();
// write score and timer
text.sections[1].value = score.to_string();
text.sections[3].value = timer.0.remaining_secs().round().to_string();
}
fn despawn_scoreboard(
mut commands: Commands,
query: Query<Entity, With<ScoreboardUi>>,
) {
let entity = query.single();
println!("scoreboard: despawned");
commands.entity(entity).despawn();
}
fn score_points(mut score: ResMut<Score>) {
if **score >= 500 { **score = 0; }
**score += 1;
}
fn reset_score(mut score: ResMut<Score>) {
**score = 0;
}
pub struct ScoreBoardPlugin;
impl Plugin for ScoreBoardPlugin {
fn build(&self, app: &mut App) {
app
.add_systems(OnEnter(AppState::Ingame), setup)
.add_systems(Update, update.run_if(in_state(AppState::Ingame)))
.add_systems(Update, score_points.run_if(in_state(AppState::Ingame)))
.add_systems(OnEnter(AppState::Gameover), despawn_scoreboard)
.add_systems(OnEnter(AppState::Gameclear), despawn_scoreboard)
.add_systems(OnExit(AppState::Gameover), reset_score)
.add_systems(OnExit(AppState::Gameclear), reset_score);
}
}
ingame/text.rs
ここにはゲーム内に表示するテキストのセットアップ、
ステート遷移した時のコンポーネントを削除するコードなどが書かれています。
use bevy::prelude::*;
use crate::{
WINDOW_SIZE,
PATH_FONT,
AppState,
Config,
};
const INGAME_TEXT: &str = "ゲーム中";
const INGAME_FONT_SIZE: f32 = 32.0;
const GAMEOVER_TEXT: &str = "ゲームオーバー: Key[A]";
const GAMECLEAR_TEXT: &str = "ゲームクリア: Key[D]";
const TEXT_COLOR: Color = Color::srgb(0.1, 0.1, 0.1);
const TEXT_FONT_SIZE: f32 = 20.0;
const TEXT_PADDING: f32 = 40.0;
#[derive(Component)]
struct IngameText;
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
config: Res<Config>,
) {
if !config.setup_ingame { return }
println!("text: setup");
// ingame
commands.spawn((
TextBundle::from_section(
INGAME_TEXT,
TextStyle {
font: asset_server.load(PATH_FONT),
font_size: INGAME_FONT_SIZE,
color: TEXT_COLOR,
}
)
.with_style(Style {
position_type: PositionType::Relative,
justify_self: JustifySelf::Center,
top: Val::Px(WINDOW_SIZE.y / 2.0 - INGAME_FONT_SIZE / 2.0 - TEXT_PADDING),
..Default::default()
}),
IngameText,
))
.insert(Name::new("ingame"));
// game over
commands.spawn((
TextBundle::from_section(
GAMEOVER_TEXT,
TextStyle {
font: asset_server.load(PATH_FONT),
font_size: TEXT_FONT_SIZE,
color: TEXT_COLOR,
}
)
.with_style(Style {
position_type: PositionType::Relative,
justify_self: JustifySelf::Center,
top: Val::Px(WINDOW_SIZE.y / 2.0 - TEXT_FONT_SIZE / 2.0),
..Default::default()
}),
IngameText,
))
.insert(Name::new("gameover"));
// game clear
commands.spawn((
TextBundle::from_section(
GAMECLEAR_TEXT,
TextStyle {
font: asset_server.load(PATH_FONT),
font_size: TEXT_FONT_SIZE,
color: TEXT_COLOR,
}
)
.with_style(Style {
position_type: PositionType::Relative,
justify_self: JustifySelf::Center,
top: Val::Px(WINDOW_SIZE.y / 2.0 - INGAME_FONT_SIZE / 2.0 + TEXT_PADDING),
..Default::default()
}),
IngameText,
))
.insert(Name::new("gameclear"));
}
fn despawn_text(
mut commands: Commands,
query: Query<Entity, With<IngameText>>,
) {
println!("text: despawned");
for entity in query.iter() {
commands.entity(entity).despawn();
}
}
pub struct TextPlugin;
impl Plugin for TextPlugin {
fn build(&self, app: &mut App) {
app
.add_systems(OnEnter(AppState::Ingame), setup)
.add_systems(OnEnter(AppState::Gameover), despawn_text)
.add_systems(OnEnter(AppState::Gameclear), despawn_text);
}
}
ingame/timer.rs
ここにはタイマーが0になった時のステート遷移や、
タイマーをリセットするコードなどが書かれています。
use bevy::prelude::*;
use crate::{
AppState,
GameTimer,
};
fn update(
mut timer: ResMut<GameTimer>,
time: Res<Time>,
mut next_state: ResMut<NextState<AppState>>,
) {
if timer.0.tick(time.delta()).just_finished() {
println!("timer: moved state to Gameover from Ingame");
next_state.set(AppState::Gameover);
}
}
fn reset_timer(mut timer: ResMut<GameTimer>) {
println!("timer: reset");
timer.0.reset();
}
pub struct TimerPlugin;
impl Plugin for TimerPlugin {
fn build(&self, app: &mut App) {
app
.add_systems(Update, update.run_if(in_state(AppState::Ingame)))
.add_systems(OnExit(AppState::Gameover), reset_timer)
.add_systems(OnExit(AppState::Gameclear), reset_timer);
}
}
使用アセット
以下はBevyでゲーム開発をする時に重宝しているツールやサイトです。
どれもこれも素晴らしいのでゲーム開発におすすめです!!!
リスト
Bevy(ゲームエンジン)
Sunny Land(タイトル画像)
美咲フォント
ICOOON MONO(ポーズボタン画像)
効果音ラボ(BGM)
Pixlr(画像編集)
まとめ
この記事はこれで以上になります。
もしBevyでゲームを開発するときにこの記事が何かの参考になれば筆者も嬉しく思います。
ここまで読んでくれてありがとう!!!
Discussion