Time-windowed input buffering that catches presses a few frames early so jump leniency, combo windows, and dodge cancels feel natural instead of silently dropped.
Summary
This page covers IInputBufferManager, the time-windowed ring buffer that records action presses as they happen and lets game code consume them when it is actually ready to respond. You reach the manager through ScyllaInput.Instance.BufferManager; each entry is a BufferedInput struct carrying the action ID, the input value, the device that produced it, and the real-time timestamp at which it was recorded. The whole thing is twenty lines of game code on the consumer side: call BufferAction(actionID) when the input comes in, call ConsumeBufferedAction(actionID) when the legal-to-act moment arrives.
Anyone who has shipped a platformer has had the QA bug where the player presses Jump a few frames before touching the ground and the jump is silently dropped. Anyone who has shipped a fighting game has had the variant where a follow-up attack is queued during the recovery frames of the previous attack and is also dropped. Anyone who has shipped an action RPG has had the variant where a dodge cancel is pressed mid-animation and is similarly lost. All three are the same bug with three different names: the player’s intent arrived correctly, the game just was not ready to honour it on the frame the input fired. Input buffering is the standard answer.
The shape of the answer is a queue with a time window. When an action’s press is detected, the manager records the action ID and the timestamp in a fixed-capacity ring buffer (32 entries by default). When the consuming system reaches a state where it can act on the input (player just landed, attack recovery just ended, dodge cancel window opened), it calls ConsumeBufferedAction(actionID). The manager looks for the most recent entry for that action whose age is still within the buffer window (150 ms by default), removes it from the buffer, and returns true. If no entry exists, or the most recent one has already aged out, the call returns false and the consuming system proceeds as if nothing was queued. Per-action overrides let a hadoken’s combo window be 250 ms while a jump’s leniency is 100 ms.
Timestamps use Time.realtimeSinceStartupAsDouble, which advances regardless of Time.timeScale or pause state. The reason is that the player’s perception of “I pressed it a few frames ago” is wall-clock, not gameplay-time; a pause that drops Time.deltaTime to zero should not also drop the age of a buffered jump from 80 ms to 80 ms forever. Real-time aging keeps the buffer honest under bullet-time, hit-stop, slow-motion, and full pause.
Use this page for:
- Jump leniency: the platformer “pressed jump just before landing” buffer window.
- Combo windows: the fighting-game “follow-up attack queued during the previous attack’s recovery” pattern.
- Dash/dodge cancels: the action-RPG “cancel out of an animation a few frames before its cancel window opens” pattern.
- Spamming a confirm during a load: queueing a UI confirm pressed during a loading screen so the menu reacts the moment it appears.
- Replaying tutorial inputs on a slight delay so the on-screen prompt has time to register before the player’s pre-emptive press is consumed.
- Auto-buffering: feeding every
InputActionPerformedEventinto the buffer so the consuming side never has to write aBufferActioncall.
Features
- Two-call workflow.
BufferAction(actionID)records;ConsumeBufferedAction(actionID, windowSeconds = -1)removes the newest entry inside the window and returns whether one was found. The whole feature surface is built around these two calls. - Fixed-capacity ring buffer. Default 32 entries, configurable via
MaxBufferSize. When the buffer is full the oldest entry is silently overwritten; this matches the “the player will not notice losing an input from one second ago” rule. - Default window plus per-action overrides.
DefaultBufferWindow(150 ms) is the fallback.SetActionBufferWindow(actionID, windowSeconds)carves out tighter or looser windows per action.windowSeconds = -1on consume/peek/count calls means “use the per-action override if one exists, otherwise default”. - Non-destructive queries.
HasBufferedActionfor boolean checks,PeekBufferedActionfor inspection without removal,GetBufferedActionCountfor the count of valid entries in window. Your consuming code can decide whether to consume based on game state without having to first take the entry and then put it back. - Real-time timestamps. Entries age by wall clock (
Time.realtimeSinceStartupAsDouble), not byTime.deltaTime. Pause, slow-motion, and hit-stop do not stop the buffer’s age counter; the player’s perception of “I pressed it a few frames ago” stays accurate under every time-scale modifier. - Global enable/disable switch.
IsEnabledtoggles the whole feature without removing buffer calls from game code. When false, everyBufferActionis a no-op and every query returns a no-result default; flipping back on resumes work without reinitialising. - Three C# events plus two bus events.
OnInputBuffered,OnInputConsumed,OnInputExpiredon the manager interface for callers that hold the reference.InputBufferedEventandInputBufferConsumedEventon the SEX (Scylla Event eXchange) bus for everywhere else. Subscribing to the consumed event with aListen<T>handler is the cleanest path to “track how often the buffer actually saved the input”. - Buffer maintenance.
ClearBuffer(drop everything, no expiry events fired),ClearBufferedAction(actionID)(drop entries for one action),ExpireOldInputs(force the per-frame age scan, fires expiry events for removed entries). The framework callsExpireOldInputsinUpdate; explicit calls are useful in time-warp scenarios.
Concepts
The pieces that show up in every snippet below.
| Concept | Type / shape | Notes |
|---|---|---|
IInputBufferManager | interface, reached via ScyllaInput.Instance.BufferManager | Owns the ring buffer. Single instance per ScyllaInput. Initialized during the module’s lifecycle; ready by the time RunWhenReady fires. |
BufferedInput | readonly struct | One entry in the buffer: ActionID, Value, Timestamp, Device, DeviceType, plus computed Age and IsValid. BufferedInput.Invalid is the sentinel for “no entry found” returns. |
DefaultBufferWindow | float, seconds (default 0.15) | The fallback maximum age. Set globally via BufferManager.DefaultBufferWindow = 0.2f. Clamped to a minimum of 1 ms so a zero window cannot accidentally invalidate every entry on the next frame. |
| Per-action window override | string actionID -> float seconds map | Set via SetActionBufferWindow("Hadoken", 0.25f). Removed via ClearActionBufferWindow. Queried via GetActionBufferWindow. Consume/peek/count calls with windowSeconds = -1 use this override when present, else DefaultBufferWindow. |
| Ring buffer capacity | int, MaxBufferSize (default 32) | Total entries the buffer can hold across all actions. Hitting the cap silently overwrites the oldest entry; this is appropriate because an input older than the buffer’s combined per-action windows is no longer actionable anyway. |
| Real-time aging | Time.realtimeSinceStartupAsDouble | Timestamps and Age advance even when Time.timeScale is zero. The player’s “I pressed it a few frames ago” intuition matches wall clock, not gameplay time. |
InputBufferedEvent / InputBufferConsumedEvent | ScyllaEvent subtypes published by the manager | Bus events for buffer record / consume transitions. InputBufferedEvent carries ActionID, Value, Timestamp, and the resulting BufferCount for that action. InputBufferConsumedEvent carries ActionID, Value, and Age (how long the entry sat before being consumed). |
Recipes
These recipes cover the main buffering use cases. Each one is self-contained, so jump to the one matching what you are building. The first two share the same setup pattern (buffer on the performed event, consume at the relevant state transition), so if you are new to the API, start with the platformer jump recipe and you will understand the fighting-game one immediately.
Buffer a platformer jump press before landing
Problem. You want the player’s Jump press to “stick” for a moment so that pressing Jump a few frames before landing still results in a jump on the first frame the player is grounded. Without buffering, a press that arrives one frame too early is silently dropped, and the player tries to jump again, making the platformer feel sticky and unforgiving rather than responsive.
Solution. Every Jump input goes into the buffer; the grounded state machine consumes it on the frame that transitions from airborne to grounded; the buffer window expires the entry if the player stayed airborne longer than the leniency allows. You set a per-action window for “Jump” so the consume call does not need to hard-code the time.
using Scylla.Core;
using Scylla.Core.Events;
using Scylla.Input;
using UnityEngine;
public sealed class PlatformerJump : MonoBehaviour
{
[SerializeField] private float _jumpForce = 8f;
[SerializeField] private float _jumpBufferWindow = 0.1f; /* 100 ms leniency */
private Rigidbody2D _rb;
private ScyllaInputAction _jumpAction;
private IInputBufferManager _buffer;
private bool _isGrounded;
private ScyllaEventSubscription _jumpSubscription;
private void Awake()
{
_rb = GetComponent<Rigidbody2D>();
}
private void Start()
{
ScyllaCore.Instance.RunWhenReady(() =>
{
var input = ScyllaInput.Instance;
_jumpAction = input.ActionManager.GetAction("Jump");
_buffer = input.BufferManager;
/* A custom window for Jump tightens the default 150 ms to 100 ms;
* a value tuned to "feels responsive without queueing presses
* the player has already given up on". */
_buffer.SetActionBufferWindow("Jump", _jumpBufferWindow);
/* Buffer on every Jump performed. The handler is one line; the
* BufferAction call records the press with the default value 1f
* and the device that produced it (for analytics later). */
_jumpSubscription = ScyllaEvents.Listen<InputActionPerformedEvent>(
subscriber: this,
handler: evt =>
{
if (evt.ActionID == "Jump")
{
_buffer.BufferAction("Jump", evt.Value, evt.Device);
}
},
priority: ScyllaEventPriority.Medium
);
});
}
private void OnDestroy()
{
_jumpSubscription?.Dispose();
}
private void Update()
{
if (_buffer == null) return;
/* On the frame the player becomes grounded, consume any pending
* Jump press inside the window. ConsumeBufferedAction returns true
* exactly once per buffered press; no need for a "did I already
* jump from this press" guard. */
if (_isGrounded && _buffer.ConsumeBufferedAction("Jump"))
{
_rb.velocity = new Vector2(_rb.velocity.x, _jumpForce);
}
}
/* Called by a ground-detector trigger or raycast on collision change. */
public void SetGrounded(bool grounded)
{
_isGrounded = grounded;
}
}
Queue a fighting-game move during recovery frames
Problem. There is always a moment in fighting-game design where the player taps the next attack during the recovery animation of the previous one, and the game has to decide whether to honour the buffered input. The canonical example: you throw a Hadoken, the recovery animation plays, and the player is already pressing LightPunch for the combo follow-up. Without buffering, that press lands during the unresponsive recovery frames and nothing happens. With buffering, the press sits in the queue and fires the moment the cancel window opens.
Solution. The shape mirrors the platformer jump, with two differences: the window is wider (250 ms is typical for combo links), and the consume happens at the start of each move’s cancel window rather than at a state transition. Priority order in the consume chain matters: check the strongest cancel first so a super-cancel press does not also trigger a LightPunch.
using Scylla.Input;
using UnityEngine;
public sealed class FighterController : MonoBehaviour
{
/* Pre-set per-action windows at startup so consume calls do not need
* to specify a window each time. Hadoken at 250 ms (the standard
* combo link), super at 350 ms (combos into a super are pickier and
* benefit from a wider window). */
private void Start()
{
Scylla.Core.ScyllaCore.Instance.RunWhenReady(() =>
{
var buffer = ScyllaInput.Instance.BufferManager;
buffer.SetActionBufferWindow("LightPunch", 0.20f);
buffer.SetActionBufferWindow("Hadoken", 0.25f);
buffer.SetActionBufferWindow("Super", 0.35f);
});
}
/* Called from the move's animation event at the frame the cancel
* window opens. Each ConsumeBufferedAction call honours that move's
* per-action window automatically because we pass windowSeconds = -1. */
public void OnCancelWindowOpen()
{
var buffer = ScyllaInput.Instance.BufferManager;
/* Priority order matters: check the strongest cancel first.
* ConsumeBufferedAction removes the entry on success, so a
* super-cancel removes the super press from the buffer and a
* subsequent Hadoken check sees only an actual Hadoken press. */
if (buffer.ConsumeBufferedAction("Super")) { PlaySuper(); return; }
if (buffer.ConsumeBufferedAction("Hadoken")) { PlayHadoken(); return; }
if (buffer.ConsumeBufferedAction("LightPunch")) { PlayLightPunch(); return; }
}
private void PlaySuper() { /* ... */ }
private void PlayHadoken() { /* ... */ }
private void PlayLightPunch() { /* ... */ }
}
Tune per-action buffer windows
Problem. The default 150 ms is a reasonable starting point, but your game almost certainly has actions that want tighter or wider windows. Jump retries cheaply when missed, so 100 ms is fine; a dodge cancel pressed too early feels cheap, so 100 ms there too; a Hadoken is timing-sensitive and the player expects a wide window, so 250 ms. Rather than passing a custom window to every consume call, you set the override once and the consume calls pick it up automatically.
Solution. Call SetActionBufferWindow once at startup, driven from a configuration asset or constants table. Every ConsumeBufferedAction call that passes windowSeconds = -1 (the default) automatically picks up the override for that action. You can query the effective window at any time with GetActionBufferWindow.
using Scylla.Input;
using UnityEngine;
public sealed class InputBufferTuning : MonoBehaviour
{
private void Start()
{
Scylla.Core.ScyllaCore.Instance.RunWhenReady(() =>
{
var buffer = ScyllaInput.Instance.BufferManager;
/* Tight: the kind of input the player will retry quickly if it
* does not register. Jump, interact, single-tap dodge. 100 ms
* is roughly 6 frames at 60 Hz, enough to absorb a frame of
* input lag without buffering presses the player has given up. */
buffer.SetActionBufferWindow("Jump", 0.10f);
buffer.SetActionBufferWindow("Interact", 0.10f);
buffer.SetActionBufferWindow("Dodge", 0.10f);
/* Medium (the default 150 ms): actions that benefit
* from a small grace period. Attack, parry, block. */
buffer.SetActionBufferWindow("Attack", 0.15f);
buffer.SetActionBufferWindow("Parry", 0.15f);
/* Wide: combo links, super cancels, and any input that the
* player times to a wind-up the game has not finished animating.
* 250-350 ms is the standard range. */
buffer.SetActionBufferWindow("Hadoken", 0.25f);
buffer.SetActionBufferWindow("Super", 0.35f);
/* Inspect what the manager has configured: */
UnityEngine.Debug.Log(
$"Default window: {buffer.DefaultBufferWindow}s, " +
$"Jump override: {buffer.GetActionBufferWindow("Jump")}s, " +
$"Has Hadoken override: {buffer.HasActionBufferWindow("Hadoken")}"
);
});
}
}
Add coyote time to a platformer jump
Problem. The “coyote time” pattern is the platformer trick that lets the player jump for a few frames after walking off a ledge, named after the cartoon character who keeps running until he looks down. You want the player to press Jump up to 100 ms after leaving the edge and still get a jump. It is not strictly an input buffer (no input is queued ahead of time), but it composes with the buffer pattern by gating the consume call with a “recently grounded” window in addition to the strict “is grounded right now” check. Together, the two windows cover both leniency directions the player perceives as forgiving.
Solution. Track the last time the player was grounded. In Update, expand the consume gate to include a “left the ground within the coyote window” condition alongside the standard is-grounded check. Use UnityEngine.Time.unscaledTime for the coyote window to keep it on the same real-time clock as the buffer’s aging.
using Scylla.Input;
using UnityEngine;
public sealed class PlatformerJumpWithCoyote : MonoBehaviour
{
[SerializeField] private float _coyoteWindow = 0.1f; /* 100 ms after leaving ground */
private IInputBufferManager _buffer;
private float _lastGroundedTime = -1f;
private bool _isGrounded;
public void SetGrounded(bool grounded)
{
if (grounded)
{
_lastGroundedTime = UnityEngine.Time.unscaledTime;
}
_isGrounded = grounded;
}
private void Update()
{
if (_buffer == null) return;
/* Coyote-eligible: either grounded now, or the player left the
* ground within the coyote window. Use unscaledTime so a pause or
* slow-motion does not extend the window (matching the buffer's
* own real-time aging). */
bool coyoteEligible =
_isGrounded ||
(UnityEngine.Time.unscaledTime - _lastGroundedTime) <= _coyoteWindow;
/* The buffer handles "pressed too early"; coyote-eligible handles
* "pressed too late". Together they cover both leniency directions
* the player perceives as forgiving. */
if (coyoteEligible && _buffer.ConsumeBufferedAction("Jump"))
{
DoJump();
}
}
private void DoJump() { /* ... */ }
}
Track buffer events for analytics and tutorial feedback
Problem. You want to know how often the buffer is actually saving the player’s input – not to decide whether to act on it (that is ConsumeBufferedAction‘s job), but to tune window sizes and catch unresponsive sections of your game. A large mean Age on consumed events means the buffer is doing real work; a near-zero mean means inputs mostly hit their cancel windows directly and the window could be tightened. A tutorial also wants to react to “you would jump if you were grounded right now” without committing the consume.
Solution. Subscribe to InputBufferedEvent and InputBufferConsumedEvent via the SEX bus. The Age field on the consumed event is the most useful tuning signal; the BufferCount field on the buffered event tells you when the player has pressed the same action multiple times inside the window without it being consumed, which is a good “this encounter feels unresponsive” indicator. See Actions and Bindings for how ScyllaEvents.Listen works more broadly.
using Scylla.Core.Events;
using Scylla.Input;
using UnityEngine;
public sealed class InputBufferAnalytics : MonoBehaviour
{
private ScyllaEventSubscription _bufferedSubscription;
private ScyllaEventSubscription _consumedSubscription;
/* Running statistics: total consumes per action, total age accumulated. */
private readonly System.Collections.Generic.Dictionary<string, int> _consumeCount = new();
private readonly System.Collections.Generic.Dictionary<string, float> _consumeAge = new();
private void Start()
{
Scylla.Core.ScyllaCore.Instance.RunWhenReady(() =>
{
_bufferedSubscription = ScyllaEvents.Listen<InputBufferedEvent>(
subscriber: this, handler: OnBuffered, priority: ScyllaEventPriority.Low
);
_consumedSubscription = ScyllaEvents.Listen<InputBufferConsumedEvent>(
subscriber: this, handler: OnConsumed, priority: ScyllaEventPriority.Low
);
});
}
private void OnDestroy()
{
_bufferedSubscription?.Dispose();
_consumedSubscription?.Dispose();
}
private void OnBuffered(InputBufferedEvent evt)
{
/* BufferCount > 1 means the player pressed the same action twice
* inside the window without it being consumed; useful as a
* "this combat encounter feels unresponsive" signal. */
if (evt.BufferCount > 1)
{
UnityEngine.Debug.Log($"Multi-press queued: {evt.ActionID} (count={evt.BufferCount})");
}
}
private void OnConsumed(InputBufferConsumedEvent evt)
{
/* Track per-action averages for the tuning pass. A large mean Age
* suggests the action's window could be tightened; a small one
* suggests it could be widened, or the consuming state machine
* could open its cancel window earlier. */
if (!_consumeCount.ContainsKey(evt.ActionID))
{
_consumeCount[evt.ActionID] = 0;
_consumeAge[evt.ActionID] = 0f;
}
_consumeCount[evt.ActionID] += 1;
_consumeAge[evt.ActionID] += evt.Age;
}
}
API Sketch
The whole buffer surface, with the C# events on the manager and the bus event payloads listed at the end.
namespace Scylla.Input
{
public interface IInputBufferManager
{
/* Configuration. */
float DefaultBufferWindow { get; set; } /* default 0.15s, clamped >= 1ms */
int MaxBufferSize { get; set; } /* default 32, clamped >= 1 */
bool IsEnabled { get; set; }
bool IsInitialized { get; }
int BufferCount { get; } /* total entries across all actions */
/* Buffer record. Three overloads: bare, with device, with device + type. */
void BufferAction(string actionID, float value = 1f);
void BufferAction(string actionID, float value, UnityEngine.InputSystem.InputDevice device);
void BufferAction(string actionID, float value, UnityEngine.InputSystem.InputDevice device, InputDeviceType deviceType);
/* Consume (destructive) and query (non-destructive). windowSeconds = -1
* picks per-action override, else DefaultBufferWindow. */
bool ConsumeBufferedAction(string actionID, float windowSeconds = -1f);
bool ConsumeBufferedAction(string actionID, float windowSeconds, out BufferedInput bufferedInput);
bool HasBufferedAction (string actionID, float windowSeconds = -1f);
bool PeekBufferedAction (string actionID, float windowSeconds, out BufferedInput bufferedInput);
int GetBufferedActionCount(string actionID, float windowSeconds = -1f);
/* Maintenance. */
void ClearBuffer (); /* drop all, no expiry events */
void ClearBufferedAction(string actionID); /* drop entries for one action */
void ExpireOldInputs (); /* force the per-frame age scan */
/* Per-action window overrides. */
void SetActionBufferWindow (string actionID, float windowSeconds);
float GetActionBufferWindow (string actionID);
void ClearActionBufferWindow(string actionID);
bool HasActionBufferWindow (string actionID);
/* C# events on the manager. OnInputExpired is NOT published on the bus. */
event Action<BufferedInput> OnInputBuffered;
event Action<BufferedInput> OnInputConsumed;
event Action<BufferedInput> OnInputExpired;
}
public readonly struct BufferedInput
{
public string ActionID { get; }
public float Value { get; }
public double Timestamp { get; } /* Time.realtimeSinceStartupAsDouble */
public UnityEngine.InputSystem.InputDevice Device { get; }
public InputDeviceType DeviceType { get; }
public float Age { get; } /* computed: realtime - Timestamp */
public bool IsValid { get; } /* false on the Invalid sentinel */
public bool IsExpired (float windowSeconds);
public bool IsWithinWindow (float windowSeconds);
public static BufferedInput Invalid { get; }
}
/* Bus event payloads. */
public class InputBufferedEvent : Scylla.Core.Events.ScyllaEvent
{
public string ActionID { get; set; }
public float Value { get; set; }
public double Timestamp { get; set; }
public int BufferCount { get; set; } /* entries for this action after the add */
}
public class InputBufferConsumedEvent : Scylla.Core.Events.ScyllaEvent
{
public string ActionID { get; set; }
public float Value { get; set; }
public float Age { get; set; } /* how long the entry sat before consume */
}
}
See the API reference for full signatures and remarks.
Buffer window guidance
A small table of windows that show up in shipped games. These are starting points, not absolutes; final values come from playtesting.
| Action class | Typical window | Notes |
|---|---|---|
| Confirm / Cancel (UI) | 80-120 ms | Tight. UI clicks register fast; a wide window queues confirms the player has already moved on from. |
| Jump (platformer) | 100-150 ms | Tight to medium. The classic example; tighter feels snappier, wider feels more forgiving. |
| Interact / Use | 100-150 ms | Similar to Jump. Avoids “I pressed E but I was still walking up to the door” misses. |
| Dodge / Dash | 100-150 ms | Tight to medium. A wide window queues dashes that no longer make sense after the next move’s animation. |
| Attack (action-RPG, hack-and-slash) | 150-200 ms | Medium. Composes with combo cancel windows; the buffer absorbs presses during recovery so chains feel responsive. |
| Block / Parry | 120-180 ms | Medium. Tighter than attack because a buffered block can trigger after the threat is gone. |
| Combo link (fighting game) | 200-300 ms | Wide. The reason buffering exists. Hadoken, super, follow-ups all fall here. |
| Super / cinematic move | 300-400 ms | Widest typical. The player is timing to a wind-up the game has not finished playing; a wide window matches that. |
| Macro / shortcut (text input contexts) | 50-100 ms | Tightest. UI text fields are not typically a place where the player expects buffering; a wide window confuses. |
Best Practices
- Auto-buffer from action events; consume from gameplay state. The pattern in the platformer jump recipe generalises: one
ScyllaEvents.Listen<InputActionPerformedEvent>handler that callsBufferAction(evt.ActionID, evt.Value, evt.Device)for every action you want buffered, and aConsumeBufferedActioncall at each gameplay state transition that should honour the buffered input. Centralising the buffer-side keeps the consume-side simple. - Set per-action windows once at startup. Windows belong with action tuning, not with the consume call site. A constants class or a tuning ScriptableObject drives the
SetActionBufferWindowcalls duringRunWhenReady; consume calls passwindowSeconds = -1and the manager picks up the override. - Use
ConsumeBufferedActionoverHasBufferedActionfor “act on the input” code. The Has-then-Consume pattern needs a guard against state changing between the two calls; the consume call is atomic with its own check. ReserveHasBufferedActionfor genuinely non-destructive use cases (a tutorial preview, a HUD indicator). - Prefer the void
BufferAction(actionID, value)overload in the common case; reach for the device overload for analytics. The default value1fis right for digital buttons; the device argument is the right shape when you want to track “the player switched to gamepad mid-combo”. - Don’t buffer continuous input. The buffer is designed for one-shot transitions (Jump, Attack, Dodge). Buffering Move or Look pollutes the buffer with values the game already polls every frame; nothing consumes them, the ring buffer fills with stale stick reads, and the actual jump press gets evicted.
- Clear the buffer on context transitions.
ClearBufferis the right call on scene reload, pause-into-menu, and any other transition where a queued press from the previous mode should not fire in the next. The framework does not auto-clear onInputContextPushedEvent; subscribe to the context event and clear when appropriate. See Contexts for how the context stack works. - Tighter is better when the action retries cheaply. Jump in a platformer fails harmlessly; a wide window only queues presses the player has already abandoned. Combo links fail more meaningfully; a wider window matches the player’s actual intent. Tune in both directions, not toward one default.
- Use
BufferManager.IsEnabled = falseas the feature flag. Disabling for a single boss fight (a “no buffering” challenge), a developer test, or an accessibility setting works without removing buffer calls from game code. Flipping back on resumes work; entries already in the buffer survive the toggle as long as they have not aged out. - Read
InputBufferConsumedEvent.Agefor tuning. A consistently small mean age (under 30 ms) suggests the window could be tightened. A consistently large mean age (over half the window) suggests the window could be widened, or the consuming state machine could open its cancel window earlier. The data is more reliable than playtester reports of “feels good”.
Pitfalls
Related
- Scylla Input
- Getting Started
- Actions and Bindings
- Contexts
- API reference:
Scylla.Input.IInputBufferManager - API reference:
Scylla.Input.BufferedInput - API reference:
Scylla.Input.InputBufferedEvent - API reference:
Scylla.Input.InputBufferConsumedEvent