Clean up connections and instances
Use the bag you were handed
Section titled “Use the bag you were handed”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))endThe bag closes when the player leaves. There is no OnPlayerRemoving teardown
to write.
Pick the right lifetime
Section titled “Pick the right lifetime”Twill.Scope.Player(player)Twill.Scope.Character(character)Twill.Scope.Alive(character)| Bag | Closes | Use it for |
|---|---|---|
Player | The player leaves. | Anything that belongs to the session. |
Character | The character is removed, which Roblox does at respawn, not at death. | Ragdoll, corpse effects, anything that should outlive the death. |
Alive | The 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.
Bags are shared
Section titled “Bags are shared”Asking again for the same scope returns the same bag.
-- in two unrelated servicesTwill.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.
Twill functions take a bag
Section titled “Twill functions take a bag”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.
Anything with Destroy fits
Section titled “Anything with Destroy fits”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.
What Destroy does not do
Section titled “What Destroy does not do”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.
Global connections
Section titled “Global connections”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.
Using Scope on its own
Section titled “Using Scope on its own”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()