Run ticks in a background thread in game_of_life
This commit is contained in:
parent
8fa9e4c94e
commit
916a1bfc70
@ -7,5 +7,6 @@ publish = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iced = { path = "../..", features = ["canvas", "tokio", "debug"] }
|
iced = { path = "../..", features = ["canvas", "tokio", "debug"] }
|
||||||
|
tokio = { version = "0.2", features = ["blocking"] }
|
||||||
itertools = "0.9"
|
itertools = "0.9"
|
||||||
rustc-hash = "1.1"
|
rustc-hash = "1.1"
|
||||||
|
@ -19,7 +19,7 @@ pub fn main() {
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct GameOfLife {
|
struct GameOfLife {
|
||||||
grid: Grid,
|
grid: Grid,
|
||||||
is_playing: bool,
|
state: State,
|
||||||
speed: u64,
|
speed: u64,
|
||||||
next_speed: Option<u64>,
|
next_speed: Option<u64>,
|
||||||
toggle_button: button::State,
|
toggle_button: button::State,
|
||||||
@ -28,6 +28,17 @@ struct GameOfLife {
|
|||||||
speed_slider: slider::State,
|
speed_slider: slider::State,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
Paused,
|
||||||
|
Playing { last_tick: Instant },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for State {
|
||||||
|
fn default() -> State {
|
||||||
|
State::Paused
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
enum Message {
|
enum Message {
|
||||||
Grid(grid::Message),
|
Grid(grid::Message),
|
||||||
@ -62,37 +73,61 @@ impl Application for GameOfLife {
|
|||||||
Message::Grid(message) => {
|
Message::Grid(message) => {
|
||||||
self.grid.update(message);
|
self.grid.update(message);
|
||||||
}
|
}
|
||||||
Message::Tick(_) | Message::Next => {
|
Message::Tick(_) | Message::Next => match &mut self.state {
|
||||||
self.grid.tick();
|
State::Paused => {
|
||||||
|
if let Some(task) = self.grid.tick(1) {
|
||||||
if let Some(speed) = self.next_speed.take() {
|
return Command::perform(task, Message::Grid);
|
||||||
self.speed = speed;
|
}
|
||||||
}
|
}
|
||||||
}
|
State::Playing { last_tick } => {
|
||||||
|
let seconds_elapsed =
|
||||||
|
last_tick.elapsed().as_millis() as f32 / 1000.0;
|
||||||
|
|
||||||
|
let needed_ticks =
|
||||||
|
(self.speed as f32 * seconds_elapsed).ceil() as usize;
|
||||||
|
|
||||||
|
if let Some(task) = self.grid.tick(needed_ticks) {
|
||||||
|
*last_tick = Instant::now();
|
||||||
|
|
||||||
|
if let Some(speed) = self.next_speed.take() {
|
||||||
|
self.speed = speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Command::perform(task, Message::Grid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
Message::Toggle => {
|
Message::Toggle => {
|
||||||
self.is_playing = !self.is_playing;
|
self.state = match self.state {
|
||||||
|
State::Paused => State::Playing {
|
||||||
|
last_tick: Instant::now(),
|
||||||
|
},
|
||||||
|
State::Playing { .. } => State::Paused,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
Message::Clear => {
|
Message::Clear => {
|
||||||
self.grid = Grid::default();
|
self.grid.clear();
|
||||||
}
|
}
|
||||||
Message::SpeedChanged(speed) => {
|
Message::SpeedChanged(speed) => match self.state {
|
||||||
if self.is_playing {
|
State::Paused => {
|
||||||
self.next_speed = Some(speed.round() as u64);
|
|
||||||
} else {
|
|
||||||
self.speed = speed.round() as u64;
|
self.speed = speed.round() as u64;
|
||||||
}
|
}
|
||||||
}
|
State::Playing { .. } => {
|
||||||
|
self.next_speed = Some(speed.round() as u64);
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Command::none()
|
Command::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscription(&self) -> Subscription<Message> {
|
fn subscription(&self) -> Subscription<Message> {
|
||||||
if self.is_playing {
|
match self.state {
|
||||||
time::every(Duration::from_millis(1000 / self.speed))
|
State::Paused => Subscription::none(),
|
||||||
.map(Message::Tick)
|
State::Playing { .. } => {
|
||||||
} else {
|
time::every(Duration::from_millis(1000 / self.speed))
|
||||||
Subscription::none()
|
.map(Message::Tick)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +137,11 @@ impl Application for GameOfLife {
|
|||||||
.push(
|
.push(
|
||||||
Button::new(
|
Button::new(
|
||||||
&mut self.toggle_button,
|
&mut self.toggle_button,
|
||||||
Text::new(if self.is_playing { "Pause" } else { "Play" }),
|
Text::new(if let State::Paused = self.state {
|
||||||
|
"Play"
|
||||||
|
} else {
|
||||||
|
"Pause"
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.on_press(Message::Toggle)
|
.on_press(Message::Toggle)
|
||||||
.style(style::Button),
|
.style(style::Button),
|
||||||
@ -111,11 +150,6 @@ impl Application for GameOfLife {
|
|||||||
Button::new(&mut self.next_button, Text::new("Next"))
|
Button::new(&mut self.next_button, Text::new("Next"))
|
||||||
.on_press(Message::Next)
|
.on_press(Message::Next)
|
||||||
.style(style::Button),
|
.style(style::Button),
|
||||||
)
|
|
||||||
.push(
|
|
||||||
Button::new(&mut self.clear_button, Text::new("Clear"))
|
|
||||||
.on_press(Message::Clear)
|
|
||||||
.style(style::Button),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
let selected_speed = self.next_speed.unwrap_or(self.speed);
|
let selected_speed = self.next_speed.unwrap_or(self.speed);
|
||||||
@ -138,10 +172,14 @@ impl Application for GameOfLife {
|
|||||||
.padding(10)
|
.padding(10)
|
||||||
.spacing(20)
|
.spacing(20)
|
||||||
.push(playback_controls)
|
.push(playback_controls)
|
||||||
.push(speed_controls);
|
.push(speed_controls)
|
||||||
|
.push(
|
||||||
|
Button::new(&mut self.clear_button, Text::new("Clear"))
|
||||||
|
.on_press(Message::Clear)
|
||||||
|
.style(style::Button),
|
||||||
|
);
|
||||||
|
|
||||||
let content = Column::new()
|
let content = Column::new()
|
||||||
.spacing(10)
|
|
||||||
.align_items(Align::Center)
|
.align_items(Align::Center)
|
||||||
.push(self.grid.view().map(Message::Grid))
|
.push(self.grid.view().map(Message::Grid))
|
||||||
.push(controls);
|
.push(controls);
|
||||||
@ -160,28 +198,40 @@ mod grid {
|
|||||||
mouse, Color, Element, Length, Point, Rectangle, Size, Vector,
|
mouse, Color, Element, Length, Point, Rectangle, Size, Vector,
|
||||||
};
|
};
|
||||||
use rustc_hash::{FxHashMap, FxHashSet};
|
use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
|
use std::future::Future;
|
||||||
|
|
||||||
pub struct Grid {
|
pub struct Grid {
|
||||||
life: Life,
|
state: State,
|
||||||
interaction: Interaction,
|
interaction: Interaction,
|
||||||
cache: Cache,
|
cache: Cache,
|
||||||
translation: Vector,
|
translation: Vector,
|
||||||
scaling: f32,
|
scaling: f32,
|
||||||
|
version: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
Populate(Cell),
|
Populate(Cell),
|
||||||
|
Ticked {
|
||||||
|
result: Result<Life, TickError>,
|
||||||
|
version: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum TickError {
|
||||||
|
JoinFailed,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Grid {
|
impl Default for Grid {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
life: Life::default(),
|
state: State::default(),
|
||||||
interaction: Interaction::None,
|
interaction: Interaction::None,
|
||||||
cache: Cache::default(),
|
cache: Cache::default(),
|
||||||
translation: Vector::default(),
|
translation: Vector::default(),
|
||||||
scaling: 1.0,
|
scaling: 1.0,
|
||||||
|
version: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -190,17 +240,44 @@ mod grid {
|
|||||||
const MIN_SCALING: f32 = 0.1;
|
const MIN_SCALING: f32 = 0.1;
|
||||||
const MAX_SCALING: f32 = 2.0;
|
const MAX_SCALING: f32 = 2.0;
|
||||||
|
|
||||||
pub fn tick(&mut self) {
|
pub fn tick(
|
||||||
self.life.tick();
|
&mut self,
|
||||||
self.cache.clear()
|
amount: usize,
|
||||||
|
) -> Option<impl Future<Output = Message>> {
|
||||||
|
use iced::futures::FutureExt;
|
||||||
|
|
||||||
|
let version = self.version;
|
||||||
|
let tick = self.state.tick(amount)?;
|
||||||
|
|
||||||
|
Some(tick.map(move |result| Message::Ticked { result, version }))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.state = State::default();
|
||||||
|
self.version += 1;
|
||||||
|
|
||||||
|
self.cache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update(&mut self, message: Message) {
|
pub fn update(&mut self, message: Message) {
|
||||||
match message {
|
match message {
|
||||||
Message::Populate(cell) => {
|
Message::Populate(cell) => {
|
||||||
self.life.populate(cell);
|
self.state.populate(cell);
|
||||||
self.cache.clear()
|
self.cache.clear()
|
||||||
}
|
}
|
||||||
|
Message::Ticked {
|
||||||
|
result: Ok(life),
|
||||||
|
version,
|
||||||
|
} if version == self.version => {
|
||||||
|
self.state.update(life);
|
||||||
|
self.cache.clear()
|
||||||
|
}
|
||||||
|
Message::Ticked {
|
||||||
|
result: Err(error), ..
|
||||||
|
} => {
|
||||||
|
dbg!(error);
|
||||||
|
}
|
||||||
|
Message::Ticked { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,11 +288,11 @@ mod grid {
|
|||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn visible_region(&self, size: Size) -> Rectangle {
|
pub fn visible_region(&self, size: Size) -> Region {
|
||||||
let width = size.width / self.scaling;
|
let width = size.width / self.scaling;
|
||||||
let height = size.height / self.scaling;
|
let height = size.height / self.scaling;
|
||||||
|
|
||||||
Rectangle {
|
Region {
|
||||||
x: -self.translation.x - width / 2.0,
|
x: -self.translation.x - width / 2.0,
|
||||||
y: -self.translation.y - height / 2.0,
|
y: -self.translation.y - height / 2.0,
|
||||||
width,
|
width,
|
||||||
@ -247,7 +324,7 @@ mod grid {
|
|||||||
let cursor_position = cursor.position_in(&bounds)?;
|
let cursor_position = cursor.position_in(&bounds)?;
|
||||||
let cell = Cell::at(self.project(cursor_position, bounds.size()));
|
let cell = Cell::at(self.project(cursor_position, bounds.size()));
|
||||||
|
|
||||||
let populate = if self.life.contains(&cell) {
|
let populate = if self.state.contains(&cell) {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(Message::Populate(cell))
|
Some(Message::Populate(cell))
|
||||||
@ -339,7 +416,7 @@ mod grid {
|
|||||||
|
|
||||||
let region = self.visible_region(frame.size());
|
let region = self.visible_region(frame.size());
|
||||||
|
|
||||||
for cell in self.life.within(region) {
|
for cell in region.view(self.state.cells()) {
|
||||||
frame.fill_rectangle(
|
frame.fill_rectangle(
|
||||||
Point::new(cell.j as f32, cell.i as f32),
|
Point::new(cell.j as f32, cell.i as f32),
|
||||||
Size::UNIT,
|
Size::UNIT,
|
||||||
@ -394,6 +471,63 @@ mod grid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
struct State {
|
||||||
|
life: Life,
|
||||||
|
births: FxHashSet<Cell>,
|
||||||
|
is_ticking: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
fn contains(&self, cell: &Cell) -> bool {
|
||||||
|
self.life.contains(cell) || self.births.contains(cell)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cells(&self) -> impl Iterator<Item = &Cell> {
|
||||||
|
self.life.iter().chain(self.births.iter())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn populate(&mut self, cell: Cell) {
|
||||||
|
if self.is_ticking {
|
||||||
|
self.births.insert(cell);
|
||||||
|
} else {
|
||||||
|
self.life.populate(cell);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, mut life: Life) {
|
||||||
|
self.births.drain().for_each(|cell| life.populate(cell));
|
||||||
|
|
||||||
|
self.life = life;
|
||||||
|
self.is_ticking = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tick(
|
||||||
|
&mut self,
|
||||||
|
amount: usize,
|
||||||
|
) -> Option<impl Future<Output = Result<Life, TickError>>> {
|
||||||
|
if self.is_ticking {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.is_ticking = true;
|
||||||
|
|
||||||
|
let mut life = self.life.clone();
|
||||||
|
|
||||||
|
Some(async move {
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
for _ in 0..amount {
|
||||||
|
life.tick();
|
||||||
|
}
|
||||||
|
|
||||||
|
life
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(|_| TickError::JoinFailed)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
pub struct Life {
|
pub struct Life {
|
||||||
cells: FxHashSet<Cell>,
|
cells: FxHashSet<Cell>,
|
||||||
}
|
}
|
||||||
@ -433,21 +567,16 @@ mod grid {
|
|||||||
self.cells.insert(cell);
|
self.cells.insert(cell);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn within(&self, region: Rectangle) -> impl Iterator<Item = &Cell> {
|
pub fn iter(&self) -> impl Iterator<Item = &Cell> {
|
||||||
let first_row = (region.y / Cell::SIZE as f32).floor() as isize;
|
self.cells.iter()
|
||||||
let first_column = (region.x / Cell::SIZE as f32).floor() as isize;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let visible_rows =
|
impl std::fmt::Debug for Life {
|
||||||
(region.height / Cell::SIZE as f32).ceil() as isize;
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let visible_columns =
|
f.debug_struct("Life")
|
||||||
(region.width / Cell::SIZE as f32).ceil() as isize;
|
.field("cells", &self.cells.len())
|
||||||
|
.finish()
|
||||||
let rows = first_row..=first_row + visible_rows;
|
|
||||||
let columns = first_column..=first_column + visible_columns;
|
|
||||||
|
|
||||||
self.cells.iter().filter(move |cell| {
|
|
||||||
rows.contains(&cell.i) && columns.contains(&cell.j)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -484,6 +613,35 @@ mod grid {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Region {
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
width: f32,
|
||||||
|
height: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Region {
|
||||||
|
fn view<'a>(
|
||||||
|
&self,
|
||||||
|
cells: impl Iterator<Item = &'a Cell>,
|
||||||
|
) -> impl Iterator<Item = &'a Cell> {
|
||||||
|
let first_row = (self.y / Cell::SIZE as f32).floor() as isize;
|
||||||
|
let first_column = (self.x / Cell::SIZE as f32).floor() as isize;
|
||||||
|
|
||||||
|
let visible_rows =
|
||||||
|
(self.height / Cell::SIZE as f32).ceil() as isize;
|
||||||
|
let visible_columns =
|
||||||
|
(self.width / Cell::SIZE as f32).ceil() as isize;
|
||||||
|
|
||||||
|
let rows = first_row..=first_row + visible_rows;
|
||||||
|
let columns = first_column..=first_column + visible_columns;
|
||||||
|
|
||||||
|
cells.filter(move |cell| {
|
||||||
|
rows.contains(&cell.i) && columns.contains(&cell.j)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum Interaction {
|
enum Interaction {
|
||||||
None,
|
None,
|
||||||
Drawing,
|
Drawing,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user