昨日の夜は、もう駄目だーみたいな気持ちになっていたんですけれども、今日の10時くらいからは意外と集中できてなんと15時30にプログラミングを終わらすことが出来た。
use bevy::{prelude::*, window::PrimaryWindow, app::AppExit, };
use rand::random;
pub const PLAYER_SPEED: f32 = 500.0;
pub const PLAYER_SIZE: f32 = 130.0;
pub const NUMBER_OF_ENEMIES: usize = 3;
pub const ENEMY_SPEED: f32 = 200.0;
pub const ENEMY_SIZE: f32 = 130.0;
pub const NUMBER_OF_STARS:usize = 10;
pub const STAR_SIZE: f32 = 30.0;
pub const STAR_SPAWN_TIME: f32 = 0.3;
pub const ENEMY_SPAWN_TIME: f32 = 5.0;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.init_resource::<Score>()
.init_resource::<StarSpawnTimer>()
.init_resource::<EnemySpawnTimer>()
.init_resource::<HighScores>()
.add_event::<GameOver>()
.add_systems(Startup, spawn_camera)
.add_systems(Startup, spawn_player)
.add_systems(Startup, (spawn_enemies, spawn_stars))
.add_systems(Update, (player_movement, confine_player_movement).chain())
.add_systems(Update, (enemy_movement, update_enemy_direction, confine_enemy_movement).chain())
.add_systems(Update, (player_hit_star, update_score, tick_star_spawn_timer, spawn_stars_over_time, enemy_hit_player))
.add_systems(Update, (handle_game_over, exit_game, spawn_enemies_over_time, tick_enemy_spawn_timer, ))
.add_systems(Update, (update_high_scores, high_scores_updated).chain())
.run();
}
#[derive(Component)]
pub struct Player{}
#[derive(Component)]
pub struct Enemy{
pub direction: Vec2,
}
#[derive(Component)]
pub struct Star{}
#[derive(Resource)]
pub struct Score{
}
impl Default for Score {
fn default() -> Self {
}
}
#[derive(Resource, Debug)]
pub struct HighScores{
pub scores: Vec<(String, u32)>
}
impl Default for HighScores{
fn default() -> Self {
HighScores{ scores: Vec::new() }
}
}
#[derive(Resource)]
pub struct StarSpawnTimer{
pub timer: Timer,
}
impl Default for StarSpawnTimer {
fn default() -> Self {
StarSpawnTimer{
timer: Timer::from_seconds(STAR_SPAWN_TIME, TimerMode::Repeating),
}
}
}
#[derive(Resource)]
pub struct EnemySpawnTimer{
pub timer: Timer,
}
impl Default for EnemySpawnTimer {
fn default() -> Self {
EnemySpawnTimer{
timer: Timer::from_seconds(ENEMY_SPAWN_TIME, TimerMode::Repeating),
}
}
}
#[derive(Event)]
pub struct GameOver{
pub score: u32,
}
pub fn spawn_player(
mut commands: Commands,
window_query: Query<&Window, With<PrimaryWindow>>,
asset_server: Res<AssetServer>,
){
let window = window_query.get_single().unwrap();
commands.spawn(
(
Sprite{
image: asset_server.load("sprites/ball_blue_large.png"),
..default()
},
Transform::from_xyz(window.width()/2.0, window.height()/2.0, 0.0),
Player {},
));
}
pub fn spawn_camera(mut commands: Commands, window_query: Query<&Window, With<PrimaryWindow>>) {
let window = window_query.get_single().unwrap();
commands.spawn(
(
Camera2d::default(),
Camera{
},
Transform::from_xyz(window.width()/2.0, window.height()/2.0, 0.0),
)
);
}
pub fn spawn_enemies(
mut commands: Commands,
window_query: Query<&Window, With<PrimaryWindow>>,
asset_server: Res<AssetServer>,
){
let window = window_query.get_single().unwrap();
for _ in 0..NUMBER_OF_ENEMIES{
let random_x = random::<f32>() * (window.width() - ENEMY_SIZE);
let random_y = random::<f32>() * (window.height() - ENEMY_SIZE);
commands.spawn(
(
Sprite{
image: asset_server.load("sprites/ball_red_large.png"),
..default()
},
Transform::from_xyz(random_x, random_y, 0.0),
Enemy {
direction: Vec2::new(random::<f32>()-0.5, random::<f32>()-0.5)
.normalize(),
},
));
}
}
pub fn spawn_stars(
mut commands: Commands,
window_query: Query<&Window, With<PrimaryWindow>>,
asset_server: Res<AssetServer>
){
let window = window_query.get_single().unwrap();
for _ in 0..NUMBER_OF_STARS{
let random_x = random::<f32>() * (window.width() - STAR_SIZE);
let random_y = random::<f32>() * (window.height() - STAR_SIZE);
commands.spawn(
(
Sprite{
image: asset_server.load("sprites/star.png"),
..default()
},
Transform::from_xyz(random_x, random_y, 0.0),
Star{},
));
}
}
pub fn player_movement(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut player_query: Query<&mut Transform, With<Player>>,
time: Res<Time>,
){
if let Ok(mut transform) = player_query.get_single_mut(){
let mut direction = Vec3::ZERO;
if keyboard_input.pressed(KeyCode::ArrowLeft)||keyboard_input.pressed(KeyCode::KeyA){
direction += Vec3::new(-1.0, 0.0, 0.0);
}
if keyboard_input.pressed(KeyCode::ArrowRight)||keyboard_input.pressed(KeyCode::KeyD){
direction += Vec3::new(1.0, 0.0, 0.0);
}
if keyboard_input.pressed(KeyCode::ArrowUp) || keyboard_input.pressed(KeyCode::KeyW) {
direction += Vec3::new(0.0, 1.0, 0.0);
}
if keyboard_input.pressed(KeyCode::ArrowDown) || keyboard_input.pressed(KeyCode::KeyS) {
direction += Vec3::new(0.0, -1.0, 0.0);
}
if direction.length() > 0.0{
direction = direction.normalize();
}
transform.translation += direction * PLAYER_SPEED * time.delta_secs();
}
}
pub fn confine_player_movement(
mut player_query: Query<&mut Transform, With<Player>>,
window_query: Query<&Window, With<PrimaryWindow>>
){
if let Ok(mut player_transform) = player_query.get_single_mut(){
let window = window_query.get_single().unwrap();
let half_player_size = PLAYER_SIZE / 2.0;
let x_min = 0.0 + half_player_size;
let x_max = window.width()/1.0 - half_player_size;
let y_min = 0.0 + half_player_size;
let y_max = window.height()/1.0 -half_player_size;
let mut translation = player_transform.translation;
translation.x = translation.x.clamp(x_min, x_max);
translation.y = translation.y.clamp(y_min, y_max);
player_transform.translation = translation;
}
}
pub fn enemy_movement(
mut enemy_query: Query<(&mut Transform, &Enemy)>, time: Res<Time>){
for(mut transform, enemy)in enemy_query.iter_mut(){
let direction = Vec3::new(enemy.direction.x, enemy.direction.y, 0.0);
transform.translation += direction * ENEMY_SPEED * time.delta_secs();
}
}
pub fn update_enemy_direction(
mut enemy_query: Query<(&Transform, &mut Enemy)>,
window_query: Query<&Window, With<PrimaryWindow>>,
mut commands: Commands,
asset_server: Res<AssetServer>,
){
let window = window_query.get_single().unwrap();
let half_enemy_size = ENEMY_SIZE / 2.0;
let x_min = 0.0 + half_enemy_size;
let x_max = window.width() - half_enemy_size;
let y_min = 0.0 + half_enemy_size;
let y_max = window.height() - half_enemy_size;
for (transform, mut enemy) in enemy_query.iter_mut() {
let mut direction_changed = false;
let translation = transform.translation;
if translation.x < x_min || translation.x > x_max {
enemy.direction.x *= -1.0;
direction_changed = true;
}
if translation.y < y_min || translation.y > y_max {
enemy.direction.y *= -1.0;
direction_changed = true;
}
if direction_changed{
let sound_effect_1 = "audio/pluck_001.ogg";
let sound_effect_2 = "audio/pluck_002.ogg";
let sound_effect:bool = random::<bool>();
if sound_effect{
commands.spawn(AudioPlayer::new(
asset_server.load(sound_effect_1),
));
}else {
commands.spawn(AudioPlayer::new(
asset_server.load(sound_effect_2),
));
};
}
}
}
pub fn confine_enemy_movement(
mut enemy_query: Query<&mut Transform, With<Enemy>>,
window_query: Query<&Window, With<PrimaryWindow>>
){
let window = window_query.get_single().unwrap();
let half_enemy_size = PLAYER_SIZE / 2.0;
let x_min = 0.0 + half_enemy_size;
let x_max = window.width()/1.0 - half_enemy_size;
let y_min = 0.0 + half_enemy_size;
let y_max = window.height()/1.0 - half_enemy_size;
for mut transform in enemy_query.iter_mut(){
let mut translation = transform.translation;
translation.x = translation.x.clamp(x_min, x_max);
translation.y = translation.y.clamp(y_min, y_max);
transform.translation = translation;
}
}
pub fn enemy_hit_player(
mut commands: Commands,
mut game_over_event_writer: EventWriter<GameOver>,
player_query: Query<(Entity, &Transform), With<Player>>,
enemy_query: Query<&Transform, With<Enemy>>,
asset_server: Res<AssetServer>,
score: Res<Score>
){
if let Ok*1 = player_query.get_single(){
for enemy_transform in enemy_query.iter(){
let distance = player_transform
.translation
.distance(enemy_transform.translation);
let player_radius = PLAYER_SIZE /2.0;
let enemy_radius = ENEMY_SIZE /2.0;
println!("Enemy hit player! Game Over!!");
let sound_effect = "audio/explosionCrunch_000.ogg";
commands.spawn(AudioPlayer::new(
asset_server.load(sound_effect),
));
commands.entity(player_entity).despawn();
game_over_event_writer.send(GameOver { score: score.value});
}
}
}
}
pub fn player_hit_star(
mut commands: Commands,
player_query: Query<&Transform, With<Player>>,
star_query: Query<(Entity, &Transform), With<Star>>,
asset_server: Res<AssetServer>,
mut score: ResMut<Score>,
){
if let Ok(player_transform) = player_query.get_single(){
for (star_entity, star_transform) in star_query.iter(){
let distance = player_transform
.translation
.distance(star_transform.translation);
if distance < PLAYER_SIZE / 2.0 + STAR_SIZE / 2.0{
println!("Player hit star!");
let sound_effect = "audio/laserLarge_000.ogg";
commands.spawn(AudioPlayer::new(
asset_server.load(sound_effect),
));
commands.entity(star_entity).despawn();
}
}
}
}
pub fn update_score(score: Res<Score>){
if score.is_changed(){
println!("score: {}", score.value.to_string())
}
}
pub fn tick_star_spawn_timer(mut star_spawn_timer: ResMut<StarSpawnTimer>, time: Res<Time>) {
star_spawn_timer.timer.tick(time.delta());
}
pub fn spawn_stars_over_time(
mut commands: Commands,
window_query: Query<&Window, With<PrimaryWindow>>,
asset_server: Res<AssetServer>,
star_spawn_timer: ResMut<StarSpawnTimer>
){
if star_spawn_timer.timer.finished(){
let window = window_query.get_single().unwrap();
let random_x = random::<f32>() * (window.width() - STAR_SIZE);
let random_y = random::<f32>() * (window.height() - STAR_SIZE);
commands.spawn(
(
Sprite{
image: asset_server.load("sprites/star.png"),
..default()
},
Transform::from_xyz(random_x, random_y, 0.0),
Star{},
));
}
}
pub fn handle_game_over(mut game_over_event_reader: EventReader<GameOver>){
for event in game_over_event_reader.read(){
println!("Your final score is : {}", event.score);
}
}
pub fn exit_game(
keyboard_input: Res<ButtonInput<KeyCode>>,
mut app_exit_event_writer: EventWriter<AppExit>,
){
if keyboard_input.just_pressed(KeyCode::Escape){
app_exit_event_writer.send(AppExit::Success);
}
}
pub fn tick_enemy_spawn_timer(mut enemy_spawn_timer: ResMut<EnemySpawnTimer>, time: Res<Time>){
enemy_spawn_timer.timer.tick(time.delta());
}
pub fn spawn_enemies_over_time(
mut commands: Commands,
window_query: Query<&Window, With<PrimaryWindow>>,
asset_server: Res<AssetServer>,
enemy_spawn_timer: Res<EnemySpawnTimer>
){
let window = window_query.get_single().unwrap();
if enemy_spawn_timer.timer.finished(){
let random_x = random::<f32>() * (window.width() - ENEMY_SIZE);
let random_y = random::<f32>() * (window.height() - ENEMY_SIZE);
commands.spawn(
(
Sprite{
image: asset_server.load("sprites/ball_red_large.png"),
..default()
},
Transform::from_xyz(random_x, random_y, 0.0),
Enemy {
direction: Vec2::new(random::<f32>()-0.5, random::<f32>()-0.5)
.normalize(),
},
));
}
}
pub fn update_high_scores(
mut game_over_event_reader: EventReader<GameOver>,
mut high_scores: ResMut<HighScores>,
){
for event in game_over_event_reader.read(){
high_scores.scores.push*2;
}
}
pub fn high_scores_updated(high_scores: Res<HighScores>){
if high_scores.is_changed(){
println!("High Scores: {:?}", high_scores)
}
}
引数で、ButtonInput<Keycode>と、AppExitを取得しています。イベントであるAppExitはEventWriterを使って可変参照しないと、変えられません。
ButtonInputのif文は、player_movementシステムの動作システムの分からコピペしてきて、キーコードを変えただけです。.pressed()と、just_pressed()では、pressed()はキーが押下されている間、ずっと信号を発し続けますが、just_pressed()では、キーが押下された瞬間だけを認知するようです。今回の場合、アプリを終了させるので、どちらでも問題ないです。
次に、spawn enemies over time システムとtick enemy spawn timerシステムの組み合わせで、敵を時間経過で発生させるシステムについて解説します。
enemy_spawn_timer.timer.finished()を用いて、if文を使って敵をランダムな位置に登場させる文を呼び出しています。ifの中身は、spawn enemiesシステムからのコピペです。
今回は、ゲームオーバーというイベントをシステムと分けて管理してみます。システムは基本的には、プレイヤーがレベルアップした時などのフラグを管理するために使う。今回の場合は、ゲームが終了した時の処理を複数書きたいため、それをイベントにした。
今回はゲームオーバー時のスコアを記録したいので、scoreというフィールドも宣言しました。例のコードでは、#[derive(Event)]がありませんでしたが、それを書かないと、エラーがでてコンパイルができません。
この二つの引数は、先の、GameOverの値を可変参照で取得します。今回は、イベントの送信をしたいので、EventWriterを使います。Scoreは何だったかというと、scoreというインスタンスの中に、u32型のvalueというフィールドを持ちます。で、valueは、スターと衝突する度に1点加算されるものでした。
そして、update_high_scoresに依存した関数として、high_scores_updated関数があります。これでは、先ほど変更したリソースのHighScoresの変更を参照して、high scoresを表示するという事になっています。
普段は、println!の中に{}を入れますが、今回は{:?}が入っています。 これはHighScoresを宣言した時に#[derive(Resource, Debug)]を指定したから使えています。
イベントの宣言、appへのイベントの追加、ゲームオーバーイベントを送信する関数、ゲームオーバーイベントを読み込んで動作する関数、この4つの組み合わせによって、ゲームオーバー時の動作を作ることが出来た。
これをみると final scoreの表示もなされている。それは、最後のスコア(score: 4)と同じ値である。そして、フォーマットありの、Vec型のHighScoresという変数もフォーマットありで出力されている。それも、scoresに、playerと4が格納されているため、最後のスコアと同じ値になっている。
これにより、イベントの宣言、appへのイベントの追加、ゲームオーバーイベントを送信する関数、ゲームオーバーイベントを読み込んで動作する関数が全て動作していることが分かった。