Skip to content

Clean up connections and instances

A service receives a player bag as the third argument of OnPlayerReady. Most code never needs anything else.

function CombatService.OnPlayerReady(player, data, bag)
bag:Connect(player.CharacterAdded, onCharacter)
bag:Add(buildHudFor(player))
end

The bag closes when the player leaves. There is no OnPlayerRemoving teardown to write.

Twill.Scope.Player(player)
Twill.Scope.Character(character)
Twill.Scope.Alive(character)
BagClosesUse it for
PlayerThe player leaves.Anything that belongs to the session.
CharacterThe character is removed, which Roblox does at respawn, not at death.Ragdoll, corpse effects, anything that should outlive the death.
AliveThe humanoid dies, or the character is removed without dying.Movement, abilities, input. Anything that must stop the instant they are dead.

The distinction between the last two is the one that causes bugs. A sprint loop in a Character bag keeps running on a corpse until the respawn.

Asking again for the same scope returns the same bag.

-- in two unrelated services
Twill.Scope.Player(player):Add(a)
Twill.Scope.Player(player):Add(b)

Neither service has to know about the other, and one close cleans up both.

Every Twill function that starts something lasting takes an owner bag as its last argument.

Twill.Loop.Every(1, tick, bag)
Twill.Loop.After(5, expire, bag)
Twill.Watch.Tagged("Door", onDoor, bag)
Twill.Tree.Build(spec, playerGui, bag)
Twill.Pool.new(template, { Size = 20 }, bag)
Twill.Navigation.new(npc, nil, bag)

Leave the last argument out and the work goes to Scope.Framework(), which lives as long as the server does. That is correct for a global system and wrong for anything tied to a player.

bag:Add(instance)
bag:Add(subscription)
bag:Add(handle)
bag:Add(errors, "Destroy")
bag:Add(function()
restoreSomething()
end)

A bag closes an instance, a connection, a function or a thread on its own, and a table through its Destroy or Disconnect method. Every handle Twill returns carries one of those. For anything else, name the method, as with "Destroy" above.

This is the usual way test code leaks, and the usual cause of a Studio session that grows steadily slower. Put the connection in a bag.

For something that genuinely lives as long as the process:

Twill.Scope.Framework():Connect(
MarketplaceService.PromptGamePassPurchaseFinished,
onPassBought
)

Nothing in Twill starts a connection nobody owns, and neither should your code.

Scope does not require the rest of the framework, but player bags normally close because Lifecycle tells them to. Without Lifecycle, ask Scope to watch for itself:

Twill.Scope.OwnPlayerClosing()