理解饥荒DST中的Class类

Lua本身没有类的定义,要在 Lua中实现面向对象,最本质的原因得益于元方法。在理解饥荒中Class类的定义之前,我们需要先了解并熟悉 Lua 编程语言中的元表和元方法,然后理解什么是面向对象,最后才有了类Class的定义。

Lua部分

元表和元方法

先看官方文档描述

Lua中的每个值都可以有一个元表。这个元表是一个普通的Lua表,定义了原始值在某些事件下的行为。您可以通过在元表中设置特定字段来更改值行为的几个方面。例如,当非数字值是加法的操作数时,Lua会检查值元表的__add字段中的函数。如果它找到一个,Lua会调用此函数来执行加法。

元表中每个事件的键是一个以两个下划线为前缀的事件名称的字符串;相应的值称为元值。对于大多数事件,元值必须是一个函数,然后称为元方法。在前面的示例中,键是字符串“__add”,元方法是执行加法的函数。除非另有说明,否则元方法实际上可能是任何可调用值,它要么是函数,要么是具有__call元方法的值。

通俗点理解就是,我要进行"abc" + 123的计算,显然值abc是一个字符串,是不具备 +这个运算符操作的,这个时候就会调用字符串的元表中的元方法__add 这部分的逻辑。

划重点

  1. 每个值都有一个元表(字符串String,数值number啊,函数function以及表table啊都有,并不局限table!)
  2. 我们能够操作的元表只有table和userdata
  3. 字符串的元表是共享的,数值元表默认为nil
  4. 元表是一个普通表
  5. 获取元表使用getmetatable,设置元表使用setmetatable

修改字符串共享元表实现Java中字符串拼接操作

getmetatable("").__add =function(t1,t2)
    return t1..t2
end

print("aaa" + 123)  -- aaa123

重点元方法

元方法触发条件参数
__index索引访问操作table[key]。当table不是表或table中没有key时,就会发生此事件。元值在table的元表中查找。如 t={},t.name,显然name不存在则会查看元表中的__index方法。可以是函数或者表t,k ==>t就是查找时候的那个表,k则是索引下标
__newindex同__index,只不过这个是赋值操作触发(索引下标不存在),可以是函数或者表t,k,v ==> 赋值的表,索引以及数值
__call把表当作函数使用时候触发,如t={},然后使用时候这么使用t(),只能是函数第一个参数是t表,剩下的参数则是t()这个函数使用方法的所有参数

总结笔记

查找就是__index,赋值则是__newindex,把表当作函数一样调用则是__call,对于参数也很好记,查找肯定是 table[key]传递的也就是 table 和 key, 而赋值肯定要 table[key] = value,所以触发__newindex时候也可以操作的参数就是table,key,value,至于__call则是当作函数使用,那么就可以操作元表table以及作为函数使用的所有参数了。

面向对象编程

面向对象编程通俗的理解就是我们编程应该专注对象,而不是具体过程,比如我需要做一道菜,我不需要知道具体进行什么操作,而是直接调用对象,比如cook,我只需要调用这个方法然后告诉他,做一道红烧肉,就可以完成这件事。而对象通常有包含属性和方法,比如人,可以具有名字,年龄,国籍等属性信息,同时拥有多种行为,如可以学习,可以吃饭等这种方法或者叫做函数,而人可以分为具体的某某某,这就是实例对象inst,而人这个抽象的概念模版就是类Class。

Lua中本身是没有类的概念的,因为无法像其他编程语言一样直接定义一个类,Lua中面向对象的实现都是借助表 table 这个数据结构实现的。

作为类具备的要素:

  • 属性
  • 方法
  • 构造函数

普通的面向对象

-- 定义一个 Person 类
Person = {
    name = "王二",
    age = 18
}

-- 定义 Person 类的一个 eat 方法
function Person.eat(person,foodName)
    print(person.name .. "正在吃" ..foodName)
end

Person.eat(Person,"红烧肉")    -- 王二正在吃红烧肉
Person:eat("驴打滚")           -- 王二正在吃驴打滚

这里有个语法糖,就是Lua中如果调用方法的第一个参数是调用者本身,则可以写成 Person:eat(“红烧肉”),方法的定义也一样。这个很重要,正常使用基本都是这个语法糖!

添加构造方法的面向对象

-- 定义一个 Person 类
Person = {
    name = "王二",
    age = 18
}

-- 定义 Person 类的一个 eat 方法
function Person.eat(person,foodName)
    print(person.name .. "正在吃" ..foodName)
end

-- 定义一个构造方法
function Person:new(obj)
    local obj = obj or {}                         -- 每次返回都是一个新的 obj 表,所以对象不同,即实例化了
    setmetatable(obj,{__index=self})    -- self 代表调用者自己,Person:new 所以是self = Person 表
    return obj
end

Person.eat(Person,"红烧肉")    -- 王二正在吃红烧肉
Person:eat("驴打滚")           -- 王二正在吃驴打滚

-- 使用构造方法创建对象
local p1 = Person:new()
local p2 = Person:new()
p1.name="p1"
p2.name="p2"
p1:eat("烧烤")        -- p1正在吃烧烤
p2:eat("火锅")        -- p2正在吃火锅

可以发现,类的定义属性和方法并没有什么,重点在于怎么写构造方法,而构造方法的本质就是设置元表,实现的方法就是通过__index 元方法指向模版表Person。简单来讲就是构造方法需要做的事情就是: 返回一个新的表,而这个表的元表应该通过__index元方法指向模版表

到现在而言,其实理解类倒也还好,但问题是,每次定义一个类我们都需要写各种属性和方法,为此饥荒给类的实现直接用函数做了一个定义,function Class(bash,_ctor,props) ,我们定义一个Person类只是规定的以后Person实例对象应该怎么写,而Class的定义则是规定了,以后每创建一个类应该怎么写(不是实例对象)。

下面正式理解饥荒源码Class类。

饥荒DST部分

饥荒Class类的定义出自文件class.lua,在游戏目录中的scripts/class.lua。

Class类源码

以下为源码核心部分:

local function __index(t, k)
    local p = rawget(t, "_")[k]
    if p ~= nil then
        return p[1]
    end
    return getmetatable(t)[k]
end

local function __newindex(t, k, v)
    local p = rawget(t, "_")[k]     -- obj._[k] = { nil, v }
    if p == nil then
        rawset(t, k, v)
    else
        local old = p[1]
        p[1] = v
        p[2](t, v, old)                     -- 比如 props = {age=123} 则 p[2] = 123 = {obj,123,nil}
    end
end

local function __dummy()
end

function Class(base, _ctor, props)
    local c = {}    -- a new class instance
    local c_inherited = {}
	if not _ctor and type(base) == 'function' then
        _ctor = base
        base = nil
    elseif type(base) == 'table' then
        -- our new class is a shallow copy of the base class!
		-- while at it also store our inherited members so we can get rid of them
		-- while monkey patching for the hot reload
		-- if our class redefined a function peronally the function pointed to by our member is not the in in our inherited
		-- table
        for i,v in pairs(base) do
            c[i] = v
            c_inherited[i] = v
        end
        c._base = base
    end

    -- the class will be the metatable for all its objects,
    -- and they will look up their methods in it.
    if props ~= nil then
        c.__index = __index
        c.__newindex = __newindex
    else
        c.__index = c
    end

    -- expose a constructor which can be called by <classname>(<args>)
    local mt = {}

    if TrackClassInstances == true and CWD~=nil then
        if ClassTrackingTable == nil then
            ClassTrackingTable = {}
        end
        ClassTrackingTable[mt] = {}
		local dataroot = "@"..CWD.."\\"
        local tablemt = {}
        setmetatable(ClassTrackingTable[mt], tablemt)
        tablemt.__mode = "k"         -- now the instancetracker has weak keys

        local source = "**unknown**"
        if _ctor then
  			-- what is the file this ctor was created in?

			local info = debug.getinfo(_ctor, "S")
			-- strip the drive letter
			-- convert / to \\
			source = info.source
			source = string.gsub(source, "/", "\\")
			source = string.gsub(source, dataroot, "")
			local path = source

			local file = io.open(path, "r")
			if file ~= nil then
				local count = 1
   				for i in file:lines() do
					if count == info.linedefined then
						source = i
						-- okay, this line is a class definition
						-- so it's [local] name = Class etc
						-- take everything before the =
						local equalsPos = string.find(source,"=")
						if equalsPos then
							source = string.sub(source,1,equalsPos-1)
						end
						-- remove trailing and leading whitespace
						source = source:gsub("^%s*(.-)%s*$", "%1")
						-- do we start with local? if so, strip it
						if string.find(source,"local ") ~= nil then
							source = string.sub(source,7)
						end
						-- trim again, because there may be multiple spaces
						source = source:gsub("^%s*(.-)%s*$", "%1")
						break
					end
					count = count + 1
				end
				file:close()
			end
		end

		mt.__call = function(class_tbl, ...)
			local obj = {}
			if props ~= nil then
				obj._ = { _ = { nil, __dummy } }
				for k, v in pairs(props) do
					obj._[k] = { nil, v }
				end
			end
			setmetatable(obj, c)
			ClassTrackingTable[mt][obj] = source
			if c._ctor then
				c._ctor(obj, ...)
			end
			return obj
		end
	else
		mt.__call = function(class_tbl, ...)
			local obj = {}
			if props ~= nil then
				obj._ = { _ = { nil, __dummy } }
				for k, v in pairs(props) do
					obj._[k] = { nil, v }
				end
			end
			setmetatable(obj, c)
			if c._ctor then
			   c._ctor(obj, ...)
			end
			return obj
		end
	end

    c._ctor = _ctor
    c.is_a = function(self, klass)
        local m = getmetatable(self)
        while m do
            if m == klass then return true end
            m = m._base
        end
        return false
    end
    setmetatable(c, mt)
    ClassRegistry[c] = c_inherited
--    local count = 0
--    for i,v in pairs(ClassRegistry) do
--        count = count + 1
--    end
--    if string.split then
--        print("ClassRegistry size : "..tostring(count))
--    end
    return c
end

复杂代码简单化

这个定义看似很多,实际上做的事情无非是,构建一个具有属性、方法和构造函数的一个普通表table,因此逃不过这三个部分,属性怎么来?,方法怎么来?构造函数怎么来?

代码分块解读

参数定义
function Class(base, _ctor, props)
  • base – 基类,是否继承某个类
  • _ctor – 构造函数
  • props – 属性值(这个不好理解,后面展开说)

这个类的本质就是一个function函数,他的作用就是返回一个 table 表。既然是一个函数,我们是不是可以这样调用

Class("Person",fn,nil)	-- 正常继承时候的类定义
-- 也可以这样调用
Class(fn)							-- 正常不需要继承时候的类定义
-- 还可以这样调用
Class(fn,nil,props)		-- replica 复制数据时有用
-- 还可以这样调用	
Class(fn,props)				-- 使用会出错的类定义

Lua到饥荒,比如参数数据没要求,参数个数没要求,参数校验没完全等其实都体现的是弱类型语言的规范大于约定的思想。严格来讲我们可以随意调用这个函数Class,但是不正常调用会出现未知的错误!

参数矫正

我愿意称做参数矫正部分的代码,是因为上面说的,参数base和_ctor以及pros本质是有预定义语意的,因为我们可能使用继承,也可能不适用继承,为了达到三个参数或者两个参数都能保证base的基本语意在,这部分代码就在做这件事。

    local c = {}    -- a new class instance
    local c_inherited = {}
	if not _ctor and type(base) == 'function' then
        _ctor = base
        base = nil
    elseif type(base) == 'table' then
        -- our new class is a shallow copy of the base class!
		-- while at it also store our inherited members so we can get rid of them
		-- while monkey patching for the hot reload
		-- if our class redefined a function peronally the function pointed to by our member is not the in in our inherited
		-- table
        for i,v in pairs(base) do
            c[i] = v
            c_inherited[i] = v
        end
        c._base = base
    end

这部分无非就是一个if语句 + 定义变量,如之前所说,整个函数的目的就是返回一个具有构造函数的 table,所以定义了两个变量:

local c = {}    -- 定义一个类就是返回这个 c 表
local c_inherited = {}	

if部分就是矫正参数了

	if not _ctor and type(base) == 'function' then
        _ctor = base
        base = nil

如果_ctor不存在,并且base为函数,则把base赋值给_ctor,然后清空base, 这就是规范了

没有继承,定义一个普通类的使用方法,即 Class(fn),再次理解,base代表继承的父类,_ctor就是构造函数,一定是函数

else if部分

    elseif type(base) == 'table' then
        -- our new class is a shallow copy of the base class!
		-- while at it also store our inherited members so we can get rid of them
		-- while monkey patching for the hot reload
		-- if our class redefined a function peronally the function pointed to by our member is not the in in our inherited
		-- table
        for i,v in pairs(base) do
            c[i] = v
            c_inherited[i] = v
        end
        c._base = base
    end

如果有继承的父类(因为类一定是返回一个table),所以这里是直接判断type为table。

这里做了三件事:

  1. 填充我们需要的c表,将父类中的属性和方法都塞进来
  2. 填充继承表c_inherited,这个不是返回的表
  3. 填充我们需要的c表的一个字段_base

而这里就规范了,我们有继承时候的类的定义使用方法了,即Class("Person",fn,props),如果一定要第一个参数传递个fn,第二个也传fn行不行,当然也是可以,就是会出现未知错误罢了!所以如果用到继承,意味着第一参数一定是个 table 。

这部分代码整体做的事情就是矫正参数,保证下面的代码在使用参数 base 的时候就是代表父类(table类型),而_ctor则是构造函数(function类型),如果有父类,那就把父类中的属性全都赋值给我们需要的c 表,这很容易理解,毕竟继承就是继承父类的属性,让我们不再需要定义。

定义元方法

这部分代码是根据属性props参数有无来决定我们的类 c表的元方法应该是什么。

    if props ~= nil then
        c.__index = __index					-- 这是一个函数,不是表(查找时候触发)
        c.__newindex = __newindex		-- 这也是一个函数,不是表(赋值时候触发)
    else
        c.__index = c
    end

这部分的代码就是根据props这个参数有无来判断的,如果有props属性,则c表的元方法为__index__newindex,如果没有则自索引c表。所以问题在于两个元方法的定义了。

查找元方法__index

再次加深印象,c.__index意味着什么?c的属性__index意味着c作为元表才有意义,而将c设置元表的那张表(实例化对象返回的那张表,见面向对象编程部分代码)在调用时候才会触发这个函数。

local function __index(t, k)				-- 实例对象触发时候传递的实例对象表t,和索引k
    local p = rawget(t, "_")[k]			-- 获取实例对象表中的`_`表,然后索引k的值,长这样 p._[k] (这部分需要结合下面的构造函数看)
    if p ~= nil then								-- p不为空,实际上是传递参数props不为空的情况
        return p[1]
    end
    return getmetatable(t)[k]
end

赋值索引__newindex

local function __newindex(t, k, v)
    local p = rawget(t, "_")[k]    -- obj._[k] = { nil, v }
    if p == nil then
        rawset(t, k, v)
    else
        local old = p[1]
        p[1] = v
        p[2](t, v, old)  -- 比如 props = {age=123} 则 p[2](t,v,old) = 123(obj,123,nil)
    end
end

p[2](t, v, old) 注意这里是函数调用的使用,意味着p[2]必须是函数或者具有构造函数的table,也就是使用的时候props必须是 {a=fn}这种形式,比如注释部分的 {age=123}这是错误的使用,在实例化对象时候就会报错的。

  1. 构造函数部分(重点
    -- expose a constructor which can be called by <classname>(<args>)
    local mt = {}

if TrackClassInstances == true and CWD~=nil then
  -- 这部分逻辑主要是看是否已经有类的实例了,可以不关注,不涉及类的定义部分
	else
		mt.__call = function(class_tbl, ...)
			local obj = {}					-- 每次实例对象后,拿到的就是obj表,如Person类创建实例对象p,Person对应上面的c表,实例对象p对应 objc表
			if props ~= nil then		--props不为空的时候,就会存在这个表obj._ 对应上面的__index看
				obj._ = { _ = { nil, __dummy } }  -- 这里对应的__dummy是一个空函数
				for k, v in pairs(props) do				-- 将props的属性存到obj._[k]这种带_的格式属性中
					obj._[k] = { nil, v }
				end
			end
			setmetatable(obj, c)			-- 又回到面向对象的本质了,实例对象的obj表通过元方法__index索引到模版表 c表,本质就是完成了构造函数的任务
			if c._ctor then
			   c._ctor(obj, ...)			-- 调用构造函数,将构造函数里边定义的属性全部填充到obj表
			end
			return obj
		end
	end
对照图解

以下说的c表,obj表分别代表类的模板表和类的实例化对象表,比如Person类实际上就是一个表,我称为c表,而通过Person()得到的实例化对象,实际上也是一张表,我称为obj表。

记住:c表代表类,obj表代表实例对象

最简单的使用(无props)

-- 定义一个Person类
local Person = Class(function(self,name)
    self.name = name
end)

-- 创基一个Person类的对象
local p = Person("周杰伦")
-- 打印属性
print(p.name)       -- 周杰伦

执行流程图

在这里插入图片描述

查看Person类的表结构和p对象的表结构

Person类信息:___________________________________
	K: 	__index	 V: 	table: 0x600000f7c600
		(Already visited	table: 0x600000f7c600	-- skipping.)
	K: 	_ctor	 V: 	function: 0x600001a7c740
	K: 	is_a	 V: 	function: 0x60000147c900
Person类对象信息:___________________________________
	K: 	name	 V: 	周杰伦

如果我们要自己写一个定义类的函数:

local c = {}		-- 1.定义一个c表用于返回
c.__index=c			-- 自索引
local mt = {}		-- 2.定义c表的元表(就一个作用,设置__call元方法让这个c表可以像Person()这样使用)
mt.__call = function(self,...)
  	local obj = {}			-- 2.1 定义一个obj表用于返回(就是那个实例化的对象)
  	setmetatable(obj,c)	-- 2.2 设置obj的元表为c
  	c._ctor(obj,...)		-- 2.3 将构造函数里边的属性赋值给obj表
  	return obj
  end

setmetatable(c,mt)	-- 3.设置c的元表为mt
return c

这是没有props的时候最简单的一个流程。

复杂的使用(具有props)

local function OnChange(self,age)
end
-- 定义一个Person类
local Person = Class(function(self,name)
    self.name = name
    self.age = 18
end,nil,{age=OnChange})

-- 创基一个Person类的对象
local p = Person("周杰伦")

执行流程图

  • Class类

在这里插入图片描述

  • __index以及__newindex

在这里插入图片描述

对比具有props和没有props的区别

-- 没有props    源码大部份是这种情况
local Person = Class(function(self,name)
    self.name = name
end)

-- 具有props		目前所知只有components/*._replica.lua这种文件中使用到
local function OnChange(self,age)
end
local Person = Class(function(self,name)
    self.name = name
    self.age = 18
end,nil,{age=OnChange})

先看两种结果的的属性存放结构图

Person类对象信息(具有props):___________________________________
	K: 	_	 V: 	table: 0x600000b74b00
		K: 	_	 V: 	table: 0x600000b74b40
			K: 	2	 V: 	function: 0x600001e768c0
		K: 	age	 V: 	table: 0x600000b74b80
			K: 	1	 V: 	18
			K: 	2	 V: 	function: 0x600001e76940
	K: 	name	 V: 	具有props
Person类对象信息(无props):___________________________________
	K: 	age	 V: 	18
	K: 	name	 V: 	无props

没有props的结构要简单的多的多,这是因为所有的属性值都直接存在自己的表中obj,而具有props由于__newindex的逻辑,增加了很多私有表进行更深层次的存储数据,比如age就是obj._[age][1] =18

那这么做有什么用?

我们来看看,当我们调用p.age=999 对 age重新赋值的时候代码会怎么走。

由于p.age 索引age在自己的obj表中不存在,所以会触发元表c表中的元方法__newindex

在这里插入图片描述

所以,加上props最主要的目的就是,同步修改props的属性值,会同步调用这个属性值变化时候所执行的函数,这就达到了同步修改的目的了!

饥荒源码应用实例

以container.lua和container_replica.lua为例,文件出自scripts/components目录。

Container的使用是作为组件的,因此在于方法AddComponent

查看使用这部分源码(scripts/prefab/bundle.lua中MakeContainer方法)

local function MakeContainer(name, build)
		-- 仅保留核心部分
    local function fn()
        local inst = CreateEntity()
        inst:AddComponent("container")
        return inst
    end
    return Prefab(name, fn, assets)
end

这是使用的入口,要知道我们的类使用需要两个东西类和实例化,所谓类就是require(“container”)返回的表就是,而实例化则是require(“container”)()了,这是往下找的思路。这里注意inst是CreateEntity()得到的对象。

查看inst:AddComponent方法(scripts/entityscripts.lua)

function EntityScript:AddComponent(name)
  	-- 只保留核心部分
    local cmp = LoadComponent(name)			-- 加载组件 name="container"
    self:ReplicateComponent(name)				-- 加载复制组件,注意这里一定需要先有这行代码
    local loadedcmp = cmp(self)					-- 创建Container实例化对象,这里一定要在上一步之后
    self.components[name] = loadedcmp		-- 这里对entity实例对象components[container]赋值实例化对象,这就是常见的 inst.components.container的由来
end

再看LoadComponent方法(scripts/entityscripts.lua)

local function LoadComponent(name)
    if Components[name] == nil then
        Components[name] = require("components/"..name)
        assert(Components[name], "could not load component "..name)
        Components[name].WatchWorldState = ComponentWatchWorldState
        Components[name].StopWatchingWorldState = ComponentStopWatchingWorldState
    end
    return Components[name]
end

ReplicateComponent方法(scripts/entityreplica.lua)

function EntityScript:ReplicateComponent(name)
		-- 只保留核心部分
    local filename = name.."_replica"
    local cmp = Replicas[filename]
    if cmp == nil then
        cmp = require("components/"..filename)
        Replicas[filename] = cmp
    end
    rawset(self.replica._, name, cmp(self))
end

看到这里应该就知道了,上面的LoadComponent实际上就是返回require(“container”),而这个文件的引入,实际上返回的就是一个Container类。而复制replica同理,require可以理解为类的加载,而cmp(self)则是创建实例化对象。

但是实例化对象最重要的参数self是什么?这就是最常见的inst = CreateEntity()实例对象。

既然使用的调用链清楚了,接下来看看container和container_replica中定义的类。

Container.lua中的类

local Container = Class(function(self, inst)
    -- 只保留核心部分
    self.inst = inst
    self.canbeopened = true
end,
nil,
{
    canbeopened = oncanbeopened,
})

这里有两个关键信息:

  1. self.inst = inst ,这个inst就是CreateEntity()
  2. props部分的canbeopend和就是定义在内部的属性canbeopened(对比就是之前的person.age)

根据之前的类的分析,这个oncanbeopened应该是函数:

local function oncanbeopened(self, canbeopened)
    self.inst.replica.container:SetCanBeOpened(canbeopened)
end

这里最重要的是理解这个方法的参数都是啥?

我们回到之前的Class理解部分,OnChange时候,传入的是实例化对象obj,以及属性age和value=18

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们每次对属性canbeopened的复制,都会同步调用这个方法oncanbeopened,而这里的方法oncanbeopened的self就是container的类的实例化对象,而self.inst是啥?上面的两个关键信息self.inst = inst ,这个inst就是CreateEntity(), 所以是inst.replica.container:SetCanBeOpened(canbeopened),现在问题就变成了,这个inst.replica是什么东西了。

我们现在看下CreateEntity()返回的数据结构(scripts/mainfunctions.lua)

function CreateEntity(name)
    local ent = TheSim:CreateEntity()
    local guid = ent:GetGUID()
    local scr = EntityScript(ent)
    if name ~= nil then
        scr.name = name
    end
    Ents[guid] = scr
    NumEnts = NumEnts + 1
    return scr
end

关键信息EntityScript(ent),而参数ent是TheSim:CreateEntity()

走到这里还是类定义,EntityScript(ent)就是返回一个实例化对象。

EntityScript = Class(function(self, entity)
    self.entity = entity
    self.components = {}
    self.replica = Replica(self)
end)

local Replica = Class(function(self, inst)
    self.inst = inst
    self._ = {}
end)

这里的信息点:

  1. self.entity = entity才是最终的entity,这个是由TheSim:CreateEntity()创建的
  2. inst 通常指的就是这个self实例,所以inst一般有inst.entity,inst.components等(具体内容参照scripts/entityscripts.lua中类的定义)
  3. Replica(self)也是在实例化对象,返回一个table,所以self.replica = {inst,_{}}其中inst就是这里的self本身

回到上面

local function oncanbeopened(self, canbeopened)
    self.inst.replica.container:SetCanBeOpened(canbeopened)
end

因此在创建container类的实例对象之前,一定需要有inst.replica.container才行,我们再回到之前AddComponent方法,里面调用ReplicateComponent方法正是给副本replica赋值操作,具体操作代码为rawset(self.replica._, name, cmp(self)),这里需要特别注意,这里是对表self.replica._.container赋值了container_replica实例化对象,但是不是self.replica.container,这里的细节体现为:

local Replica = Class(function(self, inst)
    self.inst = inst
    self._ = {}
end)

function Replica:__index(name)  -- self.inst.replica.container:XXX() name="container" self._["container"] 是存在的。
    return self._[name] == nil and getmetatable(self)[name] or self.inst:ValidateReplicaComponent(name, self._[name])
end

Replica实例化的时候定义了他的元方法__index,因此实际调用self.replica.container的时候走的是这个函数,这里需要注意 and 和 or逻辑运算符的用法,和其他编程语言不同,我们一般认为 and 就是第一个参数为true并且第二个参数为true则返回true,否则执行or后面的,因为lua中存在也可以认为条件判断的true,所以实际执行是:self._[name] == nil 为 false 所以执行or后面的ValidateReplicaComponent方法。

ValidateReplicaComponent方法

function EntityScript:ValidateReplicaComponent(name, cmp)     -- container replica._[container]
    return self:HasTag("_"..name) and cmp or nil         -- return true and cmp or nil return cmp ==> return replica._[container]
end

最后就是return true and cmp or nil 由于cmp既不是true也不是false或者nil,所以直接返回这个对象,也就是container_replica。

这就实现了container修改canbeopened,副本container_replica可以同步修改的目的。

自定义Mycontainer类实现同步修改

新建文件 mycontainer.lua

local function OnChange(self,flag)
    self.inst.replica.mycontainer:SetFlag(flag)
end

local MyContainer = Class(function(self,entity)
    self.name = "MyContainer"
    self.inst = entity

    self.flag = true
end,
nil,
        {
            flag = OnChange
        })

return MyContainer

新建文件mycontainer_replica.lua

local MyContainer_replica = Class(function(self,inst)
    self.name = "MyContainer"
    self.inst = inst

    self.flag = true
end)

function MyContainer_replica:SetFlag(flag)
    self.flag = flag
end

return MyContainer_replica

新建一个测试的文件test.lua

local Replica = Class(function(self, inst)
    self.inst = inst
    self._ = {}
end)

function Replica:__index(name)  
    return self._[name] == nil and getmetatable(self)[name] or self.inst:ValidateReplicaComponent(name, self._[name])
end

Entity = Class(function(self)
    self.components = {}
    self.replica = Replica(self)
end)

function Entity:AddComponent(name)
    local cmp = require(name.."_replica")
    rawset(self.replica._, name, cmp(self))

    self.components[name] = require(name)(self)
end

function Entity:CreateEntity()
    local inst = Entity()

    inst:AddComponent("mycontainer")

    return inst
end

function Entity:ValidateReplicaComponent(name,cmp)
    return true and cmp or nil
end

local inst = Entity:CreateEntity()
inst.components.mycontainer.flag = "hello lua"

输出的代码结构:

	K: 	components	 V: 	table: 0x6000029e1180
		K: 	mycontainer	 V: 	table: 0x6000029e07c0
			K: 	_	 V: 	table: 0x6000029e0e40
				K: 	_	 V: 	table: 0x6000029e0c40
					K: 	2	 V: 	function: 0x600003ce2ce0
				K: 	flag	 V: 	table: 0x6000029e0f00
					K: 	1	 V: 	hello lua
					K: 	2	 V: 	function: 0x600003ce2d60
			K: 	inst	 V: 	table: 0x6000029e1100
				(Already visited	table: 0x6000029e1100	-- skipping.)
			K: 	name	 V: 	MyContainer
	K: 	replica	 V: 	table: 0x6000029e1200
		K: 	_	 V: 	table: 0x6000029e1280
			K: 	mycontainer	 V: 	table: 0x6000029e1980
				K: 	flag	 V: 	hello lua
				K: 	inst	 V: 	table: 0x6000029e1100
					(Already visited	table: 0x6000029e1100	-- skipping.)
				K: 	name	 V: 	MyContainer
		K: 	inst	 V: 	table: 0x6000029e1100
			(Already visited	table: 0x6000029e1100	-- skipping.)
          

PS:

  1. 这里的Class类是直接dofile(“class.lua绝对路径”)引入的官方Class类的定义的,当然自己写一个函数也可以

  2. 查看表结构信息,如上面的结果是调用dumptable(table)方法实现的,此方法在饥荒文件scripts/debugtools.lua文件中定义

  3. 饥荒中entity通常指的是TheSim:CreateEntity()的对象,而inst则是EntityScript(ent)创建的对象,而inst通常会添加各种组件components,因此inst一般有个entity记录TheSim:CreateEntity()的实例对象,所以常见inst.entity,而component通常需要记录他属于哪个inst,所以组件定义时候通常有属性self.inst=inst,就是在创建component实例化对象时候,参数通常是inst。

参考

[1] DST的Class详解

[2] QQ群(559477977)友帮助

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值