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