prototypes/life: game of life wip

main
Elijah Voigt 1 month ago
parent c16f4793fe
commit a587c17978

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 289 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 424 B

@ -0,0 +1,384 @@
// TODO: Rearchitected so it's Cell and Coordiantes not Cell { position { coordiantes } }
use engine::*;
const GREY_TILE: &str = "tileGrey_01.png";
const BLACK_TILE: &str = "tileBlack_01.png";
const BACKGROUND: &str = "texture_08.png";
fn main() {
let file_path = {
#[cfg(not(target_arch = "wasm32"))]
{
"assets/life".into()
}
#[cfg(target_arch = "wasm32")]
{
"assets".into()
}
};
App::new()
.add_plugins(DefaultPlugins.set(AssetPlugin {
file_path,
..default()
}))
.init_resource::<Coordinates>()
.init_state::<SimulationState>()
.add_systems(Startup, setup)
.add_systems(
Update,
update_grid_position.run_if(on_message::<CursorMoved>),
)
.add_systems(Update, grid_gizmo)
.add_systems(
Update,
de_spawn_cells.run_if(input_just_pressed(MouseButton::Left)),
)
.add_systems(
Update,
assert_cell_uniqueness.run_if(any_with_component::<Cell>),
)
.add_systems(
Update,
simulation_step.run_if(input_just_pressed(KeyCode::Space)),
)
.add_observer(on_add_ui_button)
.add_observer(on_add_coordinates)
.run();
}
const SCALE: f32 = 100.0;
#[derive(Debug, Component)]
#[require(Visibility, Transform)]
enum UiButton {
Power,
Pause,
Step,
Play,
ZoomOut,
ZoomIn,
}
impl UiButton {
fn icon(&self) -> &'static str {
match self {
Self::Power => "power.png",
Self::Pause => "pause.png",
Self::Step => "next.png",
Self::Play => "forward.png",
Self::ZoomOut => "zoomOut.png",
Self::ZoomIn => "zoomIn.png",
}
}
}
fn on_add_ui_button(
trigger: On<Add, UiButton>,
mut commands: Commands,
query: Query<&UiButton>,
server: Res<AssetServer>,
) {
let button = query.get(trigger.entity).unwrap();
commands
.entity(trigger.entity)
.insert((
Button,
BackgroundColor(GRAY.into()),
ImageNode {
image: server.load(GREY_TILE),
color: WHITE.with_alpha(0.5).into(),
..default()
},
children![(
Node {
top: Val::Px(0.0),
left: Val::Px(0.0),
height: Val::Px(40.0),
..default()
},
ImageNode {
image: server.load(button.icon()),
..default()
},
)],
))
.observe(responsive_button_over)
.observe(responsive_button_out);
}
fn setup(mut commands: Commands) {
commands.spawn((
Camera2d,
Camera {
clear_color: ClearColorConfig::Custom(WHITE.into()),
..default()
},
));
commands
.spawn((Node {
top: Val::Px(0.0),
left: Val::Px(0.0),
height: Val::Px(40.0),
width: Val::Percent(100.0),
..default()
}, Transform::default()))
.with_children(|parent| {
parent.spawn(UiButton::Power);
parent.spawn(UiButton::Pause);
parent.spawn(UiButton::Step);
parent.spawn(UiButton::Play);
parent.spawn(UiButton::ZoomOut);
parent.spawn(UiButton::ZoomIn);
});
// Spawn Coordiantes
commands
.spawn((Transform::default(), Visibility::Visible))
.with_children(|parent| {
for x in -10..10 {
for y in -10..10 {
parent.spawn(Coordinates { x, y });
}
}
});
}
#[derive(Debug, Default, Clone, PartialEq, Hash, Eq, Component, Resource)]
#[require(Visibility, Transform)]
struct Coordinates {
x: isize,
y: isize,
}
impl Coordinates {
fn neighbors(&self) -> impl Iterator<Item = Self> {
[
Coordinates {
x: self.x,
y: self.y + 1,
},
Coordinates {
x: self.x + 1,
y: self.y + 1,
},
Coordinates {
x: self.x + 1,
y: self.y,
},
Coordinates {
x: self.x + 1,
y: self.y - 1,
},
Coordinates {
x: self.x,
y: self.y - 1,
},
Coordinates {
x: self.x - 1,
y: self.y - 1,
},
Coordinates {
x: self.x - 1,
y: self.y,
},
Coordinates {
x: self.x - 1,
y: self.y + 1,
},
]
.into_iter()
}
fn is_neighbor_of(&self, other: &Coordinates) -> bool {
let (a1, b1) = (self.x, self.y);
let (a2, b2) = (other.x, other.y);
(a1 == a2 + 1 || a1 == a2 - 1) && (b1 == b2 + 1 || b1 == b2 - 1)
}
fn as_translation(&self) -> Vec3 {
(Vec2::new(self.x as f32, self.y as f32) * SCALE).extend(0.0)
}
}
fn on_add_coordinates(trigger: On<Add, Coordinates>, coordinates: Query<&Coordinates>,
cells: Query<&Cell>,
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
server: ResMut<AssetServer>,
) {
// TODO: Reuse the same mesh
let mesh = {
let mesh_h = meshes.add(Rectangle::new(SCALE, SCALE));
Mesh2d(mesh_h)
};
// TODO: Resuse the same material
let mat = {
let image: Handle<Image> = if cells.contains(trigger.entity) {
server.load(BLACK_TILE)
} else {
server.load(BACKGROUND)
};
let mat_h = materials.add(image);
MeshMaterial2d(mat_h)
};
let t = {
let c = coordinates.get(trigger.entity).unwrap();
if cells.contains(trigger.entity) {
Transform::from_translation(c.as_translation() + Vec3::new(0.0, 0.0, 1.0))
} else {
Transform::from_translation(c.as_translation())
}
};
commands.entity(trigger.entity).insert((mesh, mat, t));
}
/// When the cursor moves, update the Coordiantes
fn update_grid_position(
mut coordinates: ResMut<Coordinates>,
q_window: Single<&Window>,
q_camera: Single<(&Camera, &GlobalTransform)>,
) {
if let Some(cursor_pos) = q_window.cursor_position() {
let (cam, cam_gt) = *q_camera;
if let Ok(pos) = cam.viewport_to_world_2d(cam_gt, cursor_pos) {
coordinates.x = (pos / SCALE).round().x as isize;
coordinates.y = (pos / SCALE).round().y as isize;
}
}
}
/// Draw a gizmo at the current coordiantes
fn grid_gizmo(mut gizmos: Gizmos, coordinates: Res<Coordinates>) {
let isometry = Isometry2d {
translation: coordinates.as_translation().truncate(),
..default()
};
gizmos.rect_2d(isometry, Vec2::ONE * SCALE, PURPLE)
}
#[derive(Component, Debug, PartialEq, Hash, Eq)]
struct Cell;
// When left mouse clicked, spawn a cell at GridPosition
fn de_spawn_cells(
mut commands: Commands,
coordinates: Res<Coordinates>,
cells: Query<(Entity, &Coordinates), With<Cell>>,
) {
if let Some(e) = cells
.iter()
.find_map(|(e, c)| (*coordinates == *c).then_some(e))
{
commands.entity(e).despawn();
} else {
commands.spawn((coordinates.clone(), Cell));
}
}
fn assert_cell_uniqueness(q: Query<(Entity, &Coordinates), With<Cell>>) {
q.iter().for_each(|(e1, c1)| {
q.iter().for_each(|(e2, c2)| {
if e1 != e2 && c1 == c2 {
panic!("Duplicate cells at {:?}", c1);
}
})
})
}
#[derive(States, Debug, Default, Hash, PartialEq, Eq, Clone)]
enum SimulationState {
#[default]
Off,
Step,
Run,
}
/// Simulate the Game of Life Rules:
/// 1. Any live cell with fewer than two live neighbours dies, as if by underpopulation.
/// 2. Any live cell with two or three live neighbours lives on to the next generation.
/// 3. Any live cell with more than three live neighbours dies, as if by overpopulation.
/// 4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction.
///
/// TODO: There is a lot of optimizations to be had here
fn simulation_step(q: Query<(Entity, &Coordinates), With<Cell>>, mut commands: Commands) {
fn is_cell_alive(coordinates: &Coordinates, query: &Query<(Entity, &Coordinates), With<Cell>>) -> bool {
query
.iter()
.any(|(_e, c)| *c == *coordinates)
}
fn living_neighbors(c: &Coordinates, query: &Query<(Entity, &Coordinates), With<Cell>>) -> usize {
c.neighbors()
.filter(|neighbor| {
query.iter().any(
|(_e, coordinates)| { *neighbor == *coordinates },
)
})
.count()
}
fn cell_entity(coordinates: &Coordinates, query: Query<(Entity, &Coordinates), With<Cell>>) -> Option<Entity> {
query
.iter()
.find_map(|(e, c)| (*c == *coordinates).then_some(e))
}
// Collect list of candidate positions...
let mut coordinates: HashSet<Coordinates> = HashSet::new();
// Put all dead cells neighboring living cells into the coordinates hashset
// Each living cell
q.iter().for_each(|(_e, c)| {
coordinates.insert(c.clone());
// Iterator over neighbor positions
// Filter to just dead neighbors
c.neighbors()
.filter(|c| !is_cell_alive(c, &q))
.for_each(|c| {
coordinates.insert(c);
});
});
// Get Neighbor Count of living cells
coordinates.iter().for_each(|c| {
let num_living_neighbors = living_neighbors(c, &q);
let is_living = is_cell_alive(c, &q);
match num_living_neighbors {
..2 | 4.. => {
if let Some(e) = cell_entity(c, q) {
debug!("killing {:?}", c);
commands.entity(e).despawn();
} else {
debug!("failed to kill {:?}", c);
}
}
2 => debug!("continue"),
3 => {
if !is_living {
debug!("creating {:?}", c);
commands.spawn((c.clone(), Cell));
} else {
debug!("continue")
}
}
};
});
}
fn responsive_button_over(trigger: On<Pointer<Over>>, mut bg: Query<&mut BackgroundColor>) {
let mut background_color = bg.get_mut(trigger.entity).unwrap();
background_color.0 = WHITE.into();
}
fn responsive_button_out(trigger: On<Pointer<Out>>, mut bg: Query<&mut BackgroundColor>) {
let mut background_color = bg.get_mut(trigger.entity).unwrap();
background_color.0 = GRAY.into();
}
Loading…
Cancel
Save