Add a conways game of life example (#37)
* Add a conways game of life example * Rustfmt conway example * Respond to review feedback (p_width/p_height) * Use line_drawing instead of manual bresenham * Optimize count_neibs * Remove inline(never) leftover from when profiling * Bring back wrapping behavior (without regressing perf) * Fix missing bounds check
This commit is contained in:
parent
17c6054f40
commit
e0225c145d
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -773,8 +773,10 @@ dependencies = [
|
|||
"env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"getrandom 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"gilrs 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"line_drawing 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"pixels-mocks 0.1.0",
|
||||
"randomize 3.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"simple-invaders 0.1.0",
|
||||
"wgpu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winit 0.20.0-alpha4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
|
|
@ -35,6 +35,8 @@ pixels-mocks = { path = "pixels-mocks" }
|
|||
simple-invaders = { path = "simple-invaders" }
|
||||
winit = "0.20.0-alpha4"
|
||||
winit_input_helper = "0.4.0-alpha4"
|
||||
randomize = "3.0"
|
||||
line_drawing = "0.8"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
|
|
12
examples/conway/README.md
Normal file
12
examples/conway/README.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Conway's Game of Life
|
||||
|
||||
![Conway's Game of Life](../../img/conway.png)
|
||||
|
||||
|
||||
## Controls
|
||||
|
||||
- Mouse: Left click toggles cells, dragging draws lines.
|
||||
- <kbd>P</kbd>: Toggle pause.
|
||||
- <kbd>space</kbd>: Frame step (enables pause if not already paused)
|
||||
- <kbd>R</kbd>: Randomize
|
||||
- <kbd>escape</kbd>: Quit
|
373
examples/conway/main.rs
Normal file
373
examples/conway/main.rs
Normal file
|
@ -0,0 +1,373 @@
|
|||
use log::debug;
|
||||
use pixels::{Error, Pixels, SurfaceTexture};
|
||||
use winit::dpi::{LogicalPosition, LogicalSize, PhysicalSize};
|
||||
use winit::event::{Event, VirtualKeyCode, WindowEvent};
|
||||
use winit::event_loop::{ControlFlow, EventLoop};
|
||||
use winit_input_helper::WinitInputHelper;
|
||||
|
||||
const SCREEN_WIDTH: u32 = 400;
|
||||
const SCREEN_HEIGHT: u32 = 300;
|
||||
|
||||
fn main() -> Result<(), Error> {
|
||||
env_logger::init();
|
||||
let event_loop = EventLoop::new();
|
||||
let mut input = WinitInputHelper::new();
|
||||
let (window, surface, mut p_width, mut p_height, mut hidpi_factor) =
|
||||
create_window("Conway's Game of Life", &event_loop);
|
||||
|
||||
let surface_texture = SurfaceTexture::new(p_width, p_height, surface);
|
||||
|
||||
let mut life = ConwayGrid::new_random(SCREEN_WIDTH as usize, SCREEN_HEIGHT as usize);
|
||||
let mut pixels = Pixels::new(SCREEN_WIDTH, SCREEN_HEIGHT, surface_texture)?;
|
||||
let mut paused = false;
|
||||
|
||||
let mut draw_state: Option<bool> = None;
|
||||
|
||||
event_loop.run(move |event, _, control_flow| {
|
||||
// The one and only event that winit_input_helper doesn't have for us...
|
||||
if let Event::WindowEvent {
|
||||
event: WindowEvent::RedrawRequested,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
life.draw(pixels.get_frame());
|
||||
pixels.render();
|
||||
}
|
||||
|
||||
// For everything else, for let winit_input_helper collect events to build its state.
|
||||
// It returns `true` when it is time to update our game state and request a redraw.
|
||||
if input.update(event) {
|
||||
// Close events
|
||||
if input.key_pressed(VirtualKeyCode::Escape) || input.quit() {
|
||||
*control_flow = ControlFlow::Exit;
|
||||
return;
|
||||
}
|
||||
if input.key_pressed(VirtualKeyCode::P) {
|
||||
paused = !paused;
|
||||
}
|
||||
if input.key_pressed(VirtualKeyCode::Space) {
|
||||
// Space is frame-step, so ensure we're paused
|
||||
paused = true;
|
||||
}
|
||||
if input.key_pressed(VirtualKeyCode::R) {
|
||||
life.randomize();
|
||||
}
|
||||
// Handle mouse. This is a bit involved since support some simple
|
||||
// line drawing (mostly because it makes nice looking patterns).
|
||||
let (mouse_cell, mouse_prev_cell) = input
|
||||
.mouse()
|
||||
.map(|(mx, my)| {
|
||||
let (dx, dy) = input.mouse_diff();
|
||||
let prev_x = mx - dx;
|
||||
let prev_y = my - dy;
|
||||
let dpx = hidpi_factor as f32;
|
||||
let (w, h) = (p_width as f32 / dpx, p_height as f32 / dpx);
|
||||
let mx_i = ((mx / w) * (SCREEN_WIDTH as f32)).round() as isize;
|
||||
let my_i = ((my / h) * (SCREEN_HEIGHT as f32)).round() as isize;
|
||||
let px_i = ((prev_x / w) * (SCREEN_WIDTH as f32)).round() as isize;
|
||||
let py_i = ((prev_y / h) * (SCREEN_HEIGHT as f32)).round() as isize;
|
||||
((mx_i, my_i), (px_i, py_i))
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
if input.mouse_pressed(0) {
|
||||
debug!("Mouse click at {:?}", mouse_cell);
|
||||
draw_state = Some(life.toggle(mouse_cell.0, mouse_cell.1));
|
||||
} else if let Some(draw_alive) = draw_state {
|
||||
let release = input.mouse_released(0);
|
||||
let held = input.mouse_held(0);
|
||||
debug!("Draw at {:?} => {:?}", mouse_prev_cell, mouse_cell);
|
||||
debug!("Mouse held {:?}, release {:?}", held, release);
|
||||
// If they either released (finishing the drawing) or are still
|
||||
// in the middle of drawing, keep going.
|
||||
if release || held {
|
||||
debug!("Draw line of {:?}", draw_alive);
|
||||
life.set_line(
|
||||
mouse_prev_cell.0,
|
||||
mouse_prev_cell.1,
|
||||
mouse_cell.0,
|
||||
mouse_cell.1,
|
||||
draw_alive,
|
||||
);
|
||||
}
|
||||
// If they let go or are otherwise not clicking anymore, stop drawing.
|
||||
if release || !held {
|
||||
debug!("Draw end");
|
||||
draw_state = None;
|
||||
}
|
||||
}
|
||||
// Adjust high DPI factor
|
||||
if let Some(factor) = input.hidpi_changed() {
|
||||
hidpi_factor = factor;
|
||||
}
|
||||
// Resize the window
|
||||
if let Some(size) = input.window_resized() {
|
||||
let size = size.to_physical(hidpi_factor);
|
||||
p_width = size.width.round() as u32;
|
||||
p_height = size.height.round() as u32;
|
||||
pixels.resize(p_width, p_height);
|
||||
}
|
||||
if !paused || input.key_pressed(VirtualKeyCode::Space) {
|
||||
life.update();
|
||||
}
|
||||
window.request_redraw();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// COPYPASTE: ideally this could be shared.
|
||||
|
||||
/// Create a window for the game.
|
||||
///
|
||||
/// Automatically scales the window to cover about 2/3 of the monitor height.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Tuple of `(window, surface, width, height, hidpi_factor)`
|
||||
/// `width` and `height` are in `PhysicalSize` units.
|
||||
fn create_window(
|
||||
title: &str,
|
||||
event_loop: &EventLoop<()>,
|
||||
) -> (winit::window::Window, pixels::wgpu::Surface, u32, u32, f64) {
|
||||
// Create a hidden window so we can estimate a good default window size
|
||||
let window = winit::window::WindowBuilder::new()
|
||||
.with_visible(false)
|
||||
.with_title(title)
|
||||
.build(&event_loop)
|
||||
.unwrap();
|
||||
let hidpi_factor = window.hidpi_factor();
|
||||
|
||||
// Get dimensions
|
||||
let width = SCREEN_WIDTH as f64;
|
||||
let height = SCREEN_HEIGHT as f64;
|
||||
let (monitor_width, monitor_height) = {
|
||||
let size = window.current_monitor().size();
|
||||
(size.width / hidpi_factor, size.height / hidpi_factor)
|
||||
};
|
||||
let scale = (monitor_height / height * 2.0 / 3.0).round();
|
||||
|
||||
// Resize, center, and display the window
|
||||
let min_size = PhysicalSize::new(width, height).to_logical(hidpi_factor);
|
||||
let default_size = LogicalSize::new(width * scale, height * scale);
|
||||
let center = LogicalPosition::new(
|
||||
(monitor_width - width * scale) / 2.0,
|
||||
(monitor_height - height * scale) / 2.0,
|
||||
);
|
||||
window.set_inner_size(default_size);
|
||||
window.set_min_inner_size(Some(min_size));
|
||||
window.set_outer_position(center);
|
||||
window.set_visible(true);
|
||||
|
||||
let surface = pixels::wgpu::Surface::create(&window);
|
||||
let size = default_size.to_physical(hidpi_factor);
|
||||
|
||||
(
|
||||
window,
|
||||
surface,
|
||||
size.width.round() as u32,
|
||||
size.height.round() as u32,
|
||||
hidpi_factor,
|
||||
)
|
||||
}
|
||||
|
||||
/// Generate a pseudorandom seed for the game's PRNG.
|
||||
fn generate_seed() -> (u64, u64) {
|
||||
use byteorder::{ByteOrder, NativeEndian};
|
||||
use getrandom::getrandom;
|
||||
|
||||
let mut seed = [0_u8; 16];
|
||||
|
||||
getrandom(&mut seed).expect("failed to getrandom");
|
||||
|
||||
(
|
||||
NativeEndian::read_u64(&seed[0..8]),
|
||||
NativeEndian::read_u64(&seed[8..16]),
|
||||
)
|
||||
}
|
||||
|
||||
const BIRTH_RULE: [bool; 9] = [false, false, false, true, false, false, false, false, false];
|
||||
const SURVIVE_RULE: [bool; 9] = [false, false, true, true, false, false, false, false, false];
|
||||
const INITIAL_FILL: f32 = 0.3;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
struct Cell {
|
||||
alive: bool,
|
||||
// Used for the trail effect. Always 255 if `self.alive` is true (We could
|
||||
// use an enum for Cell, but it makes several functions slightly more
|
||||
// complex, and doesn't actually make anything any simpler here, or save any
|
||||
// memory, so we don't)
|
||||
heat: u8,
|
||||
}
|
||||
|
||||
impl Cell {
|
||||
fn new(alive: bool) -> Self {
|
||||
Self { alive, heat: 0 }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn update_neibs(self, n: usize) -> Self {
|
||||
let next_alive = if self.alive {
|
||||
SURVIVE_RULE[n]
|
||||
} else {
|
||||
BIRTH_RULE[n]
|
||||
};
|
||||
self.next_state(next_alive)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
fn next_state(mut self, alive: bool) -> Self {
|
||||
self.alive = alive;
|
||||
if self.alive {
|
||||
self.heat = 255;
|
||||
} else {
|
||||
self.heat = self.heat.saturating_sub(1);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn set_alive(&mut self, alive: bool) {
|
||||
*self = self.next_state(alive);
|
||||
}
|
||||
|
||||
fn cool_off(&mut self, decay: f32) {
|
||||
if !self.alive {
|
||||
let heat = (self.heat as f32 * decay).min(255.0).max(0.0);
|
||||
assert!(heat.is_finite());
|
||||
self.heat = heat as u8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ConwayGrid {
|
||||
cells: Vec<Cell>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
// Should always be the same size as `cells`. When updating, we read from
|
||||
// `cells` and write to `scratch_cells`, then swap. Otherwise it's not in
|
||||
// use, and `cells` should be updated directly.
|
||||
scratch_cells: Vec<Cell>,
|
||||
}
|
||||
|
||||
impl ConwayGrid {
|
||||
fn new_empty(width: usize, height: usize) -> Self {
|
||||
assert!(width != 0 && height != 0);
|
||||
let size = width.checked_mul(height).expect("too big");
|
||||
Self {
|
||||
cells: vec![Cell::default(); size],
|
||||
scratch_cells: vec![Cell::default(); size],
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
fn new_random(width: usize, height: usize) -> Self {
|
||||
let mut result = Self::new_empty(width, height);
|
||||
result.randomize();
|
||||
result
|
||||
}
|
||||
|
||||
fn randomize(&mut self) {
|
||||
let mut rng: randomize::PCG32 = generate_seed().into();
|
||||
for c in self.cells.iter_mut() {
|
||||
let alive = randomize::f32_half_open_right(rng.next_u32()) > INITIAL_FILL;
|
||||
*c = Cell::new(alive);
|
||||
}
|
||||
// run a few simulation iterations for aesthetics (If we don't, the
|
||||
// noise is ugly)
|
||||
for _ in 0..3 {
|
||||
self.update();
|
||||
}
|
||||
// Smooth out noise in the heatmap that would remain for a while
|
||||
for c in self.cells.iter_mut() {
|
||||
c.cool_off(0.4);
|
||||
}
|
||||
}
|
||||
|
||||
fn count_neibs(&self, x: usize, y: usize) -> usize {
|
||||
let (xm1, xp1) = if x == 0 {
|
||||
(self.width - 1, x + 1)
|
||||
} else if x == self.width - 1 {
|
||||
(x - 1, 0)
|
||||
} else {
|
||||
(x - 1, x + 1)
|
||||
};
|
||||
let (ym1, yp1) = if y == 0 {
|
||||
(self.height - 1, y + 1)
|
||||
} else if y == self.height - 1 {
|
||||
(y - 1, 0)
|
||||
} else {
|
||||
(y - 1, y + 1)
|
||||
};
|
||||
(self.cells[xm1 + ym1 * self.width].alive as usize
|
||||
+ self.cells[x + ym1 * self.width].alive as usize
|
||||
+ self.cells[xp1 + ym1 * self.width].alive as usize
|
||||
+ self.cells[xm1 + y * self.width].alive as usize
|
||||
+ self.cells[xp1 + y * self.width].alive as usize
|
||||
+ self.cells[xm1 + yp1 * self.width].alive as usize
|
||||
+ self.cells[x + yp1 * self.width].alive as usize
|
||||
+ self.cells[xp1 + yp1 * self.width].alive as usize)
|
||||
}
|
||||
|
||||
fn update(&mut self) {
|
||||
for y in 0..self.height {
|
||||
for x in 0..self.width {
|
||||
let neibs = self.count_neibs(x, y);
|
||||
let idx = x + y * self.width;
|
||||
let next = self.cells[idx].update_neibs(neibs);
|
||||
// Write into scratch_cells, since we're still reading from `self.cells`
|
||||
self.scratch_cells[idx] = next;
|
||||
}
|
||||
}
|
||||
std::mem::swap(&mut self.scratch_cells, &mut self.cells);
|
||||
}
|
||||
|
||||
fn toggle(&mut self, x: isize, y: isize) -> bool {
|
||||
if let Some(i) = self.grid_idx(x, y) {
|
||||
let was_alive = self.cells[i].alive;
|
||||
self.cells[i].set_alive(!was_alive);
|
||||
!was_alive
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&self, screen: &mut [u8]) {
|
||||
debug_assert_eq!(screen.len(), 4 * self.cells.len());
|
||||
for (c, pix) in self.cells.iter().zip(screen.chunks_exact_mut(4)) {
|
||||
let color = if c.alive {
|
||||
[0, 0xff, 0xff, 0xff]
|
||||
} else {
|
||||
[0, 0, c.heat, 0xff]
|
||||
};
|
||||
pix.copy_from_slice(&color);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_line(&mut self, x0: isize, y0: isize, x1: isize, y1: isize, alive: bool) {
|
||||
// probably should do sutherland-hodgeman if this were more serious.
|
||||
// instead just clamp the start pos, and draw until moving towards the
|
||||
// end pos takes us out of bounds.
|
||||
let x0 = x0.max(0).min(self.width as isize);
|
||||
let y0 = y0.max(0).min(self.height as isize);
|
||||
for (x, y) in line_drawing::Bresenham::new((x0, y0), (x1, y1)) {
|
||||
if let Some(i) = self.grid_idx(x, y) {
|
||||
self.cells[i].set_alive(alive);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn grid_idx<I: std::convert::TryInto<usize>>(&self, x: I, y: I) -> Option<usize> {
|
||||
if let (Ok(x), Ok(y)) = (x.try_into(), y.try_into()) {
|
||||
if x < self.width && y < self.height {
|
||||
Some(x + y * self.width)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
BIN
img/conway.png
Normal file
BIN
img/conway.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 643 KiB |
Loading…
Reference in a new issue