这里开始,编写核心部分,也就是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中的_ENV
和load
的结合,使得我们可以直接判断”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
模块,该模块负责创建解决方案,这个地方主要的难点是算法的实现,相信实现过程中一定又会遇到各种各样的问题,不要气馁,冷静下来,从头开始分析,不要想的太复杂。