0%

饥荒mod-学习笔记

饥荒源码

位置:游戏安装目录/data/databundles/scripts.zip

饥荒中lua版本: 5.1

饥荒中的require

和lua 中是一致的,经过个人实验,在mod中增加和饥荒源码中一致的模块,并require,返回的是饥荒自带的模块,而非我mod中的

scripts目录

  • behavious: 行为,给brains目录里定义的类使用的
  • brains: 大脑,可以理解为AI,定义各种生物的默认行为的,比如猪人闲逛,看见东西就吃,看见蜘蛛就打等
  • cameras: 摄像头,引擎上的东西,不用管它
  • components: 各种各样的组件,如果想写大型mod,最好把这里面每个组件都看一遍,学习一下klei怎么定义组件的
  • languages: 各国语言翻译
  • map: 游戏中的地形生成的定义。比如马赛克地形,月岛地形,都是在这里面定义的
  • prefabs: 预制体,饥荒里的所有实体都是预制体,比如人物,花,鸟,鱼,树等等
  • scenarios: 情景,游戏中的彩蛋
  • screens: 场景,游戏里每个界面都是一个场景,比如创建世界,生成世界,加载世界,进入游戏的界面都是场景
  • stategraphs: 状态图(区别于状态机,饥荒把行为逻辑抽到了行为树(behavious)里,stategraph就只管理动画逻辑),定义不同状态播放什么动作,比如人物站立时的动画,猎犬攻击的动画,同样还能定义不同状态下的event(事件),handler(处理程序)
  • tools/utils: 各种工具
  • widgets: 小窗口,比如显示血量,季节的那个表盘就是 widget

存档与日志

客户端日志保存路径:

C:\Users\Administrator\Documents\Klei\DoNotStarveTogether\client_log.txt

服务器日志保存路径:

C:\Users\Administrator\Documents\Klei\DoNotStarveTogether\用户id目录(全数字)\Cluster_x(x为数字,对应创建的哪个世界)\server_log.txt

注意,如果没开洞穴,那就不会有服务器日志,全都保存在客户端日志中

API

scripts/modutil.lua

官方提供的可在自定义mod直接调用的API,定义在源码scripts/modutil.lua中,但需要增加GLOBAL.作为前缀才能访问.不过有些是不需要的,具体哪些需要的可查看源码scripts/mods.lua中env变量,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function CreateEnvironment(modname, isworldgen, isfrontend)

local modutil = require("modutil")
require("map/lockandkey")
local env =
{
-- lua
pairs = pairs,
ipairs = ipairs,
print = print,
math = math,
table = table,
type = type,
string = string,
tostring = tostring,
require = require,
Class = Class,

-- runtime
TUNING=TUNING,

-- worldgen
LEVELCATEGORY = LEVELCATEGORY,
GROUND = GROUND,
LOCKS = LOCKS,
KEYS = KEYS,
LEVELTYPE = LEVELTYPE,

-- utility
GLOBAL = _G,
modname = modname,
MODROOT = MODS_ROOT..modname.."/",
}
...
end

另外如果是在自定义mod文件目录/modmain.lua文件中,则均不需要增加GLOBAL.前缀

scripts/mainfunctions.lua

这里非local修复的变量或方法都可直接使用,比如经常使用的CreateEntity(name)方法(大部分代码使用时不传参数),具体原理暂不清楚.

全局对象

可查看源码scripts/strict.lua中:

1
2
3
4
5
6
-- 凡是调用次的方法的变量都是全局的
function global(...)
for _, v in ipairs{...} do mt.__declared[v] = true end
end

global("MAIN", "WORLDGEN_MAIN")

有一半全局对象都是在源码scripts目录/main.lua中声明的.

按键KEY

1
2
3
4
5
GLOBAL.TheInput:AddKeyHandler(function(key, down)  -- 监听键盘事件
if down then
print(key) -- 这里会把键盘按下的每个键的code值都给打出来
end
end)

具体每个按键的key值,可在源码scripts/constants.lua中查看(搜索KEY_可快速定位)

自定义mod文件目录

1
2
3
4
5
6
7
8
9
anim/:自定义动画文件,可不建立
images/:自定义图片文件,可不建立
sound/: 自定义声音文件,可不建立
exported/:开发使用,实际发布不需要
scripts/:代码文件,可不建立
modicon.tex: mod的图标图片
modicon.xml: 裁剪mod的图标图片的文件,用来定义tex图片
modinfo.lua: mod的相关信息的设置
modmain.lua: 游戏启动时会自动加载的文件,在里面可以调用饥荒暴露出来的api

资源

资源种类

图片

存放位置: modname/images/

图片需要png格式,然后转换成tex和xml文件,也就是每一个png文件都有两个tex和xml文件

动画

存放位置: modname/anim/

动画需要scml文件,通过spriter软件制作,然后放入modname/exported文件下(此处略过了细节,具体请百度),通过使用饥荒提供的Dont Starve Together Mod Tools软件中的autocompiler.exe,双击启动后会自动转换为压缩包.且该压缩包已放入modname/anim文件夹下.

压缩包包含以下文件:

  1. anim.bin: 动画相关
  2. atlas-0.tex(可选): 图片相关,实际如果图片很多,可能还有atlas-1.tex…
  3. build.bin(可选): 图片对应的xml文件

atlas-0.texbuild.bin可不包括是因为如果对应资源已存在的话,只需要动画相关即可.比如: 饥荒官方的动画文件player_xx.zip,都只有anim.bin文件,其它文件都已经被包含在wilson.zip/wilson_mount.zip或其它资源包中,wilson.zip含有正常人物的基本贴图,wilson_mount.zip是骑牛时候的基本人物贴图.

反编译

通过上述文件反编译时需要注意,如果反编译时.tex文件和build.bin文件不全,会导致丢失缺失的图片资源,对应的通道也会消失.

通道: Spriter打开scml文件后,右上角Palette栏中,文件夹的名称就是通道名称,其文件夹下包括对应的正视图,侧视图,后视图,放置地面上的图等等

资源加载

饥荒的资源加载(图片和动画),分为两种:

  1. modmain文件中指定Assets

    1
    2
    3
    4
    5
    6
    7
    Assets = {
    --科技栏的
    Asset("ATLAS", "images/tab/doraemon_tab.xml"),
    Asset("IMAGE", "images/tab/doraemon_tab.tex"),--可不写,只写加载xml文件即可
    --人物动画
    Asset("ANIM", "anim/doraemon_fly.zip"),--动画
    }
  2. 创建prefab时传入的assets

    1
    2
    3
    4
    5
    6
    7
      local assets ={
    Asset("ANIM", "anim/bamboo_dragonfly.zip"),--动画
    Asset("ATLAS", "images/inventoryimages/bamboo_dragonfly.xml"),--物品栏贴图
    Asset("IMAGE", "images/inventoryimages/bamboo_dragonfly.tex"),
    Asset("SOUND", "sound/common.fsb"),--旋风扇声音
    }
    return Prefab(prefab, fn, assets)
modmain中的assets

一般这里必须要有mod的对应图标,另外如果有额外的人物自定义动画也请放在这里,有些mod(神话主题mod)放在了特效的prefab中,但我不清楚其中的原理

Prefab中的assets

一般来说和该prefab有关的资源都放在这里

modinfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
-- 名称
name = "demo"
-- 描述
description = [[
Sample Mod
]]
-- 作者
author = "谅直"
-- 版本
version = "1.0"
-- klei官方论坛地址,为空则默认是工坊的地址
forumthread = ""
-- mod图标
icon_atlas = "modicon.xml"
icon = "modicon.tex"
-- 兼容联机版
dst_compatible = true
-- 兼容单机版
dont_starve_compatible = false
-- 是否是客户端mod
client_only_mod = true
-- 是否是所有客户端都需要安装
all_clients_require_mod = false
-- 饥荒api版本,联机版固定填10,单机固定为6好像
api_version = 10
-- 服务器标签可以不写
server_filter_tags = {""}
-- mod的设置
configuration_options = {
{
name = "language", -- 唯一key值,用于获取该项配置的值,不显示在UI上
label = "语言/Language" -- 该配制的文字说明,会显示在UI上
hover = "设置语言/Select Your Language", -- 鼠标移到配置项上时所显示的信息
options = {{ -- 可选设置
description = "中文", -- 可选项上显示的内容
hover = "中国人选这个", -- 鼠标移动到可选项上显示的信息
-- 可选项选中时的值,树枝类型可以为整形,布尔,浮点,字符串
data = 1
}, {
description = "英文",
hover = "外国人选这个",
data = 2
},},
default = 1 - 默认
},
{
同上
}
}

modmain

import,require,modimport

进阶讲解

装备栏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
--饥荒源码:
EQUIPSLOTS =
{
HANDS = "hands",
HEAD = "head",
BODY = "body",
}

--一般mod扩充后
EQUIPSLOTS =
{
HANDS = "hands",-- 手持
HEAD = "head",-- 头部
BODY = "body",-- 衣服
NECT = "neck",-- 项链
PACK = "pack",-- 背包 可能存在
BACK = "BACK",-- 背包 可能存在
}

动画

图层: spriter左边图片名称

通道: spriter右边图片文件夹名称

inst.AnimState.show(“ARM_carry”)/hide(“ARM_normal”): 能显示或隐藏所有以”ARM_carry/ARM_carry_XXX”或”ARM_normal/ARM_normal_XXX”命名的图层(X:0-9,XXX最好以000开始依次排序,我没测试过乱序的情况),注意如果有多个重复图层,会一起被隐藏.比如:

1
2
3
4
5
6
ARM_carry
ARM_carry_000
ARM_carry_001
ARM_carry_002
ARM_carry_003
...

另外关于动画通道名称建议和原版保持一致,比如覆盖swap_body通道,那你的动画文件名称动画通道也叫swap_body,且通道下的名称都以swap_body-数字格式命名

视图
  1. 正视图–>DOWN
  2. 右侧视图–>SIDE
  3. 左侧视图不需要提供,在游戏中是右侧视图的水平翻转
  4. 后视图–>UP,后视图在游戏中会增加水平翻转,所以设计的时候需要注意
武器动画

地上: 动画名称可随便命名,在该武器构建函数中直接播放该动画,动画文件名称记得和bank保持一致
手上: 动画名称必须是BUILD,其所属动画entity的名称并不重要,也可以和地上动画放在同一个动画entity下.

1
2
3
4
5
6
function equip()
--参数有三个,第一个是旧通道名(文件夹名称),第二个是新的build名(scml文件名),第三个是新的通道名
owner.AnimState:OverrideSymbol("swap_object", TUNING.DORAEMON_TECH.MAGIC_FLASHLIGHT_PREFAB, "swap_magicflashlight")
end

function unequip()end --不需要特殊处理,即清除通道没必要
衣服动画

衣服/护符的贴图较多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- swap_body通道下:
swap_body-0.png -- 正视图,展示在DOWN
swap_body-1.png -- 正视图,向右倾斜,被攻击时展示
swap_body-2.png -- 正视图,向左倾斜,攻击时展示
swap_body-3.png -- 侧视图,展示在SIDE,假定"\":垂直夹角为45°,"|":垂直夹角为0°,那当前视图夹角45°
swap_body-4.png -- 侧视图,向下倾斜,展示未确定,当前视图夹角25°,猜测和正视图一样
swap_body-5.png -- 侧视图,向上倾斜,展示未确定,当前视图夹角65°,猜测和正视图一样
swap_body-6.png -- 后视图,展示在UP
swap_body-7.png -- 后视图,在6基础上向左旋转(向右倾斜)20°左右,展示未确定,猜测和正视图一样
swap_body-8.png -- 后视图,在6基础上向右旋转(向左倾斜)20°左右,展示未确定,猜测和正视图一样
swap_body-14.png -- 正视图,没有项链相关的东西,展示未确定,应该是重生动画相关
swap_body-15.png -- 侧视图,只有项链相关,假定"/":垂直夹角45°,"|":垂直夹角0°,那当前视图夹角10°左右,应该是重生动画相关

-- 使用
owner.AnimState:OverrideSymbol("swap_body", "evil_passport", "swap_body")

-- ground通道下:
ground-0.png

-- 使用
在创建prefab函数中,播放地面动画即可

一般的,贴图0~8是必须的,其余并不是必须的

帽子动画

其中要说明的是: 图片中红字说明以’特殊’开头的图片都不是必须的,尤其swap_hat-5~swap_hat-7均为我自己mod需要,在制作自定义动画时我使用到了这几个文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
--以下代码抄自联机武器衣服护甲模板mod
local function onequiphat(inst, owner) --装备的函数
owner.AnimState:OverrideSymbol("swap_hat", "bamboo_dragonfly", "swap_hat")
owner.AnimState:Show("HAT")
owner.AnimState:Show("HAT_HAIR")
owner.AnimState:Hide("HAIR_NOHAT")
owner.AnimState:Hide("HAIR")
if owner:HasTag("player") then --隐藏head 显示head——hat
owner.AnimState:Hide("HEAD")
owner.AnimState:Show("HEAD_HAT")
end
end

local function onunequiphat(inst, owner) --解除帽子
owner.AnimState:Hide("HAT")
owner.AnimState:Hide("HAT_HAIR")
owner.AnimState:Show("HAIR_NOHAT")
owner.AnimState:Show("HAIR")
if owner:HasTag("player") then
owner.AnimState:Show("HEAD")
owner.AnimState:Hide("HEAD_HAT")
end
end

local function opentop_onequip(inst, owner)
--这里其实跟装备是一样的 唯一的区别是这个不会隐藏head 这样适用于花环之类的不会遮住头发的帽子
owner.AnimState:OverrideSymbol("swap_hat", "bamboo_dragonfly", "swap_hat")
owner.AnimState:Show("HAT")
owner.AnimState:Hide("HAT_HAIR")
owner.AnimState:Show("HAIR_NOHAT")
owner.AnimState:Show("HAIR")

owner.AnimState:Show("HEAD")
owner.AnimState:Hide("HEAD_HAIR")
if inst.components.fueled ~= nil then
inst.components.fueled:StartConsuming()
end
end
地上动画补充说明

地上动画记得中心点要放在图片中央,另外地上动画透明背景应该要和图像本身大小差不多,不可以过多.

如果在游戏中捡起动作很难出现,请重新设置下图片中心点,再打包一次.另外也有大佬建议:可以反编译动画,修改可选择区域.

反编译工具

Action与StateGraph,componentactions

State主要用于播放动画,Action即为动作效果,二者并发执行(先进入state,在state中(客户端)调用PerformPreviewBufferedAction或(服务器)调用PerformBufferedAction后执行动作fn,如果开启了延迟补偿则前者会生效,否则只有后者也就是服务器调用的那个方法生效)

Action的主要属性
  1. priority: 优先级,如果动作收集器收到了多个可以触发的动作,优先度最高的会被允许触发
  2. str: 文字描述(玩家能看到的)
  3. mount_valid: 该动作是否允许骑乘时触发
  4. encumbered_valid: 该动作是否允许背重物时触发
  5. instant: 立刻,即是否为无视距离,true:无视距离立刻触发,false:需先走到最小触发距离再触发
  6. distance: 最小触发距离
  7. rmb: 是否为只能鼠标右键触发,true:只能鼠标右键
  8. fn(act): 动作函数,act类型为BufferedAction
Action的类别

详见源码文件componentactions.lua

  1. SCENE: 点击一个物品栏上的实体或者世界上实体(需要拥有对应绑定的组件).例如:收集动作
  2. USEITEM: 拿起拥有对应绑定组件的实体的场景,例如:缝纫包
  3. POINT: 装备一个手持物品,或将一个物品拿起来.这是唯一一个可以对着地面而不是一个具体的物品作为变量的动作.例如:陷阱,种植,橙色法杖的闪现
  4. EQUIPPED: 装备拥有对应绑定组件物品后的场景,例如:装备一个武器后的技能,火把
  5. INVENTORY: 右键物品栏某项物品,且该物品拥有对应绑定组件的场景.例如:吃东西,装备物品
State的主要属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
State{
name = "", -- 唯一标识
tags{"busy","",...}, -- 标签,busy:不可打断
onenter = function(inst,params), -- 进入该状态时执行的函数,inst:绑定该状态的实体,params:需要传递的参数
onexit = function(inst), -- 同上,退出该状态时会执行的函数
ontimeout = function(inst), -- 需要在onenter函数中,先调用inst.sg:SetTimeout(time),time单位为秒,饥荒钟一帧为1/30秒
timeline ={ -- 用于流程控制或播放多个动画
TimeEvent(10 * FRAMES ,function(inst) end),-- 进入该状态10帧后执行
TimeEvent(20 * FRAMES ,function(inst) end),-- 进入该状态20帧后执行
},
events = { -- 事件
EventHandler("animover",function(inst) end) -- 使用EventHandle监听某个事件
}
}
状态的跳转
1
inst.sg:GoToState(statename,params) -- 跳转到某一状态
状态的延迟补偿
  1. 如果开启了延迟补偿

  1. 如果关闭了延迟补偿

触发动作 -> 客机发送RPC(即:执行inst:PerformPreviewBufferedAction()) -> 服务器收到,并触发state,执行动画->动作fn函数,且会同步到客户端上

componentactions

Action与state构成了一个完整的动作,但如何触发动作?

这时候需要ComponentAction来完成.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
local fn = function(...)
local actions = GLOBAL.select (-2,...)
for _,data in pairs(v.tests) do
if data then
if data.testfn and data.testfn(...) then
data.action_id = string.upper( data.action_id )
table.insert( actions, GLOBAL.ACTIONS[data.action_id] )
end
end
end
end
-- v.type: 即动作的类别,SCENE等
-- v.component: 挂在什么组件上,只有拥有这个组件的实体才能触发(筛选)动作
-- fn: 判断函数,如果需要添加该动作,则调用table.insert( actions, 被插入的动作)
-- 客户端显示时,会显示优先级别最高的动作
AddComponentAction(v.type, v.component, fn)

另外关于fn函数或者上述代码中fn函数中的testfn函数都要考虑到一点: 客户端.客户端执行这行代码时是拿不到一些组件的,所以通常建议modder在此处通过标签来实现是否可以触发动作的判断.

网络(单机与联机的差异)

AddNetwork

1
inst:AddNetwork()

当你添加上面这条语句时,就会让这个prefab能被所有人看见(如果它本身是可见的)。否则,只能被创建这个物品的电脑看到。比如说,几何种植显示的那些方格,其实也是prefab,但没有添加上面这条语句,所以只能被种植者本人看到,不会被其他玩家看到。

主机判断和SetPristine

1
2
3
4
5
6
--标识该行代码之后不需要网络
inst.entity:SetPristine()
--如果不是服务器,则结束
if not TheWorld.ismastersim then
return inst
end

上面这个if判断块在几乎所有的prefab中都十分重要,代码的含义是,如果这段代码运行在客机上,那么就到这里就结束prefab的初始化了。
因为在联机里,客机上存在的组件是极少的。包括物品栏组件inventoryitem,人物三围hunger,sanity,health在内的众多组件都是不存在的,所以如果不上面的那段代码,客机就会运行执行后面的代码,但由于相应的组件不存在,就一定会造成客机游戏崩溃,这也是单机转联机的人普遍会遇到的第一个崩溃问题。相应的,Tag,以及其他会被所有玩家看到的内容,比如说动画表现(AnimState),形态(Transform),人物说话的文字(talker)等,就必须加在上面这段代码之前,否则客机就看不到相应的界面表现了。

另外,对联机版的人物,初始化函数分成了两部分,一部分是common_postinit,另一部分是master_postinit。前者是会在主机和客机上都执行的代码,后者则只在主机上执行。上面的if判断已经内置在了MakePlayerCharacter函数中,所以无需再添加。

主客机数据交互

如果你想要做复杂的,深入游戏核心的联机版MOD,主客机的数据交互几乎是不可能绕过去的。如果处理得不好,造成主客机数据不同步,轻则无法顺利实现自己想要的功能,重则直接崩掉服务器(有一段时间,精灵公主联机版不稳定,经常崩服务器,就是因为没有处理好这个问题。如果你想要做复杂的MOD,又想保证一定的可靠性,那么就必须要深入理解这一段的知识。

基本机制

联机版的主机,就和单机版一样,有着几乎所有的prefab,component,stategraph和brain。但在客机上,prefab依然存在,但它们只会运行其中一小部分代码(在TheWorld.ismastersim的判断块之前的代码)。这一小部分代码,只会给prefab添加少量与界面交互有关的组件,比如动画、图片或者文字表现,或者是直接关系者游戏操作的,比如:talker,transparentonsanity,playercontroller, 和playeractionpicker。brain和stategraph都只存在于主机上。相应的,为了使得大多数组件也能在客机上流程地运转,游戏在客机上使用了replica和classified(为什么不直接使用组件?因为这会造成数据的歧义性。比如说一个人物,吃了食物之后,会加饱食度。吃东西这个行为是发生在客机上的,如果客机有Hunger组件,那么调用客机的hunger组件,增加饱食度,那么在客机上,这个人物的饱食就变化了,但主机上的饱食并没有变化,两者的数据就不一致了。为了解决这个问题,就需要把数据都放在主机上,客机只负责接收来自主机的更新数据。)

主客机之间的数据交换,则是通过一个双重系统——remote procedure call(RPC)和netvar来操作。客机通过发送RPC码向主机发出要求。主机则改变netvar的值来向客机传递数据(大多数被储存在了classified中)

Replica

Replica是component的副件,与component不同,不管是主机还客机,replica都是必定会存在的,replica的主要用途就是帮助客机玩家流畅地完成原本component要完成的操作,但这些操作通常都只是游戏界面的变化,比如说播放动画,显示文字之类的,较少涉及到与主机的数据交换(数据交换的工作,主要由classified完成)。在主机中调用component的函数,如果在replica中存在同名函数,也会被同时调用。利用这个特性,可以在同名函数的方法体中。对主机,执行更新客机数据的代码,对客机,执行动画之类的操作(如果有必要的话)。(对主客机执行不同的代码,可以用上面提到的TheWorld.ismastersim这个变量来区分,或者用TheNet:IsServer()这个函数。)

如果你想为自己自定义的新组件设置replica的话,只需要两步便可完成。
1、在components文件夹下,新建一个文件,文件名为”组件名_replica.lua”,文件里的定义格式同一般的组件,内容则自行决定。
2、在modmain中,添加一行代码AddReplicableComponent(“组件名”)
这样一来,你便有了一个自定义的replica。
具体的实例,请参看我的联机版samansha,里面有一个sa_car的replica。

Netvar&Classified

Classified和netvar的联系非常紧密,要解释Classified就需要解释netvar,这里就一并讲了。

Netvar

Netvar,正如其名,是网络变量的意思。Netvar是官方设计用来从主机向客机传递数据的一组数据类型,它们的定义和用法,在netvar.lua里有官方的说明。官方也写过一个教程并配上了相应的示例MOD。不过我觉得官方写得并不够清晰,让我当初学习的时候走了不少弯路。这里按照我自己的理解来写一下,希望能帮到各位读者。

  1. 声明网络变量

    对于有C或java之类强类型语言基础的读者来说,都很清楚声明变量时必须要声明数据类型。netvar也一样。netvar有多种数据类型以供不同规模的数据传输需要,比如net_bool-1位(true,false),net_tinybyte-3位(0-7)等等,具体可以在netvar中查到。在实际使用中,为了节约带宽,我们应当尽量选小数据类型来用。声明一个网络变量的格式为:ReferenceName = NetvarType(entity.GUID, “UniqueName”, “DirtyEvent” 。ReferenceName是引用名,NetvarType就是网络变量的数据类型,entity.GUID就是这个网络变量要依附的entity的GUID,一般来说,这个网络变量是谁的,就依附谁。比如说你把这个网络变量写在了一个人物的初始化函数里,那么这个entiy就是inst。UniqueName是唯一名,只需要保证你的网络变量不与其它网络变量重名就行,DirtyEvent是当这个网络变量的数据发生改变时,会触发的事件,一般称为这个网络变量的dirty事件。Dirty事件会在主机和客机上都触发,客机可以利用这一个事件来改变HUD的状态,对做自定义的UI非常有用。

    一个示例:inst.level = net_smallbyte(inst.GUID,"MyLevel","leveldirty")

    另外,网络变量必须要在主机和客机中都有声明,也就是说,如果你想给人物添加一个网络变量,必须要写在common_init函数里。写在其它prefab里,则要在TheWorld.ismastersim的判断语句块之前写。如果要写在组件里,则必须要保证组件也在客机上存在(否则你应该写在replica里)。

  2. 使用网络变量

    网络变量不可直接赋值,也不可直接读取,需要调用函数来完成。有三个函数可以用,分别是set,value和set_local,具体用法如下:

    1
    2
    3
    netvar:set(x)--只能在主机端调用这个函数,会自动同步客机的数据(在一个新的同步周期开始时)。如果这个函数确实改变了netvar的值,会在主机和客机上都触发相应的dirty事件。
    netvar:value()--可以在主机和客机上调用这个函数,读取当前网络变量的值。
    netvar:set_local(x)--可以在主机或客机上调用,改变相应的值但不触发数据同步或dirty事件。但当主机下一次调用set函数时,无论变量的值是否发生了改变,都会同步一次数据(即dirty事件)。
Classified

Classified实质上是一个prefab。这个prefab,根据使用的需求,打包了一些联系比较紧密的网络变量,并为他们设置一些共用的函数,以方便被调用(主要是被replica调用)。Classified有好几个,和人物联系紧密的是player_classified,其中包含了关联着饥饿、精神、血量等变量的网络变量。这里不详细展开。对于Classified,主要的用途是,当我们想让客机获取诸如饥饿之类的属性,进行一些与主机无关的操作时,去调取player_classified里的关联hunger的网络变量。但实际上这个应用的范围很小,因为要用到数据,又与主机无关的,一般就剩下控制HUD表现了。大多数时候,我们想要的是,在客机上,呼叫主机执行某一段代码。比如说呼叫主机让hunger减少50点。这个就属于客机向主机发起沟通的范畴了,而这就需要使用RPC。

RPC

RPC是Remote Procedure Call的缩写,意思就是远程程序调用。这里不需要了解其内在机理。只需要知道它是客机用来向主机发送执行代码信号的工具就行了。重点在于学习如何使用它。

RPC是不能随意发送整一段的代码给主机的,它只能发送一条简短的RPC消息,然后主机根据这条消息,寻找相应的RPC处理器,如果找得到的话,就会执行处理器预先设置的代码块。所以,我们想要使用RPC,就需要先(在主机)用AddModRPCHandler函数添加一个RPC处理器,然后在需要的时候,执行SendModRPCToServer来发送一条RPC消息给主机。
我们通过一个例子来学习:

1
2
3
4
5
6
7
8
local function GrowGiant(player)    
player.Transform:SetScale(2,2,2)
end
AddModRPCHandler(modname, RPCname, GrowGiant)--添加RPC处理器,这个语句可以写在任何会被主机执行到的地方。三个参数,第一个为命名空间的名字,建议写mod的名字;第二个参数为RPC的名字,必须是唯一的,如果有同名的,就会根据先后顺序,被后面的覆盖。这两个参数都必须是字符串。第三个参数则是要执行的函数,这个函数的第一个参数固定为玩家的引用,这里的玩家,指的是执行下面的Send
local function SendGrowGiantRPC()
SendModRPCToServer(MOD_RPC[modname]["RPCname"])--[[向主机发送RPC消息,第一个参数为MOD_RPC[modname]["RPCname"],MOD_RPC是不能修改的,后面的modname,RPCname就和上面的意思是一样的。另外,这个函数可以传入更多的参数,只要写在第一个参数后面就行。这些参数讲会被上面的AddModRPCHandler里的执行函数接收到。在某些时候会非常有用。
end
GLOBAL.TheInput:AddKeyDownHandler(118, SendGrowGiantRPC)--按v键执行上面定义的SendGrowGiantRPC函数。除了这里通过按键来触发外,我们还可以根据情况,灵活调用SendModRPCToServer来达到不同的目的。

学习资料

本文章参考了以下文章(无先后顺序),在此非常感谢大佬们,也请各位modders多多关注下以下大佬:

您的支持是对我最大的动力 (●'◡'●)