Categories

Tags

Table of Contents

  1. 数据定义
    1. 世界状态
    2. 任务定义
    3. 方法的定义
    4. 条件判断
  2. 参数的传递
    1. Substitute
    2. 任务修改
    3. 方法修改
    4. 参数传递中断的问题与修复
    5. 行动的定义
  3. 条件判断
  4. 应用效果

这里开始,编写核心部分,也就是Planner。根据前面编写编辑器的教训,最好还是先理清一下开发的思路,以及分析一下需要开发那些内容,而不是像以前那样随心所欲,想到哪写到哪,要么经常跑偏,去做别的东西,要么又要重构。

数据定义

规划器需要有输入数据来执行规划,这些数据分别是

  • 初始状态
    • 设置当前的世界状态,以便接下来的规划器在该状态下进行分析规划
  • 目标任务
    • 给出一个任务,规划器将根据当前世界状态对该任务进行分解,得到一个可执行的解决方案
  • 定义域
    • 提供各种可用的数据,如
      • 任务
      • 方法
      • 行动

这一步,需要把编辑器中定义好的数据全部转为lua数据。借助lua的便利性,把每个定义都导出一个lua文件,用来把数据导入定义域,然后把所有数据传入domain

但是,并不是所有的信息都要保存到domain中,我们只需要运行时相关的。首先,我们只看WorldStates.lua,Tasks.lua,Methods.lua,这三个核心文件。

世界状态

创建WorldStates.lua文件,定义并记录世界状态中的对象关系和对象属性

local WorldStates = {}

WorldStates.Objects = {
	LocA = {
		type = 'Location',
		position = {x = 10,y = 0,z = 0},
		missionaryNum = 3,
		cannibalNum = 3
	},
	LocB = {
		type = 'Location',
		position = {x = -10,y = 0,z = 0},
		missionaryNum = 0,
		cannibalNum = 0
	}
}
WorldStates.Relations = {
	BoatAt = {
		"LocA"
	}
}

return WorldStates

任务定义

创建任务文件 Tasks.lua,保存所有任务的定义

local Tasks = {}

Tasks.Move = {
	paramts = {
		f = "location",
		t = "location"
		},
	preconds = {
		relation = {"BoatAt(f)"}
		}
}
Tasks.Load = {
	paramts = {
		l = "location",
		m = "int32",
		c = "int32"
		},
	precond = {
		relation = {"BoatAt(l)"},
		valuelimit = {
			"l.missionaryNum > m"
			}
	}
}
Tasks.UnLoad = {
	paramts = {
		l = "location",
		m = "int32",
		c = "int32"
	},
	precond = {
		relation = {"BoatAt(l)"},
		valuelimit = {
			"l.missionaryNum + m >= l.cannibalnum + c"
		}
	}
}
Tasks.Transport = {
	paramts = {
		f = "location",
		t = "location"
	},
	precond = {
		valuelimit = {
			"LocA.missionaryNum + LocA.cannibalnum != 0"
		}
	},
	Methods = {
		"Load_2m0c_ab",
		"Load_1m1c_ab",
		"Load_1m0c_ab",
		"Load_0m1c_ab",
		"Load_0m2c_ab",
		"Load_2m0c_ba",
		"Load_1m1c_ba",
		"Load_1m0c_ba",
		"Load_0m1c_ba",
		"Load_0m2c_ba",
	}
}
return Tasks

方法的定义

创建方法文件 Methods.lua,保存所有方法的定义

local Methods = {}

Methods.Load_2m0c_ab = {
	paramts = {
		f = "Location",
		t = "Location",
		m = "int32",
		c = "int32"
	},
	precond = {
		relation = {"BoatAt(f)"},
		valueLimit = {
			"f.missionaryNum >= m",
			"f.missionaryNum - m >= f.cannibalNum"
		}
	},
	subtasks = {
		"Load(f,m,c)",
		"Move(f,t)",
		"Unload(f,m,c)",
		"Transport(t,f)"
	},
	substitues = {
		f = "LocA",
		t = "LocB",
		m = 2,
		c = 0
	}
}

return Methods

条件判断

为了可以判断条件是否满足,需要根据实际输入参数来进行判断。 实现过程中,利用了Lua中的_ENVload的结合,使得我们可以直接判断”l.missionary >= m”这样的语句是否满足,当然,需要先使用load函数把环境设置好,即把替代方法

precond = {
		relation = {"BoatAt(f)"},
		valueLimit = {
			"f.missionaryNum >= m",
			"f.missionaryNum - m >= f.cannibalNum"
		}
	}

然后,在WorldState.lua中实现条件的判断

function WorldStates:IsHaveRelations(_statements ,substitutions)
	for i, v in ipairs(_statements) do
		if not(self:IsHaveRelation(v,substitutions)) then
			return false
			end
	end
	return true
end

function WorldStates:IsHaveRelation(_statement ,substitutions)
	print(_statement)
	local words = {}

	for w in string.gmatch(_statement,"%a+") do
		words[#words+1] = w
	end

	local predicates = words[1]
	local relation = ''

	--将变量名替换为实际值
	for i = 2, #words do
		relation = relation .. substitutions[words[i]]
	end

	local relations = self.Relations[predicates]

	if relations ~= nil then
		for _,v in pairs(relations) do
			print(string.format("need %s = %s ,now is %s",predicates,relation, v))
			if v == relation then
				return true
			end
		end
	end
	return false
end

function WorldStates:IsValueLimitFit(_statements,_substitution)
	local env = {}
	local subtitue_command = ""
	for k, v in pairs(_substitution) do
		print(k,v)
		env[k] = v
		subtitue_command = subtitue_command..string.format("%s = %s ;",k,v)
	end
	--print(subtitue_command)
	for k, v in pairs(self.Objects) do
		env[k] = v
	end

	setmetatable(env,{__index = _G})

	local _ENV = env

	assert(load(subtitue_command,"数值条件判断","bt",env))()
	--assert(load("print('测试',LocA,_ENV['LocA'])","chunk","bt",env))()
	--assert(load("print(f,t,m,c)","chunk","bt",env))()

	for i, v in ipairs(_statements) do
		print(v)
		if not assert(load("return ".. v,"数值条件判断","bt",env))() then

			return false
		end
	end

	return true
end

return WorldStates

测试

local Test = {}

local methods = require 'Methods'
local worldstates = require 'WorldStates'

local m = methods.Load_2m0c_ab
--local res = worldstates:IsValueLimitFit(m.precond.valueLimit,m.substitues)
local res = worldstates:IsHaveRelations(m.precond.relation,m.substitues)
print(tostring(res))

参数的传递

在分解一个任务的时候,选取合适的方法来分解该任务,得到一个子任务序列,把这些子任务替换原任务放入任务队列,弹出第一个待分解的任务,如果该任务是复合任务,就重复前面的步骤,如果是基元任务,就开始获取该任务对应的行动实例,并把这个行动实例加入行动列表。

普通的行动

Actions.Move = {
	Effects = {
		positive = {
			"BoatAt(t)"
		},
		negative = {
			"BoatAt(f)"
		}
	},
	Execute = function(self,_ws)
		local parser = NewParser(self.substitutions)		
		parser.dostring("printf('move %s to %s',f,t)")		
		-- TODO 实现
	end
}

因为方法的定义中,记录着可以把这个任务分解成哪些子任务,并且会把数据传递给这些子任务。 如方法Load_2m0c_ab的定义

Methods.Load_2m0c_ab = {
	paramts = {
		f = "Location",
		t = "Location",
		m = "int32",
		c = "int32"
	},
	preconds = {
		relations = {
			positive = {"BoatAt(f)"}
		},
		valuelimits = {
			"f.missionaryNum >= m",
			"f.missionaryNum - m >= f.cannibalNum"
		}
	},
	subtasks = {
		"Load(f,m,c)",
		"Move(f,t)",
		"Unload(t,m,c)",
		"Transport(t,f)"
	},
	substitues = {
		f = "LocA",
		t = "LocB",
		m = 2,
		c = 0
	}
}

可以知道有四个子任务,分别是Load(f,m,c)Move(f,t)Unload(t,m,c)Load(f,m,c),另外,上面也写过这些子任务的定义,,不过,先看一下Unload(t,m,c)Unload任务的定义

Tasks.UnLoad = {
	IsPrimitive = true,
	paramts = {
		l = "location",
		m = "int32",
		c = "int32"
	},
	preconds = {
		relations = {
			positive ={"BoatAt(l)"}
		},
		valuelimits = {
			"l.missionaryNum + m >= l.cannibalnum + c"
		}
	}
}

可以注意到差距,子任务中的参数是(t,m,c),而任务定义中是(l,m,c),虽然我们可以很直接的看出来哪些参数对应着哪些值,但是要怎么制定给计算机知道呢

已知: Load(f,m,c)substitues = {f = "LocA",t = "LocB",m = 2,c = 0}

想要将任务实例化,需要对前面的任务定义方式进行改变

Tasks.UnLoad = function( ... )
	local input = {...}
	local l = input[1]
	local m = input[2]
	local c = input[3]

	local task = {}
	task.IsPrimitive = true
	task.preconds = {
		relations = { string.format("BoatAt(%s)",l) },
		valuelimits = {
			string.format("%s.missionaryNum + %s >= %s.cannibalnum + %s",l,m,l,c)			
		}
	}
	return task
end

这样子就可以返回该任务的实例了,但是,这里又涉及到编辑器的问题了,怎么让

l.missionaryNum + m >= l.cannibalnum + c 变成 string.format("%s.missionaryNum + %s >= %s.cannibalnum + %s",l,m,l,c)

Substitute

为了不影响之前的编辑流程,尽可能少的改动代码,我们约束:在编辑器输入条件时,需要使用[]把变量括起来

local orign = '[l].missionaryNum + [m] >= [l].cannibalnum + [c]'

local params = {}
string.gsub(orign,"%[(.-)%]",function ( p ) table.insert(params,#params + 1,p) end)

orign = string.gsub(orign,"%[(.-)%]","%%s")
orign = string.format("string.format('%s',%s)",orign,table.concat(params,","))

print(orign) -- =>string.format('%s.missionaryNum + %s >= %s.cannibalnum + %s',l,m,l,c)

不过这样还不够,继续对这一部分进行修改,让它变得通用

function Substitute(_orign ,_env)
	local params = {}
	string.gsub(_orign,"%[(.-)%]",function ( p ) table.insert(params,#params + 1,p) end)
	_orign = string.gsub(_orign,"%[(.-)%]","%%s")
	_orign = string.format("return string.format('%s',%s)",_orign,table.concat(params,","))
	return assert(load(_orign,"GetCondition","bt",_env))()
end

任务修改

然后开始修改任务的定义

Tasks.UnLoad = function( ... )
	local input = {...}
	local env = setmetatable(
		{
			l = input[1],
			m = input[2],
			c = input[3]
		},
		{__index = _G}
	)

	local task = {}
	task.IsPrimitive = true
	task.preconds = {
		relations = {
			Substitute("BoatAt([l])",env)
		},
		valuelimits = {
			Substitute("[l].missionaryNum + [m] >= [l].cannibalnum + [c]",env)		
		}
	}
	return task
end

测试

local task2 = "UnLoad('LocA',2,0)"

Tasks.GetTask = function( _taskDecl)
	local name, s = splitFunc( _taskDecl)
	s = string.format("return { %s }",s)
	local p = assert(load(s))()		
	return Tasks[name](table.unpack(p))
end

t = Tasks.GetTask(task2)

--[[
-- 最终可以得到任务实例
t =
{
	preconds = {
		relations = {
			"BoatAt(LocA)"
		},
		valuelimits = {
			"LocA.missionaryNum + 2 >= LocA.cannibalnum + 0"
		}
	},
	IsPrimitive = true
}
--]]

方法修改

相同的原因,为了传递变量,我们也要把Method的定义修改一下

local Methods = {}

  Methods.Load_2m0c =function(...)
  	local input = {...}
  	local env = setmetatable(
  		{
  			f = input[1],
  			t = input[2],
  			m = input[3],
  			c = input[4]
  		},
  		{__index = _G}
  	)
  	local method = {}
  	method.preconds = {
  		relations = {
  			positive = {Substitute("BoatAt([f])",env)}
  		},
  		valuelimits = {
  			Substitute("[f].missionaryNum >= [m]",env),
  			Substitute("[f].missionaryNum - [m] >= [f].cannibalNum",env)
  		}
  	}
  	method.subtasks = {
  		Substitute("Load([f],[m],[c])",env),
  		Substitute("Move([f],[t])",env),
  		Substitute("Unload([t],[m],[c])",env),
  		Substitute("Transport([t],[f])",env)
  	}
  	return method
  end

return Methods

测试

local m = Methods.GetMethods("Load_2m0c('LocA','LocB',2,0)")
--[[
-- 我们可以得到m的结构如下
m = {
	preconds = {
		valuelimits = {
			[1] = "LocA.missionaryNum >= 2";
			[2] = "LocA.missionaryNum - 2 >= LocA.cannibalNum";
		};
		relations = {
			positive = {
				[1] = "BoatAt(LocA)";
			};
		};
	};
	subtasks = {
		[1] = "Load(LocA,2,0)";
		[2] = "Move(LocA,LocB)";
		[3] = "UnLoad(LocA,2,0)";
		[4] = "Transport(LocB,LocA)";
	};
}
--]]

参数传递中断的问题与修复

但是,可以观察上面的SubTask中的字符串"Load(LocA,2,0)",我们调用时,应该传入"Load('LocA',2,0)"LocA少了两边的单引号,会导致把它当成变量,而LocA == nil,所以后面进行Substitute时,都会返回类似"nil.missionaryNum - 2 >= nil.cannibalNum"这样子的语句。为了解决这个问题。我们统一格式,输入时不需要单引号,统一在分析输入字符串时,给参数列表中的单词(非数值类型)加上单引号。

function splitFunc(_functionDecl)
	local name, params = string.match( _functionDecl,"(.*)%((.*)%)")
	params = string.gsub(params,"%a+","'%1'")
	params = string.format("return { %s }",params)
	local p = assert(load(params))()
	return name,p
end

Tasks.GetTask = function( _taskDecl)
	local name, p = splitFunc( _taskDecl)
	return Tasks[name](table.unpack(p))
end

Methods.GetMethods = function ( _methodDecl )
	local name, p = splitFunc( _methodDecl)
	return Methods[name](table.unpack(p))
end

然后就是测试

local t = Tasks.GetTask("Transport(LocA,LocB)")
table.print(t) -- 自己扩展的表格打印函数

print(t.Methods[1])
local m = Methods.GetMethods(t.Methods[1])

table.print(m) -- 这次就可以正常的传递下去了

行动的定义

经过前面的测试和重构,Action的定义也应该和Task,Method的定义类似

require "Utils.SubstituteUtility"
require "Utils.LogUtil"
local Actions = {}
function Actions:GetAction(_actiondecl)
	local name, p = splitFunc( _actiondecl)
	return self[name](table.unpack(p))
end

Actions.Move = function(...)
	local input = {...}
	local env = setmetatable(
		{
			f = input[1],
			t = input[2]
		},
		{__index = _G}
	)
	local action = {}
	action.Effects = {
		positive = {
			Substitute("BoatAt([t])")
		},
		negative = {
			Substitute("BoatAt([f])")
		}
	}
	action.Execute = function(self,_ws)
		-- TODO 关联其他功能,如动画表现,寻路移动等等
	end
	return action
end

条件判断

经过数据定义的不断修改,条件判断的方法也不再适用,因此也需要做出对应的调整。 条件分为三类,是否存在关系,是否不存在关系,世界对象的属性是否满足数值限制

--==========================
-- 判断条件是否符合
--==========================
function IsFitPreconds(_ws,_preconds)
	return
	IsHaveRelations(_ws,_preconds.relations.positive) and
		IsNotHaveRelation(_ws,_preconds.relations.negative) and
		IsValueLimitFit(_ws,_preconds.valuelimits)
end

function IsHaveRelations(_ws,_statements)
	if(_statements ~= nil) then
		for _, v in pairs(_statements) do
			if not(IsHaveRelation(_ws,v)) then
				return false
			end
		end
	end
	return true
end

function IsNotHaveRelation(_ws,_statements)
	if(_statements ~= nil) then
		for _, v in pairs(_statements) do
			if IsHaveRelation(_ws,v) then
				return false
			end
		end
	end
	return true
end

function IsHaveRelation(_ws,_statement)
	print(_statement)
	local predicate, params = string.match( _statement,"(.*)%((.*)%)")

	local relations = _ws.Relations[predicate]

	if relations ~= nil then
		for _,v in pairs(relations) do			
			if v == params then
				return true
			end
		end
	end
	return false
end

function IsValueLimitFit(_ws,_statements)
	local env = setmetatable(
		{},
		{__index = _G}
	)
	for k, v in pairs(_ws.Objects) do
		env[k] = v
	end

	if _statements ~= nil then
		local parser = NewParser("数值条件判断",env)
		for i, v in ipairs(_statements) do
			print(v)
			if not parser("return ".. v) then
				return false
			end
		end
	end
	return true
end

测试

local t = tasks:GetTask("Transport(LocA,LocB)")
local m = methods:GetMethods(t.Methods[1])
--table.print(m,"Method")
print(IsFitPreconds(worldstates,m.preconds))

应用效果

与条件对应,效果也分为三个部分,分别是添加关系,移除关系,修改世界对象数据

--==========================
-- Apply Action Effect To Planing WorldStates
--==========================

function ApplyActionEffects(_ws,_action)
	local env = setmetatable(
		{},
		{__index = _G}
	)
	for k, v in pairs(_ws.Objects) do
		env[k] = v
	end
	local parser = NewParser("应用行动效果",env)
	-- positive
	if _action.Effects.positive ~= nil then
		for _, v in ipairs(_action.Effects.positive) do
			AddRelation(_ws,v)
		end
	end

	-- negative
	if _action.Effects.negative ~= nil then
		for _, v in ipairs(_action.Effects.negative) do
			RemoveRelation(_ws,v)
		end
	end
	-- preoperties
	if _action.Effects.preoperties ~= nil then
		for _, v in ipairs(_action.Effects.preoperties) do
			parser(v)
		end
	end
end

function AddRelation(_ws,_statement)
	printf("AddRelation :%s",_statement)
	local predicate, params = string.match( _statement,"(.*)%((.*)%)")

	local relations = _ws.Relations[predicate]
	if relations ~= nil then
		for _, v in ipairs(relations) do
			if v == params then
				return
			end
		end
		table.insert(relations,params)
	else
		-- 新增关系
		_ws.Relations[predicate] = {params}
	end
end
function RemoveRelation(_ws,_statement)
	--注意考虑移除元素后产生的空洞会不会对其他系统造成影响
	printf("RemoveRelation :%s",_statement)
	local predicate, params = string.match( _statement,"(.*)%((.*)%)")

	local relations = _ws.Relations[predicate]
	if relations ~= nil then
		local index = 0
		for i, v in ipairs(relations) do
			if v == params then
				index = i
			end
		end
		if index ~= 0 then
			table.remove(relations,index)
			if #relations == 0 then
				_ws.Relations[predicate] = nil
			end
		end
	end
end

测试

local t = tasks:GetTask("Transport(LocA,LocB)")
local m = methods:GetMethods(t.Methods[1])
--table.print(m,"Method")

local t1 = tasks:GetTask(m.subtasks[1])
--table.print(t1,"Load Task")
local a1 = actions:GetAction(m.subtasks[1])
local a2 = actions:GetAction(m.subtasks[2])
local a3 = actions:GetAction(m.subtasks[3])

table.print(a3,"Load Action")

table.print(worldstates,"before")
ApplyActionEffects(worldstates,a1)
ApplyActionEffects(worldstates,a2)
ApplyActionEffects(worldstates,a3)

table.print(worldstates,"after")

在开发这些模块的过程中,遇到了许许多多的困惑,最麻烦的是如何传递数值,即通过任务传给方法和行动,方法又传给任务,又要考虑编辑器的情况,而编辑器只能传递字符串。经过不断的尝试和思考,明确自己的需求(通过已知的信息,如何去获取另一种信息),不断的这样思考着,把模块剥离,尽可能的使他们既可以进行单独的单元测试,也可以协同进行测试,为了实现某些功能,需要哪方面知识,就这样,在开发的过程不断成长,学习新的知识。

至此,这些基础模块已经准备就绪,通过它们来支撑上面的模块的运行,也就是下一步要开发的Planner模块,该模块负责创建解决方案,这个地方主要的难点是算法的实现,相信实现过程中一定又会遇到各种各样的问题,不要气馁,冷静下来,从头开始分析,不要想的太复杂。