Compare commits
No commits in common. "7e23dc9d9c9fa9f9f654b89a9573b6e08b76ad9e" and "fcca1a2db4553473849edd610143fdd1c14cdf05" have entirely different histories.
7e23dc9d9c
...
fcca1a2db4
@ -13,7 +13,6 @@ tracing = { version = "0.1", features = [
|
|||||||
"attributes",
|
"attributes",
|
||||||
], default-features = false }
|
], default-features = false }
|
||||||
turtle-lib-macros = { path = "../turtle-lib-macros" }
|
turtle-lib-macros = { path = "../turtle-lib-macros" }
|
||||||
crossbeam = "0.8"
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
# For examples and testing
|
# For examples and testing
|
||||||
|
|||||||
@ -1,106 +0,0 @@
|
|||||||
//! Example: Game Logic in Separate Thread
|
|
||||||
//!
|
|
||||||
//! This example demonstrates how to run game logic in a separate thread
|
|
||||||
//! while keeping the render loop responsive on the main thread.
|
|
||||||
//!
|
|
||||||
//! The main thread handles rendering and animation, while game logic
|
|
||||||
//! threads can perform blocking operations (like fetching data) and
|
|
||||||
//! send turtle commands via channels.
|
|
||||||
|
|
||||||
use std::thread;
|
|
||||||
use std::time::Duration;
|
|
||||||
use turtle_lib::*;
|
|
||||||
|
|
||||||
#[macroquad::main("Game Logic Threading")]
|
|
||||||
async fn main() {
|
|
||||||
let mut app = TurtleApp::new();
|
|
||||||
|
|
||||||
// Create two turtles and get their command senders
|
|
||||||
let turtle1_tx = app.create_turtle_channel(100);
|
|
||||||
let turtle2_tx = app.create_turtle_channel(100);
|
|
||||||
|
|
||||||
// Spawn first game logic thread
|
|
||||||
let _thread1 = thread::spawn({
|
|
||||||
let tx = turtle1_tx.clone();
|
|
||||||
move || {
|
|
||||||
// Simulate some blocking work (e.g., network request, calculation)
|
|
||||||
println!("Thread 1: Starting work...");
|
|
||||||
thread::sleep(Duration::from_millis(500));
|
|
||||||
|
|
||||||
// Now send turtle commands
|
|
||||||
let mut plan = create_turtle_plan();
|
|
||||||
plan.set_pen_color(BLUE)
|
|
||||||
.forward(100.0)
|
|
||||||
.right(90.0)
|
|
||||||
.forward(100.0)
|
|
||||||
.right(90.0)
|
|
||||||
.forward(100.0)
|
|
||||||
.right(90.0)
|
|
||||||
.forward(100.0);
|
|
||||||
|
|
||||||
tx.send(plan.build())
|
|
||||||
.expect("Failed to send commands for turtle 1");
|
|
||||||
println!("Thread 1: Commands sent!");
|
|
||||||
|
|
||||||
// Send more commands in a loop
|
|
||||||
for i in 0..10 {
|
|
||||||
thread::sleep(Duration::from_millis(300));
|
|
||||||
let mut step = create_turtle_plan();
|
|
||||||
step.right(36.0).forward(50.0);
|
|
||||||
let _ = tx.try_send(step.build());
|
|
||||||
println!("Thread 1: Step {} sent", i + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Spawn second game logic thread
|
|
||||||
let _thread2 = thread::spawn({
|
|
||||||
let tx = turtle2_tx.clone();
|
|
||||||
move || {
|
|
||||||
// Different timing than thread1
|
|
||||||
println!("Thread 2: Starting work...");
|
|
||||||
thread::sleep(Duration::from_millis(1000));
|
|
||||||
|
|
||||||
// Draw a circle with turtle2
|
|
||||||
let mut plan = create_turtle_plan();
|
|
||||||
plan.set_pen_color(RED).circle_left(75.0, 360.0, 72);
|
|
||||||
|
|
||||||
tx.send(plan.build())
|
|
||||||
.expect("Failed to send commands for turtle 2");
|
|
||||||
println!("Thread 2: Circle sent!");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Main render loop
|
|
||||||
let mut frame_count = 0;
|
|
||||||
loop {
|
|
||||||
// Check for quit
|
|
||||||
if macroquad::prelude::is_key_pressed(macroquad::prelude::KeyCode::Escape)
|
|
||||||
|| macroquad::prelude::is_key_pressed(macroquad::prelude::KeyCode::Q)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear background
|
|
||||||
macroquad::prelude::clear_background(WHITE);
|
|
||||||
|
|
||||||
// Process incoming commands from game logic threads
|
|
||||||
// This drains all pending commands from turtle channels
|
|
||||||
app.process_commands();
|
|
||||||
|
|
||||||
// Update animation state (tweening, etc.)
|
|
||||||
app.update();
|
|
||||||
|
|
||||||
// Render the turtles
|
|
||||||
app.render();
|
|
||||||
|
|
||||||
frame_count += 1;
|
|
||||||
if frame_count % 60 == 0 {
|
|
||||||
println!("Rendered {} frames", frame_count);
|
|
||||||
}
|
|
||||||
|
|
||||||
macroquad::prelude::next_frame().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("Finished!");
|
|
||||||
}
|
|
||||||
@ -1,370 +0,0 @@
|
|||||||
//! Hangman Game with Threading
|
|
||||||
//!
|
|
||||||
//! A classic hangman game where game logic runs in a separate thread
|
|
||||||
//! while the render loop stays responsive. The user can make guesses
|
|
||||||
//! while turtle animations play smoothly.
|
|
||||||
//!
|
|
||||||
//! Run with: `cargo run --package turtle-lib --example hangman_threaded`
|
|
||||||
|
|
||||||
use std::io::{self, Write};
|
|
||||||
use std::sync::mpsc;
|
|
||||||
use std::thread;
|
|
||||||
use turtle_lib::*;
|
|
||||||
|
|
||||||
// Word list for the game
|
|
||||||
const WORDS: &[&str] = &[
|
|
||||||
"turtle",
|
|
||||||
"graphics",
|
|
||||||
"threading",
|
|
||||||
"rust",
|
|
||||||
"animation",
|
|
||||||
"crossbeam",
|
|
||||||
"channel",
|
|
||||||
"synchronization",
|
|
||||||
"parallel",
|
|
||||||
"concurrent",
|
|
||||||
];
|
|
||||||
|
|
||||||
#[macroquad::main("Hangman")]
|
|
||||||
async fn main() {
|
|
||||||
let mut app = TurtleApp::new();
|
|
||||||
|
|
||||||
// Create three turtles: hangman, lines, and smiley
|
|
||||||
let hangman_tx = app.create_turtle_channel(100);
|
|
||||||
let lines_tx = app.create_turtle_channel(100);
|
|
||||||
let smiley_tx = app.create_turtle_channel(100);
|
|
||||||
|
|
||||||
// Channel for game logic to communicate with render thread
|
|
||||||
let (tx, rx) = mpsc::channel();
|
|
||||||
|
|
||||||
// Spawn game logic thread
|
|
||||||
let game_thread = thread::spawn({
|
|
||||||
let hangman = hangman_tx.clone();
|
|
||||||
let lines = lines_tx.clone();
|
|
||||||
let smiley = smiley_tx.clone();
|
|
||||||
let tx = tx.clone();
|
|
||||||
|
|
||||||
move || {
|
|
||||||
run_game_logic(hangman, lines, smiley, tx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Main render loop
|
|
||||||
let mut frame = 0;
|
|
||||||
loop {
|
|
||||||
// Check for quit
|
|
||||||
if macroquad::prelude::is_key_pressed(macroquad::prelude::KeyCode::Escape)
|
|
||||||
|| macroquad::prelude::is_key_pressed(macroquad::prelude::KeyCode::Q)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process incoming commands from game thread
|
|
||||||
while let Ok(msg) = rx.try_recv() {
|
|
||||||
match msg {
|
|
||||||
GameMessage::GameOver { won, word } => {
|
|
||||||
if won {
|
|
||||||
println!("🎉 You Won! The word was: {}", word);
|
|
||||||
} else {
|
|
||||||
println!("💀 You Lost! The word was: {}", word);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear and render
|
|
||||||
macroquad::prelude::clear_background(WHITE);
|
|
||||||
app.process_commands();
|
|
||||||
app.update();
|
|
||||||
app.render();
|
|
||||||
|
|
||||||
frame += 1;
|
|
||||||
if frame % 60 == 0 {
|
|
||||||
println!("Rendered {} frames", frame / 60);
|
|
||||||
}
|
|
||||||
|
|
||||||
macroquad::prelude::next_frame().await;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for game thread to finish
|
|
||||||
game_thread.join().ok();
|
|
||||||
println!("Game ended. Goodbye!");
|
|
||||||
}
|
|
||||||
|
|
||||||
enum GameMessage {
|
|
||||||
GameOver { won: bool, word: String },
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_game_logic(
|
|
||||||
hangman_tx: TurtleCommandSender,
|
|
||||||
lines_tx: TurtleCommandSender,
|
|
||||||
smiley_tx: TurtleCommandSender,
|
|
||||||
tx: mpsc::Sender<GameMessage>,
|
|
||||||
) {
|
|
||||||
let secret = choose_word();
|
|
||||||
println!("Starting hangman game...");
|
|
||||||
println!("Secret word has {} letters", secret.len());
|
|
||||||
|
|
||||||
// Setup: Position hangman turtle and draw base (hill + mast)
|
|
||||||
{
|
|
||||||
let mut plan = create_turtle_plan();
|
|
||||||
setup_hangman(&mut plan);
|
|
||||||
draw_hill(&mut plan);
|
|
||||||
hangman_tx.send(plan.build()).ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Give render thread time to process
|
|
||||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
|
||||||
|
|
||||||
let mut all_guesses = String::new();
|
|
||||||
let mut wrong_guesses = 0;
|
|
||||||
const MAX_WRONG: usize = 8; // 8 body parts after base
|
|
||||||
|
|
||||||
// Main game loop
|
|
||||||
loop {
|
|
||||||
// Draw current state of lines
|
|
||||||
draw_lines_state(&lines_tx, &secret, &all_guesses);
|
|
||||||
|
|
||||||
// Check if won
|
|
||||||
if secret.chars().all(|c| all_guesses.contains(c)) {
|
|
||||||
draw_smiley(&smiley_tx, true);
|
|
||||||
tx.send(GameMessage::GameOver {
|
|
||||||
won: true,
|
|
||||||
word: secret.to_string(),
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if lost
|
|
||||||
if wrong_guesses >= MAX_WRONG {
|
|
||||||
draw_smiley(&smiley_tx, false);
|
|
||||||
tx.send(GameMessage::GameOver {
|
|
||||||
won: false,
|
|
||||||
word: secret.to_string(),
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ask for guess
|
|
||||||
let guess = ask_for_letter();
|
|
||||||
let guess_lower = guess.to_lowercase();
|
|
||||||
|
|
||||||
// Check if already guessed
|
|
||||||
if all_guesses.contains(&guess_lower) {
|
|
||||||
println!("You already guessed '{}'", guess_lower);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
all_guesses.push_str(&guess_lower);
|
|
||||||
|
|
||||||
if secret.contains(&guess_lower) {
|
|
||||||
println!("✓ Correct! '{}' is in the word", guess_lower);
|
|
||||||
} else {
|
|
||||||
println!("✗ Wrong! '{}' is NOT in the word", guess_lower);
|
|
||||||
wrong_guesses += 1;
|
|
||||||
|
|
||||||
// Draw next hangman step
|
|
||||||
draw_hangman_step(&hangman_tx, wrong_guesses);
|
|
||||||
println!("Wrong guesses: {}/{}", wrong_guesses, MAX_WRONG);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn choose_word() -> &'static str {
|
|
||||||
WORDS[(std::time::SystemTime::now()
|
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
|
||||||
.unwrap_or_default()
|
|
||||||
.as_secs() as usize)
|
|
||||||
% WORDS.len()]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ask_for_letter() -> String {
|
|
||||||
print!("Guess a letter: ");
|
|
||||||
io::stdout().flush().ok();
|
|
||||||
|
|
||||||
let mut guess = String::new();
|
|
||||||
io::stdin().read_line(&mut guess).ok();
|
|
||||||
guess.trim().to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn setup_hangman(plan: &mut TurtlePlan) {
|
|
||||||
plan.hide()
|
|
||||||
.set_speed(1001) // Instant mode
|
|
||||||
.set_pen_width(3.0) // Thicker lines for visibility
|
|
||||||
.set_pen_color(BLACK)
|
|
||||||
.pen_up()
|
|
||||||
.go_to(vec2(-100.0, -100.0)) // More centered position
|
|
||||||
.pen_down();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_hangman_step(tx: &TurtleCommandSender, step: usize) {
|
|
||||||
let mut plan = create_turtle_plan();
|
|
||||||
plan.set_speed(1001); // Instant mode
|
|
||||||
|
|
||||||
match step {
|
|
||||||
1 => draw_mast(&mut plan),
|
|
||||||
2 => draw_bar(&mut plan),
|
|
||||||
3 => draw_support(&mut plan),
|
|
||||||
4 => draw_rope(&mut plan),
|
|
||||||
5 => draw_head(&mut plan),
|
|
||||||
6 => draw_arms(&mut plan),
|
|
||||||
7 => draw_body(&mut plan),
|
|
||||||
8 => draw_legs(&mut plan),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.send(plan.build()).ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hangman drawing functions (scaled down for visibility)
|
|
||||||
fn draw_hill(plan: &mut TurtlePlan) {
|
|
||||||
plan.circle_left(50.0, 180.0, 36)
|
|
||||||
.left(180.0)
|
|
||||||
.circle_right(50.0, 90.0, 36)
|
|
||||||
.right(90.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_mast(plan: &mut TurtlePlan) {
|
|
||||||
plan.forward(150.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_bar(plan: &mut TurtlePlan) {
|
|
||||||
plan.right(90.0).forward(75.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_support(plan: &mut TurtlePlan) {
|
|
||||||
plan.backward(50.0)
|
|
||||||
.right(135.0)
|
|
||||||
.forward(35.355)
|
|
||||||
.backward(35.355)
|
|
||||||
.left(135.0)
|
|
||||||
.forward(50.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_rope(plan: &mut TurtlePlan) {
|
|
||||||
plan.set_pen_width(2.0).right(90.0).forward(35.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_head(plan: &mut TurtlePlan) {
|
|
||||||
plan.left(90.0).circle_right(15.0, 540.0, 72);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_arms(plan: &mut TurtlePlan) {
|
|
||||||
plan.left(60.0)
|
|
||||||
.forward(50.0)
|
|
||||||
.backward(50.0)
|
|
||||||
.left(60.0)
|
|
||||||
.forward(50.0)
|
|
||||||
.backward(50.0)
|
|
||||||
.right(30.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_body(plan: &mut TurtlePlan) {
|
|
||||||
plan.forward(50.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_legs(plan: &mut TurtlePlan) {
|
|
||||||
plan.right(20.0)
|
|
||||||
.forward(60.0)
|
|
||||||
.backward(60.0)
|
|
||||||
.left(40.0)
|
|
||||||
.forward(60.0)
|
|
||||||
.backward(60.0)
|
|
||||||
.right(20.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_lines_state(tx: &TurtleCommandSender, secret: &str, all_guesses: &str) {
|
|
||||||
let mut plan = create_turtle_plan();
|
|
||||||
plan.hide()
|
|
||||||
.set_speed(1001) // Instant mode
|
|
||||||
.set_pen_color(BLACK)
|
|
||||||
.set_pen_width(2.0)
|
|
||||||
.pen_up()
|
|
||||||
.go_to(vec2(-100.0, 100.0)) // Top of screen
|
|
||||||
.pen_down()
|
|
||||||
.right(90.0);
|
|
||||||
|
|
||||||
// Print word state in console
|
|
||||||
print!("Word: ");
|
|
||||||
for letter in secret.chars() {
|
|
||||||
if all_guesses.contains(letter) {
|
|
||||||
print!("{} ", letter);
|
|
||||||
plan.forward(20.0);
|
|
||||||
} else {
|
|
||||||
print!("_ ");
|
|
||||||
plan.forward(20.0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
println!();
|
|
||||||
|
|
||||||
// Draw underscores/circles for each letter
|
|
||||||
for letter in secret.chars() {
|
|
||||||
if all_guesses.contains(letter) {
|
|
||||||
// Draw green circle for revealed letter
|
|
||||||
plan.pen_up()
|
|
||||||
.forward(2.5)
|
|
||||||
.right(90.0)
|
|
||||||
.set_pen_color(GREEN)
|
|
||||||
.pen_down()
|
|
||||||
.circle_left(7.5, 360.0, 24)
|
|
||||||
.set_pen_color(BLACK)
|
|
||||||
.left(90.0)
|
|
||||||
.backward(2.5)
|
|
||||||
.pen_up();
|
|
||||||
} else {
|
|
||||||
// Draw black underscore
|
|
||||||
plan.forward(5.0);
|
|
||||||
}
|
|
||||||
plan.forward(15.0).pen_down();
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.send(plan.build()).ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_smiley(tx: &TurtleCommandSender, won: bool) {
|
|
||||||
let mut plan = create_turtle_plan();
|
|
||||||
plan.hide()
|
|
||||||
.set_speed(1001) // Instant mode
|
|
||||||
.pen_up()
|
|
||||||
.go_to(vec2(100.0, 0.0)) // Right side of screen
|
|
||||||
.pen_down()
|
|
||||||
.set_pen_color(if won { GREEN } else { RED });
|
|
||||||
|
|
||||||
// Face
|
|
||||||
plan.circle_left(50.0, 360.0, 72);
|
|
||||||
|
|
||||||
// Left eye
|
|
||||||
plan.pen_up()
|
|
||||||
.forward(27.5)
|
|
||||||
.right(90.0)
|
|
||||||
.forward(20.0)
|
|
||||||
.pen_down()
|
|
||||||
.circle_left(3.0, 360.0, 24);
|
|
||||||
|
|
||||||
// Right eye
|
|
||||||
plan.pen_up()
|
|
||||||
.forward(42.5)
|
|
||||||
.pen_down()
|
|
||||||
.circle_left(3.0, 360.0, 24);
|
|
||||||
|
|
||||||
// Mouth
|
|
||||||
plan.pen_up()
|
|
||||||
.backward(42.5)
|
|
||||||
.left(90.0)
|
|
||||||
.backward(40.0)
|
|
||||||
.right(90.0)
|
|
||||||
.pen_down();
|
|
||||||
|
|
||||||
if won {
|
|
||||||
// Smile
|
|
||||||
plan.right(45.0).circle_left(32.5, 90.0, 36);
|
|
||||||
} else {
|
|
||||||
// Frown
|
|
||||||
plan.left(45.0).circle_right(32.5, 90.0, 36);
|
|
||||||
}
|
|
||||||
|
|
||||||
tx.send(plan.build()).ok();
|
|
||||||
}
|
|
||||||
@ -1,223 +0,0 @@
|
|||||||
//! 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;
|
|
||||||
//!
|
|
||||||
//! # #[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.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;
|
|
||||||
/// }
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
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)
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
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 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -49,7 +49,6 @@
|
|||||||
pub mod builders;
|
pub mod builders;
|
||||||
pub mod circle_geometry;
|
pub mod circle_geometry;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod commands_channel;
|
|
||||||
pub mod drawing;
|
pub mod drawing;
|
||||||
pub mod execution;
|
pub mod execution;
|
||||||
pub mod general;
|
pub mod general;
|
||||||
@ -61,7 +60,6 @@ pub mod tweening;
|
|||||||
// Re-export commonly used types
|
// Re-export commonly used types
|
||||||
pub use builders::{CurvedMovement, DirectionalMovement, Turnable, TurtlePlan, WithCommands};
|
pub use builders::{CurvedMovement, DirectionalMovement, Turnable, TurtlePlan, WithCommands};
|
||||||
pub use commands::{CommandQueue, TurtleCommand};
|
pub use commands::{CommandQueue, TurtleCommand};
|
||||||
pub use commands_channel::{turtle_command_channel, TurtleCommandReceiver, TurtleCommandSender};
|
|
||||||
pub use general::{Angle, AnimationSpeed, Color, Coordinate, Length, Precision};
|
pub use general::{Angle, AnimationSpeed, Color, Coordinate, Length, Precision};
|
||||||
pub use shapes::{ShapeType, TurtleShape};
|
pub use shapes::{ShapeType, TurtleShape};
|
||||||
pub use state::{DrawCommand, Turtle, TurtleWorld};
|
pub use state::{DrawCommand, Turtle, TurtleWorld};
|
||||||
@ -76,13 +74,10 @@ pub use macroquad::prelude::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use macroquad::prelude::*;
|
use macroquad::prelude::*;
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
/// Main turtle application struct
|
/// Main turtle application struct
|
||||||
pub struct TurtleApp {
|
pub struct TurtleApp {
|
||||||
world: TurtleWorld,
|
world: TurtleWorld,
|
||||||
// Receivers for turtle command channels
|
|
||||||
receivers: HashMap<usize, TurtleCommandReceiver>,
|
|
||||||
// Mouse panning state
|
// Mouse panning state
|
||||||
is_dragging: bool,
|
is_dragging: bool,
|
||||||
last_mouse_pos: Option<Vec2>,
|
last_mouse_pos: Option<Vec2>,
|
||||||
@ -96,7 +91,6 @@ impl TurtleApp {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
world: TurtleWorld::new(),
|
world: TurtleWorld::new(),
|
||||||
receivers: HashMap::new(),
|
|
||||||
is_dragging: false,
|
is_dragging: false,
|
||||||
last_mouse_pos: None,
|
last_mouse_pos: None,
|
||||||
zoom_level: 1.0,
|
zoom_level: 1.0,
|
||||||
@ -108,78 +102,6 @@ impl TurtleApp {
|
|||||||
self.world.add_turtle()
|
self.world.add_turtle()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a turtle and a command channel for it
|
|
||||||
///
|
|
||||||
/// This is the preferred way to set up turtles when using threading.
|
|
||||||
/// Call this ONCE per turtle during setup, before spawning game logic threads.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
/// * `buffer_size` - Maximum pending command batches before sender blocks (typically 50-200)
|
|
||||||
///
|
|
||||||
/// # Returns
|
|
||||||
/// A `TurtleCommandSender` that can be cloned and sent to game logic threads.
|
|
||||||
/// The turtle is automatically managed by TurtleApp.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```no_run
|
|
||||||
/// # use turtle_lib::*;
|
|
||||||
/// # #[macroquad::main("Threading")]
|
|
||||||
/// # async fn main() {
|
|
||||||
/// let mut app = TurtleApp::new();
|
|
||||||
///
|
|
||||||
/// // Create turtle and get sender
|
|
||||||
/// let turtle_tx = app.create_turtle_channel(100);
|
|
||||||
///
|
|
||||||
/// // Send to game threads
|
|
||||||
/// let tx_clone = turtle_tx.clone();
|
|
||||||
/// std::thread::spawn(move || {
|
|
||||||
/// let mut plan = create_turtle_plan();
|
|
||||||
/// plan.forward(100.0);
|
|
||||||
/// tx_clone.send(plan.build()).ok();
|
|
||||||
/// });
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn create_turtle_channel(&mut self, buffer_size: usize) -> TurtleCommandSender {
|
|
||||||
let turtle_id = self.world.add_turtle();
|
|
||||||
let (tx, rx) = commands_channel::turtle_command_channel(turtle_id, buffer_size);
|
|
||||||
self.receivers.insert(turtle_id, rx);
|
|
||||||
tx
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Process all pending commands from all turtle channels
|
|
||||||
///
|
|
||||||
/// Call this once per frame in your render loop, before `update()`.
|
|
||||||
/// Drains all receivers and applies commands to their respective turtles.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```no_run
|
|
||||||
/// # use turtle_lib::*;
|
|
||||||
/// # #[macroquad::main("Threading")]
|
|
||||||
/// # async fn main() {
|
|
||||||
/// # let mut app = TurtleApp::new();
|
|
||||||
/// # let _tx = app.create_turtle_channel(100);
|
|
||||||
/// loop {
|
|
||||||
/// clear_background(WHITE);
|
|
||||||
/// app.process_commands(); // ← Process channel commands
|
|
||||||
/// app.update();
|
|
||||||
/// app.render();
|
|
||||||
/// next_frame().await;
|
|
||||||
/// }
|
|
||||||
/// # }
|
|
||||||
/// ```
|
|
||||||
pub fn process_commands(&mut self) {
|
|
||||||
// Collect all turtle IDs to avoid borrow issues
|
|
||||||
let turtle_ids: Vec<usize> = self.receivers.keys().copied().collect();
|
|
||||||
|
|
||||||
for turtle_id in turtle_ids {
|
|
||||||
if let Some(receiver) = self.receivers.get(&turtle_id) {
|
|
||||||
for queue in receiver.recv_all() {
|
|
||||||
self.append_commands(turtle_id, queue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Add commands from a turtle plan to the application for the default turtle (ID 0)
|
/// Add commands from a turtle plan to the application for the default turtle (ID 0)
|
||||||
///
|
///
|
||||||
/// Speed is controlled by `SetSpeed` commands in the queue.
|
/// Speed is controlled by `SetSpeed` commands in the queue.
|
||||||
@ -235,21 +157,6 @@ impl TurtleApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Append commands from a CommandQueue to a turtle's animation queue
|
|
||||||
///
|
|
||||||
/// Used internally by `process_commands()` and can be used directly
|
|
||||||
/// when you have a `CommandQueue` instead of a `TurtlePlan`.
|
|
||||||
pub fn append_commands(&mut self, turtle_id: usize, queue: CommandQueue) {
|
|
||||||
// Ensure turtle exists
|
|
||||||
while self.world.turtles.len() <= turtle_id {
|
|
||||||
self.world.add_turtle();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(turtle) = self.world.get_turtle_mut(turtle_id) {
|
|
||||||
turtle.tween_controller.append_commands(queue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update animation state (call every frame)
|
/// Update animation state (call every frame)
|
||||||
pub fn update(&mut self) {
|
pub fn update(&mut self) {
|
||||||
// Handle mouse panning and zoom
|
// Handle mouse panning and zoom
|
||||||
|
|||||||
@ -287,8 +287,12 @@ pub struct TurtleWorld {
|
|||||||
impl TurtleWorld {
|
impl TurtleWorld {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
let mut default_turtle = Turtle::default();
|
||||||
|
default_turtle.turtle_id = 0;
|
||||||
|
default_turtle.tween_controller =
|
||||||
|
TweenController::new(CommandQueue::new(), AnimationSpeed::default());
|
||||||
Self {
|
Self {
|
||||||
turtles: vec![], // Start with no turtles
|
turtles: vec![default_turtle], // Start with one default turtle
|
||||||
camera: Camera2D {
|
camera: Camera2D {
|
||||||
zoom: vec2(1.0 / screen_width() * 2.0, 1.0 / screen_height() * 2.0),
|
zoom: vec2(1.0 / screen_width() * 2.0, 1.0 / screen_height() * 2.0),
|
||||||
target: vec2(0.0, 0.0),
|
target: vec2(0.0, 0.0),
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user