feat(input): 添加游戏输入事件处理系统

- 新增 IGameInputEvent 接口定义游戏输入事件
- 新增 IInputContext 接口用于处理输入事件的上下文
- 新增 InputContextStack 类管理输入上下文堆栈
- 新增 InputSystem 类负责整体输入事件分发与处理
- 移除旧版 Godot 输入系统相关实现代码
- 定义输入事件处理流程:上下文堆栈 -> 事件发送机制
This commit is contained in:
GwWuYou 2025-12-20 22:35:07 +08:00
parent 2db09e72d7
commit 564a7e3f24
10 changed files with 100 additions and 530 deletions

View File

@ -7,5 +7,5 @@ public static class ArchitectureEvents
/// 架构初始化完成事件
/// 在所有 Model / System Init 执行完毕后派发
/// </summary>
public readonly struct ArchitectureInitializedEvent { }
public readonly struct ArchitectureInitializedEvent;
}

View File

@ -0,0 +1,6 @@
namespace GFramework.Game.input;
/// <summary>
/// 游戏输入事件接口
/// </summary>
public interface IGameInputEvent;

View File

@ -0,0 +1,14 @@
namespace GFramework.Game.input;
/// <summary>
/// 输入上下文接口,用于处理游戏中的输入事件
/// </summary>
public interface IInputContext
{
/// <summary>
/// 处理游戏输入事件
/// </summary>
/// <param name="input">要处理的游戏输入事件</param>
/// <returns>返回 true 表示输入被吃掉,不再向下传播;返回 false 表示输入未被处理,可以继续传播给其他处理器</returns>
bool Handle(IGameInputEvent input);
}

View File

@ -0,0 +1,33 @@
namespace GFramework.Game.input;
/// <summary>
/// 输入上下文堆栈管理器,用于管理多个输入上下文的堆栈结构
/// </summary>
public class InputContextStack
{
private readonly Stack<IInputContext> _stack = new();
/// <summary>
/// 将指定的输入上下文压入堆栈顶部
/// </summary>
/// <param name="context">要压入堆栈的输入上下文对象</param>
public void Push(IInputContext context) => _stack.Push(context);
/// <summary>
/// 弹出堆栈顶部的输入上下文
/// </summary>
public void Pop() => _stack.Pop();
/// <summary>
/// 处理游戏输入事件,遍历堆栈中的所有上下文直到找到能够处理该事件的上下文
/// </summary>
/// <param name="input">要处理的游戏输入事件</param>
/// <returns>如果堆栈中任意一个上下文成功处理了输入事件则返回true否则返回false</returns>
public bool Handle(IGameInputEvent input)
{
// 遍历堆栈中的所有上下文调用其Handle方法处理输入事件
// Any方法会在第一个返回true的上下文处停止遍历
return _stack.Any(ctx => ctx.Handle(input));
}
}

View File

@ -0,0 +1,46 @@
using GFramework.Core.extensions;
using GFramework.Core.system;
namespace GFramework.Game.input;
/// <summary>
/// 输入系统类,负责管理输入上下文堆栈并处理游戏输入事件
/// </summary>
public class InputSystem : AbstractSystem
{
private readonly InputContextStack _contextStack = new();
/// <summary>
/// 将输入上下文推入上下文堆栈
/// </summary>
/// <param name="ctx">要推入的输入上下文对象</param>
public void PushContext(IInputContext ctx)
=> _contextStack.Push(ctx);
/// <summary>
/// 从上下文堆栈中弹出顶层输入上下文
/// </summary>
public void PopContext()
=> _contextStack.Pop();
/// <summary>
/// 处理游戏输入事件,首先尝试通过上下文堆栈处理,如果未被处理则发送事件
/// </summary>
/// <param name="input">要处理的游戏输入事件</param>
public void Handle(IGameInputEvent input)
{
// 尝试通过上下文堆栈处理输入事件
if (_contextStack.Handle(input))
return;
// 如果上下文堆栈未能处理,则发送该事件
this.SendEvent(input);
}
/// <summary>
/// 系统初始化方法,在系统启动时调用
/// </summary>
protected override void OnInit()
{
}
}

View File

@ -1,115 +0,0 @@
using GFramework.Game.assets;
namespace GFramework.Godot.system;
/// <summary>
/// 抽象输入系统基类实现了IInputSystem接口的基本功能
/// </summary>
public abstract class AbstractInputSystem : AbstractAssetCatalogSystem, IInputSystem
{
// 存储动作名称与按键绑定的映射关系
private readonly Dictionary<string, string> _actionBindings = new();
// 存储动作名称与当前状态的映射关系
private readonly Dictionary<string, bool> _actionStates = new();
// 存储动作名称与上一帧状态的映射关系
private readonly Dictionary<string, bool> _previousActionStates = new();
/// <inheritdoc />
public virtual void SetBinding(string actionName, string keyCode)
{
_actionBindings[actionName] = keyCode;
}
/// <inheritdoc />
public virtual string GetBinding(string actionName)
{
return _actionBindings.TryGetValue(actionName, out var binding) ? binding : string.Empty;
}
/// <inheritdoc />
public virtual bool IsActionPressed(string actionName)
{
return _actionStates.TryGetValue(actionName, out var state) && state;
}
/// <inheritdoc />
public virtual bool IsActionJustPressed(string actionName)
{
var current = _actionStates.TryGetValue(actionName, out var currentState) && currentState;
var previous = _previousActionStates.TryGetValue(actionName, out var previousState) && previousState;
return current && !previous;
}
/// <inheritdoc />
public virtual bool IsActionJustReleased(string actionName)
{
var current = _actionStates.TryGetValue(actionName, out var currentState) && currentState;
var previous = _previousActionStates.TryGetValue(actionName, out var previousState) && previousState;
return !current && previous;
}
/// <inheritdoc />
public virtual void AddAction(string actionName, string defaultKeyCode)
{
if (!_actionBindings.ContainsKey(actionName))
{
_actionBindings[actionName] = defaultKeyCode;
_actionStates[actionName] = false;
_previousActionStates[actionName] = false;
}
}
/// <inheritdoc />
public virtual void RemoveAction(string actionName)
{
_actionBindings.Remove(actionName);
_actionStates.Remove(actionName);
_previousActionStates.Remove(actionName);
}
/// <inheritdoc />
public abstract void SaveConfiguration();
/// <inheritdoc />
public abstract void LoadConfiguration();
/// <inheritdoc />
public abstract void Update(double delta);
/// <summary>
/// 更新输入状态,在每一帧调用
/// </summary>
protected virtual void UpdateInputStates()
{
// 保存当前状态作为下一帧的前一状态
foreach (var kvp in _actionStates)
{
_previousActionStates[kvp.Key] = kvp.Value;
}
// 更新当前状态
foreach (var kvp in _actionBindings)
{
var actionName = kvp.Key;
var keyCode = kvp.Value;
// 这里需要根据具体平台和引擎实现具体的按键检测逻辑
// 当前只是一个占位实现
_actionStates[actionName] = CheckKeyPressed(keyCode);
}
}
/// <summary>
/// 检查特定按键是否被按下
/// </summary>
/// <param name="keyCode">按键码</param>
/// <returns>按键是否被按下</returns>
protected virtual bool CheckKeyPressed(string keyCode)
{
// 这里需要根据Godot引擎的具体实现来检查按键状态
// 目前只是示例实现
return false;
}
}

View File

@ -1,195 +0,0 @@
// using Godot;
//
// namespace GFramework.Godot.system;
//
// /// <summary>
// /// Godot引擎专用的输入系统实现
// /// </summary>
// public class GodotInputSystem : AbstractInputSystem
// {
// private InputMap _inputMap;
// private string _configPath;
//
// public override void Init()
// {
// base.Init();
//
// _inputMap = new InputMap();
// _configPath = "user://input_config.json";
//
// // 添加一些默认的输入动作
// AddDefaultActions();
//
// // 尝试加载用户配置
// LoadConfiguration();
// }
//
// public override void Destroy()
// {
// // 保存配置
// SaveConfiguration();
// base.Destroy();
// }
//
// /// <summary>
// /// 添加默认输入动作
// /// </summary>
// private void AddDefaultActions()
// {
// _inputMap.AddAction(new InputAction("move_left", InputActionType.Axis, "Left"));
// _inputMap.AddAction(new InputAction("move_right", InputActionType.Axis, "Right"));
// _inputMap.AddAction(new InputAction("move_up", InputActionType.Axis, "Up"));
// _inputMap.AddAction(new InputAction("move_down", InputActionType.Axis, "Down"));
// _inputMap.AddAction(new InputAction("jump", InputActionType.Button, "Space"));
// _inputMap.AddAction(new InputAction("attack", InputActionType.Button, "LeftMouse"));
// _inputMap.AddAction(new InputAction("interact", InputActionType.Button, "E"));
// }
//
// /// <inheritdoc />
// public override void SaveConfiguration()
// {
// try
// {
// var configData = new Dictionary<string, string[]>();
// foreach (var action in _inputMap.GetAllActions())
// {
// configData[action.Name] = action.CurrentBindings;
// }
//
// var json = Json.Stringify(configData);
// File.WriteAllText(ProjectSettings.GlobalizePath(_configPath), json);
// }
// catch (Exception ex)
// {
// GD.PrintErr($"Failed to save input configuration: {ex.Message}");
// }
// }
//
// /// <inheritdoc />
// public override void LoadConfiguration()
// {
// try
// {
// if (!File.Exists(ProjectSettings.GlobalizePath(_configPath)))
// {
// // 配置文件不存在,使用默认配置
// return;
// }
//
// var json = File.ReadAllText(ProjectSettings.GlobalizePath(_configPath));
// var parsed = Json.ParseString(json);
// if (parsed is not Core.Godot.Collections.Dictionary dict)
// {
// GD.PrintErr("Invalid input configuration file");
// return;
// }
//
// foreach (var key in dict.Keys)
// {
// var action = _inputMap.GetAction(key.AsString());
// if (action != null && dict[key] is Core.Godot.Collections.Array array)
// {
// var bindings = new string[array.Count];
// for (int i = 0; i < array.Count; i++)
// {
// bindings[i] = array[i].AsString();
// }
// action.SetBindings(bindings);
// }
// }
// }
// catch (Exception ex)
// {
// GD.PrintErr($"Failed to load input configuration: {ex.Message}");
// }
// }
//
// /// <inheritdoc />
// protected override bool CheckKeyPressed(string keyCode)
// {
// // 根据Godot的输入系统检查按键状态
// return Input.IsPhysicalKeyPressed(GodotKeyMapper.GetKeyFromString(keyCode)) ||
// Input.IsMouseButtonPressed(GodotKeyMapper.GetMouseButtonFromString(keyCode));
// }
//
// /// <inheritdoc />
// public override void Update(double delta)
// {
// UpdateInputStates();
// }
//
// protected override void RegisterAssets()
// {
// throw new NotImplementedException();
// }
// }
//
// /// <summary>
// /// Godot按键映射辅助类
// /// </summary>
// public static class GodotKeyMapper
// {
// private static readonly Dictionary<string, Key> KeyMap = new()
// {
// { "Left", Key.Left },
// { "Right", Key.Right },
// { "Up", Key.Up },
// { "Down", Key.Down },
// { "Space", Key.Space },
// { "Enter", Key.Enter },
// { "Escape", Key.Escape },
// { "A", Key.A },
// { "B", Key.B },
// { "C", Key.C },
// { "D", Key.D },
// { "E", Key.E },
// { "F", Key.F },
// { "G", Key.G },
// { "H", Key.H },
// { "I", Key.I },
// { "J", Key.J },
// { "K", Key.K },
// { "L", Key.L },
// { "M", Key.M },
// { "N", Key.N },
// { "O", Key.O },
// { "P", Key.P },
// { "Q", Key.Q },
// { "R", Key.R },
// { "S", Key.S },
// { "T", Key.T },
// { "U", Key.U },
// { "V", Key.V },
// { "W", Key.W },
// { "X", Key.X },
// { "Y", Key.Y },
// { "Z", Key.Z },
// { "0", Key.Key0 },
// { "1", Key.Key1 },
// { "2", Key.Key2 },
// { "3", Key.Key3 },
// { "4", Key.Key4 },
// { "5", Key.Key5 },
// { "6", Key.Key6 },
// { "7", Key.Key7 },
// { "8", Key.Key8 },
// { "9", Key.Key9 }
// };
//
// private static readonly Dictionary<string, MouseButton> MouseButtonMap = new()
// {
// { "LeftMouse", MouseButton.Left },
// { "RightMouse", MouseButton.Right },
// { "MiddleMouse", MouseButton.Middle }
// };
//
// public static Key GetKeyFromString(string keyString)
// {
// return KeyMap.GetValueOrDefault(keyString, Key.None);
// }
//
// public static MouseButton GetMouseButtonFromString(string mouseButtonString)
// {
// return MouseButtonMap.GetValueOrDefault(mouseButtonString, MouseButton.None);
// }
// }

View File

@ -1,73 +0,0 @@
using GFramework.Core.system;
namespace GFramework.Godot.system;
/// <summary>
/// 输入系统接口,用于统一管理游戏中的输入操作和键位绑定
/// </summary>
public interface IInputSystem : ISystem
{
/// <summary>
/// 设置指定动作的按键绑定
/// </summary>
/// <param name="actionName">动作名称</param>
/// <param name="keyCode">按键码</param>
void SetBinding(string actionName, string keyCode);
/// <summary>
/// 获取指定动作的按键绑定
/// </summary>
/// <param name="actionName">动作名称</param>
/// <returns>绑定的按键码</returns>
string GetBinding(string actionName);
/// <summary>
/// 检查指定动作是否正在被执行
/// </summary>
/// <param name="actionName">动作名称</param>
/// <returns>如果动作正在执行则返回true否则返回false</returns>
bool IsActionPressed(string actionName);
/// <summary>
/// 检查指定动作是否刚刚开始执行
/// </summary>
/// <param name="actionName">动作名称</param>
/// <returns>如果动作刚刚开始执行则返回true否则返回false</returns>
bool IsActionJustPressed(string actionName);
/// <summary>
/// 检查指定动作是否刚刚停止执行
/// </summary>
/// <param name="actionName">动作名称</param>
/// <returns>如果动作刚刚停止执行则返回true否则返回false</returns>
bool IsActionJustReleased(string actionName);
/// <summary>
/// 添加输入动作
/// </summary>
/// <param name="actionName">动作名称</param>
/// <param name="defaultKeyCode">默认按键绑定</param>
void AddAction(string actionName, string defaultKeyCode);
/// <summary>
/// 移除输入动作
/// </summary>
/// <param name="actionName">动作名称</param>
void RemoveAction(string actionName);
/// <summary>
/// 保存输入配置到文件
/// </summary>
void SaveConfiguration();
/// <summary>
/// 从文件加载输入配置
/// </summary>
void LoadConfiguration();
/// <summary>
/// 更新输入系统状态,应在每帧调用
/// </summary>
/// <param name="delta">帧间隔时间</param>
void Update(double delta);
}

View File

@ -1,79 +0,0 @@
namespace GFramework.Godot.system;
/// <summary>
/// 输入动作类,表示游戏中的一种输入行为
/// </summary>
public class InputAction
{
/// <summary>
/// 动作名称
/// </summary>
public string Name { get; }
/// <summary>
/// 动作类型
/// </summary>
public InputActionType ActionType { get; }
/// <summary>
/// 默认按键绑定
/// </summary>
public string[] DefaultBindings { get; }
/// <summary>
/// 当前按键绑定
/// </summary>
public string[] CurrentBindings { get; private set; }
/// <summary>
/// 构造函数
/// </summary>
/// <param name="name">动作名称</param>
/// <param name="actionType">动作类型</param>
/// <param name="defaultBindings">默认按键绑定</param>
public InputAction(string name, InputActionType actionType, params string[] defaultBindings)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
ActionType = actionType;
DefaultBindings = defaultBindings ?? throw new ArgumentNullException(nameof(defaultBindings));
CurrentBindings = (string[])defaultBindings.Clone();
}
/// <summary>
/// 重置为默认按键绑定
/// </summary>
public void ResetToDefault()
{
CurrentBindings = (string[])DefaultBindings.Clone();
}
/// <summary>
/// 设置新的按键绑定
/// </summary>
/// <param name="bindings">新的按键绑定</param>
public void SetBindings(params string[] bindings)
{
CurrentBindings = bindings ?? throw new ArgumentNullException(nameof(bindings));
}
}
/// <summary>
/// 输入动作类型枚举
/// </summary>
public enum InputActionType
{
/// <summary>
/// 按钮类型,如跳跃、射击等
/// </summary>
Button,
/// <summary>
/// 轴类型,如移动、视角控制等
/// </summary>
Axis,
/// <summary>
/// 向量类型,如二维移动控制
/// </summary>
Vector
}

View File

@ -1,67 +0,0 @@
namespace GFramework.Godot.system;
/// <summary>
/// 输入映射类,管理所有的输入动作及其绑定
/// </summary>
public class InputMap
{
private readonly Dictionary<string, InputAction> _actions = new();
/// <summary>
/// 添加输入动作
/// </summary>
/// <param name="action">输入动作</param>
public void AddAction(InputAction action)
{
_actions[action.Name] = action;
}
/// <summary>
/// 移除输入动作
/// </summary>
/// <param name="actionName">动作名称</param>
public void RemoveAction(string actionName)
{
_actions.Remove(actionName);
}
/// <summary>
/// 获取输入动作
/// </summary>
/// <param name="actionName">动作名称</param>
/// <returns>输入动作如果不存在则返回null</returns>
public InputAction GetAction(string actionName)
{
return _actions.GetValueOrDefault(actionName);
}
/// <summary>
/// 获取所有输入动作
/// </summary>
/// <returns>输入动作列表</returns>
public IEnumerable<InputAction> GetAllActions()
{
return _actions.Values;
}
/// <summary>
/// 检查是否存在指定名称的动作
/// </summary>
/// <param name="actionName">动作名称</param>
/// <returns>存在返回true否则返回false</returns>
public bool ContainsAction(string actionName)
{
return _actions.ContainsKey(actionName);
}
/// <summary>
/// 根据按键查找绑定的动作
/// </summary>
/// <param name="keyCode">按键码</param>
/// <returns>绑定到该按键的所有动作</returns>
public IEnumerable<InputAction> FindActionsByBinding(string keyCode)
{
return _actions.Values.Where(action =>
action.CurrentBindings.Contains(keyCode));
}
}