Compare commits

...

4 Commits

4 changed files with 93 additions and 89 deletions

View File

@@ -2,13 +2,13 @@
include!(concat!(env!("OUT_DIR"), "/atlas_data.rs"));
use crate::constants::CANVAS_SIZE;
use crate::constants::{MapTile, CANVAS_SIZE};
use crate::error::{GameError, GameResult, TextureError};
use crate::events::GameEvent;
use crate::map::builder::Map;
use crate::map::direction::Direction;
use crate::systems::blinking::Blinking;
use crate::systems::{self, ghost_collision_system, present_system, MovementModifiers};
use crate::systems::{self, ghost_collision_system, present_system, Hidden, MovementModifiers};
use crate::systems::movement::{BufferedDirection, Position, Velocity};
use crate::systems::profiling::SystemId;
@@ -21,9 +21,11 @@ use crate::systems::{
PacmanCollider, PlayerBundle, PlayerControlled, Renderable, ScoreResource, StartupSequence, SystemTimings,
};
use crate::texture::animated::AnimatedTexture;
use crate::texture::sprite::AtlasTile;
use bevy_ecs::event::EventRegistry;
use bevy_ecs::observer::Trigger;
use bevy_ecs::schedule::{IntoScheduleConfigs, Schedule, SystemSet};
use bevy_ecs::schedule::common_conditions::resource_changed;
use bevy_ecs::schedule::{Condition, IntoScheduleConfigs, Schedule, SystemSet};
use bevy_ecs::system::ResMut;
use bevy_ecs::world::World;
use sdl2::image::LoadTexture;
@@ -81,14 +83,7 @@ impl Game {
texture_creator: &'static mut TextureCreator<WindowContext>,
event_pump: &'static mut EventPump,
) -> GameResult<Game> {
let mut world = World::default();
let mut schedule = Schedule::default();
let ttf_context = Box::leak(Box::new(sdl2::ttf::init().map_err(|e| GameError::Sdl(e.to_string()))?));
EventRegistry::register_event::<GameError>(&mut world);
EventRegistry::register_event::<GameEvent>(&mut world);
EventRegistry::register_event::<AudioEvent>(&mut world);
let mut backbuffer = texture_creator
.create_texture_target(None, CANVAS_SIZE.x, CANVAS_SIZE.y)
.map_err(|e| GameError::Sdl(e.to_string()))?;
@@ -105,6 +100,7 @@ impl Game {
.create_texture_target(None, output_size.0, output_size.1)
.map_err(|e| GameError::Sdl(e.to_string()))?;
// Debug texture is copied over the backbuffer, it requires transparency abilities
debug_texture.set_blend_mode(BlendMode::Blend);
debug_texture.set_scale_mode(ScaleMode::Nearest);
@@ -151,11 +147,10 @@ impl Game {
.map_err(|e| GameError::Sdl(e.to_string()))?;
let map = Map::new(constants::RAW_BOARD)?;
let pacman_start_node = map.start_positions.pacman;
// Create directional animated textures for Pac-Man
let mut textures = [None, None, None, None];
let mut stopped_textures = [None, None, None, None];
for direction in Direction::DIRECTIONS {
let moving_prefix = match direction {
Direction::Up => "pacman/up",
@@ -181,7 +176,9 @@ impl Game {
let player = PlayerBundle {
player: PlayerControlled,
position: Position::Stopped { node: pacman_start_node },
position: Position::Stopped {
node: map.start_positions.pacman,
},
velocity: Velocity {
speed: 1.15,
direction: Direction::Left,
@@ -204,9 +201,12 @@ impl Game {
pacman_collider: PacmanCollider,
};
// Spawn player and attach initial state bundle
let player_entity = world.spawn(player).id();
world.entity_mut(player_entity).insert(Frozen);
let mut world = World::default();
let mut schedule = Schedule::default();
EventRegistry::register_event::<GameError>(&mut world);
EventRegistry::register_event::<GameEvent>(&mut world);
EventRegistry::register_event::<AudioEvent>(&mut world);
world.insert_non_send_resource(atlas);
world.insert_non_send_resource(event_pump);
@@ -227,6 +227,7 @@ impl Game {
world.insert_resource(DebugState::default());
world.insert_resource(AudioState::default());
world.insert_resource(CursorPosition::default());
world.insert_resource(StartupSequence::new(60 * 3, 60));
world.add_observer(
|event: Trigger<GameEvent>, mut state: ResMut<GlobalState>, _score: ResMut<ScoreResource>| {
@@ -255,7 +256,12 @@ impl Game {
let debug_render_system = profile(SystemId::DebugRender, debug_render_system);
let present_system = profile(SystemId::Present, present_system);
let forced_dirty_system = |mut dirty: ResMut<RenderDirty>| {
dirty.0 = true;
};
schedule.add_systems((
forced_dirty_system.run_if(resource_changed::<ScoreResource>.or(resource_changed::<StartupSequence>)),
(
input_system,
player_control_system,
@@ -280,41 +286,46 @@ impl Game {
.chain(),
));
world.insert_resource(StartupSequence::new(60 * 3, 60));
// Spawn player and attach initial state bundle
world.spawn(player).insert((Frozen, Hidden));
// Spawn ghosts
Self::spawn_ghosts(&mut world)?;
// Spawn items
let pellet_sprite = SpriteAtlas::get_tile(world.non_send_resource::<SpriteAtlas>(), "maze/pellet.png")
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("maze/pellet.png".to_string())))?;
let energizer_sprite = SpriteAtlas::get_tile(world.non_send_resource::<SpriteAtlas>(), "maze/energizer.png")
.ok_or_else(|| GameError::Texture(TextureError::AtlasTileNotFound("maze/energizer.png".to_string())))?;
let nodes: Vec<_> = world.resource::<Map>().iter_nodes().map(|(id, tile)| (*id, *tile)).collect();
for (node_id, tile) in nodes {
let (item_type, sprite, size) = match tile {
crate::constants::MapTile::Pellet => (EntityType::Pellet, pellet_sprite, constants::CELL_SIZE as f32 * 0.4),
crate::constants::MapTile::PowerPellet => {
(EntityType::PowerPellet, energizer_sprite, constants::CELL_SIZE as f32 * 0.95)
}
_ => continue,
};
// Build a list of item entities to spawn from the map
let nodes: Vec<(usize, EntityType, AtlasTile, f32)> = world
.resource::<Map>()
.iter_nodes()
.filter_map(|(id, tile)| match tile {
MapTile::Pellet => Some((*id, EntityType::Pellet, pellet_sprite, constants::CELL_SIZE as f32 * 0.4)),
MapTile::PowerPellet => Some((
*id,
EntityType::PowerPellet,
energizer_sprite,
constants::CELL_SIZE as f32 * 0.95,
)),
_ => None,
})
.collect();
// Construct and spawn the item entities
for (id, item_type, sprite, size) in nodes {
let mut item = world.spawn(ItemBundle {
position: Position::Stopped { node: node_id },
position: Position::Stopped { node: id },
sprite: Renderable { sprite, layer: 1 },
entity_type: item_type,
collider: Collider { size },
item_collider: ItemCollider,
});
// Make power pellets blink
if item_type == EntityType::PowerPellet {
item.insert(Blinking {
timer: 0.0,
interval: 0.2,
});
item.insert((Frozen, Blinking::new(0.2)));
}
}
@@ -424,7 +435,7 @@ impl Game {
}
};
world.spawn(ghost).insert(Frozen);
world.spawn(ghost).insert((Frozen, Hidden));
}
Ok(())

View File

@@ -24,44 +24,6 @@ impl CommonPlatform for Platform {
}
fn init_console(&self) -> Result<(), PlatformError> {
#[cfg(windows)]
{
unsafe {
use winapi::{
shared::ntdef::NULL,
um::{
fileapi::{CreateFileA, OPEN_EXISTING},
handleapi::INVALID_HANDLE_VALUE,
processenv::SetStdHandle,
winbase::{STD_ERROR_HANDLE, STD_OUTPUT_HANDLE},
wincon::{AttachConsole, GetConsoleWindow},
winnt::{FILE_SHARE_READ, FILE_SHARE_WRITE, GENERIC_READ, GENERIC_WRITE},
},
};
if !std::ptr::eq(GetConsoleWindow(), std::ptr::null_mut()) {
return Ok(());
}
if AttachConsole(winapi::um::wincon::ATTACH_PARENT_PROCESS) != 0 {
let handle = CreateFileA(
c"CONOUT$".as_ptr(),
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
std::ptr::null_mut(),
OPEN_EXISTING,
0,
NULL,
);
if handle != INVALID_HANDLE_VALUE {
SetStdHandle(STD_OUTPUT_HANDLE, handle);
SetStdHandle(STD_ERROR_HANDLE, handle);
}
}
}
}
Ok(())
}

View File

@@ -7,36 +7,57 @@ use bevy_ecs::{
use crate::systems::{
components::{DeltaTime, Renderable},
Hidden,
Frozen, Hidden,
};
#[derive(Component)]
#[derive(Component, Debug)]
pub struct Blinking {
pub timer: f32,
pub interval: f32,
}
impl Blinking {
pub fn new(interval: f32) -> Self {
Self { timer: 0.0, interval }
}
}
/// Updates blinking entities by toggling their visibility at regular intervals.
///
/// This system manages entities that have both `Blinking` and `Renderable` components,
/// accumulating time and toggling visibility when the specified interval is reached.
#[allow(clippy::type_complexity)]
pub fn blinking_system(
mut commands: Commands,
time: Res<DeltaTime>,
mut query: Query<(Entity, &mut Blinking, Has<Hidden>), With<Renderable>>,
mut query: Query<(Entity, &mut Blinking, Has<Hidden>, Has<Frozen>), With<Renderable>>,
) {
for (entity, mut blinking, hidden) in query.iter_mut() {
blinking.timer += time.0;
if blinking.timer >= blinking.interval {
blinking.timer = 0.0;
// Add or remove the Visible component based on whether it is currently in the query
for (entity, mut blinking, hidden, frozen) in query.iter_mut() {
// If the entity is frozen, blinking is disabled and the entity is unhidden (if it was hidden)
if frozen {
if hidden {
commands.entity(entity).remove::<Hidden>();
} else {
commands.entity(entity).insert(Hidden);
}
continue;
}
// Increase the timer by the delta time
blinking.timer += time.0;
// If the timer is less than the interval, there's nothing to do yet
if blinking.timer < blinking.interval {
continue;
}
// Subtract the interval (allows for the timer to retain partial interval progress)
blinking.timer -= blinking.interval;
// Toggle the Hidden component
if hidden {
commands.entity(entity).remove::<Hidden>();
} else {
commands.entity(entity).insert(Hidden);
}
}
}

View File

@@ -6,7 +6,7 @@ use bevy_ecs::{
};
use tracing::debug;
use crate::systems::{Frozen, GhostCollider, Hidden, PlayerControlled};
use crate::systems::{Blinking, Frozen, GhostCollider, Hidden, PlayerControlled};
#[derive(Resource, Debug, Clone, Copy)]
pub enum StartupSequence {
@@ -72,17 +72,27 @@ impl StartupSequence {
pub fn startup_stage_system(
mut startup: ResMut<StartupSequence>,
mut commands: Commands,
mut blinking_query: Query<Entity, With<Blinking>>,
mut player_query: Query<Entity, With<PlayerControlled>>,
mut ghost_query: Query<Entity, With<GhostCollider>>,
) {
if let Some((from, to)) = startup.tick() {
debug!("StartupSequence transition from {from:?} to {to:?}");
match (from, to) {
// (StartupSequence::TextOnly { .. }, StartupSequence::CharactersVisible { .. }) => {}
(StartupSequence::CharactersVisible { .. }, StartupSequence::GameActive) => {
// Remove Frozen/Hidden tags from all entities and enable player input
(StartupSequence::TextOnly { .. }, StartupSequence::CharactersVisible { .. }) => {
// Unhide the player & ghosts
for entity in player_query.iter_mut().chain(ghost_query.iter_mut()) {
commands.entity(entity).remove::<(Frozen, Hidden)>();
commands.entity(entity).remove::<Hidden>();
}
}
(StartupSequence::CharactersVisible { .. }, StartupSequence::GameActive) => {
// Unfreeze the player & ghosts & pellet blinking
for entity in player_query
.iter_mut()
.chain(ghost_query.iter_mut())
.chain(blinking_query.iter_mut())
{
commands.entity(entity).remove::<Frozen>();
}
}
_ => {}