Popcorning Guinea Pig
Search Results for

    Input System

    The Popcorning Guinea Pig engine provides a comprehensive input management system through the IInputService and related components, supporting keyboards, gamepads, and customizable input contexts.

    Overview

    The input system features:

    • Context-based input handling with stacking
    • Customizable key bindings and remapping
    • Support for multiple input devices (keyboard, gamepad, mouse)
    • Command pattern implementation for actions
    • Input buffering and combo detection
    • Real-time input binding changes
    • Input recording and playback for testing

    Core Components

    IInputService Interface

    public interface IInputService
    {
        bool IsKeyDown(InputKey key);
        bool IsKeyPressed(InputKey key);
        bool IsKeyReleased(InputKey key);
        Vector2 GetMousePosition();
        bool IsMouseButtonDown(MouseButton button);
        bool IsMouseButtonPressed(MouseButton button);
        GamepadState GetGamepadState(int playerIndex);
        void Update();
        void RegisterInputContext(InputContext context);
        void PushContext(string contextName);
        void PopContext();
        void ClearContextStack();
    }
    

    Input Context System

    The engine uses a context-based system where different contexts (menu, gameplay, pause) can have different input mappings:

    public class InputContext
    {
        public string Name { get; set; }
        public List<InputBinding> Bindings { get; set; }
        public bool ConsumeInput { get; set; } // Prevents lower contexts from receiving input
        public int Priority { get; set; }
    }
    
    public class InputBinding
    {
        public string ActionName { get; set; }
        public InputKey Key { get; set; }
        public InputKey[] ModifierKeys { get; set; }
        public IInputCommand Command { get; set; }
    }
    

    Getting Started

    Initialize Input System

    public class MyGame : Game
    {
        private readonly IInputService _inputService;
        private readonly InputManager _inputManager;
        
        public MyGame()
        {
            _inputService = GetService<IInputService>();
            _inputManager = new InputManager(_inputService);
            
            SetupInputContexts();
        }
        
        private void SetupInputContexts()
        {
            // Create gameplay context
            var gameplayContext = new InputContext
            {
                Name = "Gameplay",
                ConsumeInput = true,
                Bindings = new List<InputBinding>
                {
                    new InputBinding
                    {
                        ActionName = "MoveLeft",
                        Key = InputKey.A,
                        Command = new MoveCommand(Vector2.Left)
                    },
                    new InputBinding
                    {
                        ActionName = "MoveRight",
                        Key = InputKey.D,
                        Command = new MoveCommand(Vector2.Right)
                    },
                    new InputBinding
                    {
                        ActionName = "Jump",
                        Key = InputKey.Space,
                        Command = new JumpCommand()
                    }
                }
            };
            
            // Create menu context
            var menuContext = new InputContext
            {
                Name = "Menu",
                ConsumeInput = true,
                Bindings = new List<InputBinding>
                {
                    new InputBinding
                    {
                        ActionName = "Navigate",
                        Key = InputKey.ArrowUp,
                        Command = new NavigateCommand(Direction.Up)
                    },
                    new InputBinding
                    {
                        ActionName = "Select",
                        Key = InputKey.Enter,
                        Command = new SelectCommand()
                    },
                    new InputBinding
                    {
                        ActionName = "Back",
                        Key = InputKey.Escape,
                        Command = new BackCommand()
                    }
                }
            };
            
            _inputManager.RegisterContext(gameplayContext);
            _inputManager.RegisterContext(menuContext);
        }
        
        protected override void Update(GameTime gameTime)
        {
            _inputService.Update();
            _inputManager.ProcessInput();
            base.Update(gameTime);
        }
    }
    

    Input Commands

    Implement the command pattern for actions:

    public interface IInputCommand
    {
        void Execute();
        void Undo(); // For commands that support undo
        bool CanExecute();
    }
    
    // Movement command
    public class MoveCommand : IInputCommand
    {
        private readonly Vector2 _direction;
        private readonly Player _player;
        
        public MoveCommand(Vector2 direction)
        {
            _direction = direction;
            _player = Game.Current.Player; // Get player reference
        }
        
        public void Execute()
        {
            if (CanExecute())
            {
                _player.Move(_direction);
            }
        }
        
        public void Undo()
        {
            _player.Move(-_direction);
        }
        
        public bool CanExecute()
        {
            return _player != null && _player.CanMove;
        }
    }
    
    // Jump command
    public class JumpCommand : IInputCommand
    {
        private readonly Player _player;
        
        public JumpCommand()
        {
            _player = Game.Current.Player;
        }
        
        public void Execute()
        {
            if (CanExecute())
            {
                _player.Jump();
            }
        }
        
        public void Undo()
        {
            // Jumping usually can't be undone
        }
        
        public bool CanExecute()
        {
            return _player != null && _player.IsGrounded;
        }
    }
    
    // UI navigation command
    public class NavigateCommand : IInputCommand
    {
        private readonly Direction _direction;
        private readonly UINavigator _navigator;
        
        public NavigateCommand(Direction direction)
        {
            _direction = direction;
            _navigator = Game.Current.UINavigator;
        }
        
        public void Execute()
        {
            _navigator.Navigate(_direction);
        }
        
        public void Undo() { }
        
        public bool CanExecute()
        {
            return _navigator != null;
        }
    }
    

    Context Management

    Context Stack

    Manage input contexts using a stack system:

    public class InputManager
    {
        private readonly IInputService _inputService;
        private readonly Dictionary<string, InputContext> _contexts;
        private readonly Stack<string> _contextStack;
        
        public InputManager(IInputService inputService)
        {
            _inputService = inputService;
            _contexts = new Dictionary<string, InputContext>();
            _contextStack = new Stack<string>();
        }
        
        public void RegisterContext(InputContext context)
        {
            _contexts[context.Name] = context;
        }
        
        public void PushContext(string contextName)
        {
            if (_contexts.ContainsKey(contextName))
            {
                _contextStack.Push(contextName);
            }
        }
        
        public void PopContext()
        {
            if (_contextStack.Count > 0)
            {
                _contextStack.Pop();
            }
        }
        
        public void ProcessInput()
        {
            var processedActions = new HashSet<string>();
            
            // Process contexts from top of stack (most recent) to bottom
            foreach (var contextName in _contextStack.Reverse())
            {
                var context = _contexts[contextName];
                
                foreach (var binding in context.Bindings)
                {
                    // Skip if action already processed by higher context
                    if (processedActions.Contains(binding.ActionName))
                        continue;
                    
                    if (IsBindingTriggered(binding))
                    {
                        binding.Command?.Execute();
                        processedActions.Add(binding.ActionName);
                        
                        // If context consumes input, don't process lower contexts
                        if (context.ConsumeInput)
                            break;
                    }
                }
            }
        }
        
        private bool IsBindingTriggered(InputBinding binding)
        {
            // Check if main key is pressed
            if (!_inputService.IsKeyPressed(binding.Key))
                return false;
            
            // Check modifier keys
            if (binding.ModifierKeys != null)
            {
                foreach (var modifierKey in binding.ModifierKeys)
                {
                    if (!_inputService.IsKeyDown(modifierKey))
                        return false;
                }
            }
            
            return true;
        }
    }
    

    Scene-Based Context Switching

    Automatically switch contexts based on current scene:

    public abstract class Scene
    {
        protected InputManager _inputManager;
        
        protected virtual void OnSceneActivated()
        {
            _inputManager = GetService<InputManager>();
            SetupInputContext();
        }
        
        protected virtual void OnSceneDeactivated()
        {
            _inputManager.PopContext();
        }
        
        protected abstract void SetupInputContext();
    }
    
    public class GameplayScene : Scene
    {
        protected override void SetupInputContext()
        {
            _inputManager.PushContext("Gameplay");
        }
    }
    
    public class MainMenuScene : Scene
    {
        protected override void SetupInputContext()
        {
            _inputManager.PushContext("Menu");
        }
    }
    
    public class PauseMenuScene : Scene
    {
        protected override void SetupInputContext()
        {
            // Push pause context on top of existing contexts
            _inputManager.PushContext("PauseMenu");
        }
        
        protected override void OnSceneDeactivated()
        {
            // Pop pause context to return to gameplay
            _inputManager.PopContext();
        }
    }
    

    Advanced Features

    Input Buffering

    Buffer inputs for precise timing in games like fighting games:

    public class InputBuffer
    {
        private readonly Queue<InputEvent> _buffer;
        private readonly int _bufferSize;
        private readonly float _bufferTime;
        
        public InputBuffer(int bufferSize = 10, float bufferTime = 0.2f)
        {
            _buffer = new Queue<InputEvent>(bufferSize);
            _bufferSize = bufferSize;
            _bufferTime = bufferTime;
        }
        
        public void AddInput(InputKey key, float timestamp)
        {
            var inputEvent = new InputEvent
            {
                Key = key,
                Timestamp = timestamp
            };
            
            _buffer.Enqueue(inputEvent);
            
            // Remove old events
            while (_buffer.Count > _bufferSize)
            {
                _buffer.Dequeue();
            }
            
            CleanExpiredEvents(timestamp);
        }
        
        public bool HasRecentInput(InputKey key, float currentTime, float timeWindow = -1f)
        {
            if (timeWindow < 0)
                timeWindow = _bufferTime;
            
            return _buffer.Any(e => e.Key == key && 
                                   (currentTime - e.Timestamp) <= timeWindow);
        }
        
        public bool HasInputSequence(InputKey[] sequence, float currentTime, float timeWindow = -1f)
        {
            if (timeWindow < 0)
                timeWindow = _bufferTime;
            
            var relevantEvents = _buffer
                .Where(e => (currentTime - e.Timestamp) <= timeWindow)
                .OrderBy(e => e.Timestamp)
                .ToList();
            
            return ContainsSequence(relevantEvents, sequence);
        }
        
        private void CleanExpiredEvents(float currentTime)
        {
            while (_buffer.Count > 0 && 
                   (currentTime - _buffer.Peek().Timestamp) > _bufferTime)
            {
                _buffer.Dequeue();
            }
        }
    }
    
    public struct InputEvent
    {
        public InputKey Key;
        public float Timestamp;
    }
    

    Combo System

    Implement combo detection for complex input sequences:

    public class ComboSystem
    {
        private readonly InputBuffer _inputBuffer;
        private readonly Dictionary<string, ComboDefinition> _combos;
        
        public ComboSystem()
        {
            _inputBuffer = new InputBuffer();
            _combos = new Dictionary<string, ComboDefinition>();
            
            SetupCombos();
        }
        
        private void SetupCombos()
        {
            // Fireball combo: Down, Down-Forward, Forward + Punch
            _combos["Fireball"] = new ComboDefinition
            {
                Name = "Fireball",
                Sequence = new[] { InputKey.S, InputKey.S | InputKey.D, InputKey.D, InputKey.J },
                TimeWindow = 1.0f,
                Command = new FireballCommand()
            };
            
            // Dragon Punch: Forward, Down, Down-Forward + Punch
            _combos["DragonPunch"] = new ComboDefinition
            {
                Name = "DragonPunch",
                Sequence = new[] { InputKey.D, InputKey.S, InputKey.S | InputKey.D, InputKey.J },
                TimeWindow = 0.8f,
                Command = new DragonPunchCommand()
            };
        }
        
        public void ProcessInput(InputKey key, float timestamp)
        {
            _inputBuffer.AddInput(key, timestamp);
            
            // Check for combo matches
            foreach (var combo in _combos.Values)
            {
                if (_inputBuffer.HasInputSequence(combo.Sequence, timestamp, combo.TimeWindow))
                {
                    combo.Command.Execute();
                    OnComboExecuted?.Invoke(combo.Name);
                }
            }
        }
        
        public event Action<string> OnComboExecuted;
    }
    
    public class ComboDefinition
    {
        public string Name { get; set; }
        public InputKey[] Sequence { get; set; }
        public float TimeWindow { get; set; }
        public IInputCommand Command { get; set; }
    }
    

    Input Remapping

    Allow players to customize their key bindings:

    public class InputRemapper
    {
        private readonly Dictionary<string, InputContext> _contexts;
        private readonly ISettingsService _settings;
        
        public InputRemapper(ISettingsService settings)
        {
            _settings = settings;
            _contexts = new Dictionary<string, InputContext>();
            
            LoadBindings();
        }
        
        public void RemapBinding(string contextName, string actionName, InputKey newKey)
        {
            if (!_contexts.ContainsKey(contextName))
                return;
            
            var context = _contexts[contextName];
            var binding = context.Bindings.FirstOrDefault(b => b.ActionName == actionName);
            
            if (binding != null)
            {
                binding.Key = newKey;
                SaveBinding(contextName, actionName, newKey);
                
                OnBindingChanged?.Invoke(contextName, actionName, newKey);
            }
        }
        
        public InputKey GetBinding(string contextName, string actionName)
        {
            if (!_contexts.ContainsKey(contextName))
                return InputKey.Unknown;
            
            var context = _contexts[contextName];
            var binding = context.Bindings.FirstOrDefault(b => b.ActionName == actionName);
            
            return binding?.Key ?? InputKey.Unknown;
        }
        
        public void ResetToDefaults(string contextName)
        {
            if (!_contexts.ContainsKey(contextName))
                return;
            
            var context = _contexts[contextName];
            foreach (var binding in context.Bindings)
            {
                var defaultKey = GetDefaultBinding(contextName, binding.ActionName);
                binding.Key = defaultKey;
                SaveBinding(contextName, binding.ActionName, defaultKey);
            }
            
            OnContextReset?.Invoke(contextName);
        }
        
        private void SaveBinding(string contextName, string actionName, InputKey key)
        {
            var settingKey = $"Input.{contextName}.{actionName}";
            _settings.SetSetting(settingKey, key.ToString());
        }
        
        private InputKey LoadBinding(string contextName, string actionName, InputKey defaultKey)
        {
            var settingKey = $"Input.{contextName}.{actionName}";
            var keyString = _settings.GetSetting(settingKey, defaultKey.ToString());
            
            return Enum.TryParse<InputKey>(keyString, out var key) ? key : defaultKey;
        }
        
        public event Action<string, string, InputKey> OnBindingChanged;
        public event Action<string> OnContextReset;
    }
    

    Input Recording and Playback

    Record and replay input for testing and demos:

    public class InputRecorder
    {
        private readonly List<RecordedInput> _recording;
        private readonly List<RecordedInput> _playback;
        private bool _isRecording;
        private bool _isPlayingBack;
        private float _startTime;
        private int _playbackIndex;
        
        public InputRecorder()
        {
            _recording = new List<RecordedInput>();
            _playback = new List<RecordedInput>();
        }
        
        public void StartRecording()
        {
            _recording.Clear();
            _isRecording = true;
            _startTime = Time.CurrentTime;
        }
        
        public void StopRecording()
        {
            _isRecording = false;
        }
        
        public void RecordInput(InputKey key, bool isPressed, float timestamp)
        {
            if (!_isRecording)
                return;
            
            _recording.Add(new RecordedInput
            {
                Key = key,
                IsPressed = isPressed,
                Timestamp = timestamp - _startTime
            });
        }
        
        public void StartPlayback()
        {
            _playback.Clear();
            _playback.AddRange(_recording);
            _isPlayingBack = true;
            _playbackIndex = 0;
            _startTime = Time.CurrentTime;
        }
        
        public void StopPlayback()
        {
            _isPlayingBack = false;
            _playbackIndex = 0;
        }
        
        public void UpdatePlayback(float currentTime, Action<InputKey, bool> inputCallback)
        {
            if (!_isPlayingBack || _playbackIndex >= _playback.Count)
                return;
            
            var relativeTime = currentTime - _startTime;
            
            while (_playbackIndex < _playback.Count && 
                   _playback[_playbackIndex].Timestamp <= relativeTime)
            {
                var input = _playback[_playbackIndex];
                inputCallback?.Invoke(input.Key, input.IsPressed);
                _playbackIndex++;
            }
            
            if (_playbackIndex >= _playback.Count)
            {
                StopPlayback();
            }
        }
        
        public void SaveRecording(string filename)
        {
            var json = JsonSerializer.Serialize(_recording, new JsonSerializerOptions
            {
                WriteIndented = true
            });
            File.WriteAllText(filename, json);
        }
        
        public void LoadRecording(string filename)
        {
            if (File.Exists(filename))
            {
                var json = File.ReadAllText(filename);
                _recording.Clear();
                _recording.AddRange(JsonSerializer.Deserialize<List<RecordedInput>>(json));
            }
        }
    }
    
    public struct RecordedInput
    {
        public InputKey Key { get; set; }
        public bool IsPressed { get; set; }
        public float Timestamp { get; set; }
    }
    

    Gamepad Support

    Gamepad Input Handling

    public class GamepadManager
    {
        private readonly IInputService _inputService;
        private readonly Dictionary<int, GamepadState> _previousStates;
        
        public GamepadManager(IInputService inputService)
        {
            _inputService = inputService;
            _previousStates = new Dictionary<int, GamepadState>();
        }
        
        public void Update()
        {
            for (int i = 0; i < 4; i++) // Support up to 4 gamepads
            {
                var currentState = _inputService.GetGamepadState(i);
                var previousState = _previousStates.GetValueOrDefault(i);
                
                ProcessGamepadInput(i, currentState, previousState);
                _previousStates[i] = currentState;
            }
        }
        
        private void ProcessGamepadInput(int playerIndex, GamepadState current, GamepadState previous)
        {
            // Check button presses
            foreach (var button in Enum.GetValues<GamepadButton>())
            {
                if (IsButtonPressed(current, previous, button))
                {
                    OnButtonPressed?.Invoke(playerIndex, button);
                }
                
                if (IsButtonReleased(current, previous, button))
                {
                    OnButtonReleased?.Invoke(playerIndex, button);
                }
            }
            
            // Check analog stick movement
            if (Vector2.Distance(current.LeftStick, previous.LeftStick) > 0.1f)
            {
                OnLeftStickMoved?.Invoke(playerIndex, current.LeftStick);
            }
            
            if (Vector2.Distance(current.RightStick, previous.RightStick) > 0.1f)
            {
                OnRightStickMoved?.Invoke(playerIndex, current.RightStick);
            }
            
            // Check trigger changes
            if (Math.Abs(current.LeftTrigger - previous.LeftTrigger) > 0.05f)
            {
                OnLeftTriggerMoved?.Invoke(playerIndex, current.LeftTrigger);
            }
            
            if (Math.Abs(current.RightTrigger - previous.RightTrigger) > 0.05f)
            {
                OnRightTriggerMoved?.Invoke(playerIndex, current.RightTrigger);
            }
        }
        
        private bool IsButtonPressed(GamepadState current, GamepadState previous, GamepadButton button)
        {
            return current.IsButtonPressed(button) && !previous.IsButtonPressed(button);
        }
        
        private bool IsButtonReleased(GamepadState current, GamepadState previous, GamepadButton button)
        {
            return !current.IsButtonPressed(button) && previous.IsButtonPressed(button);
        }
        
        public event Action<int, GamepadButton> OnButtonPressed;
        public event Action<int, GamepadButton> OnButtonReleased;
        public event Action<int, Vector2> OnLeftStickMoved;
        public event Action<int, Vector2> OnRightStickMoved;
        public event Action<int, float> OnLeftTriggerMoved;
        public event Action<int, float> OnRightTriggerMoved;
    }
    

    Best Practices

    Input Responsiveness

    1. Update frequency: Process input every frame for responsiveness
    2. Input buffering: Use buffering for precise timing requirements
    3. Dead zones: Implement dead zones for analog inputs to prevent drift
    4. Input validation: Validate input commands before execution

    Context Management

    1. Clear hierarchy: Design clear input context hierarchy
    2. Context cleanup: Always pop contexts when scenes change
    3. Priority system: Use priority for overlapping contexts
    4. Input consumption: Use input consumption to prevent unwanted actions

    Customization

    1. Key remapping: Allow players to customize controls
    2. Multiple devices: Support both keyboard and gamepad simultaneously
    3. Accessibility: Provide options for different input methods
    4. Save preferences: Persist custom key bindings

    Example: Complete Input Setup

    public class GameInputManager
    {
        private readonly IInputService _inputService;
        private readonly InputManager _inputManager;
        private readonly InputRemapper _remapper;
        private readonly ComboSystem _comboSystem;
        
        public GameInputManager()
        {
            _inputService = GetService<IInputService>();
            _inputManager = new InputManager(_inputService);
            _remapper = new InputRemapper(GetService<ISettingsService>());
            _comboSystem = new ComboSystem();
            
            SetupDefaultContexts();
            LoadCustomBindings();
        }
        
        private void SetupDefaultContexts()
        {
            // Create and register all input contexts
            var contexts = new[]
            {
                CreateGameplayContext(),
                CreateMenuContext(),
                CreatePauseContext(),
                CreateSettingsContext()
            };
            
            foreach (var context in contexts)
            {
                _inputManager.RegisterContext(context);
            }
        }
        
        private InputContext CreateGameplayContext()
        {
            return new InputContext
            {
                Name = "Gameplay",
                ConsumeInput = true,
                Bindings = new List<InputBinding>
                {
                    new InputBinding { ActionName = "MoveLeft", Key = InputKey.A, Command = new MoveCommand(Vector2.Left) },
                    new InputBinding { ActionName = "MoveRight", Key = InputKey.D, Command = new MoveCommand(Vector2.Right) },
                    new InputBinding { ActionName = "Jump", Key = InputKey.Space, Command = new JumpCommand() },
                    new InputBinding { ActionName = "Attack", Key = InputKey.J, Command = new AttackCommand() },
                    new InputBinding { ActionName = "Pause", Key = InputKey.Escape, Command = new PauseCommand() }
                }
            };
        }
        
        public void Update(float deltaTime)
        {
            _inputService.Update();
            _inputManager.ProcessInput();
            _comboSystem.ProcessInput(GetCurrentInput(), Time.CurrentTime);
        }
        
        public void SwitchToContext(string contextName)
        {
            _inputManager.ClearContextStack();
            _inputManager.PushContext(contextName);
        }
        
        public void PushContext(string contextName)
        {
            _inputManager.PushContext(contextName);
        }
        
        public void PopContext()
        {
            _inputManager.PopContext();
        }
    }\n```
    
    The input system provides a flexible and powerful foundation for handling all player interactions in your Popcorning Guinea Pig games.
    
    • Edit this page
    In this article
    Back to top © 2024 Popcorning Guinea Pig Game Engine. A custom C# game engine for indie developers.