文章目录
本文将介绍收获日2的功能性脚本mod制作流程,以本人制作的CF连杀图标和语音mod为例。Mod游戏预览点这里。Mod下载地址:https://modworkshop.net/mod/39464
项目仓库:https://github.com/Eysent/pd2-kill-streak
演示视频:https://www.bilibili.com/video/BV1DK411f7W6/
mod前置库安装和前置mod安装
SuperBlt下载地址:https://superblt.znix.xyz/
SuperBLT是Payday 2游戏的一个插件框架,它允许玩家安装和运行各种自定义MOD来增强游戏体验。它提供了更好的稳定性和性能,并且支持更多的功能和MOD。在上述网址的安装步骤里有详细说明:把下载的dll文件放到游戏根目录下,运行游戏会弹出下载对话框。完成后就出现了mods文件夹,里边就是存放各种mod文件的位置。
几乎所有的SuperBLT提供的Lua函数都有注释,在像VS Code这样的编辑器中工作时会显示提示。关于BLT功能的总体概述,请参见vanilla BLT的文档。
由SuperBLT添加的功能的文档可以在网站导航菜单中找到。
除了SuperBlt,此mod还依赖于HopLib。这个库包含了很多实用的函数,方便我们mod功能的实现。下载后,解压到\PAYDAY 2\mods
文件夹下。
如下所示:
另外,对于需要写脚本的mod来说,我推荐你下载一份源码来进行参考,这将会有利于你的开发工作。仓库地址:https://github.com/mwSora/payday-2-luajit
关于调试和问题定位:如果游戏崩溃了,你可以在C:\Users\你的用户名\AppData\Local\PAYDAY 2
下找到crash.txt
文件,里边有你崩溃的原因说明。
如果没有崩溃,但是没有你预期的效果,你可以在PAYDAY 2\mods\logs
查看日志文件,如果需要还可以mod中使用blt.log
来打印信息。
创建mod文件
在\PAYDAY 2\mods
文件夹下新建一个Kill streak
文件夹,这就是我们mod文件存放的位置。在其中新建一个assets
文件夹,里边将存放我们用到的资源,如图片,语音等等。另外新建一个mod.txt
,这个文件用于说明我们mod的名称、作者、用到的库文件等。
assets 文件夹
在此文件夹中包含了mod所需的美术资源或者声音资源等。文件夹的结构如下所示,其中assets.xml
文件中描述了资源存放的位置。
killicons
文件夹中包含了连杀图标文件,格式后缀是.texture
。可以用其他软件编辑.dds
文件最后再改成.texture
后缀。
这个文件包含了所需的连杀图标,所以mod的实现思路很简单,在对应的击杀数时播放对应的语音和展示对应的图标。Mod所需的资源可以直接从我的项目中复制。(这里可以把图标安排的更紧凑以节省空间,但我不会,所以就只能这样了)
在确定好资源文件后,我们在kill streak
文件夹中新建一个supermod.xml
, 用来告诉游戏加载我们的资源。
<?xml version="1.0"?>
<mod>
<:include src="assets/assets.xml"/>
</mod>
编辑mod.txt
如下,里边包含了mod的基本信息:
{
"name" : "KillStreak",
"author" : "Eysent",
"blt_version" : 2.3,
"hooks" : [
]
}
此时打开游戏,就可以看到新建的mod了,只不过还没添加关键代码,是一个空mod。
实现基础图片显示
新建两个文本文档,分别重命名为KillStreakPanel.lua
和PlayerManager.lua
。
下面我们先对核心功能代码文件进行说明。在KillStreakPanel.lua
中包含对连杀图标的切换控制、语音的播放等内容。
编辑KillStreakPanel.lua
文件,添加如下代码
KillStreakPanel = KillStreakPanel or class()
function KillStreakPanel:init(hud) -- 初始化方法
self._full_hud = hud
self.kills = 0 -- 当前的连杀数
self._headshot = false -- 是否是爆头击杀
-- 图像绘制的面板定义
self._kill_panel = self._full_hud:panel({
name = "kill_panel",
alpha = 0, -- 不透明度为0,就是看不见 不然一开始就会有图标显示
layer = 100,
})
-- 设定哪一个图标,初始化选择第一个图标; _kill_icon是绘制在_kill_panel上的
self._kill_icon = self._kill_panel:bitmap({
vertical = "center", -- 居中
align = "center",
name = "kill_icon",
texture = "guis/textures/killstreak/killicons/streak", -- 资源位置
texture_rect = {0,0,450,450}, -- 选择文件中的显示区域方框大小及位置,这里框住了第一个图标
blend_mode = "normal",
alpha = 1, -- 透明度
w = 450 , -- 显示的长宽高,可以缩放或放大
h = 450 ,
layer = 4
})
self:Set_kill_icon() -- 根据连杀数更改设定显示的图标
self:MakeFine() -- 调整图标位置
end
下面添加Set_kill_icon()
和MakeFine()
函数,在文件中添加以下代码:
function KillStreakPanel:Set_kill_icon()
local index = self.kills -- 判断用哪个图标的变量
if self.kills == 1 and self._headshot then -- 第一个击杀并且是爆头
if math.random() > 0.7 then -- 没办法根据子弹命中位置选用哪个爆头图标,选用概率判断
index = 8 -- 有0.3的概率选用黄金爆头
else
index = 9 -- 0.7的概率选用白金爆头
end
elseif self.kills >7 then -- 连杀图标最多只有这么多
index = 7
end
local x = 0+450*(index-1) -- 根据index判断方框的位置
self._kill_icon:set_texture_rect(x, 0, 450, 450) -- 根据方框更改贴图
end
function KillStreakPanel:MakeFine() -- 在_kill_panel居中显示
self._kill_panel:set_size(self._kill_icon:w() + 2, self._kill_icon:h() + 2)
self._kill_icon:set_center(self._kill_panel:w() / 2, self._kill_panel:h() / 2)
self._kill_panel:set_center(self._full_hud:center_x(), self._full_hud:center_y() )
end
好了现在我们需要告诉脚本我们什么时候统计并更新击杀数,以及进行初始化工作。编辑PlayerManager.lua
文件并添加如下代码:
dofile(ModPath .. "/KillStreakPanel.lua") -- 执行KillStreakPanel.lua
-- Hook文件名 函数名 自定义名称
Hooks:PostHook(PlayerManager, "on_killshot", "KillStreakHook1", function()
KillStreakPanel.kills = KillStreakPanel.kills + 1
KillStreakPanel:Set_kill_icon() -- 更换图标
KillStreakPanel._kill_panel:set_alpha(1) -- 不透明度为1,这样才看得见
end)
Hooks:PostHook(PlayerManager, "on_lethal_headshot_dealt", "KillStreakHook2", function()
KillStreakPanel._headshot = true
end)
-- 在hudmaneger的init_finalize函数执行后调用该代码,完成初始化
Hooks:PostHook(HUDManager,"init_finalize","killstreak_createhud",function(self,...)
KillStreakPanel = KillStreakPanel:new(managers.gui_data:create_fullscreen_workspace():panel())
end)
另外,我们用到了Hook,那就需要在mod.txt
文件中添加我们用到的Hook。
{
"name" : "KillStreak",
"author" : "Eysent",
"blt_version" : 2.3,
"hooks" : [
{"hook_id" : "lib/managers/menumanager", "script_path" : "PlayerManager.lua"},
{"hook_id" : "lib/managers/playermanager", "script_path" : "PlayerManager.lua"}
{"hook_id" : "lib/managers/hudmanager", "script_path" : "PlayerManager.lua"}
]
}
Hook你可以理解为在系统对应的函数调用后调用你自己的代码,也就是决定你的代码什么时候工作。具体的函数列表可以参考第一节末尾提到的源码。比如打开我们下载好的源码,在\payday-2-luajit\pd2-lua\lib\managers
目录下可以看到很多文件,其中就包含了我们在中调用的Hooklib/managers/hudmanager
等。打开可以查看其包含哪些代码:
第189行就是我们在PlayerManager.lua
中初始化KillStreakPanel
的函数。
好了,言归正传,现在我们打开游戏就可以看到击杀图标和它的切换了。
好,现在我们以看到图标会随着击杀数的改变而改变了。但是新的问题来了,我们没有设置连杀数的清零机制,图标也不会消失,图标的大小和位置都需要调整。这些问题我们将在后文中解决。
增加图片的消失动画
在前文我们可以看到mod已经实现了,图片的展示功能以及根据击杀数切换图片显示的功能。但是仍然缺失图片的消失动画和击杀数一段时间后清零的机制。本小节将解决这两个问题。图片的位置调整在后文将通过菜单设置选项来完善。
首先来着手处理图片的渐变消失效果,在KillStreakPanel.lua
中添加一个函数,它的功能就是添加渐变效果:
function KillStreakPanel:Show_fading(rect)
-- fadetime 是开始渐变消失之前的持续时间
local fadetime = 3
local wait_t = fadetime
local t = 0.5 -- 从完全不透明到完全消失的时间
-- 展示图层
self._kill_panel:set_alpha(1)
-- 等待anim_t秒
while wait_t > 0 do
wait_t = wait_t - coroutine.yield()
end
-- fade animation
while t > 0 do
t = t - coroutine.yield()
local n = math.sin((t / 2) * 350)
self._kill_panel:set_alpha(math.lerp(0, 1, n))
end
self.kills = 0
self._kill_panel:set_alpha(0)
self._kill_panel:set_x(self._full_hud:center_x())
end
在此函数中,击杀图标出现的fadetime
秒内不会有渐变消失的效果,在此期间可以正常切换击杀图标。之后的0.5s内击杀图标才会慢慢消失,并且会把击杀数清零。这样击杀图标消失后可以再次触发从0杀开始的图片切换循环。现在我们有了渐变的函数,还需要一个调用渐变动画的函数。再新写一个函数如下:
function KillStreakPanel:SetKills()
self:Set_kill_icon()
self._headshot = false -- 已经有击杀了,就不能再显示爆头图标了
self:MakeFine()
self._kill_panel:stop()
self._kill_panel:animate(callback(self, self, "Show_fading"))
end
另外再修改PlayerManager.lua
中的Hook调用函数KillStreakHook1
:
Hooks:PostHook(PlayerManager, "on_killshot", "KillStreakHook1", function()
KillStreakPanel.kills = KillStreakPanel.kills + 1
KillStreakPanel:SetKills()
end)
好,保存,运行游戏来看看效果吧!
嗯,效果不错,但是好像有点不得劲。图片突然出现,感觉缺少一点打击感。那就再添加一个瞬间变大再复原,并来附加一个短暂的渐变。
添加如下函数:
function KillStreakPanel:Show_brust(panel)
local t = 0
local w = self._kill_icon:w()
while t < 0.1 do
t = t + coroutine.yield()
local n = math.sin((t / 2) * 350)
local t_size = math.lerp( w, 2*w, n)
-- 放大icon并居中,不然会以左上角为原点放大
self._kill_icon:set_size(t_size,t_size)
self._kill_icon:set_center(self._kill_panel:w() / 2, self._kill_panel:h() / 2)
self._kill_panel:set_alpha(math.lerp(0, 1, n))
end
self._kill_panel:set_alpha(1)-- 保证完全显示
self:Reset_icon() -- 回复原来大小并居中
end
function KillStreakPanel:Reset_icon()
self._kill_icon:set_size(450 ,450 )
self:MakeFine()
end
完成上述函数后,还需要调用新加的动画效果,修改SetKills()
如下:
function KillStreakPanel:SetKills()
self:Set_kill_icon()
self._headshot = false
self:MakeFine()
self._kill_panel:stop()
self._kill_panel:animate(callback(self, self, "Show_brust"))
self._kill_panel:animate(callback(self, self, "Show_fading"))
end
好,现在基本的击杀图标显示功能就实现了,接下来就是调整位置和大小了。
添加菜单选项
我们当然可以直接在代码里修改图片的位置和大小,但这样可能不能适应其他用户的使用情况,比如安装了其他HUD mod,可能会存在遮挡的情况。因此,我们需要添加一个游戏内的选项菜单来自定义这些选项,也方便后续控制新添加的变量。
在Mod目录下新建一个Menu.lua
:
_G.KillStreak = _G.KillStreak or {}
KillStreak.ModPath = ModPath
KillStreak.SavePath = SavePath .. "KillStreak.txt" -- 设置保存位置
KillStreak.LocPath = ModPath .. "loc/" -- 本地化保存位置 方便翻译成多国语言
KillStreak.Opt = {}
KillStreak.OptMenuId = "KillStreakOptions"
function KillStreak:Init()
dofile(self.ModPath .. "/KillStreakPanel.lua")
self:Load()
self.Opt.fadetime = self.Opt.fadetime or 3 -- 后边的数字是默认设置
self.Opt.pos = self.Opt.pos or 142
self.Opt.scale = self.Opt.scale or 0.35
self.Opt.alpha = self.Opt.alpha or 1.0
HopLib:load_localization(KillStreak.LocPath, managers.localization)
self.Panel = KillStreakPanel:new(managers.gui_data:create_fullscreen_workspace():panel())
self.InitDone = true
end
function KillStreak:Save()
local file = io.open(self.SavePath, "w+")
if file then
file:write(json.encode(self.Opt))
file:close()
end
end
function KillStreak:Load()
local file = io.open(self.SavePath, "r")
if file then
self.Opt = json.decode(file:read("*all"))
file:close()
end
end
function MenuCallbackHandler:KillStreak_value(item)
KillStreak.Opt[item._parameters.name:gsub("KillStreak_", "")] = item:value()
KillStreak:Save()
KillStreak.Panel:Reset_icon()
end
Hooks:Add("MenuManagerPopulateCustomMenus", "KillStreakOptions", function(self, nodes)
if not KillStreak.InitDone then
KillStreak:Init()
end
-- 添加各种选项
MenuHelper:NewMenu(KillStreak.OptMenuId)
MenuHelper:AddSlider({
id = "KillStreak_fadetime",
title = "KillStreak_fadetime_title",
callback = "KillStreak_value",
menu_id = KillStreak.OptMenuId,
max = 60,
min = 0.5,
step = 0.1,
show_value = true,
value = KillStreak.Opt.fadetime,
})
MenuHelper:AddSlider({
id = "KillStreak_pos",
title = "KillStreak_pos_title",
callback = "KillStreak_value",
menu_id = KillStreak.OptMenuId,
max = 300,
min = 16,
step = 1,
show_value = true,
value = KillStreak.Opt.pos,
})
MenuHelper:AddSlider({
id = "KillStreak_scale",
title = "KillStreak_scale_title",
callback = "KillStreak_value",
menu_id = KillStreak.OptMenuId,
max = 2.0,
min = 0.1,
step = 0.1,
show_value = true,
value = KillStreak.Opt.scale,
})
MenuHelper:AddSlider({
id = "KillStreak_alpha",
title = "KillStreak_alpha_title",
callback = "KillStreak_value",
menu_id = KillStreak.OptMenuId,
max = 1.0,
min = 0.0,
step = 0.1,
show_value = true,
value = KillStreak.Opt.alpha,
})
nodes[KillStreak.OptMenuId] = MenuHelper:BuildMenu(KillStreak.OptMenuId)
MenuHelper:AddMenuItem(nodes.lua_mod_options_menu or nodes.blt_options, KillStreak.OptMenuId, "KillStreak_options_title", "KillStreak_options_desc")
end)
在mod目录中新建一个loc
文件夹,里边放翻译文件,这里我们先创建简体中文的文件schinese.txt
。
{
"KillStreak_options_title" : "连杀图标&语音选项",
"KillStreak_options_desc" : "设定参数",
"KillStreak_fadetime_title" : "连杀持续时间",
"KillStreak_pos_title" : "连杀图标y轴偏移",
"KillStreak_scale_title" : "连杀图标缩放因子",
"KillStreak_alpha_title" : "图标透明度",
}
为了方便,这里统一把KillStreakPanel
的实例化放在这里,所以对应的PlayerManager.lua
也要修改,让它的代码更简洁。
Hooks:PostHook(PlayerManager, "on_killshot", "KillStreakHook1", function()
KillStreak.Panel.kills = KillStreak.Panel.kills + 1
KillStreak.Panel:SetKills()
end)
Hooks:PostHook(PlayerManager, "on_lethal_headshot_dealt", "KillStreakHook2", function()
KillStreak.Panel._headshot = true
end)
因为更改了Hook相关代码,这里更新下mod.txt
{
"name" : "KillStreak",
"author" : "Eysent",
"blt_version" : 2.3,
"hooks" : [
{"hook_id" : "lib/managers/playermanager", "script_path" : "PlayerManager.lua"},
{"hook_id" : "lib/managers/menumanager", "script_path" : "Menu.lua"},
{"hook_id" : "lib/states/ingameplayerbase", "script_path" : "Menu.lua"},
]
}
最后,我们把相关参数应用到以下几个函数中:
function KillStreakPanel:init(hud)
self._full_hud = hud
self.kills = 0
self._headshot = false
self._kill_panel = self._full_hud:panel({
name = "kill_panel",
alpha = 0,
layer = 100,
})
self._kill_icon = self._kill_panel:bitmap({
vertical = "center",
align = "center",
name = "kill_icon",
texture = "guis/textures/killstreak/killicons/streak",
texture_rect = {0,0,450,450},
blend_mode = "normal",
alpha = KillStreak.Opt.alpha, -- 自定义参数
w = 450 * KillStreak.Opt.scale,
h = 450 * KillStreak.Opt.scale,
layer = 4
})
self:Set_kill_icon()
self:MakeFine()
end
function KillStreakPanel:MakeFine()
self._kill_panel:set_size(self._kill_icon:w() + 2, self._kill_icon:h() + 2)
self._kill_icon:set_center(self._kill_panel:w() / 2, self._kill_panel:h() / 2)
self._kill_panel:set_center(self._full_hud:center_x(), self._full_hud:center_y() + KillStreak.Opt.pos) -- Y轴位置
end
function KillStreakPanel:Show_fading(rect)
-- fadetime 是开始渐变消失之前的持续时间
local fadetime = KillStreak.Opt.fadetime -- 持续时间
local wait_t = fadetime
local t = 0.5 -- 从完全不透明到完全消失的时间
-- 展示图层
self._kill_panel:set_alpha(1)
-- 等待anim_t秒
while wait_t > 0 do
wait_t = wait_t - coroutine.yield()
end
-- fade animation
while t > 0 do
t = t - coroutine.yield()
local n = math.sin((t / 2) * 350)
self._kill_panel:set_alpha(math.lerp(0, 1, n))
end
self.kills = 0
self._kill_panel:set_alpha(0)
self._kill_panel:set_x(self._full_hud:center_x())
end
function KillStreakPanel:Reset_icon()
self._kill_icon:set_size(450*KillStreak.Opt.scale ,450*KillStreak.Opt.scale) -- 大小
self:MakeFine()
end
击杀图标的部分告一段落,下边就是最后一步,添加击杀语音。
添加语音提示
首先,从网上准备语音素材,我找到了部分CF角色的语音包,大概有这几个:"猎狐者", "零", "瞳", "毛妹"(不知道是谁,听起来有俄罗斯口音),"夏日之兰","奥摩","飞虎队"。不知道语音跟人对不对的上,毕竟我退坑很久了,大部分角色语音都没听过。每个角色都包含以下语音,通过名字很清楚了解到对应什么语音。如果你要自定义的话,替换同名文件就可以了。
关于语音的菜单部分,即Menu.lua
,这里就不贴出来了,和前文大同小异,只是多几个而已。在KillStreakPanel.lua
中添加的函数如下:
function KillStreakPanel:play_kill_sound()
blt.xaudio.setup()
local table_index = self.kills
if self.kills ==1 and not self._headshot then
return
elseif self.kills > 8 then
table_index = 8
end
local filename = self:get_sound_filename(table_index)
local buffer = XAudio.Buffer:new(filename)
table.insert(self._sound_sources,XAudio.Source:new(buffer))
end
function KillStreakPanel:update_sound_sources()
for i, src in ipairs(self._sound_sources) do
if src then
if not src:is_closed() then
blt.xaudio.setup()
src:set_volume(KillStreak.Opt.volume/100)
if managers.player:player_unit() then
src:set_position(managers.player:player_unit():position())
end
else
src = nil
end
end
end
end
function KillStreakPanel:play_voices(voice_type)
if KillStreak.Opt.enable_sound then
blt.xaudio.setup()
local filename = self:get_sound_filename(voice_type)
local buffer = XAudio.Buffer:new(filename)
table.insert(self._sound_sources,XAudio.Source:new(buffer))
end
end
function KillStreakPanel:get_sound_filename(voice_type)
local filename = KillStreak.ModPath .. "assets/sounds/"
--filename = filename .. "women1"
filename = filename .. KillStreak.voices[KillStreak.Opt.voice_source].src
filename = filename .. "/"
filename = filename .. self._ogg_table[voice_type]
return filename
end
修改部分函数:
function KillStreakPanel:init(hud)
self._full_hud = hud
self.kills = 0
self._headshot = false
self._ogg_table = {
--[0] = "Knifekill.ogg"
[1] = "Headshot.ogg",
[2] = "MultiKill_2.ogg",
[3] = "MultiKill_3.ogg",
[4] = "MultiKill_4.ogg",
[5] = "MultiKill_5.ogg",
[6] = "MultiKill_6.ogg",
[7] = "MultiKill_7.ogg",
[8] = "MultiKill_8.ogg",
[9] = "GrenadeKill.ogg",
[10] = "Round_Start.ogg",
[11] = "Fireinthehole_Grenade.ogg",
}
self._sound_sources = {}
... ...
... ...
end
function KillStreakPanel:SetKills()
self:Set_kill_icon()
if KillStreak.Opt.enable_sound then
self:play_kill_sound()
end
self._headshot = false
self:MakeFine()
self._kill_panel:stop()
self._kill_panel:animate(callback(self, self, "Show_brust"))
self._kill_panel:animate(callback(self, self, "Show_fading"))
end
最后在PlayerManager.lua
中添加扔手雷等其他语音函数并修改mod.txt
:
Hooks:PostHook(PlayerManager, "on_throw_grenade", "killsteak_play_grenade_voice", function()
KillStreak.Panel:play_voices(11)
end)
Hooks:PostHook(HUDManager, "update", "killstreak_update_sound", function (self, ...)
KillStreak.Panel:update_sound_sources(...)
end)
-- use voice manager to make character speak one setence at same time. see https://superblt.znix.xyz/doc/xaudio/
Hooks:PostHook(IngameMaskOffState, "at_exit", "killsteak_play_start_voice", function (self, ...)
KillStreak.Panel:play_voices(10)
end)
{
"name" : "KillStreak",
"author" : "Eysent",
"description" : "Add Crossfire kill streak icons and voices.",
"blt_version" : 2.3,
"hooks" : [
{"hook_id" : "lib/managers/playermanager", "script_path" : "PlayerManager.lua"},
{"hook_id" : "lib/managers/menumanager", "script_path" : "Menu.lua"},
{"hook_id" : "lib/states/ingameplayerbase", "script_path" : "Menu.lua"},
{"hook_id" : "lib/managers/hudmanager", "script_path" : "PlayerManager.lua"}
]
}
大功告成!希望你也可以自定义出自己的MOD,如有问题或者反馈可以留言或者联系我。
0