饥荒Mod 开发(六):基础知识总结

饥荒Mod 开发(五):制作一个烹饪锅食物
饥荒Mod 开发(七):调试技巧

在前五篇文章大致介绍了一个Mod 的结构,一些基础概念已经如何创建一个简单的物品。 之前并没有说到太多的细节,主要是一开始就说细节的话太枯燥了,没有成就感,先按照文章做出一个物品来,也会很有成就感,那这里就针对之前的代码做一些细节的说明。

预制物

饥荒中的物品都是预制物的一个实例,这就好比是类和对象的关系。 预制物可以看成是一个类, 物品则是这个类实例化的各个对象。
一种类型的预制物只有一个,但是实体可以有很多个。 比如游戏中的树枝,草,猪人等, 他们各自都对应着一个预制物,但是却是不同的实体。所以我们制作物品其实是在制作预制物(定义一个类),然后由饥荒创建 预制物的实体。饥荒中有一个Prefab类,所有的预制物都是Prefab的对象。

require("class") -- 引入 "class" 模块,用于创建类

-- Prefab 类的定义
Prefab = Class( function(self, name, fn, assets, deps)
    self.name = name or "" -- Prefab 的名称,如果没有提供,那么默认值为 ""
    self.path = name or nil -- Prefab 的路径,如果没有提供,那么默认值为 nil
    self.name = string.sub(name, string.find(name, "[^/]*$")) -- 从路径中提取 Prefab 的名称
    self.desc = "" -- Prefab 的描述,初始值为 ""
    self.fn = fn -- Prefab 的函数,用于创建 Prefab 的实例
    self.assets = assets or {} -- Prefab 的资源,如果没有提供,那么默认值为 {}
    self.deps = deps or {} -- Prefab 的依赖,如果没有提供,那么默认值为 {}
end)

-- Prefab 类的 __tostring 方法,用于将 Prefab 对象转换为字符串
function Prefab:__tostring()
    return string.format("Prefab %s - %s", self.name, self.desc) -- 返回 "Prefab 名称 - 描述" 的格式
end

-- Asset 类的定义
Asset = Class( function(self, type, file, param)
    self.type = type -- Asset 的类型
    self.file = file -- Asset 的路径
    self.param = param -- Asset 的参数
end)

Prefab 大部分只用到name, fn, assets 这三个参数。

  1. name. 全局唯一的名字,用来标识预制物的名称,不能相同。
  2. fn. 一个回调函数,当我们创建一个实例的时候,会调用这个回调函数,我们会在这个回调函数中创建实体,添加组件,设置AI, 状态图等等。
  3. assets. 需要使用的资源,实例基本都会需要资源,最简单就是一个贴图和一个动画。

fn 函数

fn 函数有固定的格式,也是创建实例的必须函数,我们可以简单分为几个步骤

  1. 创建实体.
  2. 添加 饥荒引擎底层组件,这部分组件是没有lua源码的,仅仅是有一些封装好的接口调用
  3. 设置动画,一般这个是设置空闲时的动画,比如 物品放在地上的时候
  4. 设置tag. tag 主要对实例进行分类。比如怪物类,可放入冰箱,可保鲜等等。也可以不设置,并不是必须的
  5. 设置状态图和AI。 对于生物,角色 会设置,一般的普通物品不需要设置
  6. 添加普通组件。设置组件的属性等。 这部分组件提供lua源码
  7. 其余杂项,比如事件的监听, 加载保存数据等等
    下面是长矛的fn函数。

local function fn(Sim)
    --第一步 创建实体
	local inst = CreateEntity()
	--第二步 添加引擎底层组件,系统引擎底层组件使用 inst.entity:AddXXXX 添加
	local trans = inst.entity:AddTransform()
	local anim = inst.entity:AddAnimState()
    MakeInventoryPhysics(inst)
    --第三步 设置动画
    anim:SetBank("spear")
    anim:SetBuild("spear")
    anim:PlayAnimation("idle")
    --第四步 添加标签, 非必须
    inst:AddTag("sharp")
    --添加普通组件并且设置组件的属性,普通组件使用 inst:AddComponent("xxx") 添加
    inst:AddComponent("weapon")
    inst.components.weapon:SetDamage(TUNING.SPEAR_DAMAGE)
    
    inst:AddComponent("finiteuses")
    inst.components.finiteuses:SetMaxUses(TUNING.SPEAR_USES)
    inst.components.finiteuses:SetUses(TUNING.SPEAR_USES)
    
    inst.components.finiteuses:SetOnFinished( onfinished )

    inst:AddComponent("inspectable")
    
    inst:AddComponent("inventoryitem")
    
    inst:AddComponent("equippable")
    inst.components.equippable:SetOnEquip( onequip )
    inst.components.equippable:SetOnUnequip( onunequip )
    
    return inst
end

实体

所有的实体都是通过CreateEntity创建出来的,调用这个函数将获取一个实体,后面的操作都是基于这个实体,源码路径
data\scripts\mainfunctions.lua

function CreateEntity()
    local ent = TheSim:CreateEntity()
    local guid = ent:GetGUID()
    local scr = EntityScript(ent)
    Ents[guid] = scr
    NumEnts = NumEnts + 1
    return scr
end

从上图可以看出实际上返回的inst 是一个 EntityScript 对象,这个类定义在 \data\scripts\entityscript.lua。

EntityScript = Class(function(self, entity)
    self.entity = entity
    self.components = {}
    self.GUID = entity:GetGUID()
    self.spawntime = GetTime()
    self.persists = true
    self.inlimbo = false
    self.name = nil
    
    self.data = nil
    self.listeners = nil
    self.updatecomponents = nil
    self.inherentactions = nil
    self.event_listeners = nil
    self.event_listening = nil
    self.pendingtasks = nil
    self.children = nil
    self.age = 0
end)

从源码可以看出返回的inst有很多属性, GUID ,name 等等

系统底层组件

这种组件比较少,也看不到源码,使用起来也简单,通过 inst.entity:AddXXX 添加,从下面代码也可以看出是有一定规律的。源码中也提供了一些封装,方便使用系统组件
饥荒Mod 开发(六):基础知识总结_第1张图片

--系统底层组件
inst.entity:AddTransform()
inst.entity:AddAnimState()
inst.entity:AddSoundEmitter()
inst.entity:AddDynamicShadow()
inst.entity:AddNetwork()
inst.entity:AddLightWatcher()
--使用封装的方法添加系统组件
MakeInventoryPhysics(inst)
--inst.组件名:
inst.DynamicShadow:SetSize(1, .75)
inst.Transform:SetFourFaced()

tag 标签

标签是一个字符串,可以用来对物品进行分类,饥荒内置了很多标签,我们也可以自定义标签。
比如我们自己做了一个超大背包,可以放很多东西,我们希望食物放进去可以保鲜,那就可以给这个背包加上一个标签。

--保鲜功能,一般用于背包,冰箱等容器
inst:AddTag("fridge")
--可以吃的东西
inst:AddTag("edible")
--可以放入冰箱中,  树枝,草不能放入冰箱,因为没有添加这个标签
 inst:AddTag("icebox_valid")

状态图和AI

这个比较复杂,先不介绍,后面有专门的地方

普通组件

普通组件非常多,都是用lua写的,提供源码。我们的物品会用到各种各样的组件。组件之前也简单介绍过,可以简单理解为饥荒提供的功能集合,像积木一样,把各个组件堆起来就可以制作各种功能的物品了。也可以自定义个组件,都有固定的格式。实现参考一下源码,也是很简单。 下面示例 添加和使用组件代码


local function onsave(inst, data)
    --将物品的属性保存起来,会自动存储在 磁盘上。关闭游戏或者关机也不会丢失
    data.timestamp = inst.timestamp
end

local function onload(inst, data)
    if data then
         --当游戏进入的时候,会读取上次保存的数据。
        inst.timestamp = data.timestamp
    end
end

 --添加组件,参数名是一个字符串  inst:AddComponent({组件名})
 inst:AddComponent("inventoryitem")
 --直接使用inst.components.{组件名}.xx
 inst.components.inventoryitem.cangoincontainer = false
 inst.components.inventoryitem.atlasname = "images/inventoryimages/mybackpack.xml" 

 --每个物品都可能会有自身的一些属性要保存,当退出游戏的时候需要保存起来,下次继续的时候要读取数据。
 inst.OnSave = onsave
 inst.OnLoad = onload

当我们制作一个物品的时候,如果不知道需要使用什么组件,可以看看饥荒本身相似的物品,看看源码就知道需要用什么组件了。比如我们需要制作一个武器,那可以先参考一下长矛的源码,看看他添加了哪些组件。
饥荒为了更方便的使用组件,对一些常用的功能进行了封装,data\scripts\standardcomponents.lua。
饥荒Mod 开发(六):基础知识总结_第2张图片
比如我们制作了一个物品,可以被火点着,那可以直接使用封装的函数

--可以被火点着
MakeSmallBurnableCharacter(inst)

这部分的内容很多,可以看看饥荒源码就知道怎么用了,整体比较简单

事件监听

组件可以通过 PushEvent 函数触发事件, 预制物可以通过 ListenForEvent 监听事件。比如 拾取物品和丢弃物品都会调用 PushEvent 触发事件(“itemget”, “itemlose”) ,

--container 组件触发 itemlose事件
self.inst:PushEvent("itemlose", {slot = slot})


 --监听事件
 inst:AddComponent("inventory")
 inst.components.inventory.maxslots = 1
 inst:ListenForEvent("itemlose", OnLoseAmmo)
 inst:ListenForEvent("itemget", OnTakeAmmo)

--监听角色死亡,复活事件
 local player = GetPlayer()
  --监听player死亡事件
  player:ListenForEvent("death", function() 
   
  end)

  --监听player复活事件
  player:ListenForEvent("respawn", function() 
     
  end)

--监听被攻击事件
local function OnAttacked(inst, data)
  
end
inst:ListenForEvent("attacked", OnAttacked)

饥荒中的事件太多了,可以直接查看饥荒的组件源码,能查到组件触发的所有事件

其他设置

定时器

饥荒中有一次性定时器,也有周期性定时器。定时器可以由 实体启动。

--周期性定时器
self.checkTask = self.inst:DoPeriodicTask(5, function() 
            
end)

--一次性定时器
GLOBAL.GetPlayer():DoTaskInTime(2, function()
      
end )

构造拦截

饥荒中有很多内置物品,我们可以通过拦截的方式改变这些物品的特性,比如 树枝不能放进冰箱,是因为没有树枝没有"icebox_valid" tag,那我们可以拦截 树枝的创建过程,增加一个标签就可以了。 这种拦截方式用的非常多,核心思想就是拦截物品的创建,然后添加或者修改物品。

AddPrefabPostInit("twigs", function(inst)
    inst:AddTag("icebox_valid")
end)

比如物品堆叠默认只有30个,那我们拦截物品的构造,把堆叠增加到999

AddPrefabPostInitAny(function(inst)
    --如果可以堆叠
    if inst.components.stackable then
        --设置最大堆叠999
        inst.components.stackable.maxsize = 999
    end
end)

不仅可以增加组件,tag,还可以替换 实例的函数,比如inst 中有一个A函数,我们可以用自定义的B 函数来替换,这样就HOOK 了这个A方法。

--鼠标悬浮在物品上显示信息
AddClassPostConstruct("widgets/hoverer",function(self)
    --保存旧的 SetString 函数
	local old_SetString = self.text.SetString
	--设置新的函数
	self.text.SetString = function(text,str)
		--实现自己的逻辑

		--调用原先的函数
		return old_SetString(text,str)
	end
end)

拦截组件的构造

AddComponentPostInit("health", function(Health, inst)
	
end)

模拟环境已经设置好之后,但在游戏开始之前

--监听世界的构造
AddSimPostInit(function()
    print("Simulation has been initialized!")
end)

GLOBAL 使用

对于新手来说分不清调用函数或者使用属性的时候到底需不要要GLOBAL。所以经常会出错,复制一段代码,不同的文件里面表现不一样。比如上面的打印堆栈的代码。

--这两句代码看上去是一样的,但是他们是一旦使用不对地方就会造成异常。
print(debug.traceback())

print(GLOBAL.debug.traceback())

所以也有很多的教程会把GLOBAL 这个全局变量设置到env里面去。
需要使用GLOBAL的代码

  • modmain.lua
  • 在modmain.lua 文件中使用 modimport(“showinfo.lua”)导入的lua文件

其他的lua文件中可以不必使用GLOBAL

饥荒中提供了大量的拦截函数,很多Mod 都会用到这些拦截。
下一篇将会介绍一下Mod 开发中的调试技巧
饥荒Mod 开发(五):制作一个烹饪锅食物
饥荒Mod 开发(七):调试技巧

你可能感兴趣的:(饥荒Mod,游戏,lua,饥荒Mod,饥荒)