重构键盘输入为 IKeyboardState 接口,消除 App 层对 SDL 的直接依赖

重构键盘输入为 IKeyboardState 接口,消除 App 层对 SDL 的直接依赖

- 新增 IKeyboardState 抽象接口及 SdlKeyboardState/EvdevKeyboardState 实现
- PlayerController 改用 IKeyboardState 替代直接调用 SDL_GetKeyboardState
- 移除 Camera2D.h 中对 Core 私有头文件 Camera.h 的引用
- 将 Timer.h 从 Core/Core 移至 Core/Platform,符合架构边界规范
- 键盘输入优先级调整为高于指针输入
This commit is contained in:
SepComet 2026-06-10 15:22:30 +08:00
parent fb216de107
commit e00fc1799d
15 changed files with 164 additions and 28 deletions

View File

@ -21,6 +21,7 @@ set(CORE_SOURCES
src/Core/Platform/AlsaAudioInput.cpp src/Core/Platform/AlsaAudioInput.cpp
src/Core/Platform/AlsaAudioOutput.cpp src/Core/Platform/AlsaAudioOutput.cpp
src/Core/Platform/EvdevButtonInput.cpp src/Core/Platform/EvdevButtonInput.cpp
src/Core/Platform/EvdevKeyboardState.cpp
src/Core/Platform/EvdevTouchInput.cpp src/Core/Platform/EvdevTouchInput.cpp
src/Core/Rasterizer/Rasterizer.cpp src/Core/Rasterizer/Rasterizer.cpp
src/Core/Rasterizer/TriangleRasterizer.cpp src/Core/Rasterizer/TriangleRasterizer.cpp
@ -39,6 +40,7 @@ else()
src/Core/Platform/SdlAudioInput.cpp src/Core/Platform/SdlAudioInput.cpp
src/Core/Platform/SdlAudioOutput.cpp src/Core/Platform/SdlAudioOutput.cpp
src/Core/Platform/SdlKeyboardButtonInput.cpp src/Core/Platform/SdlKeyboardButtonInput.cpp
src/Core/Platform/SdlKeyboardState.cpp
src/Core/Platform/SdlPointerInput.cpp src/Core/Platform/SdlPointerInput.cpp
src/Core/Platform/SdlPhotoSensor.cpp src/Core/Platform/SdlPhotoSensor.cpp
) )

View File

@ -0,0 +1,64 @@
# Architecture Boundaries
This document defines the boundary between **Core** (engine library) and **Apps** (application layer) in the IMX6U-Game project.
## Public API (App may include)
App code should only include headers from these Core subdirectories:
| Subdirectory | Contents |
|---|---|
| `Core/RenderData/` | Color, Image, Sprite, Tilemap, BoundingBox, BitmapFont |
| `Core/Math/` | Vector2, Vector3, Vector4, Matrix4x4, MathUtil |
| `Core/Platform/` | Display, ButtonInput, IKeyboardState, PointerInput, AudioInput, AudioOutput, IPhotoSensor, TimeSource, Timer, DefaultHardware |
| `Core/Draw2D/` | DrawContext |
## Private API (App must not include)
These subdirectories are Core internals. App code must not include headers from them or reference their types directly.
| Subdirectory | Contents |
|---|---|
| `Core/Core/` | FrameBuffer, DepthBuffer, Renderer |
| `Core/Scene/` | Camera, Mesh, Model, Transform, Vertex |
| `Core/Rasterizer/` | Rasterizer, TriangleRasterizer |
| `Core/Shading/` | BlinnPhongShader, ShaderTypes |
## Rules
1. **Dependency direction**: Core must never depend on Apps. Apps depend on Core through the public API only.
2. **Platform isolation**: No `#include <SDL.h>`, `#include <alsa*>`, or `#include <linux/input.h>` outside `src/Core/Platform/`. App code accesses hardware through Platform interfaces (`IButtonInput`, `IKeyboardState`, `IPointerInput`, etc.).
3. **Interface usage**: App code should use abstract interfaces from Platform, not concrete implementation classes. Use `DefaultHardware.h` typedefs when you need a concrete type.
4. **No Core private types in App headers**: App headers must not forward-declare or reference types from private Core subdirectories.
## Violation Checklist
Use this during code review:
- [ ] No `#include <SDL.h>` outside `src/Core/Platform/`
- [ ] No `#include <alsa*>` outside `src/Core/Platform/`
- [ ] No `#include <linux/input.h>` outside `src/Core/Platform/`
- [ ] App code includes only from public subdirectories (RenderData, Math, Platform, Draw2D)
- [ ] No App header forward-declares or references Core private types (Scene, Rasterizer, Shading, Core/Core)
- [ ] Core source files contain no references to App namespaces or types
## Input Interface Architecture
The Platform layer provides separate interfaces for different input modalities:
| Interface | Purpose | Backend (Desktop) | Backend (Embedded) |
|---|---|---|---|
| `IButtonInput` | Single physical button | `SdlKeyboardButtonInput` | `EvdevButtonInput` |
| `IKeyboardState` | Multi-key keyboard state | `SdlKeyboardState` | `EvdevKeyboardState` (stub) |
| `IPointerInput` | Mouse/touch pointer | `SdlPointerInput` | `EvdevTouchInput` |
| `IPhotoSensor` | Light sensor | `SdlPhotoSensor` | `LinuxPhotoSensor` |
App code accesses these through `DefaultHardware.h` typedefs (`DefaultButtonInput`, `DefaultKeyboardState`, etc.).
### Input Priority
When multiple input sources are active simultaneously, keyboard input takes priority over pointer input for movement direction. This is an intentional design choice for desktop usability.
### Future Consolidation
When a 4th input modality is added (e.g., gamepad), consider consolidating `IButtonInput*` + `IKeyboardState*` + `IPointerInput*` into a single `IInputState` interface to prevent parameter accumulation in application classes.

View File

@ -2,7 +2,6 @@
#include <cstdint> #include <cstdint>
#include "Vector2.h" #include "Vector2.h"
#include "Camera.h"
#include "Level.h" #include "Level.h"
namespace LightGame namespace LightGame

View File

@ -2,6 +2,7 @@
#include "DrawContext.h" #include "DrawContext.h"
#include "BitmapFont.h" #include "BitmapFont.h"
#include "ButtonInput.h" #include "ButtonInput.h"
#include "IKeyboardState.h"
#include "PointerInput.h" #include "PointerInput.h"
#include "LevelData.h" #include "LevelData.h"
#include "LevelLoader.h" #include "LevelLoader.h"
@ -25,11 +26,13 @@ namespace LightGame
int32_t screen_width, int32_t screen_width,
int32_t screen_height, int32_t screen_height,
Platform::IButtonInput* button_input, Platform::IButtonInput* button_input,
Platform::IKeyboardState* keyboard_input,
Platform::IPointerInput* pointer_input, Platform::IPointerInput* pointer_input,
Platform::IPhotoSensor* photo_sensor) Platform::IPhotoSensor* photo_sensor)
: screen_width_(screen_width), : screen_width_(screen_width),
screen_height_(screen_height), screen_height_(screen_height),
button_input_(button_input), button_input_(button_input),
keyboard_input_(keyboard_input),
pointer_input_(pointer_input), pointer_input_(pointer_input),
photo_sensor_(photo_sensor), photo_sensor_(photo_sensor),
level_(), level_(),
@ -146,7 +149,7 @@ namespace LightGame
GameObject* player = level_.get_object(player_id_); GameObject* player = level_.get_object(player_id_);
if (player) if (player)
{ {
player_controller_.update(*player, level_, physics_, button_input_, pointer_input_, dt_ms); player_controller_.update(*player, level_, physics_, button_input_, keyboard_input_, pointer_input_, dt_ms);
camera_.follow(player->position); camera_.follow(player->position);
if (player_controller_.is_dead()) if (player_controller_.is_dead())

View File

@ -23,6 +23,7 @@ namespace RenderData
namespace Platform namespace Platform
{ {
class IButtonInput; class IButtonInput;
class IKeyboardState;
class IPointerInput; class IPointerInput;
} }
@ -35,8 +36,10 @@ namespace LightGame
int32_t screen_height_; int32_t screen_height_;
Platform::IButtonInput* button_input_; Platform::IButtonInput* button_input_;
Platform::IKeyboardState* keyboard_input_;
Platform::IPointerInput* pointer_input_; Platform::IPointerInput* pointer_input_;
Platform::IPhotoSensor* photo_sensor_; Platform::IPhotoSensor* photo_sensor_;
// TODO: consolidate input interfaces into IInputState when a 4th modality arrives
Level level_; Level level_;
Camera2D camera_; Camera2D camera_;
@ -56,6 +59,7 @@ namespace LightGame
int32_t screen_width, int32_t screen_width,
int32_t screen_height, int32_t screen_height,
Platform::IButtonInput* button_input, Platform::IButtonInput* button_input,
Platform::IKeyboardState* keyboard_input,
Platform::IPointerInput* pointer_input, Platform::IPointerInput* pointer_input,
Platform::IPhotoSensor* photo_sensor); Platform::IPhotoSensor* photo_sensor);

View File

@ -1,12 +1,9 @@
#include "PlayerController.h" #include "PlayerController.h"
#include "ButtonInput.h" #include "ButtonInput.h"
#include "IKeyboardState.h"
#include "PointerInput.h" #include "PointerInput.h"
#include <algorithm> #include <algorithm>
#ifndef USE_FRAMEBUFFER
#include <SDL.h>
#endif
namespace LightGame namespace LightGame
{ {
PlayerController::PlayerController() PlayerController::PlayerController()
@ -48,6 +45,7 @@ namespace LightGame
void PlayerController::update(GameObject& obj, Level& level, Physics2D& physics, void PlayerController::update(GameObject& obj, Level& level, Physics2D& physics,
const Platform::IButtonInput* button, const Platform::IButtonInput* button,
const Platform::IKeyboardState* keyboard,
const Platform::IPointerInput* pointer, const Platform::IPointerInput* pointer,
uint32_t dt_ms) uint32_t dt_ms)
{ {
@ -61,7 +59,7 @@ namespace LightGame
return; return;
} }
update_input(button, pointer); update_input(button, keyboard, pointer);
update_movement(obj, dt_ms); update_movement(obj, dt_ms);
physics.apply_gravity(obj, dt_ms); physics.apply_gravity(obj, dt_ms);
@ -114,6 +112,7 @@ namespace LightGame
} }
void PlayerController::update_input(const Platform::IButtonInput* button, void PlayerController::update_input(const Platform::IButtonInput* button,
const Platform::IKeyboardState* keyboard,
const Platform::IPointerInput* pointer) const Platform::IPointerInput* pointer)
{ {
move_dir_ = 0; move_dir_ = 0;
@ -147,29 +146,16 @@ namespace LightGame
} }
} }
if (move_dir_ == 0) if (keyboard)
{
#ifndef USE_FRAMEBUFFER
SDL_PumpEvents();
const Uint8* keys = SDL_GetKeyboardState(nullptr);
if (keys)
{
if (keys[SDL_SCANCODE_LEFT] || keys[SDL_SCANCODE_A])
{ {
if (keyboard->is_key_down(Platform::KEY_LEFT) || keyboard->is_key_down(Platform::KEY_A))
move_dir_ = -1; move_dir_ = -1;
} if (keyboard->is_key_down(Platform::KEY_RIGHT) || keyboard->is_key_down(Platform::KEY_D))
if (keys[SDL_SCANCODE_RIGHT] || keys[SDL_SCANCODE_D])
{
move_dir_ = 1; move_dir_ = 1;
} if (keyboard->is_key_down(Platform::KEY_SPACE) || keyboard->is_key_down(Platform::KEY_UP) || keyboard->is_key_down(Platform::KEY_W))
if (keys[SDL_SCANCODE_SPACE] || keys[SDL_SCANCODE_UP] || keys[SDL_SCANCODE_W])
{
jump_held_ = true; jump_held_ = true;
} }
} }
#endif
}
}
void PlayerController::update_movement(GameObject& obj, uint32_t dt_ms) void PlayerController::update_movement(GameObject& obj, uint32_t dt_ms)
{ {

View File

@ -7,6 +7,7 @@
namespace Platform namespace Platform
{ {
class IButtonInput; class IButtonInput;
class IKeyboardState;
class IPointerInput; class IPointerInput;
} }
@ -72,6 +73,7 @@ namespace LightGame
void update(GameObject& obj, Level& level, Physics2D& physics, void update(GameObject& obj, Level& level, Physics2D& physics,
const Platform::IButtonInput* button, const Platform::IButtonInput* button,
const Platform::IKeyboardState* keyboard,
const Platform::IPointerInput* pointer, const Platform::IPointerInput* pointer,
uint32_t dt_ms); uint32_t dt_ms);
@ -79,7 +81,7 @@ namespace LightGame
bool is_dead() const { return state_ == PlayerState::Dead; } bool is_dead() const { return state_ == PlayerState::Dead; }
private: private:
void update_input(const Platform::IButtonInput* button, const Platform::IPointerInput* pointer); void update_input(const Platform::IButtonInput* button, const Platform::IKeyboardState* keyboard, const Platform::IPointerInput* pointer);
void update_movement(GameObject& obj, uint32_t dt_ms); void update_movement(GameObject& obj, uint32_t dt_ms);
void update_animation(GameObject& obj, uint32_t dt_ms); void update_animation(GameObject& obj, uint32_t dt_ms);
void check_death(GameObject& obj, Level& level); void check_death(GameObject& obj, Level& level);

View File

@ -53,6 +53,7 @@ int main(int argc, char* argv[])
} }
Platform::DefaultButtonInput buttonInput; Platform::DefaultButtonInput buttonInput;
Platform::DefaultKeyboardState keyboardState;
Platform::DefaultPointerInput pointerInput; Platform::DefaultPointerInput pointerInput;
Platform::DefaultPhotoSensor photoSensor; Platform::DefaultPhotoSensor photoSensor;
@ -86,6 +87,7 @@ int main(int argc, char* argv[])
ScreenWidth, ScreenWidth,
ScreenHeight, ScreenHeight,
&buttonInput, &buttonInput,
&keyboardState,
&pointerInput, &pointerInput,
&photoSensor); &photoSensor);

View File

@ -4,12 +4,14 @@
#include "AlsaAudioInput.h" #include "AlsaAudioInput.h"
#include "AlsaAudioOutput.h" #include "AlsaAudioOutput.h"
#include "EvdevButtonInput.h" #include "EvdevButtonInput.h"
#include "EvdevKeyboardState.h"
#include "EvdevTouchInput.h" #include "EvdevTouchInput.h"
#include "LinuxPhotoSensor.h" #include "LinuxPhotoSensor.h"
#else #else
#include "SdlAudioInput.h" #include "SdlAudioInput.h"
#include "SdlAudioOutput.h" #include "SdlAudioOutput.h"
#include "SdlKeyboardButtonInput.h" #include "SdlKeyboardButtonInput.h"
#include "SdlKeyboardState.h"
#include "SdlPointerInput.h" #include "SdlPointerInput.h"
#include "SdlPhotoSensor.h" #include "SdlPhotoSensor.h"
#endif #endif
@ -20,12 +22,14 @@ namespace Platform
typedef AlsaAudioInput DefaultAudioInput; typedef AlsaAudioInput DefaultAudioInput;
typedef AlsaAudioOutput DefaultAudioOutput; typedef AlsaAudioOutput DefaultAudioOutput;
typedef EvdevButtonInput DefaultButtonInput; typedef EvdevButtonInput DefaultButtonInput;
typedef EvdevKeyboardState DefaultKeyboardState;
typedef EvdevTouchInput DefaultPointerInput; typedef EvdevTouchInput DefaultPointerInput;
typedef LinuxPhotoSensor DefaultPhotoSensor; typedef LinuxPhotoSensor DefaultPhotoSensor;
#else #else
typedef SdlAudioInput DefaultAudioInput; typedef SdlAudioInput DefaultAudioInput;
typedef SdlAudioOutput DefaultAudioOutput; typedef SdlAudioOutput DefaultAudioOutput;
typedef SdlKeyboardButtonInput DefaultButtonInput; typedef SdlKeyboardButtonInput DefaultButtonInput;
typedef SdlKeyboardState DefaultKeyboardState;
typedef SdlPointerInput DefaultPointerInput; typedef SdlPointerInput DefaultPointerInput;
typedef SdlPhotoSensor DefaultPhotoSensor; typedef SdlPhotoSensor DefaultPhotoSensor;
#endif #endif

View File

@ -0,0 +1,10 @@
#include "EvdevKeyboardState.h"
namespace Platform
{
bool EvdevKeyboardState::is_key_down(int /*scancode*/) const
{
// Embedded target uses physical buttons via IButtonInput, not keyboard.
return false;
}
}

View File

@ -0,0 +1,13 @@
#pragma once
#include "IKeyboardState.h"
namespace Platform
{
// Embedded target uses physical buttons via IButtonInput, not keyboard.
class EvdevKeyboardState : public IKeyboardState
{
public:
bool is_key_down(int scancode) const override;
};
}

View File

@ -0,0 +1,22 @@
#pragma once
namespace Platform
{
// Key constants matching SDL scancode values.
// App-layer code uses these instead of including <SDL.h>.
constexpr int KEY_A = 4;
constexpr int KEY_D = 7;
constexpr int KEY_W = 26;
constexpr int KEY_SPACE = 44;
constexpr int KEY_LEFT = 80;
constexpr int KEY_RIGHT = 79;
constexpr int KEY_UP = 82;
constexpr int KEY_DOWN = 81;
class IKeyboardState
{
public:
virtual ~IKeyboardState() {}
virtual bool is_key_down(int scancode) const = 0;
};
}

View File

@ -0,0 +1,12 @@
#include "SdlKeyboardState.h"
#include <SDL.h>
namespace Platform
{
bool SdlKeyboardState::is_key_down(int scancode) const
{
// Relies on SdlKeyboardButtonInput::update() having called SDL_PumpEvents() in the same frame.
const Uint8* state = SDL_GetKeyboardState(nullptr);
return state != nullptr && state[scancode] != 0;
}
}

View File

@ -0,0 +1,13 @@
#pragma once
#include "IKeyboardState.h"
namespace Platform
{
class SdlKeyboardState : public IKeyboardState
{
public:
// Must be called after SdlKeyboardButtonInput::update() or equivalent SDL_PumpEvents() call.
bool is_key_down(int scancode) const override;
};
}