Is it legal to stop a thread from the inside? Are there any alternatives?

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.

1 Like
  • coroutine.yield
  • return
1 Like

Could you explain a little more? I’m currently trying to study coroutines in more depth. But I don’t know how to use it yet.

BindableEvents are a great solution, because any signal callback is called on a separate thread. You’ve correctly found out that the current executing thread cannot cancel itself (docs on task.cancel confirm this), but of course, is a bindable event is called, it will happily cancel the targeted thread.

May I ask why you dislike state machines explicitly? They’re very popular because they’re very clean, easy to implement, easy to plan and to graph out, even on paper. Conversion between states is defined easily, and the logic can be made flexible to customise which state transitions are allowed.
PS. Your logic is very state-machiney and could be converted easily (each function is a state, and you can set up restrictions so e.g. choose_a_store can only transition to go_to_store). Your states can be made to yield, and when the state finishes (e.g. go_to_store), it can decide which state to get into next by itself. If you do this, you can make the current executing state run in a new thread, and if you need to interrupt the NPC and give it a new task, you can just cancel the thread.

My approach has one downside, and it’s that you can only stack task.spawn so much. However, if your states are finite (e.g. At home → Find store → Go to store → Wait for store to open → Shop → Go back home), you can have some managerial code that will simply restart the sequence once it finishes. This way you will never hit a stack overflow.

2 Likes

I’ll try to explain why I don’t like “finiti state machines”.:

  • I looked at several implementation options, and they were all either unnecessarily complicated (where, with the addition of a new state, it was necessary to manually describe the relationship with other states, necessarily all of them). I think it would be easier to specify only the available transitions.
  • They all use binding to RenderStepped or RenderHeartbeat. I don’t think this is a big problem, but in my implementation it’s just not necessary)

After that, I came across BehaviorTrees, it seemed to me that it was very convenient for my purposes. I immediately started using Tyridge’s ready-made solution.
And soon I realized that I could not change the order of actions during the work of the tree. I tried using the flag, and I tracked it between transitions from node to node. But it’s extremely inconvenient, and it doesn’t work instantly.

The way you explained it, I think that settles the question. Perhaps I did not understand the work of “finite states machines” well… Could you share any (video, article) lesson explaining the implementation of “finite states machines” that suits your vision?

Can’t you just return the entire code block? Or you mean pause the thread (instead of completely stopping it)?

I’ve already thought about implementing it like this:
The task is running and returns a callback (the next task)
But I haven’t been able to make it work yet..

coroutine.yield() will just yield the current thread, which of course can still be resumed outside the thread, but if there aren’t any references to the thread and you yield inside the thread it’ll be gc’d

1 Like

Does it run the next task specifically after it has completely finished running? Or is it possible for the task to continue running and invoke other tasks? You should model the behavior it will help you with simplifying the code.

As you said, it will start a new task after it is completely finished.
That is, only one task is running at the moment! No more.
And I still can’t figure out how to do it as simply, but without using BindableEvent.

So if the tasks run in order one after the other, what’s the point of using threads in the first place? If there’s no time overlap between running code.

1 Like

Yes, the tasks are completed one after the other. If I didn’t want to interfere with the work of NPCs, their life cycle would be a simple sequence of tasks. But the point here is that tasks serve as states. And other NPCs or something else (other scripts) can change the state of an NPC at any given time, i.e. they can change the position of the npc’s lifecycle. Below I have made a very simplified version of the FSM(state machine) without using BindableEvent.

local states = {}
local is_running: boolean = false
local current_task: thread = nil
local next_state_id = "one"

local function next_state(state_id: string)
	print("Changed state: "..state_id)
	next_state_id = state_id
	is_running = false
end

function states.one() --Example of a state1
	for i = 1, 10 do
		task.wait(0.5)
		print(tostring(i).."A")
		if math.random(1,10) < 2 then next_state("three") end
	end
	
	next_state("two")
end

function states.two() --Example of a state2
	for i = 1, 10 do
		task.wait(0.5)
		print(tostring(i).."B")
	end
	next_state("one")
end

function states.three() --Example of a state3
	for i = 1, 10 do
		task.wait(0.5)
		print(tostring(i).."C")
	end
	next_state("one")
end

game:GetService("RunService").Stepped:Connect(function()
	if is_running then return end --We check whether the task is running?
	if current_task ~= nil and typeof(current_task) == "thread" then 
		task.cancel(current_task) --If the state has been changed, cancel the task
	end
	current_task = task.spawn(states[next_state_id]) --Launching a new task (state)
	is_running = true
end)

Simply put, I need each task to have a link (in any form) to the next task so that I can build a sequence of tasks. But I also need to be able to change a task from other scripts to (a task from the list of existing tasks) or interrupt this sequence altogether.

I use thread in order to:

  • He could have been easily stopped.
  • There was no stack overflow (as in cases with recursion)

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.