Day 15: Input Mapping
Welcome back, architects and engineers. Today, we're diving into a critical, yet often underestimated, component of any real-time system: Input Mapping. You might think, "It's just reading a keyboard, right?" If only it were that simple in a production-grade engine. The truth is, how you design your input system can make or break your engine's flexibility, maintainability, and even its ability to adapt to future hardware.
In our journey to build a robust C++ rendering engine, we've moved from setting up the foundational infrastructure to mastering camera rotations with quaternions. Now, it's time to connect the user's intent—a key press, a mouse click, a gamepad trigger—to meaningful actions within our virtual world. This isn't just about reacting; it's about architecting a system that understands and translates user interaction into precise, predictable engine behavior.
The Problem with Naive Input Handling: Why Polling is a Pitfall
Let's start with the common trap: direct polling. Many beginners simply if (key_is_pressed(KEY_W)) { camera.moveForward(); } inside their game loop. This works for a simple demo, but it’s a non-starter for anything serious.
Here’s why direct polling is a problem, and why big tech avoids it:
Tight Coupling: Your camera object directly knows about
KEY_W. What if you want to remap 'W' to 'Up Arrow'? You're digging into your camera code. What if you add gamepad support? Moreifstatements, more coupling. This is brittle.Lack of Abstraction:
KEY_Wis a physical input.MoveForwardis a logical action. Mixing these blurs the line, making it hard to reason about your system.Inflexible Remapping: Want to let users customize controls at runtime? Good luck with a thousand
ifstatements scattered everywhere.Platform Dependency: Different OSes or libraries (GLFW, SDL, DirectInput, XInput) have different key codes. Your game logic becomes tied to your chosen input API.
Order of Operations & Prioritization: What if UI input should block game input? Or a contextual action (like opening a door) should override general movement? Direct polling makes prioritization a nightmare.
This is where a well-designed Input Mapping System comes in. It acts as a sophisticated translator, transforming raw, low-level physical input events into high-level, logical game actions.
Core Concepts: Decoupling and the Command Pattern
At its heart, a robust input system embraces decoupling and often leverages the Command Pattern.
1. Abstraction Layer: From Physical to Logical
The first step is to introduce an abstraction layer. Instead of KEY_W, we think ACTION_MOVE_FORWARD. Instead of MOUSE_BUTTON_LEFT, we think ACTION_FIRE_PRIMARY. This is our logical action layer.
The input system's job is to:
Receive raw input events (e.g., GLFW callbacks for key presses, mouse movements).
Map these raw events to one or more logical actions.
2. The Command Pattern: Decoupling Action from Receiver
Once we have a logical action, how do we execute it without tightly coupling our input system to every possible game object? Enter the Command Pattern.
Imagine an interface, ICommand, with a single execute() method.
MoveForwardCommandimplementsICommandand, when executed, callscamera.moveForward().FireWeaponCommandimplementsICommandand callsplayer.weapon.fire().
Our input system doesn't know what a MoveForwardCommand does, or who (camera or player) will receive the action. It just knows it has a Command object to execute. This is powerful.
Control Flow:
Keep going with the full lesson
The rest of this lesson — plus its implementation guide, runnable scripts and source code — is included with the course or an All Access plan. Create a free account to start the free lessons in every course.
Already a member? Sign in