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();
|
||||||
|
}
|
||||||