按键
有一些mod会使用按键触发一些效果,这是很容易实现的。
按键的定义
点击鼠标、按下按键、移动手柄等操作,在游戏里被视作一种输入,它们都会用一个整数表示,参考 constants.lua
constants.lua-- 81行
CONTROL_PRIMARY = 0 -- 主动作,默认绑定在鼠标左键
CONTROL_SECONDARY = 1 -- 副动作,默认绑定在鼠标右键
CONTROL_ATTACK = 2 -- 攻击,默认绑定在F
-- ...
-- 246行
KEY_A = 97 -- 按下或松开A
KEY_B = 98 -- 按下或松开B
KEY_C = 99
KEY_D = 100
KEY_E = 101
KEY_F = 102
KEY_G = 103
KEY_H = 104
注意CONTROL_
开头的变量和KEY_
开头的变量有一些微妙的区别。
KEY_F
只会在按下和松开F键的时候触发。
CONTROL_ATTACK
会在按下和松开攻击键的时候触发,默认F,但如果玩家在设置面板中把攻击键改成了J,那么按下J才会触发 CONTROL_ATTACK
。
在本章节,我们只关注KEY_
开头的变量。
监听器
在 input.lua 中定义了一些函数,可以用来监听按键的按下和松开。
- 添加一个通用按键监听器,当任意按键被按下或松开时,fn就会被调用,传入两个参数:key:int(按键值),down:boolean(是否被按下)。
- 该函数会返回一个监听器对象。
试一试
启动饥荒,进入游戏主界面(不需要进世界),在控制台运行如下代码:
TheInput:AddKeyHandler(print)
- 按ctrl+L打开输出面板,然后随意的敲击键盘按键,仔细观察输出面板打印的内容。
- 长按某个按键(比如F),看看输出面板会打印什么?
- 再次运行一遍上面的代码,看看发生了什么变化?
- 添加一个特定按键的监听器,当key被按下的时候,fn就会被调用,无参数。
- 该函数也会返回一个监听器对象。
试一试
这里简单监听一下A按键。
启动饥荒,如果已经启动过了,在控制台运行如下代码:
等待游戏重启后,运行如下代码:
TheInput:AddKeyDownHandler(KEY_A, function() print("A 被按下了", GetTick()) end)
- 按下或者松开A,看看输出面板打印了什么?
- 再次运行一遍上面的代码,看看发生了什么变化,和你的预期一致吗?
Warning
GetTick()
函数返回当前的帧时间,执行这个函数是为了区分每一次的打印,并没有其他效果。
- 添加一个特定按键的监听器,当key被松开的时候,fn就会被调用,无参数。
- 该函数还是会返回一个监听器对象。
这个函数和 TheInput:AddKeyDownHandler
非常类似,只不过在按键松开时才触发。
试一试
启动饥荒,如果已经启动过了,在控制台运行如下代码:
等待游戏重启后,运行如下代码:
TheInput:AddKeyUpHandler(KEY_A, function() print("A 被松开了", GetTick()) end)
- 按下或者松开A,看看输出面板打印了什么?
- 再次运行一遍上面的代码,看看发生了什么变化,和你的预期一致吗?
上面这些知识都不重要!
按键监听器是一个比较底层的东西,在入门级的mod开发中并不会使用它,因为,当监听器识别到A键被按下时:
- 游戏可能在主界面或者是其他配置界面(玩家还未创建)
- 玩家可能在聊天框打字,输入了一个A
- 玩家可能在控制台敲指令,输入了一个A
- 玩家可能按下A来向左移动
- 可能还有其他骚操作...
按键监听器并不能完美的区分这些不同的情形,所以并不常用。
把上面这些暂时忘掉吧。
游戏内监听器
这里实现一个简单的功能:当按键A被按下时,当前玩家的生命值扣10。
标准
一个合格的游戏内按键监听器应该符合以下标准:
- 当玩家加入世界时才生效,当玩家离开时自动移除。
- 只在进入世界后才生效,在游戏主界面无效果。
- 当玩家在暂停页面或设置页面操作时,不会有反应。
- 当玩家在聊天框或控制台等输入框中打字时,不会有反应。
为了实现1/2/3,我们将按键监听器绑定到玩家界面: screens/playerhud。
为了实现4,通过修改 OnRawKey() 函数排除输入框。
原理
玩家界面是显示玩家信息和一些操作的面板,它包含了右上角三围图标、时钟图标,左侧制造栏,下方物品栏,右侧背包栏,左下聊天框,右下暂停键、地图按钮,以及过热、过冷、烧伤、受伤等时机下触发的全屏特效提示。
- 玩家界面仅在玩家加入世界后才会被创建。
- 主界面上没有玩家界面,进入世界后才有。
- 当暂停页面或者设置页面显示的时候,玩家界面会被盖住,一切输入控制会被上方的界面阻断。
因此,完美符合标准。
接着,看一下 PlayerHud:OnRawKey() 函数的定义:
screens/playerhud.lua |
---|
| function PlayerHud:OnRawKey(key, down)
if PlayerHud._base.OnRawKey(self, key, down) then
return true
elseif down and self.shown and key == KEY_SEMICOLON and TheInput:IsKeyDown(KEY_SHIFT) then
local chat_input_screen = ChatInputScreen(false)
chat_input_screen.chat_edit:SetString(":")
TheFrontEnd:PushScreen(chat_input_screen)
return true
end
end
|
不需要深入了解这个函数的细节,只需要注意一点:当 OnRawKey() 函数“干了什么事情”的时候,它会返回true
。
显然,在输入框打字也算“干了什么事情”,所以为了排除这种情况,当 OnRawKey() 函数返回true
时,不触发按键监听器,否则触发。了解原理后,我们进入实战部分。
实战
设置监听器
创建一个新mod,或者在老mod中添加如下代码:
modmain.lua |
---|
| AddClassPostConstruct("screens/playerhud", function(self)
local old_OnRawKey = self.OnRawKey
function self:OnRawKey(key, down, ...)
if old_OnRawKey(self, key, down, ...) then return true end
if key == KEY_A and down then
print("在这里修改玩家血量")
end
end
end)
|
注意第4行,如果函数执行的返回值为真,就直接返回 true
,跳过下面“修改血量”的代码。
开启mod,进入游戏后,按A键,看看有没有打印出什么东西?
Warning
mod类型是服务器mod。
如果加载mod后崩了,请在modmain的第一行添加:
GLOBAL.setmetatable(env,{__index=function(t,k) return GLOBAL.rawget(GLOBAL,k) end})
排除连击
值得注意的是,长按A键会触发多次打印,这和平时电脑键盘的输入逻辑是一致的:
- 敲一下A键,输入一个A
- 按住A键不松,快速输入一大堆的A
为了排除连击,可以用一个变量记录按键的状态:
modmain.lua |
---|
| AddClassPostConstruct("screens/playerhud", function(self)
local isdown = false
local old_OnRawKey = self.OnRawKey
function self:OnRawKey(key, down, ...)
if old_OnRawKey(self, key, down, ...) then return true end
if key == KEY_A then
if isdown ~= down then
isdown = down
if isdown then
print("在这里修改玩家血量(长按不会连击)")
end
end
end
end
end)
|
判定修饰键
如果需要监听的键位是ctrl+A(只按A不触发),那么加一句判定即可。
modmain.lua |
---|
| AddClassPostConstruct("screens/playerhud", function(self)
local isdown = false
local old_OnRawKey = self.OnRawKey
function self:OnRawKey(key, down, ...)
if old_OnRawKey(self, key, down, ...) then return true end
if key == KEY_A then
if isdown ~= down then
isdown = down
if isdown and TheInput:IsKeyDown(KEY_CTRL) then
print("在这里修改玩家血量(按了ctrl)")
end
end
end
end
end)
|
更多的判定
上面的代码有很强的扩展性,通过一些修改就可以实现更多更复杂的判定,思考一下如何实现下面的功能:
- 按下A时扣血,松开A时加血。
- 按下A或者B时都触发扣血。
- 按下A时扣血,按下B时加血。
- 按下某个按键时扣血,这个按键可以在mod配置界面修改。
- 快速连按两下A才扣血。(提示:
GetTime()
函数可以获取当前游戏运行时间)
如何扣血?
这并不是本章会详细介绍的内容,以下只简单提一下思路:
- 按下按键时发送 mod rpc。
- 扣血使用 health:DoDelta() 函数。
完整示例
modmain.lua |
---|
| GLOBAL.setmetatable(env,{__index=function(t,k) return GLOBAL.rawget(GLOBAL,k) end})
-- 键盘监听 完整示例
AddClassPostConstruct("screens/playerhud", function(self)
local isdown = false
local old_OnRawKey = self.OnRawKey
function self:OnRawKey(key, down, ...)
if old_OnRawKey(self, key, down, ...) then return true end
if key == KEY_A then
if isdown ~= down then
isdown = down
if isdown then
SendModRPCToServer(MOD_RPC["testmod"]["health"])
end
end
end
end
end)
AddModRPCHandler("testmod", "health", function(inst)
inst.components.health:DoDelta(-10)
end)
|
注意,按下A的同时,角色也会向左移动。作为玩家,当按下A时,究竟想要向左走还是触发按键,是有歧义的。因此,在监听按键时,最好不要使用会和原版操作冲突的按键,如WASD。
另外,你可能想实现比扣血更炫酷的功能,比如释放某个技能,这些只需要修改RPC的触发函数即可,按键监听的思路是相通的。
总结
按键监听是一件非常容易的事情,可以看到最终的代码很简洁,只有20行,希望你能完全理解这些代码背后的思路,根据自己的需求修改这些代码,而不是只会copy/paste。
如果要实现非常高级的功能,你可以再回去研究一下通用的监听器,比如 TheInput:AddKeyHandler(fn)。
扩展
以下内容适合有钻研精神的开发者。
OnRawKey() 函数的细节
我们重新看一下 PlayerHud:OnRawKey() 函数的定义:
screens/playerhud.lua, 1083 |
---|
| function PlayerHud:OnRawKey(key, down)
if PlayerHud._base.OnRawKey(self, key, down) then
return true
elseif down and self.shown and key == KEY_SEMICOLON and TheInput:IsKeyDown(KEY_SHIFT) then
local chat_input_screen = ChatInputScreen(false)
chat_input_screen.chat_edit:SetString(":")
TheFrontEnd:PushScreen(chat_input_screen)
return true
end
end
|
注意,在第二行,该函数调用了其基类(._base)的 OnRawKey() 函数。一个类的基类应该查看其构造函数。往上翻源代码,在第45行可以看见:
screens/playerhud.lua, 45 |
---|
| local PlayerHud = Class(Screen, function(self)
Screen._ctor(self, "HUD")
self.overlayroot = self:AddChild(Widget("overlays"))
-- 其他内容...
|
表明 PlayerHud 的基类是 Screen。
接着去看看 Screen 类的定义位置 widgets/screen.lua,会发现里面并没有函数 OnRawKey()。所以我们接着找 Screen 类的基类 Widget,思路和上面一样。
widgets/screen.lua, 4 |
---|
| local Screen = Class(Widget, function(self, name)
Widget._ctor(self, name)
--self.focusstack = {}
--self.focusindex = 0
self.handlers = {}
--self.inst:Hide()
self.is_screen = true
end)
|
widgets/widget.lua, 105 |
---|
| function Widget:OnRawKey(key, down)
if not self.focus then return false end
for k,v in pairs (self.children) do
if v.focus and v:OnRawKey(key, down) then return true end
end
end
|
至此,OnRawKey() 函数的运行细节才被揭开。具体来说,当玩家按下或者松开键盘时,界面会遍历自己的子ui,当遇到任何一个成功执行的 OnRawKey() 函数时,就立刻返回true
,并跳过其他的ui。注意这里有类似递归的现象。
你可能已经猜到为什么 OnRawKey() 可以排除文本输入框了,我把代码贴在下面,尝试分析一下吧。(文本输入ui被定义在 widgets/textedit)
widgets/textedit.lua, 266 |
---|
| function TextEdit:OnRawKey(key, down)
if self.editing and self.prediction_widget ~= nil and self.prediction_widget:OnRawKey(key, down) then
self.editing_enter_down = false
return true
end
if TextEdit._base.OnRawKey(self, key, down) then
self.editing_enter_down = false
return true
end
if self.editing then
if down then
if TheInput:IsPasteKey(key) then
self.pasting = true
local clipboard = TheSim:GetClipboardData()
for i = 1, #clipboard do
local success, overflow = self:OnTextInput(clipboard:sub(i, i))
if overflow then
break
end
end
self.pasting = false
elseif self.allow_newline and key == KEY_ENTER and down then
self:OnTextInput("\n")
else
self.inst.TextEditWidget:OnKeyDown(key)
end
self.editing_enter_down = key == KEY_ENTER
elseif key == KEY_ENTER and not self.focus then
-- this is a fail safe incase the mouse changes the focus widget while editing the text field. We could look into FrontEnd:LockFocus but some screens require focus to be soft (eg: lobbyscreen's chat)
if self.editing_enter_down then
self.editing_enter_down = false
if not self.allow_newline then
self:OnProcess()
end
end
return true
elseif key == KEY_TAB and self.nextTextEditWidget ~= nil then
self.editing_enter_down = false
local nextWidg = self.nextTextEditWidget
if type(nextWidg) == "function" then
nextWidg = nextWidg()
end
if nextWidg ~= nil and type(nextWidg) == "table" and nextWidg.inst.TextEditWidget ~= nil then
self:SetEditing(false)
nextWidg:SetEditing(true)
end
-- self.nextTextEditWidget:OnControl(CONTROL_ACCEPT, false)
else
self.editing_enter_down = false
self.inst.TextEditWidget:OnKeyUp(key)
end
if self.OnTextInputted ~= nil then
self.OnTextInputted()
end
end
--gobble up unregistered valid raw keys, or we will engage debug keys!
return not self.validrawkeys[key]
end
|