Files
esengine/packages/engine-core/src/Input/InputSystem.ts
YHH 823e0c1d94 feat(engine-core): 添加统一输入系统 (#282)
* perf(core): 优化 EntitySystem 迭代性能,添加 CommandBuffer 延迟命令

ReactiveQuery 快照优化:
- 添加快照机制,避免每帧拷贝数组
- 只在实体列表变化时创建新快照
- 静态场景下多个系统共享同一快照

CommandBuffer 延迟命令系统:
- 支持延迟添加/移除组件、销毁实体、设置实体激活状态
- 每个系统拥有独立的 commands 属性
- 命令在帧末统一执行,避免迭代过程中修改实体列表

Scene 更新:
- 在 lateUpdate 后自动刷新所有系统的命令缓冲区

文档:
- 更新系统文档,添加 CommandBuffer 使用说明

* fix(ci): upgrade first-interaction action to v1.3.0

Fix Docker build failure in welcome workflow.

* fix(ci): upgrade pnpm/action-setup to v4 and fix unused import

- Upgrade pnpm/action-setup@v2 to v4 in all workflow files
- Remove unused CommandType import in CommandBuffer.test.ts

* fix(ci): remove duplicate pnpm version specification

* feat(engine-core): 添加统一输入系统

添加完整的输入系统,支持平台抽象:

- IPlatformInputSubsystem: 扩展接口支持键盘/鼠标/滚轮事件
- WebInputSubsystem: 浏览器实现,支持事件绑定/解绑
- InputManager: 全局输入状态管理器(键盘、鼠标、触摸)
- InputSystem: ECS 系统,连接平台事件到 InputManager
- GameRuntime 集成: 自动创建 InputSystem 并绑定平台子系统

使用方式:
```typescript
import { Input, MouseButton } from '@esengine/engine-core';

if (Input.isKeyDown('KeyW')) { /* 移动 */ }
if (Input.isKeyJustPressed('Space')) { /* 跳跃 */ }
if (Input.isMouseButtonDown(MouseButton.Left)) { /* 射击 */ }
```

* fix(runtime-core): 添加缺失的 platform-common 依赖

* fix(runtime-core): 移除 platform-web 依赖避免循环依赖

* fix(runtime-core): 使用工厂函数注入 InputSubsystem 避免循环依赖

- BrowserPlatformAdapter 通过 inputSubsystemFactory 配置接收输入子系统
- 在 IPlatformInputSubsystem 接口添加可选的 dispose 方法
- 移除对 @esengine/platform-web 的直接依赖
2025-12-05 18:15:50 +08:00

265 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 输入系统 - 将平台输入事件连接到 InputManager
* Input System - Connects platform input events to InputManager
*
* 在 ECS 更新循环中运行,负责:
* 1. 在帧开始时已经由事件驱动更新了 InputManager
* 2. 在帧末清理临时状态justPressed, justReleased 等)
*
* Runs in ECS update loop, responsible for:
* 1. InputManager is already updated by events at frame start
* 2. Clear temporary state at frame end (justPressed, justReleased, etc.)
*/
import { EntitySystem, Matcher, ECSSystem } from '@esengine/ecs-framework';
import type { Entity } from '@esengine/ecs-framework';
import type {
IPlatformInputSubsystem,
KeyboardEventInfo,
MouseEventInfo,
WheelEventInfo,
TouchEvent
} from '@esengine/platform-common';
import { Input, InputManager } from './InputManager';
/**
* 输入系统配置
* Input system configuration
*/
export interface InputSystemConfig {
/**
* 输入管理器实例,默认使用全局 Input
* Input manager instance, defaults to global Input
*/
inputManager?: InputManager;
/**
* 是否在编辑器模式下禁用(防止与编辑器输入冲突)
* Whether to disable in editor mode (prevent conflict with editor input)
*/
disableInEditor?: boolean;
}
/**
* 输入系统
* Input System
*
* 处理平台输入事件并更新 InputManager 状态。
* Handles platform input events and updates InputManager state.
*
* @example
* ```typescript
* // 在 GameRuntime 中注册
* const inputSystem = new InputSystem({
* inputSubsystem: webInputSubsystem
* });
* scene.addSystem(inputSystem);
*
* // 在游戏系统中使用
* import { Input, MouseButton } from '@esengine/engine-core';
*
* class PlayerSystem extends EntitySystem {
* protected process(entities: readonly Entity[]): void {
* if (Input.isKeyDown('KeyW')) {
* // 移动玩家
* }
* }
* }
* ```
*/
@ECSSystem('InputSystem', { updateOrder: -1000 }) // 最先更新 | Update first
export class InputSystem extends EntitySystem {
private _inputManager: InputManager;
private _inputSubsystem: IPlatformInputSubsystem | null = null;
private _disableInEditor: boolean;
private _isInitialized: boolean = false;
constructor(config: InputSystemConfig = {}) {
// 不匹配任何实体,只用于生命周期 | Match no entities, only for lifecycle
super(Matcher.nothing());
this._inputManager = config.inputManager ?? Input;
this._disableInEditor = config.disableInEditor ?? false;
}
/**
* 设置平台输入子系统
* Set platform input subsystem
*
* @param subsystem 平台输入子系统 | Platform input subsystem
*/
setInputSubsystem(subsystem: IPlatformInputSubsystem): void {
// 如果已有子系统,先解绑 | Unbind if already has subsystem
if (this._inputSubsystem && this._isInitialized) {
this.unbindEvents();
}
this._inputSubsystem = subsystem;
// 如果已初始化,立即绑定 | Bind immediately if initialized
if (this._isInitialized) {
this.bindEvents();
}
}
/**
* 获取输入管理器
* Get input manager
*/
get inputManager(): InputManager {
return this._inputManager;
}
protected override onInitialize(): void {
this._isInitialized = true;
if (this._inputSubsystem) {
this.bindEvents();
}
}
/**
* 绑定平台输入事件
* Bind platform input events
*/
private bindEvents(): void {
if (!this._inputSubsystem) return;
const sub = this._inputSubsystem;
// 键盘事件 | Keyboard events
if (sub.onKeyDown) {
sub.onKeyDown(this._handleKeyDown);
}
if (sub.onKeyUp) {
sub.onKeyUp(this._handleKeyUp);
}
// 鼠标事件 | Mouse events
if (sub.onMouseMove) {
sub.onMouseMove(this._handleMouseMove);
}
if (sub.onMouseDown) {
sub.onMouseDown(this._handleMouseDown);
}
if (sub.onMouseUp) {
sub.onMouseUp(this._handleMouseUp);
}
if (sub.onWheel) {
sub.onWheel(this._handleWheel);
}
// 触摸事件 | Touch events
sub.onTouchStart(this._handleTouchStart);
sub.onTouchMove(this._handleTouchMove);
sub.onTouchEnd(this._handleTouchEnd);
sub.onTouchCancel(this._handleTouchEnd); // 取消当作结束处理 | Treat cancel as end
}
/**
* 解绑平台输入事件
* Unbind platform input events
*/
private unbindEvents(): void {
if (!this._inputSubsystem) return;
const sub = this._inputSubsystem;
// 键盘事件 | Keyboard events
if (sub.offKeyDown) {
sub.offKeyDown(this._handleKeyDown);
}
if (sub.offKeyUp) {
sub.offKeyUp(this._handleKeyUp);
}
// 鼠标事件 | Mouse events
if (sub.offMouseMove) {
sub.offMouseMove(this._handleMouseMove);
}
if (sub.offMouseDown) {
sub.offMouseDown(this._handleMouseDown);
}
if (sub.offMouseUp) {
sub.offMouseUp(this._handleMouseUp);
}
if (sub.offWheel) {
sub.offWheel(this._handleWheel);
}
// 触摸事件 | Touch events
sub.offTouchStart(this._handleTouchStart);
sub.offTouchMove(this._handleTouchMove);
sub.offTouchEnd(this._handleTouchEnd);
sub.offTouchCancel(this._handleTouchEnd);
}
// ========== 事件处理函数 | Event handlers ==========
// 使用箭头函数保持 this 绑定 | Use arrow functions to preserve this binding
private _handleKeyDown = (event: KeyboardEventInfo): void => {
this._inputManager.handleKeyDown(event);
};
private _handleKeyUp = (event: KeyboardEventInfo): void => {
this._inputManager.handleKeyUp(event);
};
private _handleMouseMove = (event: MouseEventInfo): void => {
this._inputManager.handleMouseMove(event);
};
private _handleMouseDown = (event: MouseEventInfo): void => {
this._inputManager.handleMouseDown(event);
};
private _handleMouseUp = (event: MouseEventInfo): void => {
this._inputManager.handleMouseUp(event);
};
private _handleWheel = (event: WheelEventInfo): void => {
this._inputManager.handleWheel(event);
};
private _handleTouchStart = (event: TouchEvent): void => {
this._inputManager.handleTouchStart(event.changedTouches);
};
private _handleTouchMove = (event: TouchEvent): void => {
this._inputManager.handleTouchMove(event.changedTouches);
};
private _handleTouchEnd = (event: TouchEvent): void => {
this._inputManager.handleTouchEnd(event.changedTouches);
};
// ========== 系统生命周期 | System lifecycle ==========
protected override process(_entities: readonly Entity[]): void {
// 不处理实体,仅用于生命周期 | No entity processing, only for lifecycle
}
protected override lateProcess(_entities: readonly Entity[]): void {
// 在帧末清理临时状态 | Clear temporary state at end of frame
this._inputManager.endFrame();
}
protected override onDestroy(): void {
this.unbindEvents();
this._inputManager.reset();
this._isInitialized = false;
}
/**
* 检查是否应该启用输入
* Check if input should be enabled
*/
protected override onCheckProcessing(): boolean {
// 如果设置了编辑器模式禁用,检查场景是否在编辑器模式
if (this._disableInEditor && this.scene?.isEditorMode) {
return false;
}
return true;
}
}