If you’ve ever tried to stop a thread from the inside, you’ve probably encountered an error.
local current_task: thread = task.spawn(function()
--code--
task.cancel(current_task)
end)
--OUTPUT:
01:16:20.490 Workspace.Script:3: invalid argument #1 to 'cancel' (thread expected, got nil) - Server - Script:3
01:16:20.490 Stack Begin - Studio
01:16:20.490 Script 'Workspace.Script', Line 3 - Studio - Script:3
01:16:20.490 Stack End - Studio
I tried different methods, and lo and behold! BindableEvent helped me realize my desires. The bottom line is that I’m using this approach to implement complex AI tree logic. And it’s very convenient, because I can stop and change the task of intelligence both from the outside and from the inside.
local bindable: BindableEvent = Instance.new("BindableEvent")
local current_task: thread = nil
local function change_task(callback : Function)
bindable.Event:Once(function()
task.cancel(current_task) -- cancel runned task
current_task = task.spawn(callback) -- starting a new task
end)
end
--Example of logic construction--
function first_function()
--code--
change_task(second_function)
end
function second_function()
--another code--
change_task(first_function)
end
--It will work cyclically, and I can, for example, stop it from the outside at any time.
I have not seen such a practice on the Internet, and the state machines or behavior trees system is being promoted to implement AI. I don’t like any of this. Can I continue to use my approach? Or may it stop working with API updates in the future???
--I'm sorry about 'snake_case'
local stores = {
store1 = {is_open = true, position = Vector3.new(0,0,10)},
store2 = {is_open = true, position = Vector3.new(10,0,0)}
}
local home_position: Vector3 = Vector3.new(0, 0, 0)
local npc_class = {}
npc_class.__index = npc_class
function npc_class.new(character: Model)
local self = setmetatable({}, npc_class)
self.character = character
self.humanoid = character.Humanoid
self.humanoid_root_part = character.HumanoidRootPart
self.task = nil
self.bindable = Instance.new("BindableEvent")
return self
end
function npc_class:change_task(callback)
if callback == nil then return end
self.bindable.Event:Once(function()
if self.task ~= nil and typeof(self.task) == "thread" then
task.cancel(self.task)
end
self.task = task.spawn(function() callback(self) end) -- Passing self as the first argument
end)
end
function npc_class:choose_a_store() -- Choosing a store
local choose_store = nil
for store_name, store in pairs(stores) do
if store.is_open == true then
choose_store = store
end
end
if choose_store == nil then
self:change_task()
else
self:change_task()
end
end
function npc_class:go_to_store(store: {position: Vector3}) -- He goes to the store
self.humanoid:MoveTo(store.position)
self.humanoid.MoveToFinished:Wait()
--shopping code--
self:change_task(self.go_back_home)
end
function npc_class:wait_for_store_to_open() -- Waiting for the store to open
task.wait(10)
self:change_task(self.choose_a_store)
end
function npc_class:go_back_home() -- Returns home after shopping
self.humanoid:MoveTo(home_position)
self.humanoid.MoveToFinished:Wait()
self:destroy()
end
function npc_class:destroy()
self.bindable:Destroy()
self.character:Destroy()
setmetatable(self, nil)
table.clear(self)
table.freeze(self)
end
--[[
As you can see, if all the stores were closed
It will endlessly repeat the loop: choose_a_store > wait_for_store_to_open > choose_a_store
And if I need to, I can change the NPC's task from the outside at any time.
For example, if the store he was going to closes, I can change his task to choose_a_store.]]
As you can see, if all the stores were closed
It will endlessly repeat the loop:
choose_a_store > wait_for_store_to_open > choose_a_store
And if I need to, I can change the NPC’s task from the outside at any time.
For example, if the store he was going to closes, I can change his task to choose_a_store.
The examples you see above are very simplified versions. In reality, my NPCs have more than 20 states. For example, an NPC is a customer when visiting a store:
-Looks through the store’s assortment and makes a shopping list.
-Moves to the storefronts containing the product from the list.
-Analyzes the price of the product.
-Makes a purchase decision based on monetary possibilities.
-Instantly reacts to changes in the position of the target (showcase) in the store.
-Various reactions to the discrepancy between the price and the market, the quantity of products needed, etc.
-Looking for the most accessible and free cash register
-Also reacts instantly to the closure / breakdown of the cash register
-Selects the payment method
etc. The list can be continued indefinitely…
Now I am actively studying the Internet, and I am trying different approaches to implement my plans.
Any suggestions you have will be considered!
I will try to write an implementation of the same example (above) for “finiti state machines” soon.
Or maybe I’ll make changes to the ready-made BehaviorTree V5 solution from Tiridge.
I do not intend to give up the opportunity to react instantly to external changes.
An example of external changes:
when an NPC chooses a store, it adds itself to the list of customers.
When the store closes, it goes through the list of NPCs and notifies them all of the closure.
The NPC instantly changes tactics and the current task.
I’d like to answer a few questions.:
-
Can I continue to use binding to BindableEvents to cancel a Thread? Or is this a bad practice, please explain why?
-
If you claim that my approach is not optimal, what should I do? Which implementation of such AI would you choose? It is advisable to provide examples or articles.