turtle/turtle-lib/src/commands_channel.rs

226 lines
6.2 KiB
Rust

//! Per-turtle command channels for multi-threaded game logic
//!
//! Enables sending turtle commands from game logic threads to the render thread
//! without blocking the render loop.
//!
//! # Usage
//!
//! ```no_run
//! use turtle_lib::*;
//! use std::thread;
//! use macroquad::prelude::{next_frame, clear_background, WHITE};
//! # #[macroquad::main("Threading")]
//! # async fn main() {
//! let mut app = TurtleApp::new();
//!
//! // Create a turtle and get its command sender
//! let turtle_tx = app.create_turtle_channel(100);
//!
//! // Spawn a game logic thread
//! thread::spawn({
//! let tx = turtle_tx.clone();
//! move || {
//! let mut plan = create_turtle_plan();
//! plan.forward(100.0).right(90.0);
//! tx.send(plan.build()).ok();
//! }
//! });
//!
//! // Main render loop
//! loop {
//! clear_background(WHITE);
//! app.process_commands();
//! app.update();
//! app.render();
//! next_frame().await;
//! }
//! # }
//! ```
use crate::commands::CommandQueue;
use crossbeam::channel::{bounded, Receiver, Sender};
/// Sender for turtle commands from a game logic thread
///
/// This is tied to a specific turtle created via `TurtleApp::create_turtle_channel()`.
/// The turtle is guaranteed to exist on the render thread.
///
/// # Thread Safety
/// Can be cloned and shared across threads. Multiple game threads can send
/// commands to the same turtle safely.
///
/// # Examples
/// ```no_run
/// # use turtle_lib::*;
/// # fn example() -> Result<(), String> {
/// # let mut app = TurtleApp::new();
/// let tx = app.create_turtle_channel(100);
///
/// // Send commands from game thread
/// let mut plan = create_turtle_plan();
/// plan.forward(50.0);
/// tx.send(plan.clone().build())?;
///
/// // Or non-blocking variant
/// tx.try_send(plan.build()).ok();
/// # Ok(())
/// # }
/// ```
#[derive(Clone)]
pub struct TurtleCommandSender {
turtle_id: usize,
tx: Sender<CommandQueue>,
}
/// Receiver for turtle commands on the render thread
///
/// Paired with `TurtleCommandSender` via `turtle_command_channel()`.
/// Automatically managed by `TurtleApp::process_commands()`.
pub struct TurtleCommandReceiver {
turtle_id: usize,
rx: Receiver<CommandQueue>,
}
impl TurtleCommandSender {
/// Get the turtle ID this sender is bound to
#[must_use]
pub fn turtle_id(&self) -> usize {
self.turtle_id
}
/// Send commands (blocking)
///
/// Blocks if the channel buffer is full. This is appropriate for game logic
/// threads where blocking is acceptable. The buffer size is specified when
/// creating the channel.
///
/// # Errors
/// Returns error if the receiver has been dropped (render thread exited).
///
/// # Examples
/// ```no_run
/// # use turtle_lib::*;
/// # fn example() -> Result<(), String> {
/// # let mut app = TurtleApp::new();
/// # let tx = app.create_turtle_channel(100);
/// let mut plan = create_turtle_plan();
/// plan.forward(100.0);
/// tx.send(plan.build())?;
/// # Ok(())
/// # }
/// ```
pub fn send(&self, queue: CommandQueue) -> Result<(), String> {
self.tx
.send(queue)
.map_err(|e| format!("Channel disconnected: {e}"))
}
/// Send commands (non-blocking)
///
/// Returns immediately. If the channel buffer is full, returns an error
/// without blocking.
///
/// # Errors
/// Returns error if the buffer is full or the receiver has been dropped.
///
/// # Examples
/// ```no_run
/// # use turtle_lib::*;
/// # fn example() {
/// # let mut app = TurtleApp::new();
/// # let tx = app.create_turtle_channel(100);
/// let mut plan = create_turtle_plan();
/// plan.forward(100.0);
/// tx.try_send(plan.build()).ok(); // Ignore if buffer full
/// # }
/// ```
pub fn try_send(&self, queue: CommandQueue) -> Result<(), String> {
self.tx
.try_send(queue)
.map_err(|e| format!("Failed to send: {e}"))
}
}
impl TurtleCommandReceiver {
/// Get the turtle ID this receiver is bound to
#[must_use]
pub fn turtle_id(&self) -> usize {
self.turtle_id
}
/// Drain all pending commands for this turtle (non-blocking)
///
/// # Examples
/// ```no_run
/// # use turtle_lib::*;
/// # async fn example() {
/// # let mut app = TurtleApp::new();
/// # let _tx = app.create_turtle_channel(100);
/// // This is called automatically by app.process_commands()
/// // But you can also do it manually:
/// loop {
/// app.update();
/// app.render();
/// # break;
/// }
/// # }
/// ```
#[must_use]
pub fn recv_all(&self) -> Vec<CommandQueue> {
self.rx.try_iter().collect()
}
/// Try to receive one command batch (non-blocking)
#[must_use]
pub fn try_recv(&self) -> Option<CommandQueue> {
self.rx.try_recv().ok()
}
/// Check if this receiver's queue is empty
#[must_use]
pub fn is_empty(&self) -> bool {
self.rx.is_empty()
}
/// Get the number of pending command batches
#[must_use]
pub fn len(&self) -> usize {
self.rx.len()
}
}
/// Create a command channel for a specific turtle
///
/// The tuple represents (sender, receiver) where:
/// - Sender goes to game logic threads (cloneable, can be distributed)
/// - Receiver stays in the render thread (part of `TurtleApp` internally)
///
/// # Arguments
/// * `turtle_id` - The ID of the turtle this channel is for (must be valid)
/// * `buffer_size` - Maximum number of pending command batches before sender blocks
///
/// # Panics
/// Panics if `buffer_size` is 0.
///
/// # Examples
/// ```no_run
/// # use turtle_lib::*;
/// # fn example() {
/// let (tx, _rx) = turtle_command_channel(0, 100);
/// // Sender goes to game threads
/// // Receiver stays in render thread (or `TurtleApp`)
/// # }
/// ```
#[must_use]
pub fn turtle_command_channel(
turtle_id: usize,
buffer_size: usize,
) -> (TurtleCommandSender, TurtleCommandReceiver) {
assert!(buffer_size > 0, "buffer_size must be > 0");
let (tx, rx) = bounded(buffer_size);
(
TurtleCommandSender { turtle_id, tx },
TurtleCommandReceiver { turtle_id, rx },
)
}