跳转至内容

18. 输入,判断,连招

3.14,周六,宅。

a327ex/boipushy 的输入模块

lua
local input_path = (...):match('(.-)[^%.]+$') .. '.'
local Input = {}
Input.__index = Input

-- 把键盘、鼠标、手柄所有可能的按键都列出来,方便后续统一处理
Input.all_keys = {
    " ", "return", "escape", "backspace", "tab", -- ... 省略一大堆按键名 ...
}

function Input.new()
    local self = {}

    -- 【核心数据结构】
    self.prev_state = {}     -- 上一帧的按键状态
    self.state = {}          -- 当前的按键状态(把键盘、鼠标、手柄的状态全混在一起存)
    self.binds = {}          -- 动作绑定表
    self.functions = {}      -- 直接绑定到按键的函数
    self.repeat_state = {}   -- 记录按住连发的状态
    self.sequences = {}      -- 记录“搓招/连招”的状态

    -- 获取手柄(作者偷懒了,目前只支持玩家1的手柄)
    self.joysticks = love.joystick.getJoysticks()

    -- 【黑科技:自动劫持(Hook)LÖVE 的底层事件】
    -- 这里把 LÖVE 引擎原生的按键回调函数全部拦截下来,
    -- 在执行你写的原生代码之前,先执行 Input 自己的记录逻辑。
    local callbacks = { 'keypressed', 'keyreleased', 'mousepressed', 'mousereleased', 'gamepadpressed', 'gamepadreleased',
        'gamepadaxis', 'wheelmoved', 'update' }
    local old_functions = {}
    local empty_function = function() end
    for _, f in ipairs(callbacks) do
        old_functions[f] = love[f] or empty_function
        love[f] = function(...)
            old_functions[f](...) -- 先执行引擎原有的功能
            self[f](self, ...)    -- 再执行本库自己对应的函数(比如往 self.state 里记录按键)
        end
    end

    return setmetatable(self, Input)
end
lua
-- 绑定功能:不仅能把键绑定给“字符串动作”,还能直接绑定给一个“函数”
function Input:bind(key, action)
    if type(action) == 'function' then
        self.functions[key] = action; return
    end
    if not self.binds[action] then self.binds[action] = {} end
    table.insert(self.binds[action], key)
end

-- 判断动作是否“刚刚按下”
function Input:pressed(action)
    if action then
        -- 遍历这个动作对应的所有按键,有一个刚按下就算 true
        for _, key in ipairs(self.binds[action]) do
            if self.state[key] and not self.prev_state[key] then return true end
        end
    else
        -- 如果没传 action,就检查是否触发了直接绑定的函数
        for _, key in ipairs(Input.all_keys) do
            if self.state[key] and not self.prev_state[key] then
                if self.functions[key] then self.functions[key]() end
            end
        end
    end
end

-- 判断动作是否“刚刚松开”
function Input:released(action)
    for _, key in ipairs(self.binds[action]) do
        if self.prev_state[key] and not self.state[key] then return true end
    end
end


-- 【特色功能:按住并支持“连发” (Auto-repeat)】
-- delay: 按下多久后开始连发
-- interval: 连发的时间间隔
function Input:down(action, interval, delay)
    if action and delay and interval then
        -- 带有初始延迟的连发(类似长按删除键,先删一个,等一小会儿,然后开始狂删)
        -- ... 逻辑省略 ...
    elseif action and interval and not delay then
        -- 没有初始延迟,直接开始按频率连发
        -- ... 逻辑省略 ...
    elseif action and not interval and not delay then
        -- 最普通的“按住”检测(回退到轮询 LÖVE 原生的 isDown 方法)
        -- ... 逻辑省略 ...
    end
end

-- 解除绑定
function Input:unbind(key)
    -- ...
end
lua
-- 【特色功能:搓招/连招系统 (Sequence)】
-- 用法类似:input:sequence('up', 0.5, 'down', 'combo')
-- 意思是:按下上,在0.5秒内按下下,触发'combo'动作
function Input:sequence(...)
    local sequence = { ... }
    -- ... 参数合法性检查省略 ...

    -- 把传入的参数拼成一个字符串作为唯一标识符
    local sequence_key = ''
    for _, seq in ipairs(sequence) do sequence_key = sequence_key .. tostring(seq) end

    -- 状态机:检查玩家按下的顺序和时间间隔是否符合要求
    if not self.sequences[sequence_key] then
        self.sequences[sequence_key] = { sequence = sequence, current_index = 1 }
    else
        -- ... 具体的连招判定逻辑,通过 love.timer.getTime() 计算按键时间差 ...
    end
end

-- ... 各种设备按键名称映射表省略 ...
lua
-- 每帧更新(由于前面劫持了 love.update,这个函数也会自动被调用)
function Input:update()
    self:pressed()
    self.prev_state = copy(self.state) -- 备份上一帧状态
    self.state['wheelup'] = false      -- 滚轮状态用完即清
    self.state['wheeldown'] = false

    -- 处理按住连发的时间计时器
    -- ...
end

-- 以下是一系列被 LÖVE 底层触发的回调函数,作用都是把按键状态写入 self.state
function Input:keypressed(key) self.state[key] = true end
function Input:keyreleased(key) self.state[key] = false; self.repeat_state[key] = false end
function Input:mousepressed(x, y, button) self.state[button_to_key[button]] = true end
-- ... 鼠标松开、滚轮、手柄按下/松开/摇杆的记录逻辑省略 ...

return setmetatable({}, { __call = function(_, ...) return Input.new(...) end })

SNKRX 的输入模块

lua
Input = Object:extend()

-- 初始化函数:当你创建一个 Input 对象时,首先会运行这里的代码
function Input:init(joystick_index)
  -- 定义鼠标的按键名字(左键、右键、中键、侧键、滚轮上下)
  self.mouse_buttons = {"m1", "m2", "m3", "m4", "m5", "wheel_up", "wheel_down"}
  -- 定义手柄的按键名字(上下左右、十字键、开始、返回等)
  self.gamepad_buttons = {"fdown", "fup", "fleft", "fright", "dpdown", "dpup", "dpleft", "dpright", "start", "back", "guide", "leftstick", "rightstick", "rb", "lb"}

  -- 手柄按键和摇杆的映射表(为了兼容不同手柄的叫法)
  self.index_to_gamepad_button = {["a"] = "fdown", ["b"] = "fright", ["x"] = "fleft", ["y"] = "fup", ["back"] = "back", ["start"] = "start", ["guide"] = "guide", ["leftstick"] = "leftstick",
    ["rightstick"] = "rightstick", ["leftshoulder"] = "lb", ["rightshoulder"] = "rb", ["dpdown"] = "dpdown", ["dpup"] = "dpup", ["dpleft"] = "dpleft", ["dpright"] = "dpright",
  }
  self.index_to_gamepad_axis = {["leftx"] = "leftx", ["rightx"] = "rightx", ["lefty"] = "lefty", ["righty"] = "righty", ["triggerleft"] = "lt", ["triggerright"] = "rt"}

  self.gamepad_axis = {} -- 记录手柄摇杆的推动程度

  -- 获取连接的第几个手柄,默认是第1个
  self.joystick_index = joystick_index or 1
  self.joystick = love.joystick.getJoysticks()[self.joystick_index]

  -- 【核心概念:状态追踪】
  -- 这里分别记录键盘、鼠标、手柄“当前帧”和“上一帧”的按键状态
  self.keyboard_state = {}
  self.previous_keyboard_state = {}
  self.mouse_state = {}
  self.previous_mouse_state = {}
  self.gamepad_state = {}
  self.previous_gamepad_state = {}

  self.actions = {} -- 用来存放你绑定的动作(比如 "jump" 对应 空格键)
  self.textinput_buffer = '' -- 用来存放玩家打字的缓存
end

-- 更新函数:游戏每运行一帧(大概每秒60次),就会执行一次这个函数
function Input:update(dt)
  -- 第一步:先把你绑定的所有动作的状态清零
  for _, action in ipairs(self.actions) do
    self[action].pressed = false  -- 刚刚按下
    self[action].down = false     -- 一直按着
    self[action].released = false -- 刚刚松开
  end

  -- 第二步:检查每个动作对应的按键状态
  for _, action in ipairs(self.actions) do
    for _, key in ipairs(self[action].keys) do
      -- 如果这个键是鼠标按键
      if table.contains(self.mouse_buttons, key) then
        -- 核心逻辑:如果当前帧按下了,但上一帧没按下,说明是“刚刚按下” (pressed)
        self[action].pressed = self[action].pressed or (self.mouse_state[key] and not self.previous_mouse_state[key])
        -- 如果当前帧按着,说明是“一直按着” (down)
        self[action].down = self[action].down or self.mouse_state[key]
        -- 如果当前帧没按,但上一帧按了,说明是“刚刚松开” (released)
        self[action].released = self[action].released or (not self.mouse_state[key] and  self.previous_mouse_state[key])

      -- 如果这个键是手柄按键,逻辑同上
      elseif table.contains(self.gamepad_buttons, key) then
        self[action].pressed = self[action].pressed or (self.gamepad_state[key] and not self.previous_gamepad_state[key])
        self[action].down = self[action].down or self.gamepad_state[key]
        self[action].released = self[action].released or (not self.gamepad_state[key] and  self.previous_gamepad_state[key])

      -- 否则就是键盘按键,逻辑同上
      else
        self[action].pressed = self[action].pressed or (self.keyboard_state[key] and not self.previous_keyboard_state[key])
        self[action].down = self[action].down or self.keyboard_state[key]
        self[action].released = self[action].released or (not self.keyboard_state[key] and self.previous_keyboard_state[key])
      end
    end
  end

  -- 第三步:把当前帧的状态保存起来,作为下一帧的“上一帧状态”
  self.previous_mouse_state = table.copy(self.mouse_state)
  self.previous_gamepad_state = table.copy(self.gamepad_state)
  self.previous_keyboard_state = table.copy(self.keyboard_state)

  -- 鼠标滚轮的状态只保留一帧,用完即清空
  self.mouse_state.wheel_up = false
  self.mouse_state.wheel_down = false
end

-- 锁定鼠标(让鼠标出不去游戏窗口)
function Input:set_mouse_grabbed(v)
  love.mouse.setGrabbed(v)
end

-- 设置鼠标是否可见
function Input:set_mouse_visible(v)
  love.mouse.setVisible(v)
end

-- 【重要】绑定函数:把物理按键(如 "space")绑定到逻辑动作(如 "jump")
function Input:bind(action, keys)
  if not self[action] then self[action] = {} end
  if type(keys) == "string" then self[action].keys = {keys}
  elseif type(keys) == "table" then self[action].keys = keys end
  table.insert(self.actions, action)
end

-- 取消绑定
function Input:unbind(action)
  self[action] = nil
end

-- 获取手柄摇杆的值
function Input:axis(key)
  return self.gamepad_axis[key]
end

-- 接收玩家打字的文本
function Input:textinput(text)
  self.textinput_buffer = self.textinput_buffer .. text
  return self.textinput_buffer
end

-- 获取并清空打字缓存(比如按了回车键发送消息后清空)
function Input:get_and_clear_textinput_buffer()
  local buffer = self.textinput_buffer
  self.textinput_buffer = ""
  return buffer
end

-- 暴力绑定:给键盘上所有的键都绑定一个和自己名字一样的动作
-- 通常用于制作输入框、或者不需要自定义改键的内部工具
function Input:bind_all()
  local keyboard_binds = {['a'] = {'a'}, ['b'] = {'b'}, ... } -- 省略了长长的列表
  for k, v in pairs(keyboard_binds) do self:bind(k, v) end
  self:bind('m1', {'m1'})
  -- ... 绑定鼠标等
end