A robust input system for Raylib games built on .NET. Provides both state access and event-driven input handling with powerful action mapping capabilities.
- ⚡ High Performance - Efficient single-poll-per-frame architecture
- 🎯 Action Mapping - Bind multiple input sources to a single action (e.g., WASD + Arrow Keys + Gamepad)
- 📡 Event-Driven - Subscribe to input events for reactive programming
- 🎛️ State-Based - Query input state directly for fast prototyping
- 🎨 Composable - Build complex input schemes with simple bindings
- 🚀 Simple - Designed to have a simple and straight forward API implementation
dotnet add package RedOwl.InputYou only need to call Input.Update() once per frame as this will poll for the entire input state this frame and store it
so that input state queries are fast. This will also trigger any input events to be fired at this time if the input
control has changed its value.
using RedOwl;
using Raylib_cs;
// Use Reactive Pattern - Will be triggered when Input.Update is called
// Will only trigger if key was pressed or released this frame
Input.OnKeyboard += (e) =>
{
if (e.Key == KeyboardKey.Space && e.Pressed)
Jump();
};
// Or use action mappings - library has several predefined action mappings
var move = Input.StandardMoveControl();
// Or define your own mappings
var move = new TwoAxisControl()
.Bind(Input.Gamepad.LeftStick)
.Bind(
Input.Keyboard.KeyA, Input.Keyboard.KeyD, // A/D for X axis
Input.Keyboard.KeyW, Input.Keyboard.KeyS // W/S for Y axis
)
.Bind(
Input.Keyboard.KeyLeftArrow, Input.Keyboard.KeyRightArrow, // Arrows X
Input.Keyboard.KeyUpArrow, Input.Keyboard.KeyDownArrow // Arrows Y
)
.Enable();
while (!Raylib.WindowShouldClose())
{
var dt = Raylib.GetFrameTime();
// Poll and store input state for this frame and emit change events
// This also processes all registered controls and their interactions
Input.Update(dt);
// Query input state directly
if (Input.Keyboard.KeyW.Pressed)
{
MoveForward();
}
// Query input state using bound controls
MoveCharacter(move.Value);
// ... rest of game loop
}RedOwl.Input provides two complementary ways to handle input:
- State Query - Check input state stored this frame directly
- Event Handlers - React to input changes this frame
Both can be used together if needed.
The library's powerful action mapping system lets you combine multiple input sources into unified actions. For example, a "Move" action can combine:
- WASD keyboard keys
- Arrow keys
- Gamepad left stick
All inputs are automatically normalized and combined.
Use the convenient shortcuts:
Input.Keyboard // Access keyboard state
Input.Mouse // Access mouse state
Input.Gamepad // Access gamepad stateMost keys are a ButtonState with these properties:
bool Pressed // Key is currently down
bool Released // Key is currently up
bool WasPressedThisFrame // Key was pressed this frame (edge trigger)
bool WasReleasedThisFrame // Key was released this frame (edge trigger)
bool ChangedThisFrame // Key state changed this frame
float Value // 1.0 if pressed, 0.0 if not pressedSome keys are a TwoAxisState with these properties:
OneAxisState X
OneAxisState Y
bool ChangedThisFrame // X or Y state changed this frame
Vector2 Value // 0.0 -> 1.0 per axisTwoAxisState are made up of OneAxisState with these properties
bool ChangedThisFrame // state changed this frame
float Value // 0.0 - 1.0// Check if a key is currently pressed
if (Input.Keyboard.KeyW.Pressed)
{
// Moving forward
}
// Check if key was pressed this frame
if (Input.Keyboard.KeySpace.WasPressedThisFrame)
{
// Jump!
}
// Check if key was released this frame
if (Input.Keyboard.KeyEscape.WasReleasedThisFrame)
{
// Pause game
}
// Check modifier keys directly
if (Input.Keyboard.Control && Input.Keyboard.KeyS.Pressed)
{
SaveGame();
}// Current mouse position
var pos = Input.Mouse.Position.Value; // Vector2
// Mouse movement delta (this frame)
var delta = Input.Mouse.PositionDelta.Value; // Vector2
// Clamped delta (between -1 and 1)
var clamped = Input.Mouse.PositionDeltaClamped.Value;
// Check button state
if (Input.Mouse.LeftButton.Pressed)
{
// Left mouse button held
}
if (Input.Mouse.RightButton.WasPressedThisFrame)
{
// Right mouse button clicked
}
// Scroll delta
var scroll = Input.Mouse.ScrollDelta.Value; // Vector2 (X and Y scrolling)
// Clamped scroll (between -1 and 1)
var clamped = Input.Mouse.ScrollDeltaClamped.Value;
// Move mouse cursor to screen position
Input.Mouse.MoveTo(x, y); Deadzone is automatically applied (default 0.15). Adjust via Input.AxisDeadzone.
// Left stick (normalized to -1 to 1, with deadzone applied)
var leftStick = Input.Gamepad.LeftStick.Value; // Vector2
// Right stick
var rightStick = Input.Gamepad.RightStick.Value;
// Face buttons (PlayStation: Triangle/Circle/Cross/Square, Xbox: Y/B/A/X)
if (Input.Gamepad.ButtonNorth.Pressed) { } // Top face button
if (Input.Gamepad.ButtonSouth.Pressed) { } // Bottom face button
if (Input.Gamepad.ButtonEast.Pressed) { } // Right face button
if (Input.Gamepad.ButtonWest.Pressed) { } // Left face button
// Shoulders and triggers
if (Input.Gamepad.LeftShoulder.Pressed) { }
if (Input.Gamepad.RightShoulder.Pressed) { }
if (Input.Gamepad.LeftTrigger.Pressed) { }
if (Input.Gamepad.RightTrigger.Pressed) { }
// D-Pad
if (Input.Gamepad.DPadUp.Pressed) { }
if (Input.Gamepad.DPadDown.Pressed) { }
if (Input.Gamepad.DPadLeft.Pressed) { }
if (Input.Gamepad.DPadRight.Pressed) { }
// Special buttons
if (Input.Gamepad.Select.Pressed) { } // Back/Select button
if (Input.Gamepad.Start.Pressed) { } // Start/Menu button
// Stick press buttons
if (Input.Gamepad.LeftStickPress.Pressed) { }
if (Input.Gamepad.RightStickPress.Pressed) { }
// Rumble: left motor, right motor, duration (seconds)
Input.Gamepad.Rumble(0.5f, 0.5f, 0.3f);Subscribe to input events for reactive programming:
// Keyboard events
Input.OnKeyboard += (e) =>
{
Console.WriteLine($"Key: {e.Key}, Down: {e.Pressed}, Modifiers: {e.Modifiers}");
};
// Mouse events
Input.OnMouse += (e) =>
{
Console.WriteLine($"Mouse moved to {e.Position}, delta: {e.Delta}");
};
Input.OnMouseButton += (e) =>
{
Console.WriteLine($"Mouse button {e.Button} {(e.Pressed ? "pressed" : "released")}");
};
Input.OnMouseWheel += (e) =>
{
Console.WriteLine($"Wheel scrolled: {e.Delta}");
};
// Gamepad events
Input.OnGamepadButton += (e) =>
{
Console.WriteLine($"Gamepad button {e.Button} {(e.Pressed ? "pressed" : "released")}");
};
Input.OnGamepadAxis += (e) =>
{
Console.WriteLine($"Gamepad axis: {e.Value}");
};
// Text input (character typing)
Input.OnTextInput += (e) =>
{
Console.WriteLine($"Character typed: {e.Character}");
};
// Catch-all event handler
Input.OnAny += (e) =>
{
switch (e.Type)
{
case Input.Event.Types.Keyboard:
// Handle keyboard event
break;
// ... other types
}
};GamepadAxisEvent- Gamepad stick movementGamepadButtonEvent- Gamepad button press/releaseMouseEvent- Mouse movementMouseWheelEvent- Scroll wheel movementMouseButtonEvent- Mouse button press/releaseKeyboardEvent- Keyboard key press/releaseTextInputEvent- Character input (for text entry)
The action mapping system lets you combine multiple input sources into unified actions.
Combine multiple buttons into a single action. By default, any binding being active will trigger the control:
var jumpAction = new ButtonControl()
.Bind(Input.Keyboard.KeySpace) // Space key
.Bind(Input.Gamepad.ButtonSouth) // A/X button
.Enable();
// Check the action
if (jumpAction.Pressed)
{
Jump(); // Triggers if Space OR Gamepad A/X is pressed
}
if (jumpAction.WasPressedThisFrame)
{
StartJumpAnimation();
}For actions that require all bindings to be active simultaneously (like keyboard shortcuts or gamepad combos), use .RequireAll():
// Keyboard shortcut: Ctrl + C
var copyAction = new ButtonControl()
.Bind(Input.Keyboard.Control) // Ctrl modifier
.Bind(Input.Keyboard.KeyC) // C key
.RequireAll() // Both must be pressed together
.Enable();
if (copyAction.WasPressedThisFrame)
{
CopyToClipboard(); // Only triggers when Ctrl+C is pressed together
}
// Gamepad combo: Left Shoulder + D-Pad Up
var specialMove = new ButtonControl()
.Bind(Input.Gamepad.LeftShoulder)
.Bind(Input.Gamepad.DPadUp)
.RequireAll()
.Enable();
if (specialMove.Pressed)
{
PerformSpecialMove(); // Only active when both buttons are held
}Combine single-axis inputs:
var zoomAction = new OneAxisControl()
.Bind(Input.Mouse.ScrollDeltaClamped.Y) // Mouse scroll wheel
.Enable();
var zoomAmount = zoomAction.Value; // -1 to 1
ZoomCamera(zoomAmount);Can also bind the axis to a button control
var HotbarIncrement = new ButtonControl()
.Bind(Input.Mouse.ScrollDeltaClamped.Y.Neg) // Negative values Trigger ButtonControl
.Bind(Input.Gamepad.DPadRight)
.Enable();
var HotbarDecrement = new ButtonControl()
.Bind(Input.Mouse.ScrollDeltaClamped.Y.Pos) // Positive values Trigger ButtonControl
.Bind(Input.Gamepad.DPadLeft)
.Enable();Combine multiple 2D inputs (keyboard + gamepad + mouse):
var moveAction = new TwoAxisControl()
.Bind(Input.Gamepad.LeftStick) // Gamepad left stick
.Bind(
Input.Keyboard.KeyA, Input.Keyboard.KeyD, // A/D for X axis
Input.Keyboard.KeyW, Input.Keyboard.KeyS // W/S for Y axis
)
.Bind(
Input.Keyboard.KeyLeftArrow, Input.Keyboard.KeyRightArrow, // Arrows X
Input.Keyboard.KeyUpArrow, Input.Keyboard.KeyDownArrow // Arrows Y
)
.Enable();
// Get combined input value
var moveDirection = moveAction.Value; // Vector2, normalized to -1 to 1
MoveCharacter(moveDirection);Create an axis from two buttons:
var horizontalAxis = new ButtonAxis(
Input.Keyboard.KeyD, // Positive
Input.Keyboard.KeyA // Negative
);
var value = horizontalAxis.Value; // 1.0 if D pressed, -1.0 if A pressed, 0.0 if neither/bothButtonControl supports composable interactions that process button state each frame. Interactions are automatically processed during Input.Update(), so you can query their results as simple properties without passing delta time.
Detects double-tap gestures:
var useAction = new ButtonControl()
.Bind(Input.Keyboard.KeySpace)
.Bind(Input.Gamepad.ButtonSouth)
.With(new DoubleTapInteraction { Threshold = 0.3f }) // 0.3 second window for double-tap
.Enable();
// Query double-tap state (no delta time needed!)
if (useAction.WasDoubleTappedThisFrame)
{
PerformSpecialAction(); // Double-tap detected
}
if (useAction.WasPressedThisFrame)
{
PerformNormalAction(); // Single tap
}Configuration:
Threshold(default: 0.3f) - Maximum time in seconds between two taps to be considered a double-tap
Handles hold behavior for button presses. Supports two modes that can be used independently or together:
- Repeat Mode - Triggers repeatedly while held (like rapid-fire weapons)
- Long Press Mode - Detects when held for a specified duration (like delete confirmation or power button)
Repeat Mode Example:
var scrollAction = new ButtonControl()
.Bind(Input.Keyboard.KeyUpArrow)
.With(new HoldInteraction
{
RepeatMode = true, // Enable repeat mode
InitialRepeatDelay = 0.3f, // Wait 0.3s before first repeat
RepeatInterval = 0.15f, // Then repeat every 0.15s
FireOnPress = true // Fire immediately on press, then repeat
})
.Enable();
// Query repeat state
var repeatCount = scrollAction.HoldCountThisFrame; // 0, 1, or >1 if frame time is large
for (int i = 0; i < repeatCount; i++)
{
ScrollInventory(); // Process each repeat
}Long Press Mode Example:
var deleteAction = new ButtonControl()
.Bind(Input.Keyboard.KeyDelete)
.With(new HoldInteraction
{
LongPressMode = true, // Enable long press mode
LongPressThreshold = 3.0f // Must hold for 3 seconds
})
.Enable();
// Query long press state
if (deleteAction.WasHeldThisFrame)
{
// Threshold just crossed - show confirmation UI
ShowDeleteConfirmation();
}
if (deleteAction.IsHeld)
{
// Still held past threshold - perform the action continuously
PerformDelete();
}
// Query progress for UI (0.0 to 1.0)
if (deleteAction.Pressed)
{
var progress = deleteAction.HoldProgress; // Only meaningful when button is held
UpdateProgressBar(progress); // Drive a fill bar UI element
}
else
{
HideProgressBar(); // Hide when not being held
}Both Modes Example:
var action = new ButtonControl()
.Bind(Input.Keyboard.KeySpace)
.With(new HoldInteraction
{
RepeatMode = true, // Enable repeat
InitialRepeatDelay = 0.5f,
RepeatInterval = 0.1f,
LongPressMode = true, // Also enable long press
LongPressThreshold = 2.0f // Hold for 2 seconds
})
.Enable();
// Use repeat mode
var repeatCount = action.HoldCountThisFrame;
for (int i = 0; i < repeatCount; i++)
{
PerformAction();
}
// Use long press mode
if (action.WasHeldThisFrame)
{
PerformSpecialAction(); // Triggered once after 2 seconds
}
// Show progress bar
if (action.Pressed)
{
UpdateProgressBar(action.HoldProgress);
}Configuration:
Repeat Mode:
RepeatMode(default: true) - Enable repeat modeInitialRepeatDelay(default: 0.3f) - Delay in seconds before repeat startsRepeatInterval(default: 0.15f) - Interval in seconds between repeat triggers after initial delayFireOnPress(default: true) - If true, triggers once immediately on press, then repeats after delay. If false, only triggers after initial delay, then repeats.
Long Press Mode:
LongPressMode(default: false) - Enable long press modeLongPressThreshold(default: 3.0f) - Duration in seconds the button must be held before triggering
Properties:
Repeat Mode:
HoldCountThisFrame- Returns the number of times to trigger this frame (0, 1, or >1 if frame time is large). Only meaningful whenRepeatModeis true.
Long Press Mode:
IsHeld- Returnstrueif the button has been held past the threshold and is still being held. Only meaningful whenLongPressModeis true.WasHeldThisFrame- Returnstrueif the threshold was crossed this frame (edge detection). Only meaningful whenLongPressModeis true.HoldProgress(onButtonControl) - Returns the progress as a normalized value between 0.0 and 1.0. Returns 0.0 when not held or just pressed, increases while held, and reaches 1.0 when threshold is reached. Perfect for driving UI progress bars. Only meaningful whenLongPressModeis true and the button is currently pressed.
Notes:
HoldCountThisFramecan be greater than 1 if the frame time is large (e.g., during lag spikes). This ensures consistent behavior regardless of frame rate.- Long press resets immediately when the button is released, so you must hold continuously for the full duration.
You can add multiple interactions to a single control:
var action = new ButtonControl()
.Bind(Input.Keyboard.KeySpace)
.With(new DoubleTapInteraction { Threshold = 0.3f })
.With(new HoldInteraction
{
RepeatMode = true,
InitialRepeatDelay = 0.5f,
RepeatInterval = 0.1f,
FireOnPress = false, // Don't fire on initial press, wait for hold
LongPressMode = true,
LongPressThreshold = 2.0f
})
.Enable();
// Query all interaction states
if (action.WasDoubleTappedThisFrame)
{
// Handle double-tap
}
else if (action.WasHeldThisFrame)
{
// Handle long press threshold crossed
}
else if (action.HoldCountThisFrame > 0)
{
// Handle hold/repeat
}
else if (action.WasPressedThisFrame)
{
// Handle single tap
}If you query an interaction property that wasn't configured for the control, it returns a safe default value:
WasDoubleTappedThisFramereturnsfalseifDoubleTapInteractionis not configuredHoldCountThisFramereturns0ifHoldInteractionis not configured
This allows you to query interaction properties without checking if they're configured first.
Controls automatically register themselves when created and are processed each frame during Input.Update(). If you no longer need a control, you can dispose it to stop processing:
var action = new ButtonControl()
.Bind(Input.Keyboard.KeySpace)
.Enable();
// ... use the control ...
// When done, dispose it to stop processing
action.Dispose();Controls will also be automatically cleaned up when garbage collected (via finalizer), but it's recommended to explicitly dispose long-lived controls when they're no longer needed.
The library includes helper methods for common input schemes:
var input = Input.State;
// Standard movement (WASD + Arrows + Gamepad Left Stick)
var move = input.StandardMoveControl();
// Standard look (Mouse Delta + Gamepad Right Stick)
var look = input.StandardLookControl();
// Standard zoom (Mouse Scroll Wheel)
var zoom = input.StandardZoomControl();
// Standard back/cancel (Escape + Gamepad B/Circle)
var back = input.StandardBackControl();
// Standard use/interact (Space + Gamepad A/Cross) - configured with double-tap interaction
var use = input.StandardUseControl();
// Standard action (Left Click + F + Gamepad X/Square)
var action = input.StandardActionControl();
// Standard special (Right Click + R + Gamepad Y/Triangle)
var special = input.StandardSpecialControl();Configure global input settings:
// Gamepad stick deadzone (default: 0.15)
Input.AxisDeadzone = 0.2f;
// Threshold for axis change detection (default: 0.0001)
Input.AxisChangeThreshold = 0.001f;Disable the entire input system:
Input.State.Enabled = false; // Prevents all input polling
Input.Keyboard.Enabled = false; // Just Disable Keyboard
Input.Mouse.Enabled = false; // Just Disable Mouse
Input.Gamepad.Enabled = false; // Just Disable Gamepadvar attack = new ButtonControl()
.Bind(Input.Mouse.LeftButton)
.Bind(Input.Keyboard.KeyF)
.Bind(Input.Gamepad.ButtonEast)
.Enable();
var block = new ButtonControl()
.Bind(Input.Mouse.RightButton)
.Bind(Input.Gamepad.LeftShoulder)
.Enable();
while (running)
{
Input.Update();
if (attack.Pressed && !block.Pressed)
{
Attack();
}
else if (block.Pressed)
{
Block();
}
}See LICENSE file for details.
Built on top of Raylib-cs using Raylib.
Made with ❤️ by Red Owl Games