prototypes/life: game of life wip
|
After Width: | Height: | Size: 257 B |
|
After Width: | Height: | Size: 289 B |
|
After Width: | Height: | Size: 183 B |
|
After Width: | Height: | Size: 451 B |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 304 B |
|
After Width: | Height: | Size: 304 B |
|
After Width: | Height: | Size: 430 B |
|
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();
|
||||
}
|
||||