refactor: huge refactor into node/graph-based movement system

This commit is contained in:
2025-07-28 12:23:57 -05:00
parent 413f9f156f
commit 464d6f9ca6
24 changed files with 868 additions and 2067 deletions

View File

@@ -1,47 +1,41 @@
//! This module provides a simple animation and atlas system for textures.
use anyhow::Result;
use sdl2::render::WindowCanvas;
use sdl2::rect::Rect;
use sdl2::render::{Canvas, RenderTarget};
use crate::texture::sprite::AtlasTile;
use crate::texture::sprite::{AtlasTile, SpriteAtlas};
/// An animated texture using a texture atlas.
#[derive(Clone)]
pub struct AnimatedTexture {
pub frames: Vec<AtlasTile>,
pub ticks_per_frame: u32,
pub ticker: u32,
pub paused: bool,
tiles: Vec<AtlasTile>,
frame_duration: f32,
current_frame: usize,
time_bank: f32,
}
impl AnimatedTexture {
pub fn new(frames: Vec<AtlasTile>, ticks_per_frame: u32) -> Self {
AnimatedTexture {
frames,
ticks_per_frame,
ticker: 0,
paused: false,
pub fn new(tiles: Vec<AtlasTile>, frame_duration: f32) -> Self {
Self {
tiles,
frame_duration,
current_frame: 0,
time_bank: 0.0,
}
}
/// Advances the animation by one tick, unless paused.
pub fn tick(&mut self) {
if self.paused || self.ticks_per_frame == 0 {
return;
pub fn tick(&mut self, dt: f32) {
self.time_bank += dt;
while self.time_bank >= self.frame_duration {
self.time_bank -= self.frame_duration;
self.current_frame = (self.current_frame + 1) % self.tiles.len();
}
self.ticker += 1;
}
pub fn current_tile(&mut self) -> &mut AtlasTile {
if self.ticks_per_frame == 0 {
return &mut self.frames[0];
}
let frame_index = (self.ticker / self.ticks_per_frame) as usize % self.frames.len();
&mut self.frames[frame_index]
pub fn current_tile(&self) -> &AtlasTile {
&self.tiles[self.current_frame]
}
pub fn render(&mut self, canvas: &mut WindowCanvas, dest: sdl2::rect::Rect) -> Result<()> {
let tile = self.current_tile();
tile.render(canvas, dest)
pub fn render<T: RenderTarget>(&self, canvas: &mut Canvas<T>, atlas: &mut SpriteAtlas, dest: Rect) -> Result<()> {
let mut tile = self.current_tile().clone();
tile.render(canvas, atlas, dest)
}
}

View File

@@ -1,48 +1,36 @@
//! A texture that blinks on/off for a specified number of ticks.
use anyhow::Result;
use sdl2::render::WindowCanvas;
use crate::texture::animated::AnimatedTexture;
use crate::texture::sprite::AtlasTile;
#[derive(Clone)]
pub struct BlinkingTexture {
pub animation: AnimatedTexture,
pub on_ticks: u32,
pub off_ticks: u32,
pub ticker: u32,
pub visible: bool,
tile: AtlasTile,
blink_duration: f32,
time_bank: f32,
is_on: bool,
}
impl BlinkingTexture {
pub fn new(animation: AnimatedTexture, on_ticks: u32, off_ticks: u32) -> Self {
BlinkingTexture {
animation,
on_ticks,
off_ticks,
ticker: 0,
visible: true,
pub fn new(tile: AtlasTile, blink_duration: f32) -> Self {
Self {
tile,
blink_duration,
time_bank: 0.0,
is_on: true,
}
}
/// Advances the blinking state by one tick.
pub fn tick(&mut self) {
self.animation.tick();
self.ticker += 1;
if self.visible && self.ticker >= self.on_ticks {
self.visible = false;
self.ticker = 0;
} else if !self.visible && self.ticker >= self.off_ticks {
self.visible = true;
self.ticker = 0;
pub fn tick(&mut self, dt: f32) {
self.time_bank += dt;
if self.time_bank >= self.blink_duration {
self.time_bank -= self.blink_duration;
self.is_on = !self.is_on;
}
}
/// Renders the blinking texture.
pub fn render(&mut self, canvas: &mut WindowCanvas, dest: sdl2::rect::Rect) -> Result<()> {
if self.visible {
self.animation.render(canvas, dest)
} else {
Ok(())
}
pub fn is_on(&self) -> bool {
self.is_on
}
pub fn tile(&self) -> &AtlasTile {
&self.tile
}
}

View File

@@ -1,65 +1,57 @@
//! A texture that changes based on the direction of an entity.
use crate::entity::direction::Direction;
use crate::texture::sprite::AtlasTile;
use anyhow::Result;
use sdl2::render::WindowCanvas;
use sdl2::rect::Rect;
use sdl2::render::{Canvas, RenderTarget};
use std::collections::HashMap;
use crate::entity::direction::Direction;
use crate::texture::animated::AnimatedTexture;
use crate::texture::sprite::SpriteAtlas;
#[derive(Clone)]
pub struct DirectionalAnimatedTexture {
pub up: Vec<AtlasTile>,
pub down: Vec<AtlasTile>,
pub left: Vec<AtlasTile>,
pub right: Vec<AtlasTile>,
pub ticker: u32,
pub ticks_per_frame: u32,
textures: HashMap<Direction, AnimatedTexture>,
stopped_textures: HashMap<Direction, AnimatedTexture>,
}
impl DirectionalAnimatedTexture {
pub fn new(
up: Vec<AtlasTile>,
down: Vec<AtlasTile>,
left: Vec<AtlasTile>,
right: Vec<AtlasTile>,
ticks_per_frame: u32,
) -> Self {
pub fn new(textures: HashMap<Direction, AnimatedTexture>, stopped_textures: HashMap<Direction, AnimatedTexture>) -> Self {
Self {
up,
down,
left,
right,
ticker: 0,
ticks_per_frame,
textures,
stopped_textures,
}
}
pub fn tick(&mut self) {
self.ticker += 1;
pub fn tick(&mut self, dt: f32) {
for texture in self.textures.values_mut() {
texture.tick(dt);
}
}
pub fn render(&mut self, canvas: &mut WindowCanvas, dest: sdl2::rect::Rect, direction: Direction) -> Result<()> {
let frames = match direction {
Direction::Up => &mut self.up,
Direction::Down => &mut self.down,
Direction::Left => &mut self.left,
Direction::Right => &mut self.right,
};
let frame_index = (self.ticker / self.ticks_per_frame) as usize % frames.len();
let tile = &mut frames[frame_index];
tile.render(canvas, dest)
pub fn render<T: RenderTarget>(
&self,
canvas: &mut Canvas<T>,
atlas: &mut SpriteAtlas,
dest: Rect,
direction: Direction,
) -> Result<()> {
if let Some(texture) = self.textures.get(&direction) {
texture.render(canvas, atlas, dest)
} else {
Ok(())
}
}
pub fn render_stopped(&mut self, canvas: &mut WindowCanvas, dest: sdl2::rect::Rect, direction: Direction) -> Result<()> {
let frames = match direction {
Direction::Up => &mut self.up,
Direction::Down => &mut self.down,
Direction::Left => &mut self.left,
Direction::Right => &mut self.right,
};
// Show the last frame (full sprite) when stopped
let tile = &mut frames[1];
tile.render(canvas, dest)
pub fn render_stopped<T: RenderTarget>(
&self,
canvas: &mut Canvas<T>,
atlas: &mut SpriteAtlas,
dest: Rect,
direction: Direction,
) -> Result<()> {
if let Some(texture) = self.stopped_textures.get(&direction) {
texture.render(canvas, atlas, dest)
} else {
Ok(())
}
}
}

View File

@@ -1,14 +1,5 @@
use std::cell::RefCell;
use std::rc::Rc;
use crate::texture::sprite::{AtlasTile, SpriteAtlas};
pub mod animated;
pub mod blinking;
pub mod directional;
pub mod sprite;
pub mod text;
pub fn get_atlas_tile(atlas: &Rc<RefCell<SpriteAtlas>>, name: &str) -> AtlasTile {
SpriteAtlas::get_tile(atlas, name).unwrap_or_else(|| panic!("Could not find tile {name}"))
}

View File

@@ -4,9 +4,7 @@ use sdl2::pixels::Color;
use sdl2::rect::Rect;
use sdl2::render::{Canvas, RenderTarget, Texture};
use serde::Deserialize;
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
#[derive(Clone, Debug, Deserialize)]
pub struct AtlasMapper {
@@ -21,26 +19,28 @@ pub struct MapperFrame {
pub height: u16,
}
#[derive(Clone)]
#[derive(Copy, Clone, Debug)]
pub struct AtlasTile {
pub atlas: Rc<RefCell<SpriteAtlas>>,
pub pos: U16Vec2,
pub size: U16Vec2,
pub color: Option<Color>,
}
impl AtlasTile {
pub fn render<C: RenderTarget>(&mut self, canvas: &mut Canvas<C>, dest: Rect) -> Result<()> {
let color = self
.color
.unwrap_or(self.atlas.borrow().default_color.unwrap_or(Color::WHITE));
self.render_with_color(canvas, dest, color)
pub fn render<C: RenderTarget>(&mut self, canvas: &mut Canvas<C>, atlas: &mut SpriteAtlas, dest: Rect) -> Result<()> {
let color = self.color.unwrap_or(atlas.default_color.unwrap_or(Color::WHITE));
self.render_with_color(canvas, atlas, dest, color)
}
pub fn render_with_color<C: RenderTarget>(&mut self, canvas: &mut Canvas<C>, dest: Rect, color: Color) -> Result<()> {
pub fn render_with_color<C: RenderTarget>(
&mut self,
canvas: &mut Canvas<C>,
atlas: &mut SpriteAtlas,
dest: Rect,
color: Color,
) -> Result<()> {
let src = Rect::new(self.pos.x as i32, self.pos.y as i32, self.size.x as u32, self.size.y as u32);
let mut atlas = self.atlas.borrow_mut();
if atlas.last_modulation != Some(color) {
atlas.texture.set_color_mod(color.r, color.g, color.b);
atlas.last_modulation = Some(color);
@@ -68,10 +68,8 @@ impl SpriteAtlas {
}
}
pub fn get_tile(atlas: &Rc<RefCell<SpriteAtlas>>, name: &str) -> Option<AtlasTile> {
let atlas_ref = atlas.borrow();
atlas_ref.tiles.get(name).map(|frame| AtlasTile {
atlas: Rc::clone(atlas),
pub fn get_tile(&self, name: &str) -> Option<AtlasTile> {
self.tiles.get(name).map(|frame| AtlasTile {
pos: U16Vec2::new(frame.x, frame.y),
size: U16Vec2::new(frame.width, frame.height),
color: None,
@@ -87,6 +85,6 @@ impl SpriteAtlas {
}
}
pub unsafe fn texture_to_static<'a>(texture: Texture<'a>) -> Texture<'static> {
pub unsafe fn texture_to_static(texture: Texture) -> Texture<'static> {
std::mem::transmute(texture)
}

View File

@@ -50,38 +50,34 @@ use anyhow::Result;
use glam::UVec2;
use sdl2::render::{Canvas, RenderTarget};
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use crate::texture::sprite::{AtlasTile, SpriteAtlas};
/// A text texture that renders characters from the atlas.
pub struct TextTexture {
atlas: Rc<RefCell<SpriteAtlas>>,
char_map: HashMap<char, AtlasTile>,
scale: f32,
}
impl TextTexture {
/// Creates a new text texture with the given atlas and scale.
pub fn new(atlas: Rc<RefCell<SpriteAtlas>>, scale: f32) -> Self {
pub fn new(scale: f32) -> Self {
Self {
atlas,
char_map: HashMap::new(),
scale,
}
}
/// Maps a character to its atlas tile, handling special characters.
fn get_char_tile(&mut self, c: char) -> Option<AtlasTile> {
fn get_char_tile(&mut self, atlas: &SpriteAtlas, c: char) -> Option<AtlasTile> {
if let Some(tile) = self.char_map.get(&c) {
return Some(tile.clone());
return Some(*tile);
}
let tile_name = self.char_to_tile_name(c)?;
let tile = SpriteAtlas::get_tile(&self.atlas, &tile_name)?;
self.char_map.insert(c, tile.clone());
let tile = atlas.get_tile(&tile_name)?;
self.char_map.insert(c, tile);
Some(tile)
}
@@ -89,9 +85,7 @@ impl TextTexture {
fn char_to_tile_name(&self, c: char) -> Option<String> {
let name = match c {
// Letters A-Z
'A'..='Z' => format!("text/{c}.png"),
// Numbers 0-9
'0'..='9' => format!("text/{c}.png"),
'A'..='Z' | '0'..='9' => format!("text/{c}.png"),
// Special characters
'!' => "text/!.png".to_string(),
'-' => "text/-.png".to_string(),
@@ -108,15 +102,21 @@ impl TextTexture {
}
/// Renders a string of text at the given position.
pub fn render<C: RenderTarget>(&mut self, canvas: &mut Canvas<C>, text: &str, position: UVec2) -> Result<()> {
pub fn render<C: RenderTarget>(
&mut self,
canvas: &mut Canvas<C>,
atlas: &mut SpriteAtlas,
text: &str,
position: UVec2,
) -> Result<()> {
let mut x_offset = 0;
let char_width = (8.0 * self.scale) as u32;
let char_height = (8.0 * self.scale) as u32;
for c in text.chars() {
if let Some(mut tile) = self.get_char_tile(c) {
if let Some(mut tile) = self.get_char_tile(atlas, c) {
let dest = sdl2::rect::Rect::new((position.x + x_offset) as i32, position.y as i32, char_width, char_height);
tile.render(canvas, dest)?;
tile.render(canvas, atlas, dest)?;
}
// Always advance x_offset for all characters (including spaces)
x_offset += char_width;
@@ -136,7 +136,7 @@ impl TextTexture {
}
/// Calculates the width of a string in pixels at the current scale.
pub fn text_width(&mut self, text: &str) -> u32 {
pub fn text_width(&self, text: &str) -> u32 {
let char_width = (8.0 * self.scale) as u32;
let mut width = 0;