Coroutine.yield() not yielding

So, I’ve been working on implementing a “Skip Cutscene” button for the game I’ve been working on. The script controlling the cutscene runs entirely in coroutines, as there needs to be multiple things happening at once, and at different intervals. Therefore, to implement a “Skip Cutscene” button, I should just have to implement coroutine.yield(), right? Apparently not. Anyone know what to do about this? (The button works, but doesn’t get past any of the yields.)

    --Skip Cutscene

	skipGui.Button.Activated:Connect(function()
		skipGui.Enabled = false
		script.Parent.Parent.RemoteEvents.MainMenu.MenuEvent:Fire()
		coroutine.yield(cutscene)
		coroutine.yield(dialogue)
		coroutine.yield(animations)
		coroutine.yield(blackout)
	end)
cutscene = coroutine.create(function()
        --First half of cutscene script

            --Calling the other coroutines
			coroutine.resume(dialogue)

			coroutine.resume(animations)

			coroutine.resume(blackout)

			--Second half of cutscene script
end)
Full script
local TweenService = game:GetService("TweenService")

local camera = game.Workspace.Camera

local cutsceneInfo

local tween

local moveTime

local delayTime

local cameraCounter = 1

local dialogueCounter = 0

local animationCounter = 0

local blackoutCounter = 0

local nextPosition

local nextDialogue

local nextAnimation

local nextBlackout

local dialogueGui = game.Players.LocalPlayer.PlayerGui:WaitForChild("CutsceneGui")

local dialogue

local animations

local blackout

local loadedAnim

local humanoid

local blackScreen = game.Players.LocalPlayer.PlayerGui:WaitForChild("BlackScreen")

local blackScreenTweenInfo = TweenInfo.new(1, Enum.EasingStyle.Linear, Enum.EasingDirection.InOut, 0, false, 0)

local chosenMap = game:GetService("ReplicatedStorage").Values.Chosen.ChosenMap

local debounce = false

local skipGui = game.Players.LocalPlayer.PlayerGui:WaitForChild("SkipCutscene")

local cutscene

--Cutscene Event

function startCutscene(deciderString)
	
	cutscene = nil
	dialogue = nil
	animations = nil
	blackout = nil
	
	--Dialogue

	dialogue = coroutine.create(function()

		while true do

			repeat

				dialogueCounter += 1

				nextDialogue = cutsceneInfo.Dialogue:FindFirstChild("Dialogue" .. dialogueCounter)

				wait(tonumber(nextDialogue.DelayTime.Value))

				dialogueGui.Enabled = true

				dialogueGui.Speaker.Text = nextDialogue.Speaker.Value

				dialogueGui.Speaker.TextColor3 = nextDialogue.SpeakerColor.Value

				dialogueGui.Dialogue.TextColor3 = nextDialogue.SpeakerColor.Value

				require(game:GetService("ReplicatedStorage").Functions.MiscFunctionStorage).Typewriter(nextDialogue.Value, dialogueGui.Dialogue)

				wait(tonumber(nextDialogue.AfterDelayTime.Value))

				dialogueGui.Enabled = false

			until dialogueCounter == #cutsceneInfo.Dialogue:GetChildren()

			dialogueCounter = 0

			coroutine.yield(dialogue)

		end

	end)

	--Animations

	animations = coroutine.create(function()

		while true do

			repeat

				if animationCounter ~= #cutsceneInfo.Animations:GetChildren() then

					animationCounter += 1

					nextAnimation = cutsceneInfo.Animations:FindFirstChild("Animation" .. animationCounter)

					wait(nextAnimation.DelayTime.Value)

					for _, id in pairs(cutsceneInfo.Parent:GetDescendants()) do
						if id:IsA("IntValue") and id.Value == nextAnimation.HumanoidID.Value and id ~= nextAnimation.HumanoidID then
							humanoid = id.Parent.Humanoid
							break
						end
					end

					humanoid:LoadAnimation(nextAnimation):Play()

				end

			until animationCounter == #cutsceneInfo.Animations:GetChildren()

			animationCounter = 0

			coroutine.yield(animations)

		end

	end)

	--Blackout

	blackout = coroutine.create(function()

		while true do

			repeat

				if blackoutCounter ~= #cutsceneInfo.Blackout:GetChildren() then

					blackoutCounter += 1

					nextBlackout = cutsceneInfo.Blackout:FindFirstChild("Blackout" .. blackoutCounter)

					wait(nextBlackout.Delay.Value)

					blackScreen.Enabled = true

					if nextBlackout.Value then
						TweenService:Create(blackScreen.BlackScreen, blackScreenTweenInfo, {BackgroundTransparency = 0}):Play()
					else
						nextBlackout.BlackScreen.BackgroundTransparency = 0
					end

					if nextBlackout.Text.Value then

						blackScreen.BlackScreen.TextLabel.Text = nextBlackout.Text.Text.Value

						TweenService:Create(blackScreen.BlackScreen.TextLabel, blackScreenTweenInfo, {TextTransparency = 0}):Play()

					end

					wait(nextBlackout.AfterDelay.Value)

					if nextBlackout.AfterFade.Value then
						TweenService:Create(blackScreen.BlackScreen, blackScreenTweenInfo, {BackgroundTransparency = 1}):Play()
						if nextBlackout.Text.Value then
							TweenService:Create(blackScreen.BlackScreen.TextLabel, blackScreenTweenInfo, {TextTransparency = 1}):Play()
						end
						wait(1)
						blackScreen.Enabled = false
					else
						blackScreen.Enabled = false
						blackScreen.BlackScreen.BackgroundTransparency = 1
						blackScreen.BlackScreen.TextLabel.TextTransparency = 1
					end

				end

			until blackoutCounter == #cutsceneInfo.Blackout:GetChildren()

			blackoutCounter = 0

			coroutine.yield(blackout)

		end

	end)
	
	cutscene = coroutine.create(function()

		if not debounce then

			debounce = true
			
			game.SoundService.MainTheme:Stop()
			
			blackScreen.Parent.Menu.PlayGui.Enabled = false

			if deciderString == "End" then skipGui.Enabled = true end

			blackScreen.Enabled = true

			TweenService:Create(blackScreen.BlackScreen, blackScreenTweenInfo, {BackgroundTransparency = 0}):Play()

			wait(1)

			camera.CameraType = Enum.CameraType.Scriptable

			cutsceneInfo = game.Workspace.LoadedMaps:FindFirstChild(chosenMap.Value).Cutscenes:FindFirstChild(deciderString).CutsceneInfo

			camera.CFrame = cutsceneInfo.PartPositions.Part1.CFrame

			wait(0.5)

			blackScreen.Enabled = false

			blackScreen.BlackScreen.BackgroundTransparency = 1
			
			--Call other coroutines
			
			coroutine.resume(dialogue)

			coroutine.resume(animations)

			coroutine.resume(blackout)

			wait(tonumber(cutsceneInfo.PartPositions.Part1.DelayTime.Value))

			--Camera Movement

			repeat

				if cameraCounter ~= #cutsceneInfo.PartPositions:GetChildren() then

					cameraCounter += 1

					nextPosition = cutsceneInfo.PartPositions:FindFirstChild("Part" .. tostring(cameraCounter))

					moveTime = tonumber(nextPosition.MoveTime.Value)

					delayTime = tonumber(nextPosition.DelayTime.Value)

					tween = TweenService:Create(

						camera, 

						TweenInfo.new(
							moveTime,
							Enum.EasingStyle.Sine,
							Enum.EasingDirection.InOut,
							0,
							false,
							0	
						), 

						{CFrame = nextPosition.CFrame}

					)

					tween:Play()

					wait(tonumber(nextPosition.DelayTime.Value))

				end

			until cameraCounter == #cutsceneInfo.PartPositions:GetChildren()
			
			--End cutscene

			cameraCounter = 1

			blackScreen.Enabled = true

			TweenService:Create(blackScreen.BlackScreen, blackScreenTweenInfo, {BackgroundTransparency = 0}):Play()

			wait(1)

			game:GetService("ReplicatedStorage").Events.CutsceneTrigger:FireServer()

			wait(0.5)
			
			if deciderString == "Start" then
				camera.CameraType = Enum.CameraType.Custom
				camera.CameraSubject = game.Players.LocalPlayer.Character.Humanoid
			else
				script.Parent.Parent.RemoteEvents.MainMenu.MenuEvent:Fire()
			end

			blackScreen.Enabled = false

			blackScreen.BlackScreen.BackgroundTransparency = 1

			debounce = false

		end

	end)
	
	coroutine.resume(cutscene)
	
	--Skip Cutscene

	skipGui.Button.Activated:Connect(function()
		skipGui.Enabled = false
		script.Parent.Parent.RemoteEvents.MainMenu.MenuEvent:Fire()
		coroutine.yield(cutscene)
		coroutine.yield(dialogue)
		coroutine.yield(animations)
		coroutine.yield(blackout)
	end)
end

game:GetService("ReplicatedStorage").Events.CutsceneTrigger.OnClientEvent:Connect(startCutscene)
1 Like

You should check out coroutine documentation to understand what you’re doing. coroutine.yield is used to suspend the coroutine it was called in and the arguments are returned to a resume. The yield is being applied to the function connected to Actiated, not to your other coroutines.

You sure you absolutely need coroutines here, or that you can’t substitute them out with the task library? I don’t know what your other coroutines are doing since only the cutscene one was provided but yeah you can’t yield other coroutines, only the current one.

1 Like

Oh, I see. That makes sense now that I think about it. I’ll look up what task library is, and see how I can implement that.

After doing some research, it seems that task.spawn() and task.defer() would be what I need, but nothing seems to be telling me what a “Resumption Cycle” is. Do you know what it is? I just need to know if it’s something I need to worry about in my script, as I plan on deferring the function permanently.

From my understanding, they are special points in the schedule where functions connected to task.defer will run.

1 Like

So, everything except those will stop?

I mean everything except these things will no longer be running. Is that right?

What’s an AFAIK event?

Please read the following post:

[Beta] Deferred Lua Event Handling - Updates / Announcements - DevForum | Roblox

I did, and I didn’t understand a thing in it.

Where all event handlers will run in the future. Deferred SignalBehavior will become the default in the future, meaning that all code handling signals will be ran at those points. task.defer will push a function or thread to be resumed at the next invocation point from the current one. “Invocation points” defines a place where event handlers can be executed; “resumption cycle” is a collection of those (AFAIK).

Not what you’re looking for; “deferring permanently” is the same as not running the code at all. You need to think about the structuring of your code itself. Specifically the pattern you’re looking for looks like it can still be done in sequence. Tackle the root problem: why did you want coroutines? What are you running that needs coroutines and why does it not work without them?

1 Like

It didn’t work without coroutines because the scripts yield when calling the functions, but I need the script to be running multiple things at once.

Also, about the defer thing, I see the basic idea of what you mean. In that case, how would I pause and reset a function in the middle of it running?

You should still consider the structure of your code.

As it stands, the cause for your yields are your while and repeat loops. Indeed, outside of a coroutine they would yield subsequent code, but you should ask yourself if you actually need loops here or if you can do this in an event-based manner where the events of something are controlling the flow of logic here instead of running them all independently.

It’s a little hard to read the code as-is but I can tell that you could easily all of these coroutines together or at least create a proper modular system that allows you to iterate through scripts that are each responsible for handling a scene. For skipping, it’d just be cleaning up the resources and stopping animations then resetting the camera. It’s roughly similar to what I do for my own experience.

You don’t. Defer pushes the time in which your function is executed. No function can be “paused and reset” while it’s executing. You can do it very roughly but it’d still require you to have points where you exit the scope and an outside instruction to call the function again.

1 Like

I’m not really sure what you mean by

Could you maybe explain? Do you mean firing a bindable event to other scripts?


Also, I’m pretty sure I need the loops, as the way I’ve gone about creating the cutscenes is by placing all of the information that they have inside of a group, and cycling through said information.

Roughly. I meant something like this:

image

You could have signals in these modules that get fired based on the state of the current scene and then iterate through them as you go (e.g. when Scene1.Finished fires, move to Scene2). When skip is called, then you end the current scene without moving to the next one.

If you follow through with an event-based solution, you would not need the loops, hence why I’m keen on telling you to review your code structure and whether or not it actually needs loops or if you can design an event-based system to handle your cutscene. My personal mantra is that loops should only be an absolute last resort to a problem, not a first; and that’s assuming the system in question can be accomplished without loops. Cutscenes can be fully event-based.

The thing is, I’m not separating anything by scenes. Everything is contained in these loops, and each one controls a different part of the cutscene. cutscene controls the main logic as well as the camera, dialogue controls the dialogue, animations controls the animations, and blackout controls making the entire screen black, great for transitions. All of the information for these things are stored inside of a model inside of the cutscenes, separated into folders for everything except the camera positions, which are stored inside of a model. That is why I need to use the loops.

Also, the cutscenes are all handled in a localscript, as different players may be experiencing the cutscene at different times.

Scenes is just a heavy example as well as an assumption of mine but event-based structuring is still applicable here - it’s applicable in 90% of systems you will ever make or want to make. The crux of my response is not to focus in on my proposed solutions but rather what I’m getting at which is that you need to take a look at the structuring of your code.

None of this needs to be pseudothreaded; you can have events that advance every “step” of your cutscene, thus running a new dialogue/animation/transition as needed, then connect functions to those steps that will trigger one or more of those actions. As for your skip button, its responsibility would be to just end the cycle of events. Subsequently connected ones would not need to do anything because if those steps aren’t reached the relevant events wouldn’t be fired to begin with.

I come at this from exactly the same way I do my cutscenes which are also client-sided. We have animations for different NPCs as well as a dummy object that represents the camera. The camera’s animation sets use keyframe markers which primarily control how certain things during the cutscene should be triggered be it dialogue, transitions or marking when the overall cutscene should end.

You do not need to use loops.

I don’t understand, how would I cycle through all of the camera positions without loops?

Event-based system. Connect to those events to determine when the camera’s position should be changed or just use an animation to control the camera positioning. It’s either that or you have a single controllable variable that you can increment and as it increments you activate a new action in your scene. One to control them all, not four different coroutines running independently.

Reviewing your structuring, be it your code or the way you’ve set up your cutscene resources, is incredibly pivotal to fixing the root problem here.

For reference, again, my own cutscene:

image

Everything is run via animations, including the camera. This allows us to make use of keyframe markers to create an event-driven solution to cutscenes. We connect effects to happen based on those markers, as per the third quoted excerpt.

So, I would just have a separate script for every cutscene?