vsrg 0.3.0

Data structures for vertical scrolling rhythm games
Documentation
//! # vsrg: Data structures for vertical scrolling rhythm games
//!
//! This crate provides the building blocks that lets you quickly create vertical scrolling rhythm
//! gameplay (hence vsrg) and its editor. Namely:
//! - [`rhythm`]: represent beats with [`BeatTime`], seconds with [`ClockTime`], and scrolling
//!   coordinates with [`ScrollPosition`]. [`rhythm::TempoTrack`] convert between beats and seconds
//!   [`rhythm::ScrollSpeedTrack`] convert clock time to scroll position. Both support eased changes.
//! - [`notes`]: store short and long notes in a struct-of-arrays layout, indexed by both the
//!   note's clock time and scroll position, which means querying visible notes for rendering and
//!   querying eligible notes for judgement is fast.
//! - [`generate_notes_storage!`] is a macro that generates storage for a game's note types with
//!   ergonomic querying and editing methods.
//! - [`notes::relations`]: maintain relationships during edits, such as chords, matching
//!   endpoints, parent-child links, and chains of notes.
//! - [`math`]: general-purpose math utilities, including [`math::Fraction`] and easing
//!   curves used by [`rhythm`]. [`Easing`] provides monotonic curves; [`math::AdvancedEasing`]
//!   adds curves that may be non-monotonic, with a more limited set of supported operations.
//!
//! # Helper modules
//!
//! - [`collections`]: general-purpose data structures, such as [`collections::IntervalTree`]. Used internally by [`notes`].
//!
//! # Optional features
//!
//! - `glam`: implement [`math::Easable`] for supported [`glam`] vector types.
//! - `serde`: enable serialization and deserialization of the easing enums.
//! - `lut_integration`: enable integration of [`math::AdvancedEasing`] curves using bundled lookup
//!   tables, at the cost of bundling the tables in static data (about 450kb).
//!
//! # Getting started
//!
//! You do not have to use all functionalities of this crate, the [`math`] and [`rhythm`] modules are
//! designed to be usable in isolation. For more details on them, check the respective modules'
//! documentations.
//!
//! The rest of this section will guide you through how to quickly scaffold your rhythm game with
//! this crate. This is a birds' eye view that will gloss over many details. A full example is available
//! on the git repository if you need more details.
//!
//! # Examples
//! ```
//! use vsrg::{generate_notes_storage, ClockTime, Easing, Tempo, Time};
//! use vsrg::notes::{
//!     query_intersecting, GroupId, Interval, LongNoteData, LongNoteTypeStorage,
//!     NoteGroup, NoteGroups, NotesStorage, ShortNoteData, ShortNoteTypeStorage,
//! };
//! use vsrg::rhythm::{
//!     ScrollSpeedChangeEvent, ScrollSpeedTrack, ScrollSpeedTracks,
//!     ScrollPosition, TempoChangeEvent, TempoTrack, TempoTracks, TrackId,
//! };
//! use soa_rs::{SoaClone, Soars};
//!
//! // 1. Define tap and hold notes for a 4K rhythm game.
//! #[derive(Debug, Clone)]
//! struct TapNote {
//!     // Clock time in this example; other games can store BeatTime or Time instead.
//!     time: ClockTime,
//!     lane: u16, // 0 to 3
//! }
//!
//! #[derive(Debug, Clone)]
//! struct HoldNote {
//!     time: ClockTime,
//!     duration: ClockTime,
//!     lane: u16,
//! }
//!
//! // Runtime state supports judgement and rendering, and starts at Default::default().
//! // Both note types share these components in this example, but they can be different types.
//! #[derive(Clone, Soars, SoaClone, Default)]
//! struct NoteState {
//!     completed: bool,
//!     highlighted: bool,
//! }
//!
//! // Stored note values other than timing and group membership.
//! // Both note types share these components in this example, but they can be different types.
//! #[derive(Clone, Soars, SoaClone)]
//! struct NoteValue {
//!     lane: u16,
//! }
//!
//! impl ShortNoteData for TapNote {
//!     type ValueData = NoteValue;
//!     type RuntimeData = NoteState;
//!
//!     fn time(&self) -> Time { Time::Clock(self.time) }
//!     // Main group only in this example, but you can store the concrete group id in the note.
//!     fn group_id(&self) -> &GroupId { &GroupId::Main }
//!     fn value_data(&self) -> NoteValue { NoteValue { lane: self.lane } }
//!
//!     fn reconstruct<'a>(time: Time, data: <NoteValue as Soars>::Ref<'a>, group: GroupId) -> Self {
//!         // This example stores only clock-based notes in the main group.
//!         assert_eq!(group, GroupId::Main);
//!         let Time::Clock(time) = time else { panic!("expected clock time") };
//!         Self { time, lane: *data.lane }
//!     }
//! }
//!
//! impl LongNoteData for HoldNote {
//!     type ValueData = NoteValue;
//!     type RuntimeData = NoteState;
//!
//!     fn start_time(&self) -> Time { Time::Clock(self.time) }
//!     fn duration(&self) -> Time { Time::Clock(self.duration) }
//!     fn group_id(&self) -> &GroupId { &GroupId::Main }
//!     fn value_data(&self) -> NoteValue { NoteValue { lane: self.lane } }
//!
//!     fn reconstruct<'a>(
//!         time: Time, duration: Time, data: <NoteValue as Soars>::Ref<'a>, group: GroupId,
//!     ) -> Self {
//!         assert_eq!(group, GroupId::Main);
//!         let (Time::Clock(time), Time::Clock(duration)) = (time, duration) else {
//!             panic!("expected clock time and duration")
//!         };
//!         Self { time, duration, lane: *data.lane }
//!     }
//! }
//!
//! // 2. Generate storage and enums for all note types.
//! generate_notes_storage! {
//!     enum MyNoteType,
//!     enum MyNote,
//!     enum MyNoteSnapshot,
//!     enum MyNoteRef,
//!     enum MyNoteMut,
//!     struct My4kNotesStorage {
//!         note_type Tap(TapNote) => taps: ShortNoteTypeStorage<TapNote>,
//!         note_type Hold(HoldNote) => holds: LongNoteTypeStorage<HoldNote>,
//!     }
//! }
//!
//! // 3. Define the note groups and their event tracks.
//! // Collections always have a main track. Named tracks can be added for other groups.
//! struct My4kEventTracks {
//!     tempo_tracks: TempoTracks,
//!     scroll_tracks: ScrollSpeedTracks,
//! }
//!
//! struct MyNoteGroup {
//!     tempo_track: TrackId,
//!     scroll_track: TrackId,
//!     // The above two are the bare minimum, if your games requires other properties per note
//!     // group, add them here.
//! }
//! impl NoteGroup for MyNoteGroup {
//!     fn main_group() -> Self {
//!         Self {
//!             tempo_track: TrackId::Main,
//!             scroll_track: TrackId::Main,
//!         }
//!     }
//!     fn scroll_track_id(&self) -> &TrackId { &self.scroll_track }
//!     fn tempo_track_id(&self) -> &TrackId { &self.tempo_track }
//! }
//!
//! // Keep groups and tracks available for later chart edits and rendering.
//! struct Chart {
//!     notes: My4kNotesStorage,
//!     groups: NoteGroups<MyNoteGroup>,
//!     tracks: My4kEventTracks,
//! }
//!
//! // 4. Load the chart, for example after deserializing your chart format.
//! fn load_chart(tap_notes: Vec<TapNote>, hold_notes: Vec<HoldNote>) -> Chart {
//!     let tempo = TempoTrack::with_events(vec![TempoChangeEvent {
//!         time: Time::Clock(ClockTime::ZERO),
//!         tempo: Tempo::from_bpm(120.0).unwrap(),
//!         ease: Easing::InConst,
//!     }]).unwrap();
//!     let mut tempo_tracks = TempoTracks::with_tracks(tempo, std::iter::empty());
//!     let scroll = ScrollSpeedTrack::with_events(
//!         TrackId::Main,
//!         vec![ScrollSpeedChangeEvent::new(
//!             Time::Clock(ClockTime::ZERO), 1.0, Easing::InConst,
//!         ).unwrap()],
//!         &tempo_tracks,
//!     );
//!     let mut scroll_tracks = ScrollSpeedTracks::with_tracks(scroll, std::iter::empty());
//!     let mut groups = NoteGroups::new();
//!     let values = tap_notes.into_iter().map(MyNote::Tap)
//!         .chain(hold_notes.into_iter().map(MyNote::Hold)).collect();
//!     let notes = My4kNotesStorage::with_notes(
//!         values, &mut groups, &mut tempo_tracks, &mut scroll_tracks,
//!     );
//!     Chart { notes, groups, tracks: My4kEventTracks { tempo_tracks, scroll_tracks } }
//! }
//!
//! // 5. During gameplay loop:
//! // Query a judgement window in clock time.
//! // The game can use these candidates to choose which note a key press should hit.
//! fn judge_notes(notes: &My4kNotesStorage, current_time: ClockTime) {
//!     let window = ClockTime::from_milis(100.0).unwrap();
//!
//!     let notes_in_range = notes.notes_ref_overlapping_range(Interval::new(
//!         current_time - window, current_time + window,
//!     ));
//!
//!     for _note in notes_in_range {
//!         todo!("Perform judgement logic here");
//!     }
//! }
//!
//! // Query the visible scroll range for each note type.
//! fn render_notes(chart: &Chart, current_time: ClockTime) {
//!     // How long your visible range is
//!     let render_distance = ScrollPosition::new(10.0).expect("Not NaN");
//!
//!     for group in chart.notes.taps().scroll_sorted_groups() {
//!         let properties = chart
//!             .groups
//!             .get_group_or_main(group.group_id());
//!         let scroll_track = chart
//!             .tracks
//!             .scroll_tracks
//!             .get_track_or_main(properties.scroll_track_id());
//!         let current_pos = scroll_track.calculate_scroll_position(current_time);
//!         let far_pos = current_pos + render_distance;
//!         let visible = Interval::new(current_pos, far_pos);
//!
//!         // Quickly query for notes in range
//!         for i in query_intersecting(group.scroll_position(), visible) {
//!             // Retrieve any data that you need
//!             let _note_value = group.value_data().get(i).unwrap();
//!             let _note_state = group.runtime_data().get(i).unwrap();
//!             let _note_scroll_pos = group.scroll_position()[i];
//!             todo!("Draw the note");
//!         }
//!     }
//!
//!     for group in chart.notes.holds().scroll_sorted_groups() {
//!         let properties = chart
//!             .groups
//!             .get_group_or_main(group.group_id());
//!         let scroll_track = chart
//!             .tracks
//!             .scroll_tracks
//!             .get_track_or_main(properties.scroll_track_id());
//!         let current_pos = scroll_track.calculate_scroll_position(current_time);
//!         let far_pos = current_pos + render_distance;
//!         let visible = Interval::new(current_pos, far_pos);
//!
//!         // Quickly query for notes in range
//!         for i in group.query_scroll_intersecting(visible) {
//!             // Retrieve any data that you need
//!             let _note_value = group.value_data().get(i).unwrap();
//!             let _note_state = group.runtime_data().get(i).unwrap();
//!             let _note_scroll_pos = group.scroll_position()[i];
//!             todo!("Draw the note");
//!         }
//!     }
//! }
//!
//! // 6. Modify the chart at runtime in your editor.
//! // Adding, removing, and replacing notes is supported, as well as operating in batches.
//! // Each note is identified by a u64 id.
//! fn editor(chart: &mut Chart, tap_id: u64) {
//!     chart.notes.replace_note(
//!         &mut chart.groups,
//!         &mut chart.tracks.tempo_tracks,
//!         &mut chart.tracks.scroll_tracks,
//!         tap_id,
//!         MyNote::Tap(TapNote { time: ClockTime::from_seconds(4.0).unwrap(), lane: 3 }),
//!     );
//! }
//! ```
//!
//! # Other tips
//! - vsrg uses float for beat time, which may be imprecise. You may want to store beat time as
//!   [`math::Fraction`] in your game instead.
//!
//! - During hot loop, prefer querying the individual storage of each note type directly, similar
//!   to `render_notes` in the above example. It both avoids cloning, allocating and has better
//!   memory access pattern.
//!
//! - Avoid storing Vec or similar data structure in the note's value data and runtime data since
//!   cloning them is expensive. Prefer Arc<[T]>, or even Rc<[T]> if your game logic is single
//!   threaded.
//!
//! - [BeatTime], [ClockTime], [Tempo], [ScrollPosition] and other types are newtypes of [f64], and
//!   have strict invariants (must be non NaN, infinities are allowed) to allow for [Ord]
//!   implementation. Any time a math operation produces NaN, it instead returns [f64::INFINITY]
//!   with the expectation that downstream code will safely ignore them. This should not be a
//!   problem in most rhythm games, but is something you may want to keep in mind.

pub mod collections;
pub mod math;
pub mod notes;
pub mod rhythm;
pub use generativity;
pub use soa_rs;

pub use math::Easing;
pub use rhythm::{BeatTime, ClockTime, ScrollPosition, Tempo, Time};