这一章讲解Prefab。Prefab是饥荒世界构成的基础,也是Mod技术的基本内容。
Prefab,中文译名叫预制物,也可以广义地称之为物体。
在饥荒的世界中,Prefab是最基础的元素,除了操作面板和地图外,所有的一切,包括食物、人物、动物、植物、水池、矿石乃至于特效等等,都是Prefab。可以说,了解Prefab,是制作饥荒MOD的基础。
本章将讲解Prefab的相关基础知识,下一章将讲解如何构建不同种类的Prefab。如果你急于知道如何做一件衣服,一顶帽子,或者一件法杖,那么你可以先跳过本章,看下一章。但我建议你认真学习Prefab的基础知识。这些基础知识是构建的根基,只有掌握它们,你才有能力发挥你的创造力。
本章内容较多,先了解知识结构有助于更好地学习。
知识结构如下图,本章只讲述基础知识的部分
基本概念
Prefab,可以说是饥荒世界的原子,一棵树,一块卵石,一口锅等等……凡是在世界地图中能够参与互动的,全部都是Prefab。
想要构建一个Prefab对象,可以用Prefab类的构造函数来表示:Prefab("common/inventory/lotus_umbrella", fn, assets)
一个Prefab由三部分组成,对应Prefab类构造函数的三个参数,分别是Prefab名,描述函数和加载资源表。
- Prefab名:用于向系统注册,从而使得系统能够精确地定位到某个Prefab进行操作,比如生成一棵草。在Prefab类构造函数中,只会识别最后一个
/
后面的字符。比如上面的例子,系统会认为Prefab名为lotus_umbrella - 描述函数:用于描述Prefab的内涵,比如说它的外表是什么,有什么功能等等,应该传入一个函数。
- 加载资源表:用于向系统说明,为了正确地呈现这个Prefab,需要加载那些动画、图片、声音文件,应该传入一张表。
Prefab分解
--------------------------------------- 加载资源表 -----------------------------------
local assets =
{
Asset("ANIM", "anim/lotus_umbrella.zip"),
Asset("ANIM", "anim/swap_lotus_umbrella.zip"),
Asset("ATLAS", "images/inventoryimages/lotus_umbrella.xml"),
}
--------------------------------------- end 加载资源表 -----------------------------------
--------------------------------------- 描述函数 -----------------------------------
... 一些定义在外部的函数
local function fn() -- 描述函数
local inst = CreateEntity() -- 创建实体
... 对inst添加各种各样的组件,并对每个组件进行一些设置
return inst
end
--------------------------------------- end 描述函数 -----------------------------------
return Prefab("common/inventory/lotus_umbrella", fn, assets) -- 第一个参数就是Prefab名,系统只会识别最后一个斜杠后面的名字,fn代表描述函数,assets代表加载资源表
Prefab名是很容易理解的,直接写一个自己喜欢的名字就可以了,重点在于了解如何描述加载资源表和描述函数。
加载资源表
加载资源表本质上就是一个Lua table,其中的元素都是Asset对象。如果不明白这句话也没关系,使用起来是非常简单的。你只需要弄清楚当前的Prafab需要什么样的动画、图片、声音资源,以及它们的存放位置,然后用Asset逐行描述出来就行了。
一张典型的加载资源表
local assets =
{
Asset("ANIM", "anim/lotus_umbrella.zip"),
Asset("ANIM", "anim/swap_lotus_umbrella.zip"),
Asset("ATLAS", "images/inventoryimages/lotus_umbrella.xml"),
}
Asset第一个参数是资源类型,第二个参数则是资源文件的路径。
各类型资源的对应名称如下,注意要大写
- 动画:类型名为ANIM
- 图片:只需要指明xml文档的路径就行,tex文件不需要给出(xml文档会指明tex文件的所在)。类型名为ATLAS
- 声音:fev后缀的声音集合文件,对应类型"SOUNDPACKAGE",fsb后缀的声音实体文件,对应类型为"SOUND"
描述函数
描述函数是Prefab中最重要的部分,这部分的编程逻辑看起来很复杂,但本质也是很简单的,在描述函数里,我们实质上只是在做三件事:
- 创建实体
local inst = CreateEntity()
- 为实体(entity)添加各种组件,并设定组件的初始状态
- 返回实体
return inst
可以用前一节的部分代码表述如下
--------------------------------------- 描述函数 -----------------------------------
... 一些定义在外部的函数
local function fn() -- 描述函数
local inst = CreateEntity() -- 创建实体
... 对inst添加各种各样的组件,并对每个组件进行一些设置
return inst
end
--------------------------------------- end 描述函数 -----------------------------------
创建实体的方法就是local inst = CreateEntity()
,这里变量名inst
就是instance
的简写,是一种约定的写法,建议沿用。
最简单的Prefab,就是不添加任何组件的Prefab,创建实体后直接返回该实体即可。只是不添加任何组件的话这个Prefab就没有任何功能可言,这不是我们想要的。由此我们了解到,Prefab的本质就是一个可以填充各种内容的实体。为了更便于理解,下文中当实体和Prefab之间没有歧义的时候,会选择用实体这个称呼。
组成结构
创建实体实体和返回实体都是非常简单的,所有的Prefab在这一点上都是一致的,重要的是了解如何向实体添加组件,以及应该添加哪些组件,如何设置组件的初始状态。不同的组件和组件初始状态,造就了饥荒世界丰富多彩的Prefab。
在游戏系统中,有很少一部分组件是需要和游戏引擎进行直接交互的,要添加这类组件,必须使用inst.entity:AddXXX()
这样的代码。这些组件是用C++进行编写的,我们无法了解其内部细节,更无法进行修改,我称之为Entity组件。我们只能了解Entity组件的各种方法,并用这些方法来设置组件的初始状态。
除去少数Entity组件,更多的组件是用lua语言编写的,要添加这些组件,代码格式为inst:AddComponent("xxx")
。我们还可以在官方的compoentns文件夹下的lua文件中直接看到相应源码,了解相关的所有细节。这样的组件,就直接沿用官方的称呼,Component。
两类组件只在添加方式上有些差异,使用的方法是相似的。下面将重点讲解Entity组件的使用方法,而Component由于数量众多,方法繁杂,会在后续单独划出一章来讲解。
在联机版中,常常需要考虑联机的问题。如果你希望你的Prefab在生成之后,还能与其它电脑进行通信,就需要添加一段设置网络代码,鉴于大部分Prefab都需要在主客机之间进行通信交互,所以此片段几乎是必定要添加的。本章属于入门性质,不会过多探讨主客机网络通信的问题,仅仅给出描述函数更为详细的代码结构如下:
--------------------------------------- 描述函数 -----------------------------------
... 一些定义在外部的函数
local function fn() -- 描述函数
local inst = CreateEntity() -- 创建实体
-- 在网络代码往上的这部分代码,会在所有主机和客户端上都运行
-- Entity组件,Tag和网络变量
-- 主客机通用Component(如用于处理说话的talker)
-- 主客机通用自定义处理
-------------- 网络代码 -----------------
inst.entity:AddNetwork()
inst.entity:SetPristine()
if not TheWorld.ismastersim then
return inst
end
-------------- END 网络代码 -------------
-- 从这里往下的代码,只会在主机上运行。
-- 大多数Component
-- sg和brain
-- 主机端自定义处理
return inst
end
--------------------------------------- end 描述函数 -----------------------------------
Entity组件
Entity组件的使用频率很高,几乎每个Prefab都会有,而Component则不一定会有。需要注意的是,Entity组件是在主客机端都会运行的,所以在描述函数里,初始化设置的部分必须写在网络代码片段的上面。
Entity组件的数量不多,所以可以直接全部列出来:
- Transform:变换组件,控制Prefab的位置、方向、缩放等等
- AnimState:动画组件,控制Prefab的材质(Build),动画集合(Bank)和动画播放(Animation)
- Phiysics:物理组件,控制Prefab的物理行为,比如速度,碰撞类型等等。
- Light:光照组件,添加该组件可使得Prefab成为一个光源。
- Network:网络组件,添加与否决定了一个Prefab在主机上生成时,是否会被客户端“看”到。
- MapEntity:地图实体组件,使用该组件可以为Prefab在小地图上创建一个图标。
- SoundEmitter:声音组件,控制Prefab的声音集合和播放
添加一个Entity组件的代码是inst.entity:AddXXX()
,其中XXX是组件名,比如添加Transform组件,可以写成inst.entity:AddTransform()
使用一个Entity组件的某个方法的代码是inst.XXX:YYY()
,其中XXX是组件名,YYY是方法名。比如说设定实体的材质(Build)为lotus_umbrella,则可以写成 inst.AnimState:SetBuild("lotus_umbrella")
下面就来逐一讲解Entity组件的常用方法,本章只会讲解一部分Entity组件。
注意:下文中的inst
表示引用实体对象的引用,请自行根据上下文改变对应变量名。
Transform-变换
Transform的中文译名为变换,主要控制实体的位置、方向和缩放大小。
- 位置
饥荒的空间坐标系是右手坐标系,x,z为地面坐标,y为高度坐标。坐标轴的具体的方向是随着视角的旋转而变化的,但坐标系本身是保持相对不变的。
不理解上面的这段话?没关系,你只要知道x,z是地面坐标,y是高度坐标就行了。
获取位置
inst.Transform:GetWorldPosition()
会返回三个值,分别对应实体当前所在的世界坐标x,y,z
官方还提供了一个用lua代码封装好的更简洁的用法
inst:GetPosition()
与上面的Transform的方法不同,这个用法不会直接返回x,y,z的坐标,而是把坐标封装成一个Point对象(x,y,z)。如果你想要返回x,y,z坐标,可以写成:
inst:GetPosition():Get()
设置位置
inst.Transform:SetPosition(x, y, z)
其中x,y,z为世界坐标
- 方向
虽然饥荒的人物看起来只有4个方向,但在系统中是能描述出实体的360°准确方向的。
具体的数值区间为-180°到180°
获取方向
inst.Transform:GetRotation()
返回实体的当前方向角度degree,区间范围为-180°到180°
设置方向
inst.Transform:SetPosition(degree)
设置实体的方向角度,如果degree为大于180或者小于-180的数,会自动转化到对应-180到180区间上的角度。
- 缩放
缩放是按比例来算的,也有x,y,z三个值,所有的实体默认初始缩放的x,y,z值为1,1,1
x,y决定缩放比例,z和x,y的比例则决定实体在左右和上下两个方向上的速度。
一般情况下,推荐设置x,y,z为相同的值。成比例地扩大或缩小。
获取缩放
inst.Transform:GetScale()
会返回三个值,分别对应缩放比例x,y,z
设置缩放
inst.Transform:SetScale(x, y, z)
其中x,y,z为缩放比例
- Face
所谓Face,是指同一个实体在面向不同角度时,播放同一个动画,会显示不同形态,比如人物静止不动时,面向上下左右,会有不同的姿态。
人物不同方向的站立姿态
这一点在特效施放时很重要,如果Face设置不当,特效就可能在某个朝向上不能正确显示。
inst.Transform:SetNoFaced()
--设置无面,始终只有一个动画形态
inst.Transform:SetTwoFaced()
--2面,只有下、右
inst.Transform:SetFourFaced()
--4面,上下左右
inst.Transform:SetSixFaced()
--6面,上下左右+左下、右上
inst.Transform:SetEightFaced()
--8面,上下左右+四个斜向
常用方法表
方法名 | 参数 | 返回值 | 描述 |
---|---|---|---|
GetWorldPosition | 无 | 坐标x,y,z | 获取实体的位置 |
SetPosition | 坐标x,y,z | 无 | 设置实体的位置 |
GetPredictionPosition | 无 | 坐标x,y,z | 用于客机预测位置,降低网络延迟造成的影响 |
GetLocalPosition | 无 | 坐标x,y,z | 对Prefab来说和GetWorldPosition没有区别。但实体不止可以表示Prefab,也可以表示UI组件。这个方法一般是用在UI组件上 |
GetRotation | 无 | 角度degree | 获取实体的方向角度 |
SetPosition | 角度degree | 无 | 设置实体的方向角度 |
GetScale | 无 | 缩放比例x,y,z | 获取实体的缩放比例 |
SetScale | 缩放比例x,y,z | 无 | 设置实体的缩放比例 |
SetNoFaced | 无 | 无 | 设置无面 |
SetTwoFaced | 无 | 无 | 设置2面 |
SetFourFaced | 无 | 无 | 设置4面 |
SetSixFaced | 无 | 无 | 设置6面 |
SetEightFaced | 无 | 无 | 设置8面 |
AnimState
AnimState负责处理实体的动画内容。要掌握如何使用AnimState,就先需要了解饥荒中的动画概念。
最基本的概念是Animation,也就是动画。当一个Prefab在世界地图上被呈现出来的时候,就是在播放一段动画。不动的动画也算是动画,是只有1帧的动画。动画播放结束后,不会自己凭空消失,如果没有进行设置的话,就会在世界地图上呈现出最后一帧的画面。对应到Spriter项目,就是单个动画。
每个动画会由多个部分组成,我们可以通过代码来局部改变某个部分。这样的一个部分就叫Symbol。最典型的例子就是在装备物品时,会改变人物的外观,这就是通过覆盖Symbol来实现的。
然后是Bank,就是一堆动画的集合,对应到Spriter项目,就是多个动画的上级,一般来说,Bank一旦设定好了就不会改变,但也有例外,比如骑牛。骑在牛上和在地上用的是两套不同的Bank。
最后是Build,Build就是材质,对应Spriter项目文件的名字。材质就是动画的外在表现。比如说兔子,有黄色和白色两种,区别只在于它们用了不同的Build。
- 设置
- SetBuild:设置Build,例:
inst.AnimState:SetBuild("samansha")
- SetBank:设置Bank,例:
inst.AnimState:SetBank("wilson")
- SetLayer:设置层级,会影响到多个物体重叠时的动画呈现(谁在最前面),例:
inst.AnimState:SetLayer(LAYER_WORLD)
- SetOrientation:设置朝向,在设置Prefab紧贴地面时会很有用,比如农场,池塘都是紧贴地面的。例:
inst.AnimState:SetOrientation(ANIM_ORIENTATION.OnGround)
- SetSortOrder:设置排序顺序,在层级相同时有影响。例:
inst.AnimState:SetSortOrder(3)
- 动画播放
- PlayAnimation:播放动画,例:
inst.AnimState:PlayAnimation("idle")
- PushAnimation:推送动画到动画播放队列中,Prefab会按队列中各动画的先后推送顺序依次播放,与PlayAnimation的区别是,Push会等待队列动画播放结束,而Play是直接打断播放队列,立刻播放设定的动画。例:
inst.AnimState:PushAnimation("idle")
- 局部修改
- OverrideSymbol:用其它动画的某个Symbol来覆盖当前Prefab的Symbol,其中,三个参数分别为要覆盖的当前Prefab的Symbol名,覆盖用的动画文件名(无后缀),覆盖用的Symbol名:
inst.AnimState:OverrideSymbol("swap_object", "swap_lotus_umbrella", "swap_lotus_umbrella")
- Show:展现Prefab的某个Symbol,例:
inst.AnimState:Show("ARM_carry")
- Hide:隐藏Prefab的某个Symbol,例:
inst.AnimState:Hide("ARM_normal")
常用方法表
方法名 | 参数 | 返回值 | 描述 |
---|---|---|---|
SetBuild | Build名 | 无 | 设置Build |
SetBank | Bank名 | 无 | 设置Bank |
SetLayer | Layer值 | 无 | 设置Layer |
SetOrientation | Orientation值 | 无 | 设置Orientation |
SetSortOrder | Order值 | 无 | 设置SortOrder |
SetFinalOffset | Offset值 | 无 | 设置tFinalOffset |
PlayAnimation | Animation名,是否循环 | 无 | 播放动画,动画名为第一个参数,第二个参数决定是否循环播放,默认为否,一般可以不填 |
PushAnimation | Animation名,是否循环 | 无 | 推送动画到播放队列中,动画名为第一个参数,第二个参数决定是否循环播放,默认为否,一般可以不填 |
OverrideSymbol | 要覆盖的Symbol名,覆盖用的Build名,覆盖用的Symbol名 | 无 | 用新Symbol覆盖原Symbol |
Show | Symbol名 | 无 | 展示某个Symbol |
Hide | Symbol名 | 无 | 隐藏某个Symbol |
常见AnimState方法组合
AnimState常用于改变Prefab的动画表现,但单个方法能改变的内容很少,一般都是组合使用,这里给出一些常用的片段。
- Prefab初始化
凡是需要在游戏中能被看到的Prefab,都需要添加初始化代码。
初始化代码应该写在描述函数里,且在网络代码片段之前,约定默认播放动画为idle。
内容很简单,就是设置Build,Bank和初始播放动画。
inst.AnimState:SetBank("bank名")
inst.AnimState:SetBuild("build名")
inst.AnimState:PlayAnimation("idle")
- 将农场、池塘等Prefab平放在地图上
如果你注意观察,会发现农场、池塘的动画表现和一般的Prefab不一样,它们似乎是被平放在了地图上,而不像一般的Prefab是呈二维状态,立起来的。你当然可以画一个平放的图,不过,这事用代码来做更容易也更精确。
inst.AnimState:SetOrientation(ANIM_ORIENTATION.OnGround) -- 设置Prefab平放在地上
inst.AnimState:SetLayer(LAYER_BACKGROUND) -- 设置图层位置,会影响到重叠动画的表现,贴在地上的图层会位于一般Prefab图层之下。比如人物会遮盖农场的动画。
inst.AnimState:SetSortOrder(3) -- 设置排序顺序,使用官方常用的3就行了。
- 装备/卸载物品
在装备、卸载物品时,我们常常能看到人物的局部变化,这也是通过AnimState来完成的。
原理就是为Prefab的equipable组件设置装备和卸载回调函数,在人物装备、卸载物品时执行相应回调函数。
下面代码片段中给出的就是回调函数,第一个参数inst
是物品,第二个参数owner
是人物。
其中核心语句是owner.AnimState:OverrideSymbol("swap_owner_symbol", "swap_build", "swap_symbol")
,关于这一句代码,请参照上文对OverrideSymbol
方法的解释。
对手持、身体和头部装备,有各自不同的固定代码,分别列出如下
手持装备
-- 装备回调
local function onequip(inst, owner)
owner.AnimState:OverrideSymbol("swap_object", "swap_build", "swap_symbol") --
owner.AnimState:Show("ARM_carry")
owner.AnimState:Hide("ARM_normal")
end
-- 卸载回调
local function onunequip(inst, owner)
owner.AnimState:Hide("ARM_carry") -- 和上面的装备回调类似,可以试试去掉的结果
owner.AnimState:Show("ARM_normal")
end
身体装备
-- 装备回调
local function onequip(inst, owner)
owner.AnimState:OverrideSymbol("swap_body", "swap_build", "swap_symbol")
end
-- 卸载回调
local function onunequip(inst, owner)
owner.AnimState:ClearOverrideSymbol("swap_body")
end
头部装备
-- 装备回调
local function onequip(inst, owner)
owner.AnimState:OverrideSymbol("swap_hat", "swap_build", "swap_symbol")
owner.AnimState:Show("HAT")
owner.AnimState:Show("HAT_HAIR")
owner.AnimState:Hide("HAIR_NOHAT")
owner.AnimState:Hide("HAIR")
end
-- 卸载回调
local function onunequip(inst, owner)
owner.AnimState:Hide("HAT")
owner.AnimState:Hide("HAT_HAIR")
owner.AnimState:Show("HAIR_NOHAT")
owner.AnimState:Show("HAIR")
end
- 按顺序播放多个动画
假如你有一组动画需要连起来按顺序播放,比如人物的一个完整的砍树流程,有两个动画,一个是前摇chop_pre,一个是砍树chop。这时候就需要考虑使用动画队列了。使用的方法很简单,播放第一个动画,然后把剩下的动画推送到动画队列里。人物完整的砍树流程动画代码如下:
inst.AnimState:PlayAnimation("chop_pre") -- 播放前摇动画
inst.AnimState:PushAnimation("chop") -- 推送砍树动画
- 播放完动画就移除Prefab
在饥荒中,特效都是通过生成一个特效Prefab,播放特效动画的方式来实现的。但很多时候我们只需要播放一次特效动画,之后希望能移除这个Prefab。在动画播放完成后,Prefab会自动推送一个animover事件。只要监听该事件,在播放完后直接移除Prefab即可,具体代码就一行:inst:ListenForEvent("animover", function() inst:Remove() end)
,通常直接写在描述函数里,网络代码片段的下方,也就是只在主机执行这段代码。
Physics-物理
Physics是为实体提供物理效果的组件,能够使得实体拥有质量、半径、速度和碰撞属性,进而实现各种物理运动。
- 设置物理类型
官方提供了标准化组件,可以通过一行代码来设置各种不同类型的物理实体的碰撞属性和质量、半径等。
详情可以参考游戏根目录/data/scripts/standardcomponents.lua
这里讲解实体物理类型的方法,这些方法不可在同一个Prefab中使用。使用方法是在描述函数中添加语句,需要写在网络代码的上方。
物品栏物品(各种可以放进物品栏的小物品)
MakeInventoryPhysics(inst)
特点:可以通过inst.Physics:SetVel(x,y,z)来提供初速度,并且遵循重力、摩擦、碰撞等物理规律。
人物角色(人物,行走的生物)
MakeCharacterPhysics(inst, mass, rad)
其中,mass为质量,rad为碰撞半径,下面类似参数名也有同样含义。
特点:无视摩擦力,无法越过障碍物(小型:浆果丛,一般:池塘、围墙)
飞行生物(蚊子,蜜蜂)
MakeFlyingCharacterPhysics(inst, mass, rad)
特点:类似人物角色,但可以越过像池塘、浆果丛这样的障碍物。
极小飞行生物(蝴蝶)
MakeTinyFlyingCharacterPhysics(inst, mass, rad)
特点:类似飞行生物,但不会和飞行生物发生碰撞(很多蝴蝶可以在同一个位置重叠,而蜜蜂不行)
巨型生物(各大BOSS)
MakeGiantCharacterPhysics(inst, mass, rad)
特点:类似人物角色,但会越过浆果丛等小型障碍物。
飞行巨型生物(龙蝇,蜂后)
MakeFlyingGiantCharacterPhysics(inst, mass, rad)
特点:类似巨型生物,但可以越过池塘这样的一般障碍物
幽灵(阿比盖尔,蝙蝠,格罗姆,幽灵,玩家的灵魂)
MakeGhostPhysics(inst, mass, rad)
特点:类似人物角色,但无视障碍物。
障碍物(围墙,各种建筑,猪王等等)
MakeObstaclePhysics(inst, rad, height)
特点:无
小型障碍物(浆果丛,尸骨)
MakeObstaclePhysics(inst, rad, height)
特点:无
重型障碍物(各种可以背的石块)
MakeHeavyObstaclePhysics(inst, rad, height)
特点:类似障碍物,需要结合组件heavyobstaclephysics使用
小型重型障碍物(knighthead,bishophead,rooknose)
MakeSmallHeavyObstaclePhysics(inst, rad, height)
特点:类似小型障碍物,需要结合组件heavyobstaclephysics使用
- 改变物理属性
我们可以在游戏中动态地改变Prefab的物理属性,以实现某种物理上的视觉效果,比如与猪王交易时,猪王抛出的物品就具有一个斜向上的初始速度,比直接在地上生成物品显得更为自然。
无视碰撞
RemovePhysicsColliders(inst)
移除所有碰撞效果,自由穿梭
停止运动
inst.Physics:Stop()
会将实体的速度设为0
设置/获取运动速度
inst.Physics:SetMotorVel(x,y,z)
为实体设置运动速度,驱使Prefab移动,x,y,z为各个轴向的速度。该方法只对人物、生物类型有效。
此处的坐标系为相对坐标系,x轴正向为Prefab当前面向方向,y轴向上,z轴方向由x,y用右手系法则确定。直观地说,大多数时候我们只需要让Prefab按照当前方向前进,那么只需要设置第一个参数的值为目标速度,y,z均取0。
inst.Physics:GetMotorVel()
获取实体的当前速度
设置初速度
inst.Physics:SetVel(x,y,z)
为Prefab设置初始速度,驱使Prefab移动,x,y,z为各个轴向的速度。该方法只对物品栏物品类型有效。
与SetMotorVel
不同,此方法的坐标系与世界地图的坐标系一致。
此速度只是初始速度,物品栏物品的运动会受到重力、摩擦力、弹力等影响。
Light-光
这个组件能允许实体提供照明功能,有各种参数来设定照明效果,学习这个组件主要是了解光源的各种参数。常见的提供光源的代表Prefab有火把、火坑、矿灯等等。
安装组件:inst.entity:AddLight()
启用/禁用:
inst.Light:Enable(bool)
bool为true,启用组件,能发光。bool为false,禁用组件,不能发光。
判断是否启用了组件:inst.Light:IsEnabled()
Radius - 半径
这一参数代表光照的范围,半径越大,光照的范围越远,注意这个半径是三维的,也就意味着如果你搞个在半空中的光源的话,在地面上的光半径是小于实际半径的。
设置半径:inst.Light:SetRadius(radius)
获取半径:inst.Light:GetRadius()
radius就是半径,单位与游戏中的距离一致,取值为任意正数
以下为不同半径的展示。
其它参数:intensity = 0.5,falloff = 0.5,RGB = (180 / 255, 195 / 255, 225 / 255)
Intensity - 光强
这里的光强并不是指光的强烈程度,而是指光线的集中程度。实际上,光强的提升是通过聚集更多的光线得到的,在其它参数相同的情况下,越高的光强,就意味着越集中的光线,过高的光强会使得光源附近的物体更黯淡。
可以类比生活中的聚光灯,越高的光强,就越集中在一点上。
设置光强:inst.Light:SetIntensity(percent)
获取光强:inst.Light:GetIntensity()
percent为光强百分比,取值范围为0到1之间。超出范围的值,光强取0。
percent越接近1,光线越集中,中心位置越明亮,周围也越暗;越接近0,光线越分散,中心位置越黯淡。当取为1时,会造成数据溢出,光线变成了一个四方形
以下为不同光强的展示
其它参数:radius = 1,falloff = 0.5,RGB = (180 / 255, 195 / 255, 225 / 255)
Falloff - 衰减
在生活中,对于一个单一发散的光源,比如蜡烛,距离越远,光线就越暗。我们就用光线的衰减速度来描述光线随着光源距离而变暗的动态变化。衰减速度越慢,就意味着能以更高的光强传播到远处。四周就显得更明亮。
设置衰减:inst.Light:SetFalloff(percent)
获取衰减:inst.Light:GetFalloff()
percent为衰减百分比,取值范围为0到1之间。大于1则取1,小于0则取0。衰减越高,光线的传播距离就越小。
在实际应用中,percent不可取值过小,因为光线的实际照明半径有一定的计算公式,取过小的衰减会造成实际照明半径溢出。通常来说,取值不可小于0.05。
以下为不同衰减的展示
其它参数:radius = 1,intensity = 0.5,RGB = (180 / 255, 195 / 255, 225 / 255)
实际光照半径
光照的实际范围是由光照半径,光强,衰减三个因素共同决定的。系统提供了一个获取光照实际范围的方法GetCalculatedRadius。
使用方法:inst.Light:GetCalculatedRadius()
Colour - 颜色
组件还允许设置光照的颜色,可以设置红,绿,蓝三种光,取值为0到1之间,但实质上,每个颜色的可能取值为256种,即 0/255 到 255/255。一般也按这个格式来写具体参数。
设置颜色:inst.Light:SetColour(red,green,blue)
获取颜色:inst.Light:GetColour()
虽然不同的颜色在游戏的视觉效果上有差异,但实质光照效果是不变的,这可以通过调用GetCalculatedRadius方法确认
Network-网络
这一个组件本身有很多方法,但大多数是底层通信逻辑,与游戏功能没有直接联系,所以我们只需要简单地添加一句inst.entity:AddNetwork()
,为实体添加Network组件即可。
有无Network的区别在于这个Prefab是否会被其它玩家看到。比如著名的几何种植Mod,种植时呈现出来的网格线也是一个Prefab,但它无法被其它玩家看到,也不需要和主机进行数据交换,就是因为没有添加Network组件。
对于自定义的物品,我们当然是希望其它玩家也能看到的,更重要的是要和主机进行数据交换,所以是一定要添加Network组件。
Component
Component是一类用lua语言编写的组件,可以在components文件夹下看到源码各个Component的源码。与Entity组件最大的区别是,Component不仅能调用方法,还能储存数据,而无需依赖Prefab提供数据。这种设计能够解耦功能与Prefab,使得同一个Component能够被多个不同的Prefab使用,大大提高代码的重用率。Component用到的数据由Component自行存取,逻辑上也更为清晰。比如,人物的血量就是一个Component,这个Component有两个核心数据:当前血量和最高血量,相关的一系列方法都围绕着这两个核心数据进行操作。这个方法与人物本身是无关的,除了应用在人物上,同样也可以应用在任何Prefab上。
Component的内容非常多,官方的Component已经达到了300多个,其中不少更是数百行代码的巨大组件,完全足够再开一章讲解。此处只介绍如何添加和使用一个Component。后续讲解Component时会有更详细的解释。
添加Component只需要一行代码,inst:AddComponent("component名")
,component名就是components文件夹下的各个component文件的名字。
所有的Component都由实体的components表统一管理,引用一个名为XX的Component的格式为inst.components.XX
调用XXComponent的YY方法:inst.components.XX:YY(参数列表)
调用XXComponent的yy属性:inst.components.XX.yy
完善Prefab
在上面的内容中,主要是针对Prefab的组件功能进行了解释,重点在于如何编写描述函数内的代码。
但还有一部分对Prefab的描述性内容,与Prefab的实际功能无关,不写在描述函数里。比如,Prefab在游戏中显示的名字,人物检查Prefab时的描述,以及让Prefab变成可以制作的东西等等。本节关注的就是这些内容。
描述
游戏中的许多描述性的内容,是通过全局变量STRINGS表来储存的。游戏通过一系列的机制来获取相应的字符串,所以只要按照特别的约定,修改STRINGS表的内容,就可以添加我们想要的描述了。
STRINGS.NAMES.LOTUS_UMBRELLA = “荷叶伞” -- 物体在游戏中显示的名字
STRINGS.CHARACTERS.GENERIC.DESCRIBE.LOTUS_UMBRELLA = "这伞能挡雨吗?" -- 物体的检查描述
STRINGS.RECIPE_DESC.LOTUS_UMBRELLA = "荷叶做的雨伞" -- 物体的制作栏描述
名字
所有的Prefab在游戏中显示的名字,都是由STRINGS.NAMES
表储存的。要添加我们自己的Prefab的描述性名字,只需要增加一个元素,key值取Prefab名的全大写即可。比如荷叶伞的Prefab名为lotus_umbrella,则可以写
STRINGS.NAMES.LOTUS_UMBRELLA = "荷叶伞"
人物描述
人物在检查Prefab时,会说出一段描述性的文字。
游戏在取出描述文本时,首先检查人物的特殊描述:STRINGS.CHARACTERS.人物Prefab名全大写.DESCRIBE.物体Prefab名全大写
是否为nil,如果不为nil,就取这个文本。反之,则取通用描述:STRINGS.CHARACTERS.GENERIC.DESCRIBE.物体Prefab名
。也就是说,你完全可以为一个Prefab设置多个人物描述。
同样地,对于荷叶伞,可以添加通用描述:
STRINGS.CHARACTERS.GENERIC.DESCRIBE.LOTUS_UMBRELLA = "这伞能挡雨吗?"
然后再对willow添加特殊描述
STRINGS.CHARACTERS.WILLOW.DESCRIBE.LOTUS_UMBRELLA = "荷叶伞?这不科学"
在制作人物MOD时,如果想为人物专属物品添加人物专属描述,可以把willow换成人物的Prefab名。
物品制作栏描述
和名字的设置方法是类似的,只是换了一张表。
代码格式:STRINGS.RECIPE_DESC.物体Prefab名全大写 = 描述字符串
仍拿荷叶伞举例:STRINGS.RECIPE_DESC.LOTUS_UMBRELLA = "荷叶做的雨伞"
Recipe - 可制作
在游戏中,每一个可制造的物品都有一个Recipe。如果把游戏里的制作栏比作菜谱的话,那么一个Recipe就可以理解为菜单上的一道菜的描述说明。在制作栏上我们可以看到这个Prefab的名字,图片和制作栏描述,以及制作需要的资源。还有另外一些隐藏的东西比如说所需的科技,一次制作能获取几个,谁能制作等等,这些都属于Recipe的管辖范围。
打开游戏根目录/data/scripts/recipe.lua
,可以看到Recipe的定义。构造函数的参数有name, ingredients, tab, level, placer, min_spacing, nounlock, numtogive, builder_tag, atlas, image, testfn。在定义Recipe时,我们并不需要填完所有的参数,只填我们需要的就可以了,不需要的参数,用nil来占位,最后几个连续的nil则可以直接省略。
参数简略含义如下
- name:Prefab名
- ingredients:成分表
- tab:物品栏分类
- level:科技等级
- placer:建筑物放置物,也就是制造建筑时显示的那个图像(实质上也是个Prefab)
- min_spacing:最小间隔
- nounlock:是否可以离开制作台制作--远古物品只能在制作台上制作。nil则可以离开制作台
- numtogive:制作数量,若填nil则为制作1个。
- builder_tag:制作者需要拥有的Tag(标签),填nil则所有人都可以做
- atlas:制作栏图片文档路径
- image:制作栏图片文件名,当名字与Prefab名相同时,可省略。
- testfn:自定义检测函数,需要满足该函数才能制作物品,不常用。
在上面的参数中,name, ingredients, tab, level这些项是必填的,只要填了这四项就可以让物品可制作了。其它参数都是选填的。
成分表ingredients较特殊,其中元素必须是Ingredient对象,也就是一个成分。构建一个成分十分简单,只需要写Ingredient(ingredienttype, amount, atlas, deconstruct)
即可。其中ingredienttype是成分的prefab名或者特殊成分名(针对精神、血量等特殊成分),amount是所需数量,atlas是图片文档路径。desconstruct很少使用,可以直接忽略。
对于MOD来说,官方提供了专门的MOD API,只需要使用这个MOD API来添加Recipe就行了,使用方法和Recipe的构造函数是一致的。
一般物品
要制作一般物品,只需要name, ingredients, tab, level,再添加atlas,用于显示图片即可。
在modmain里写下相关代码,使得荷叶伞可制作
modmain.lua
AddRecipe("lotus_umbrella", {Ingredient("cutgrass", 1), Ingredient("twigs", 1)}, RECIPETABS.SURVIVAL, TECH.NONE, nil, nil, nil, nil, nil,"images/inventoryimages/lotus_umbrella.xml")
代码中用到了RECIPETABS
和TECH
这两项常数,具体的值可以查阅游戏根目录/scripts/constants.lua,如果要更换荷叶伞的制作归类和科技等级,可以查阅这两项常数的值。
建筑物
要制作建筑物,则比一般物品更复杂一些,需要先生成一个Prefab放置物(约定命名为Prefab名_placer,然后在添加Recipe时设置这个放置物Prefab。
下面通过讲解制作一个荷花池(Prefab名为lotus_pond)来展示如何制作建筑物。
首先,需要在Prefab文件中,最后的return处添加语句,创建一个放置物:MakePlacer(name, bank, build, anim, onground, snap, metersnap, scale, fixedcameraoffset, facing, postinit_fn)
。
- name:放置物的Prefab名,一般约定为原Prefab名_placer
- bank:放置物的Bank
- build:放置物的Build
- anim:放置物用于播放的动画,一般约定为idle
- onground:取值为true或false,是否设置为紧贴地面。请参考前面AnimState的内容
- snap:取值为true或false,这个参数目前无用,设置为nil即可
- metersnap:取值为true或false,与围墙有关,一般建筑物用不上,设置为nil即可。
- scale:缩放大小
- fixedcameraoffset:固定偏移
- facing:设置有几个面,参考AnimState的内容
- postinit_fn:特殊处理
虽然参数众多,但对于一般的建筑物,只需要使用前5个参数:name, bank, build, anim, onground
对荷花池来说,需要添加的Placer语句为:MakePlacer("common/lotus_pond_placer", "lotus_pond", "lotus_pond", "idle", true)
然后按照一般的Prefab制作流程制作一个Prefab。因为荷花池需要紧贴地面,所以需要一些额外的AnimState设置,代码如下:
inst.AnimState:SetOrientation(ANIM_ORIENTATION.OnGround)
inst.AnimState:SetLayer(LAYER_BACKGROUND)
inst.AnimState:SetSortOrder(3)
然后,给荷花池添加一个Recipe,这个Recipe与一般物品不同,需要设置placer,添加语句:AddRecipe("lotus_pond", {Ingredient("shovel", 2), Ingredient("cutstone", 2),Ingredient("poop", 6)}, RECIPETABS.FARM, TECH.SCIENCE_TWO, "lotus_pond_placer", 5, nil, nil, nil,"images/map_icons/lotus_pond.xml")
作业
本章内容偏基础知识,没有太多可以实践的东西,只需要做一个建筑 - 荷花池即可。要求能够达到类似建造农场的效果:可以控制建造的位置,并且紧贴地面。
相关素材已经准备好了,直接编写代码即可。
动画文件 - anim/lotus_pond.zip
图片文档 - images/map_icon/lotus_pond.xml
作业Mod在我的网盘下载,文件路径为饥荒Mod指南/charpter_4_homework.zip
,另外有答案文件,路径为饥荒Mod指南/charpter_4_homework.zip