Layering
saddle-systems-game-feel is split into pure effect math plus a thin Bevy runtime layer.
Pure or mostly-pure modules:
tween.rsconfig.rschannels.rs- the math-heavy helpers inside
shake.rs,punch.rs,squash.rs,knockback.rs, andtime_scale.rs
Bevy-facing runtime modules:
messages.rsrecipe.rslib.rs- the queue/runtime/output systems in
shake.rs,punch.rs,flash.rs,squash.rs, andtime_scale.rs
The pure layer owns stacking, attenuation, spring behavior, tween sampling, and time-scale resolution. The Bevy layer only reads messages, advances runtime state, applies outputs, and cleans up adapters.
Optional authored sample content now lives separately in presets.rs. That module contains example-facing channel aliases and named recipe packs, but the core runtime initializes no gameplay vocabulary by default.
Runtime Flow
Gameplay messages
-> recipe playback (optional)
-> effect request processing
-> shake / punch / time-scale / flash / rumble / squash runtimes
-> public output components/resources
-> built-in adapters (transform, projection, sprite, screen pulse)
-> diagnosticsGameplay messages
-> recipe playback (optional)
-> effect request processing
-> shake / punch / time-scale / flash / rumble / squash runtimes
-> public output components/resources
-> built-in adapters (transform, projection, sprite, screen pulse)
-> diagnosticsMore concretely:
AddTrauma,RequestCameraImpulse,RequestHitstop,RequestTimeScale,RequestFlash,RequestRumble,RequestSquashStretch,RequestKnockback, andPlayFeedbackRecipeenter through the message layer.- Recipes expand into concrete request messages. The runtime only knows about whatever recipes a game inserts into
FeedbackRecipeLibrary; optional sample recipes come frompresets::recipes. - Request processors resolve channels, attenuation, and per-target runtime state.
- Simulation systems advance effect queues and sampled outputs.
- Built-in adapters apply those outputs additively and reversibly.
- Cleanup systems drop empty runtime containers and maintain overlay ownership.
Screen pulses now intentionally support two presentation modes:
ScreenPulsePresentation::OutputOnlykeepsScreenPulseOutputas pure data for downstream renderer crates.ScreenPulsePresentation::LegacyBuiltInalso enables the crate's older UI-overlay andChromaticAberrationwriteback path.
Schedule Ordering
GameFeelSystems is intentionally public:
ProcessRequestsUpdateSimulationApplyOutputsCleanup
When the plugin runs on the normal Update schedule, the built-in presentation restore step happens in PreUpdate and the adapter writeback happens in PostUpdate. That keeps normal gameplay logic from seeing permanently polluted transforms.
When the caller uses a non-Update schedule, the crate falls back to running restore and apply work inside the injected schedule itself. This preserves injectable schedules while still keeping the public ordering hooks usable in sandboxes or custom simulation loops.
Adapter Model
The runtime is deliberately split between generic outputs and concrete adapters.
Generic output surfaces:
GlobalTimeScaleEntityTimeScaleShakeStatePunchStateFlashOutputScreenPulseOutputRumbleOutputSquashStretchStateKnockbackState
Built-in adapters:
Transformwriteback for shake, punch, and squash/stretchProjection::PerspectiveFOV writeback for punchSpriteflash writeback forFlashOutput- optional UI-overlay and
ChromaticAberrationwriteback forScreenPulseOutput - no built-in platform haptics backend;
RumbleOutputintentionally stays as a portable output surface for downstream gamepad or platform adapters - no built-in transform application for knockback;
KnockbackStateprovides displacement as an output surface that consumer systems apply to their own movement logic
This means downstream projects can:
- use the built-in adapters directly
- ignore them and read the output surfaces themselves
- mix both patterns in one game
- keep game-feel authored separately while routing screen pulses into another crate's unified screen-effects stack
Stacking Behavior
Shake
- Trauma is additive and clamped to
[0, 1]. - Per-frame budget limits how much new trauma can land in one frame.
- Fatigue reduces acceptance when many sources stack aggressively.
- Directional bias is additive and decays separately from scalar trauma.
Punch
- Impulses add into velocity state.
- The return motion is spring-damped, not a fixed interpolation.
- Local-space and world-space impulses coexist.
Time Scale
- Ramps are resolved by priority.
- Equal-priority ramps choose the lowest scale.
- Hitstop is orthogonal to ramps and composes multiplicatively.
IgnoreGlobalTimeScaleandIgnoreHitstoplet consumer systems opt out selectively.
Flash
- Entity flash picks the strongest weighted flash each frame.
- Screen flash picks the strongest flash alpha for color ownership.
- Chromatic aberration and vignette use max composition.
Squash / Stretch
Multiplycomposes all active scale multipliers.Strongestpicks the effect furthest fromVec3::ONE.Replaceuses the most recently added effect only.
Knockback
- All active knockback effects sum their displacements into
KnockbackState. KnockbackReceiver.max_displacementclamps the total magnitude.- Displacement decays via an eased envelope (default
QuadraticOut). - The crate computes displacement only; consumer systems decide how to apply it (add to transform, feed into physics, etc.).
Time Domains
Every authored effect chooses one of two clocks:
UnscaledGlobalScaled
Use Unscaled for:
- flashes that should keep fading during hitstop
- recoil tails that should finish even while the world is slowed
- UI or reward pulses
Use GlobalScaled for:
- effects that should slow down together with gameplay
- follow-through that should feel stretched by slow motion
Hitstop itself is frame-counted and deterministic. It does not depend on wall-clock frame rate.
Cleanup And Drift Protection
Presentation adapters always restore the previous frame's authored transform/projection/sprite state before writing the new output. This prevents:
- permanent transform drift
- flash color accumulation
- projection FOV drift after punch
The cleanup stage only removes empty runtime containers. Public output components are allowed to remain present with zeroed values so consumer adapters can choose stable query shapes if they prefer them.
Testing Strategy
The crate verifies the boundary in four layers:
- pure unit tests for tweening, attenuation, hitstop stacking, springs, and scale helpers
- App-level integration tests for schedule injection, message entrypoints, and drift recovery
- runnable standalone examples for focused manual smoke checks
- a crate-local lab with E2E scenarios and screenshot gates