Release Notes for 564

Expected behavior change for Tools:

If you’re seeing incorrect behavior for other Instance types please let us know.

Arabic and some other written languages are actually written from right-to-left rather than left-to-right like English text is. Properly localizing your game to these languages with no help from the engine is quite an undertaking. Most UI systems, soon to include Roblox, offer some affordances to make localizing to these languages less of a pain.

5 Likes

ListObjectsAsync has a new optional parameter called “excludeDeleted” which if enabled, will hide any deleted objects from being returned.

It appears a slight error was made here, went searching for a bit and found that this was referring to the ListKeysAsync function of ‘DataStore’, ListObjectsAsync is not a valid API member

4 Likes

UnionAsync, NegateAsync, but what is this?
Screenshot_20230224_075249

1 Like

probably something like this
image
(kept geometry is blue)

6 Likes

@JoolCaat is right. You can see it in action in an example by @Maximum_ADHD https://twitter.com/MaximumADHD/status/1626002378162860032

8 Likes

@JoolCaat

That is really interesting! Honestly just mentioning “inversed negation” would’ve made me understand but seeing it implemented is awesome. What’s the keybind for it?

2 Likes

…?

for i, v in pairs(Character:GetDescendants()) do 
      if v:IsA("Weld") then -- and other classes
            v:Destroy()
      end
end
2 Likes

Um, actually :nerd_face:

for _, Object in Character:GetDescendants() do
    if Object:IsA("Weld") then
        Object:Destroy()
    end
end

Generalized iteration exists :nerd_face:

2 Likes

Actually…

for _, Object in Character:GetDescendants() do
    if Object:IsA("JointInstance") then
        Object:Destroy()
    end
end

So you dont have to add each individual class

9 Likes

(cc @Judgy_Oreo @Vundeq)

Do I have to explain why BreakJoints is obviously faster than iterating over possibly hundreds of instances.

5 Likes

Behind the curtain the code is going to be the same, its only slightly more convenient than writing a module for your projects that can contain custom functions for your own desired use cases (which you should be doing), some API is bloat, and some is helpful, whether or not breakjoints is bloat is the discussion we should be having here, not whether or not it is more performant.

2 Likes

Fun fact, the code linked here is actually substantially different than what BreakJoints does. Did you know that if you put your parts under a Folder (or any other non-Model non-Part Instance) within the model you’re calling it on, they won’t have their joints broken by BreakJoints!

BreakJoints as a very old function which was implemented in the initial engine API so it has some pretty old janky behavior.

14 Likes

What if the joint is parented to another part? e.g if Part1 has a weld that connects to Part2, and breakJoints is called on Part2, looping through the descendants of Part2 wouldn’t work to destroy them.

2 Likes

This is the code that’s required to replicate exactly what BreakJoints does (which probably isn’t what you want because it doesn’t handle parts under other parts, or folders, etc. But if you do want an exact replacement that’s what it would be):

local function breakJoints(root)
    if root:IsA("Model") then
        for _, child in root:GetChildren() do
            breakJoints(child)
        end
    elseif root:IsA("BasePart") then
        for _, joint in root:GetJoints() do
            if joint:IsA("JointInstance") or joint:IsA("WeldConstraint") then
                joint:Destroy()
            end
        end
    end
end
14 Likes

I still think it would be useful to have one of two things to replace this

A filtered GetChildren/GetDescendants
OR A more modern replacement to BreakJoints

2 Likes

I opened some discussion on it but we’re probably not going to immediately revert the deprecation.

As you can see from the code I just posted BreakJoints is such a messy old function that there’s good reason for it to be deprecated. Though as you note from a practical perspective having some better API to work with joints would be quite useful. Maybe separating out break internal vs external joints for instance.

3 Likes

9/3/2023:

Someone asked me for what I did in that Doomspire clone in more detail. Here’s what I sent them…

My full solution for resolving performance issues was to collect all joints to be deleted into a big list, defer (if I didn’t already) and then destroy all the joints in one go at the end of the tick/frame.

I empirically found deferring before destroying joints to be the most performant. The hunch I had that led me to try deferring to resolve the big lag spikes I was getting was that in the microprofiler I saw assemblies were getting recalculated almost immediately after destroying joints (likely triggered by some kind of queries or accesses that I or the engine does after destroying the joints?). I figured maybe doing all the updates all at once could maybe reduce the amount of recalculations down to just one, which it certainly seemed to or at least seemed to simplify the work somehow as it did result in much better performance, and the microprofiler showed all those recalculations as one big bar immediately proceeding the deferred threads bar.

Presumably something must cause the engine to do recalculations after joints are destroyed even mid-tick. But deferring gets around this by doing all the destructions together at once with no interruptions, presumably resulting in only one recalculation of the assembly/model being done.

My implementation roughly looked like this:

local BreakJointsService = {}

local jointsToDestroy = {}
local destroyJointsThread
function BreakJointsService:_deferAndDestroy()
	-- If a joint destruction thread is already active, cancel
	if destroyJointsThread then
		return
	end

	-- Spawn a thread which will destroy the joints
	destroyJointsThread = task.defer(function()
		-- Clear the destroy joints thread so that another may be spawned
		destroyJointsThread = nil

		-- Destroy all the pending joints
		for _, joint in jointsToDestroy do
			joint:Destroy()
		end

		-- Replace the joints to destroy table with a brand new fresh table
		jointsToDestroy = {}
	end)
end

function BreakJointsService:_destroyJoints(instance: Instance)
	-- If the instance is not a BasePart, it doesn't have any joints to be destroyed
	if not instance:IsA("BasePart") then
		return
	end

	-- Get the joints of the part
	local joints = instance:GetJoints()

	-- Copy the joints into the joints destruction list
	table.move(joints, 1, #joints, #jointsToDestroy + 1, jointsToDestroy)

	-- Defer & destroy all pending joints
	self:_deferAndDestroy()
end

-- Breaks the joints of the given instance and all of its descendants
function BreakJointsService:BreakJoints(instance: Instance)
	self:_destroyJoints(instance)
	for _, instance in instance:GetDescendants() do
		self:_destroyJoints(instance)
	end
end

return BreakJointsService

My stance on ipairs/pairs has also changed to align with tnavarts’ a lot more. Generalized iteration does seem to result in more readable code with less noise.

My logic of communicating if something is an array via ipairs or pairs doesn’t really have a huge benefit when generalized iteration makes that irrelevant anyways, because the way that you mutate tables will make them clearly arrays or non-arrays, and any mutations that go against that aren’t really going to affect the iteration, so, it ends up being a bit redundant.

Ultimately, I have switched to using generalized iteration for the cleaner code, and because of the __iter metamethod. I did not value the __iter metamethod enough until recently, it’s pretty great.

Old post

Summary

I don’t think BreakJoints is getting deprecated? Nevermind, I guess I didn’t read enough haha.

You can do this for parts:

for _, joint in ipairs(part:GetJoints()) do
    joint:Destroy()
end

Model:BreakJoints() is less trivial but it would be pretty much the same thing, just over all BasePart descendants of the model.

Also note that unlike BreakJoints this would also break RigidConstraint and all other constraints for that matter. WeldConstraint is the only Constraint instance that will be broken by BreakJoints.

And a note on performance… In a little weird sci-fi themed doomspire clone I made, I actually had to re-implement BreakJoints myself because the engine method was causing ginormous lag spikes lasting 3-4 seconds long.

I narrowed the cause down to some sort of assembly recalculation thing that was always triggered immediately after a call to BreakJoints, and after re-implementing it myself, I get consistently (and noticeably) better performance particularly when destroying 100 or more joints. (Whereas iirc lower numbers of joints seemed to perform a little bit slower than BreakJoints but not to an unreasonable extent). I couldn’t tell you why that might be, but that was my observation, it was consistent, and, it took many hours to discover the root cause.

I have a strong (personal) preference for using ipairs and pairs because it explicitly indicates that I’m expecting and using a table, and it’s a little bit more specific about the kind of table operation I want to do. For example in that case I would use ipairs because I expect an array. Another problem I have with generalized iteration is that it is (sometimes) a bit more ambiguous about what is going on due to __iter. ipairs & pairs guarantee that I am looping over the data I want (the data in the table). I reserve my own use of generalized iteration to cases where that’s actually what I want, e.g. in APIs or modules that want abstract iterators and don’t care about table data.

But again, that’s just my preference, I just wanted to point it out because neither method is really objectively better than the other or anything. :smile:

3 Likes

FWIW I have literally the inverse take: I have a strong preference for the new generalized iteration because I want to know right away if there is the wrong kind of data in my table. I don’t want to use ipairs and have it silently mask the fact that there’s the wrong type of data in the table creating hard to debug issues.

I’ve definitely run into that in practice before where I pass a different table than I intended to ipairs and no errors get thrown but now my code now isn’t working right. Especially in cases where I have both a map and an array of the same data with the map acting as some kind of index.

8 Likes

Reposting here because I posted on the wrong release (this one might be wrong too because it was the update I just had):

This update appears to have broken the copy/paste feature with Studio. While it works specifically with Studio, the moment I paste code anywhere outside of Studio, it loses the indentation and has weird symbols instead:

image

Also sorry, I didn’t mean to reply to you specifically. Just realized that. I meant to reply to the main topic.

This appears to be fixed now.

1 Like

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