--- title: "Scenario Gallery" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Scenario Gallery} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 5 ) ``` `boids4R` includes named scenarios for common swarm motifs: compact schools, obstacle corridors, predator avoidance, and 3D murmurations. The examples below run each scenario with a fixed seed, then summarize the recorded frames with plain data-frame operations. ```{r} library(boids4R) gallery <- data.frame( scenario = c( "schooling_2d", "obstacle_corridor_2d", "predator_avoidance_2d", "murmuration_3d", "mixed_species_3d" ), n = c(120L, 120L, 120L, 160L, 150L), steps = c(60L, 70L, 70L, 55L, 55L), record_every = c(5L, 5L, 5L, 5L, 5L), seed = c(111L, 112L, 113L, 114L, 115L), stringsAsFactors = FALSE ) sims <- setNames( lapply(seq_len(nrow(gallery)), function(i) { boids_scenario( gallery$scenario[i], n = gallery$n[i], steps = gallery$steps[i], record_every = gallery$record_every[i], seed = gallery$seed[i] ) }), gallery$scenario ) ``` ## Compare recorded swarms A simulation stores every recorded boid as one row per frame. This makes it straightforward to compute summaries without any renderer-specific object model. ```{r} final_frame <- function(sim) { frames <- as.data.frame(sim) frames[frames$frame == max(frames$frame), , drop = FALSE] } mean_spread <- function(frame) { center <- colMeans(frame[, c("x", "y", "z"), drop = FALSE]) distance <- sqrt( (frame$x - center["x"])^2 + (frame$y - center["y"])^2 + (frame$z - center["z"])^2 ) mean(distance) } mean_nearest_neighbor <- function(frame) { if (nrow(frame) < 2L) return(NA_real_) coords <- as.matrix(frame[, c("x", "y", "z"), drop = FALSE]) distances <- as.matrix(stats::dist(coords)) diag(distances) <- NA_real_ mean(apply(distances, 1L, min, na.rm = TRUE), na.rm = TRUE) } scenario_summary <- function(sim) { frames <- as.data.frame(sim) final <- final_frame(sim) data.frame( scenario = sim$scenario, dimension = sim$dimension, boids = length(unique(final$id)), species = paste(sort(unique(final$species)), collapse = ", "), recorded_frames = length(unique(frames$frame)), mean_final_speed = round(mean(final$speed), 3), mean_final_spread = round(mean_spread(final), 3), mean_nearest_neighbor = round(mean_nearest_neighbor(final), 3), stringsAsFactors = FALSE ) } do.call(rbind, lapply(sims, scenario_summary)) ``` The same summaries can be split by species. This is useful for mixed flocks or cases where scouts and schooling agents are initialized together. ```{r} species_speed <- do.call(rbind, lapply(sims, function(sim) { final <- final_frame(sim) out <- stats::aggregate(speed ~ species, final, mean) out$scenario <- sim$scenario out$mean_final_speed <- round(out$speed, 3) out[, c("scenario", "species", "mean_final_speed")] })) species_speed ``` ## Snapshot plots The frame table is also enough for quick base-R diagnostics. The helper below draws a final-frame x/y projection, including obstacles, attractors, and predator influence radii when the scenario defines them. For 3D scenarios this is an overhead projection; point size varies with the z coordinate. ```{r} scenario_palette <- function(species) { keys <- sort(unique(species)) stats::setNames(grDevices::hcl.colors(length(keys), "Dark 3"), keys) } draw_world_marks <- function(world) { if (nrow(world$obstacles)) { graphics::symbols( world$obstacles$x, world$obstacles$y, circles = world$obstacles$radius, inches = FALSE, add = TRUE, fg = "gray45", bg = grDevices::adjustcolor("gray70", alpha.f = 0.28) ) } if (nrow(world$predators)) { graphics::symbols( world$predators$x, world$predators$y, circles = world$predators$radius, inches = FALSE, add = TRUE, fg = "#B24C63", lty = 2 ) graphics::points(world$predators$x, world$predators$y, pch = 4, col = "#B24C63", lwd = 2) } if (nrow(world$attractors)) { graphics::points(world$attractors$x, world$attractors$y, pch = 8, col = "#2F7E79", lwd = 2) } } draw_snapshot <- function(sim) { final <- final_frame(sim) world <- sim$world palette <- scenario_palette(final$species) z_span <- diff(range(final$z)) cex <- if (z_span > 0) 0.45 + 0.85 * (final$z - min(final$z)) / z_span else 0.75 graphics::plot( final$x, final$y, xlim = world$bounds["x", ], ylim = world$bounds["y", ], asp = 1, xlab = "x", ylab = "y", main = sim$scenario, col = palette[final$species], pch = 16, cex = cex ) draw_world_marks(world) graphics::legend( "topright", legend = names(palette), col = palette, pch = 16, bty = "n", cex = 0.75 ) } ``` ```{r scenario-snapshots, fig.width = 8, fig.height = 8} old_par <- graphics::par(mfrow = c(2, 2), mar = c(3, 3, 3, 1)) draw_snapshot(sims$schooling_2d) draw_snapshot(sims$obstacle_corridor_2d) draw_snapshot(sims$predator_avoidance_2d) draw_snapshot(sims$murmuration_3d) graphics::par(old_par) ``` ## Hand off to ggWebGL When `ggWebGL` 0.4.0 or later is installed, the same simulation object can be converted into a timeline-aware WebGL specification. The adapter keeps current boids visually dominant, draws recent history as faint trails, colours velocity vectors by species by default, and includes visible rings for obstacles and predator influence zones. This step is optional and leaves the core simulation object renderer-neutral. ```{r eval = FALSE} if (requireNamespace("ggWebGL", quietly = TRUE) && utils::packageVersion("ggWebGL") >= "0.4.0") { ggWebGL::ggWebGL( as_ggwebgl_spec(sims$mixed_species_3d, trail_length = 30), height = 520 ) } ```