Create behaviour tree implementation
parent
dc4271cc7e
commit
aa2c86c580
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"spellright.language": [
|
||||
"en_GB"
|
||||
],
|
||||
"spellright.documentTypes": [
|
||||
"markdown",
|
||||
"latex",
|
||||
"plaintext"
|
||||
]
|
||||
}
|
18
README.md
18
README.md
|
@ -48,6 +48,24 @@ A session is a running game. Units and countries from different sessions cannot
|
|||
* `join(player) --> country` - Player joins session, new country created.
|
||||
* `set_player_country(player, country_id) --> boolean`
|
||||
|
||||
### Behaviour Trees
|
||||
|
||||
Conquer uses a simple [behaviour tree](https://en.wikipedia.org/wiki/Behavior_tree_(artificial_intelligence,_robotics_and_control)
|
||||
implementation to drive the unit actions.
|
||||
Behaviour trees can be used to compose complex behaviour,
|
||||
but their use in conquer is simple.
|
||||
|
||||
Composite nodes:
|
||||
|
||||
* `FickleSequence:new(child1, child2)` - runs each child every tick.
|
||||
* `Sequence:new(child1, child2)` - runs each child once, remembering the last ran child.
|
||||
|
||||
Action nodes:
|
||||
|
||||
* `FollowPath:new(path, object_animations)` - follow a path.
|
||||
* `MoveToObject:new(target, object_animations)` - move to an object.
|
||||
* `MeleeAttack:new(target, object_animations)` - attack.
|
||||
|
||||
### Utilities
|
||||
|
||||
* `conquer.class() --> table` - makes a Lua class.
|
||||
|
|
|
@ -121,11 +121,13 @@ function UnitEntity:get_staticdata()
|
|||
})
|
||||
end
|
||||
|
||||
assert(conquer.behaviour.states.RUNNING == "running")
|
||||
|
||||
function UnitEntity:on_step(delta)
|
||||
if self.action then
|
||||
self.action:on_step(delta)
|
||||
|
||||
if self.action:is_complete() then
|
||||
if self.action:get_state() ~= conquer.behaviour.states.RUNNING then
|
||||
self.action = nil
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,216 @@
|
|||
local states = {
|
||||
RUNNING = "running",
|
||||
FAILED = "failed",
|
||||
SUCCESS = "success",
|
||||
}
|
||||
|
||||
conquer.behaviour = {
|
||||
states = states,
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Behaviour Tree Primitives
|
||||
--
|
||||
|
||||
|
||||
|
||||
-- A fickle sequence runs each child every tick
|
||||
local FickleSequence = conquer.class()
|
||||
conquer.behaviour.FickleSequence = FickleSequence
|
||||
function FickleSequence:constructor(...)
|
||||
self.children = { ... }
|
||||
self.state = states.RUNNING
|
||||
end
|
||||
|
||||
function FickleSequence:get_state()
|
||||
return self.state
|
||||
end
|
||||
|
||||
function FickleSequence:on_step(delta)
|
||||
self.state = states.RUNNING
|
||||
|
||||
for i=1, #self.children do
|
||||
local child = self.children[i]
|
||||
child:on_step(delta)
|
||||
|
||||
local state = child:get_state()
|
||||
if state == states.FAILED then
|
||||
self.state = states.FAILED
|
||||
return
|
||||
elseif state == states.RUNNING then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
self.state = states.SUCCESS
|
||||
end
|
||||
|
||||
|
||||
-- A sequence will remember which was the last to run
|
||||
local Sequence = conquer.class()
|
||||
conquer.behaviour.Sequence = Sequence
|
||||
|
||||
function Sequence:constructor(...)
|
||||
self.failed = false
|
||||
self.children = {...}
|
||||
end
|
||||
|
||||
function Sequence:get_state()
|
||||
if self.failed then
|
||||
return states.FAILED
|
||||
elseif #self.children == 0 then
|
||||
return states.SUCCESS
|
||||
else
|
||||
return states.RUNNING
|
||||
end
|
||||
end
|
||||
|
||||
function Sequence:on_step(delta)
|
||||
local child = self.children[1]
|
||||
if child then
|
||||
child:on_step(delta)
|
||||
|
||||
local state = child:get_state()
|
||||
if state == states.SUCCESS then
|
||||
table.remove(self.children, 1)
|
||||
elseif state == states.FAILED then
|
||||
self.failed = true
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
--
|
||||
-- Implementations
|
||||
--
|
||||
|
||||
|
||||
|
||||
local FollowPath = conquer.class()
|
||||
conquer.behaviour.FollowPath = FollowPath
|
||||
|
||||
function FollowPath:constructor(path, animations)
|
||||
self.path = {}
|
||||
|
||||
for i=1, #path do
|
||||
self.path[i] = vector.add(path[i], vector.new(0, -0.49, 0))
|
||||
end
|
||||
|
||||
self.path_i = 1
|
||||
self.animation = nil
|
||||
self.animations = animations
|
||||
end
|
||||
|
||||
function FollowPath:get_state()
|
||||
return self.path and states.RUNNING or states.SUCCESS
|
||||
end
|
||||
|
||||
function FollowPath:on_step(delta)
|
||||
if not self.path then
|
||||
return
|
||||
end
|
||||
|
||||
local next = self.path[self.path_i]
|
||||
if not next then
|
||||
self.object:set_pos(self.path[#self.path])
|
||||
self.object:set_velocity(vector.new())
|
||||
self:set_walking(false)
|
||||
self.path = nil
|
||||
return
|
||||
end
|
||||
|
||||
local from = self.object:get_pos()
|
||||
|
||||
local distance = vector.distance(from, next)
|
||||
if distance < 0.1 then
|
||||
self.path_i = self.path_i + 1
|
||||
self:on_step(delta)
|
||||
return
|
||||
end
|
||||
|
||||
local step = vector.multiply(vector.normalize(vector.subtract(next, from)), 1)
|
||||
self.object:set_velocity(step)
|
||||
local target = vector.add(from, vector.multiply(step, delta))
|
||||
self.object:move_to(target, true)
|
||||
|
||||
self.object:set_yaw(math.atan2(step.z, step.x) - math.pi / 2)
|
||||
self:set_walking(true)
|
||||
end
|
||||
|
||||
function FollowPath:set_walking(walking)
|
||||
if self.animation ~= nil and self.animation == walking then
|
||||
return
|
||||
end
|
||||
|
||||
self.animation = walking
|
||||
self.object:set_animation(walking and self.animations.walk or self.animations.stand, 30, 0)
|
||||
end
|
||||
|
||||
|
||||
local MoveToObject = conquer.class()
|
||||
conquer.behaviour.MoveToObject = MoveToObject
|
||||
|
||||
function MoveToObject:constructor(target, animations)
|
||||
self.target = target
|
||||
self.animations = animations
|
||||
|
||||
self.child = nil
|
||||
self.last_pos = nil
|
||||
self.failed = false
|
||||
end
|
||||
|
||||
function MoveToObject:get_state()
|
||||
if self.failed or not self.target or not self.target:get_luaentity() then
|
||||
return states.failed
|
||||
elseif vector.distance(self.object:get_pos(), self.target:get_pos()) < 2 then
|
||||
return states.success
|
||||
else
|
||||
return states.running
|
||||
end
|
||||
end
|
||||
|
||||
function MoveToObject:on_step(delta)
|
||||
local target_pos = self.target:get_pos()
|
||||
if not self.last_pos or
|
||||
not vector.equals(vector.round(self.last_pos), vector.round(target_pos)) then
|
||||
local path = minetest.find_path(self.object:get_pos(), target_pos, 20, 1, 1)
|
||||
if not path then
|
||||
self.failed = true
|
||||
return
|
||||
end
|
||||
|
||||
local unit_type = self.object:get_luaentity():get_unit_type()
|
||||
self.child = conquer.behaviour.FollowPath:new(path, unit_type.animations)
|
||||
self.last_pos = target_pos
|
||||
end
|
||||
|
||||
if self.child then
|
||||
self.child:on_step(self, delta)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local MeleeAttack = conquer.class()
|
||||
conquer.behaviour.MeleeAttack = MeleeAttack
|
||||
|
||||
function MeleeAttack:constructor(target)
|
||||
self.target = target
|
||||
end
|
||||
|
||||
function MeleeAttack:get_state()
|
||||
if self.target and self.target:get_luaentity() then
|
||||
return states.RUNNING
|
||||
else
|
||||
return states.SUCCESS
|
||||
end
|
||||
end
|
||||
|
||||
function MeleeAttack:on_step(delta) --luacheck: ignore
|
||||
end
|
|
@ -1,61 +0,0 @@
|
|||
conquer.actions = {}
|
||||
|
||||
local FollowPath = conquer.class()
|
||||
conquer.actions.FollowPath = FollowPath
|
||||
|
||||
function FollowPath:constructor(path, animations)
|
||||
self.path = {}
|
||||
|
||||
for i=1, #path do
|
||||
self.path[i] = vector.add(path[i], vector.new(0, -0.49, 0))
|
||||
end
|
||||
|
||||
self.path_i = 1
|
||||
self.animation = nil
|
||||
self.animations = animations
|
||||
end
|
||||
|
||||
function FollowPath:is_complete()
|
||||
return not self.path
|
||||
end
|
||||
|
||||
function FollowPath:on_step(delta)
|
||||
if not self.path then
|
||||
return
|
||||
end
|
||||
|
||||
local next = self.path[self.path_i]
|
||||
if not next then
|
||||
self.object:set_pos(self.path[#self.path])
|
||||
self.object:set_velocity(vector.new())
|
||||
self:set_walking(false)
|
||||
self.path = nil
|
||||
return
|
||||
end
|
||||
|
||||
local from = self.object:get_pos()
|
||||
|
||||
local distance = vector.distance(from, next)
|
||||
if distance < 0.1 then
|
||||
self.path_i = self.path_i + 1
|
||||
self:on_step(delta)
|
||||
return
|
||||
end
|
||||
|
||||
local step = vector.multiply(vector.normalize(vector.subtract(next, from)), 1)
|
||||
self.object:set_velocity(step)
|
||||
local target = vector.add(from, vector.multiply(step, delta))
|
||||
self.object:move_to(target, true)
|
||||
|
||||
self.object:set_yaw(math.atan2(step.z, step.x) - math.pi / 2)
|
||||
self:set_walking(true)
|
||||
end
|
||||
|
||||
function FollowPath:set_walking(walking)
|
||||
if self.animation ~= nil and self.animation == walking then
|
||||
return
|
||||
end
|
||||
|
||||
self.animation = walking
|
||||
self.object:set_animation(walking and self.animations.walk or self.animations.stand, 30, 0)
|
||||
end
|
2
init.lua
2
init.lua
|
@ -5,7 +5,7 @@ assert(minetest.features.pathfinder_works, "This mod requires Minetest 5.2 or la
|
|||
dofile(minetest.get_modpath("conquer") .. "/utils.lua")
|
||||
dofile(minetest.get_modpath("conquer") .. "/Session.lua")
|
||||
dofile(minetest.get_modpath("conquer") .. "/api.lua")
|
||||
dofile(minetest.get_modpath("conquer") .. "/entity_actions.lua")
|
||||
dofile(minetest.get_modpath("conquer") .. "/behaviour.lua")
|
||||
dofile(minetest.get_modpath("conquer") .. "/UnitEntity.lua")
|
||||
dofile(minetest.get_modpath("conquer") .. "/wands.lua")
|
||||
dofile(minetest.get_modpath("conquer") .. "/nodes.lua")
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
_G.conquer = {}
|
||||
|
||||
dofile("tests/mocks.lua")
|
||||
dofile("utils.lua")
|
||||
dofile("tests/mocks.lua")
|
||||
dofile("Session.lua")
|
||||
dofile("api.lua")
|
||||
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
_G.conquer = {}
|
||||
|
||||
dofile("utils.lua")
|
||||
|
||||
local mocks = dofile("tests/mocks.lua")
|
||||
dofile("behaviour.lua")
|
||||
|
||||
local MockBehaviour = mocks.MockBehaviour
|
||||
|
||||
describe("FickleSequence", function()
|
||||
local FickleSequence = conquer.behaviour.FickleSequence
|
||||
assert(FickleSequence)
|
||||
|
||||
it("runs in order", function()
|
||||
local a = MockBehaviour:new()
|
||||
local b = MockBehaviour:new()
|
||||
|
||||
local function reset()
|
||||
a.ran = false
|
||||
b.ran = false
|
||||
end
|
||||
|
||||
local seq = FickleSequence:new(a, b)
|
||||
assert.equals("running", seq:get_state())
|
||||
|
||||
assert.is_false(a.ran)
|
||||
assert.is_false(b.ran)
|
||||
seq:on_step(0.1)
|
||||
assert.is_true(a.ran)
|
||||
assert.is_false(b.ran)
|
||||
assert.equals("running", seq:get_state())
|
||||
|
||||
reset()
|
||||
seq:on_step(0.1)
|
||||
assert.is_true(a.ran)
|
||||
assert.is_false(b.ran)
|
||||
assert.equals("running", seq:get_state())
|
||||
|
||||
a.state = "success"
|
||||
|
||||
reset()
|
||||
seq:on_step(0.1)
|
||||
assert.is_true(a.ran)
|
||||
assert.is_true(b.ran)
|
||||
assert.equals("running", seq:get_state())
|
||||
|
||||
reset()
|
||||
seq:on_step(0.1)
|
||||
assert.is_true(a.ran)
|
||||
assert.is_true(b.ran)
|
||||
assert.equals("running", seq:get_state())
|
||||
|
||||
b.state = "success"
|
||||
|
||||
reset()
|
||||
seq:on_step(0.1)
|
||||
assert.is_true(a.ran)
|
||||
assert.is_true(b.ran)
|
||||
assert.equals("success", seq:get_state())
|
||||
end)
|
||||
|
||||
it("handles failed", function()
|
||||
local a = MockBehaviour:new()
|
||||
local b = MockBehaviour:new()
|
||||
a.state = "success"
|
||||
|
||||
local function reset()
|
||||
a.ran = false
|
||||
b.ran = false
|
||||
end
|
||||
|
||||
local seq = FickleSequence:new(a, b)
|
||||
assert.equals("running", seq:get_state())
|
||||
|
||||
assert.is_false(a.ran)
|
||||
assert.is_false(b.ran)
|
||||
seq:on_step(0.1)
|
||||
assert.is_true(a.ran)
|
||||
assert.is_true(b.ran)
|
||||
assert.equals("running", seq:get_state())
|
||||
|
||||
b.state = "failed"
|
||||
|
||||
reset()
|
||||
seq:on_step(0.1)
|
||||
assert.is_true(a.ran)
|
||||
assert.is_true(b.ran)
|
||||
assert.equals("failed", seq:get_state())
|
||||
end)
|
||||
end)
|
||||
|
||||
|
||||
describe("Sequence", function()
|
||||
local Sequence = conquer.behaviour.Sequence
|
||||
assert(Sequence)
|
||||
|
||||
it("runs in order", function()
|
||||
local a = MockBehaviour:new()
|
||||
local b = MockBehaviour:new()
|
||||
|
||||
local function reset()
|
||||
a.ran = false
|
||||
b.ran = false
|
||||
end
|
||||
|
||||
local seq = Sequence:new(a, b)
|
||||
assert.equals("running", seq:get_state())
|
||||
|
||||
assert.is_false(a.ran)
|
||||
assert.is_false(b.ran)
|
||||
seq:on_step(0.1)
|
||||
assert.is_true(a.ran)
|
||||
assert.is_false(b.ran)
|
||||
assert.equals("running", seq:get_state())
|
||||
|
||||
reset()
|
||||
seq:on_step(0.1)
|
||||
assert.is_true(a.ran)
|
||||
assert.is_false(b.ran)
|
||||
assert.equals("running", seq:get_state())
|
||||
|
||||
a.state = "success"
|
||||
|
||||
reset()
|
||||
seq:on_step(0.1)
|
||||
assert.is_true(a.ran)
|
||||
assert.is_false(b.ran)
|
||||
assert.equals("running", seq:get_state())
|
||||
|
||||
reset()
|
||||
seq:on_step(0.1)
|
||||
assert.is_false(a.ran)
|
||||
assert.is_true(b.ran)
|
||||
assert.equals("running", seq:get_state())
|
||||
|
||||
b.state = "success"
|
||||
|
||||
reset()
|
||||
seq:on_step(0.1)
|
||||
assert.is_false(a.ran)
|
||||
assert.is_true(b.ran)
|
||||
assert.equals("success", seq:get_state())
|
||||
end)
|
||||
|
||||
|
||||
it("handles failed", function()
|
||||
local a = MockBehaviour:new()
|
||||
local b = MockBehaviour:new()
|
||||
a.state = "success"
|
||||
|
||||
local function reset()
|
||||
a.ran = false
|
||||
b.ran = false
|
||||
end
|
||||
|
||||
local seq = Sequence:new(a, b)
|
||||
assert.equals("running", seq:get_state())
|
||||
|
||||
assert.is_false(a.ran)
|
||||
assert.is_false(b.ran)
|
||||
seq:on_step(0.1)
|
||||
assert.is_true(a.ran)
|
||||
assert.is_false(b.ran)
|
||||
assert.equals("running", seq:get_state())
|
||||
|
||||
reset()
|
||||
seq:on_step(0.1)
|
||||
assert.is_false(a.ran)
|
||||
assert.is_true(b.ran)
|
||||
assert.equals("running", seq:get_state())
|
||||
|
||||
b.state = "failed"
|
||||
|
||||
reset()
|
||||
seq:on_step(0.1)
|
||||
assert.is_false(a.ran)
|
||||
assert.is_true(b.ran)
|
||||
assert.equals("failed", seq:get_state())
|
||||
end)
|
||||
end)
|
|
@ -22,3 +22,24 @@ _G.vector = {
|
|||
}
|
||||
end,
|
||||
}
|
||||
|
||||
|
||||
|
||||
local MockBehaviour = conquer.class()
|
||||
|
||||
function MockBehaviour:constructor()
|
||||
self.state = "running"
|
||||
self.ran = false
|
||||
end
|
||||
|
||||
function MockBehaviour:get_state()
|
||||
return self.state
|
||||
end
|
||||
|
||||
function MockBehaviour:on_step()
|
||||
self.ran = true
|
||||
end
|
||||
|
||||
return {
|
||||
MockBehaviour = MockBehaviour
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
_G.conquer = {}
|
||||
|
||||
dofile("tests/mocks.lua")
|
||||
dofile("utils.lua")
|
||||
dofile("tests/mocks.lua")
|
||||
dofile("Session.lua")
|
||||
|
||||
local Session = conquer.Session
|
||||
|
|
|
@ -61,7 +61,7 @@ conquer.register_wand("conquer:move", {
|
|||
end
|
||||
|
||||
local unit_type = unit:get_luaentity():get_unit_type()
|
||||
local action = conquer.actions.FollowPath:new(path, unit_type.animations)
|
||||
local action = conquer.behaviour.FollowPath:new(path, unit_type.animations)
|
||||
unit:get_luaentity():set_action(action)
|
||||
end,
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue