What is the cleanest, most efficient way to stop a function from running immediately?

I know what you’re most likely gonna say: a return. But I wanna explain what I’m trying to accomplish and why I want something a bit better.

I’m making a Turn-Based Combat type of game where players and enemies attack each other with card attacks. Each attack comes with its own script with animations and sounds and camera movements. For example, I have a Sword card where its script has the player pull out a sword, walk up to the enemy, swing it, and then walk back to their sigil. This lasts about 10 seconds. More powerful cards will have longer and more complicated scripts so that they look cooler.

The thing is, I’m trying to handle this case where if a player leaves DURING the animation, the script would error due to the player no longer existing. If the player leaves, I want the script to stop immediately so that the battle can end. So yes, I could use return, but I’d have to literally do it every line like this:

(Pseudocode)

local move = player:MoveTo(enemy.Position)
if player == nil then
    return
end
wait(3)
if player == nil then
    return
end
slash()
dealDamage()
wait(1)
if player == nil then
    return
end
local move2 = player:MoveTo(player.Position)
wait(3)
if player == nil then
    return
end

So, you see my dilemma? I don’t wanna have to put a return statement after every single line in which the player might disappear due to leaving or losing connection. It makes my code look messy and it can get really repetitive when I’m dealing with bigger scripts for more powerful attacks.

Is there a much nicer, more clean way of handling this?

3 Likes

can always use pcall

local success,error=pcall(function()
–do code
end)
if not success then print(error) end

2 Likes

To me this is a super interesting topic, so this is kind of just my own ideas about how you might go about it. I’m not sure how practical any of this is, but you might find it interesting too :sweat_smile:


You’re calling functions that depend on a specific game state (the player still existing) when that might not be the case.

Create a wrapper for those functions that gracefully deal with different game states. E.g.:

local function tryPlayerMoveTo(player, targetPosition)
	--Moves player to targetPosition
	--	returns true indicating success or false indicating failure (in case player left)

	if player == nil then
		--Calling with nil for player is valid, because the player might have left
		return false
	elseif typeof(player) ~= "Instance" or player.ClassName ~= "Player" then
		error(string.format("Called tryPlayerMoveTo with invalid arguments. Expected Player or nil, got %s", getClassNameOrValueType(player)))
	end

	player:MoveTo(targetPosition)
	return true
end

Alternatively, yield after every action and have a condition check in a loop that keeps doing the next action if that’s still valid (i.e. player still exists):

local function performActionSequence(player, fActionSequence)
	local cActionSequence = coroutine.create(fActionSequence)
	while player do
		coroutine.resume(cActionSequence)
	end
end

Your action sequence would then look something like this:

local function basicSlashActionSequence()
	local y = coroutine.yield

	local move = player:MoveTo(enemy.Position)
	y()
	wait(3)
	y()
	slash()
	y()
	dealDamage()
	y()
	wait(1)
	y()
	local move2 = player:MoveTo(player.Position)
	y()
	wait(3)
end

Still kind of ugly, but a lot less IMO and much easier to e.g. change the conditions for doing the next action.


If you want to get even fancier/weirder/more unnecessarily complex, you could have a system where each actionSequence is just a table, where each entry is a table where the first value is a function and the rest are parameters to pass to that function. Then you could define an actionSequence like so:

local basicSlashActionSequenceFactory = function(player, enemy)
	return {
		{moveTo, {enemy.Position}},
		{delaySeconds, {3}},
		{slash},
		{dealDamage},
		{delaySeconds, {1}},
		{moveTo, {player.Position}},
		{delaySeconds, {1}}
	}
end

Preforming an action sequence might go like this:

local function performAction(action)
	local fAction = action[1]
	local fArgs = action[2]
	return fAction(unpack(fArgs))
end

local function performActionSequence(actionSequence, player, ...)
	for action in pairs(basicSlashActionSequenceFactory(player, ...)) do
		if player then --or whatever other checks you might want
			performAction(action)
		end
	end
end
4 Likes