@kunosyn/shatterbox 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/types.ts ADDED
@@ -0,0 +1,504 @@
1
+ /// <reference types="@rbxts/types" />
2
+
3
+ /** Represents a 3D hitbox for destruction operations */
4
+ export interface Hitbox {
5
+ CFrame: CFrame;
6
+ Size: Vector3;
7
+ Shape: Enum.PartType;
8
+ }
9
+
10
+ /** World information used for area-based operations */
11
+ export interface WorldInfo {
12
+ CFrame: CFrame;
13
+ Size: Vector3;
14
+ }
15
+
16
+ /** Information about a destroyed voxel passed to callbacks */
17
+ export interface DestroyedVoxelInfo {
18
+ DirtyGroupID?: string;
19
+ CuttingPart?: Hitbox;
20
+ IsEdge?: boolean;
21
+ IsAlreadyDebris?: boolean;
22
+ UserData?: Record<string, unknown>;
23
+ }
24
+
25
+ export type OnVoxelDestruct = (voxel: Part, info: DestroyedVoxelInfo) => void;
26
+
27
+ export type OnDestructCompleted = (
28
+ destroyedVoxelCount: number,
29
+ affectedDirtyGroups: Map<string, boolean>
30
+ ) => void;
31
+
32
+ /** An imaginary voxel that hasn't been instantiated yet */
33
+ export interface ImaginaryVoxel {
34
+ CFrame: CFrame;
35
+ Size: Vector3;
36
+ DirtyGroupID: string;
37
+ DestructionID: string;
38
+ GridSize: number;
39
+ isEdge?: boolean;
40
+ }
41
+
42
+ /** Parameters for destruction operations */
43
+ export interface DestructionParams {
44
+ /** The cutting part or hitbox performing the destruction */
45
+ CuttingPart?: Part | Model | Hitbox;
46
+ /** Alternative syntax: provide CFrame, Size, and Shape directly */
47
+ CFrame?: CFrame;
48
+ Size?: Vector3;
49
+ Shape?: Enum.PartType;
50
+ /** Filter destruction to only parts with these tags */
51
+ FilterTagged?: string | Array<string>;
52
+ /** Delay before cleaning up destroyed parts (smooth cleanup) */
53
+ CleanupDelay?: number;
54
+ /** Name of the registered OnVoxelDestruct callback */
55
+ OnVoxelDestruct?: string;
56
+ /** Grid size for voxelization */
57
+ GridSize?: number;
58
+ /** Skip creating voxels that are fully encapsulated */
59
+ SkipEncapsulatedVoxels?: boolean;
60
+ /** Callback invoked when destruction completes */
61
+ OnDestructCompleted?: OnDestructCompleted;
62
+ /** Custom user data passed to callbacks */
63
+ UserData?: Record<string, unknown>;
64
+ /** Players to exclude from replication (server-side only) */
65
+ ExcludePlayersReplication?: Player | Array<Player>;
66
+ /** Skip destroying floor surfaces */
67
+ SkipFloors?: boolean;
68
+ /** Skip destroying wall surfaces */
69
+ SkipWalls?: boolean;
70
+ /** Unique identifier for this destruction operation */
71
+ ID?: string;
72
+ }
73
+
74
+ /** Parameters specific to imaginary voxel operations */
75
+ export interface ImaginaryDestructionParams extends Omit<DestructionParams, "OnVoxelDestruct" | "OnDestructCompleted"> {}
76
+
77
+ /** Settings configuration object */
78
+ export interface Settings {
79
+ /** Whether to use client-server replication */
80
+ USE_CLIENT_SERVER?: boolean;
81
+ /** Default grid size for voxelization */
82
+ DefaultGridSize?: number;
83
+ /** Default smooth cleanup delay in seconds */
84
+ DefaultSmoothCleanupDelay?: number;
85
+ /** Whether to use smooth cleanup */
86
+ UseSmoothCleanup?: boolean;
87
+ /** Whether to use greedy meshing optimization */
88
+ UseGreedyMeshing?: boolean;
89
+ /** Number of concurrent greedy meshing workers */
90
+ GMWorkerCount?: number;
91
+ /** Traversals per frame for greedy meshing */
92
+ GMTraversalsPerFrame?: number;
93
+ /** Part creations per frame for greedy meshing */
94
+ GMPartCreationsPerFrame?: number;
95
+ /** Maximum divisions per frame */
96
+ MaxDivisionsPerFrame?: number;
97
+ /** Maximum operations per frame */
98
+ MaxOpsPerFrame?: number;
99
+ /** Whether to use priority queue for operations */
100
+ UsePriorityQueue?: boolean;
101
+ /** Number of recent operations to prioritize */
102
+ PrioritizeRecentN?: number;
103
+ /** Maximum puppet count for replication */
104
+ PuppetMaxCount?: number;
105
+ /** Puppet replication frequency in Hz */
106
+ PuppetReplicationFrequency?: number;
107
+ /** Velocity threshold for puppet sleep */
108
+ PuppetSleepVelocity?: number;
109
+ /** Timeout before anchoring sleeping puppets */
110
+ PuppetAnchorTimeout?: number;
111
+ /** Whether to tween puppets on client */
112
+ ClientTweenPuppets?: boolean;
113
+ /** Distance limit for client-side tweening */
114
+ ClientTweenDistanceLimit?: number;
115
+ /** Behavior for non-divisible parts */
116
+ NonDivisibleInteraction?: "NONE" | "FALL" | "REMOVE";
117
+ /** Custom instance skip check function */
118
+ SkipInstanceCheck?: (instance: Instance) => boolean;
119
+ }
120
+
121
+ /** A hitbox object with additional functionality */
122
+ export interface HitboxObject {
123
+ CFrame: CFrame;
124
+ Size: Vector3;
125
+ Shape: Enum.PartType;
126
+ DestructDelay?: number;
127
+ FilterTagged?: string | Array<string>;
128
+ OnVoxelDestruct?: string;
129
+ UserData: Record<string, unknown>;
130
+ ExcludePlayersReplication: Array<Player>;
131
+ OnDestructCompleted?: OnDestructCompleted;
132
+ CleanupDelay?: number;
133
+ GridSize?: number;
134
+ SkipEncapsulatedVoxels?: boolean;
135
+ SkipWalls?: boolean;
136
+ SkipFloors?: boolean;
137
+ DestructionType: "DEFAULT" | "IMAGINARY";
138
+ ImaginaryCallback?: (imaginaryVoxels: Array<ImaginaryVoxel>, existingDebris: Array<BasePart>) => void;
139
+ StartConnectionEvent: RBXScriptSignal;
140
+ WeldConnectionEvent: RBXScriptSignal;
141
+ VelocityPrediction: boolean;
142
+ VelocityBias?: number;
143
+
144
+ /** Performs voxel destruction around the hitbox */
145
+ Destroy(): void;
146
+ /** Returns imaginary voxels bounded by the hitbox */
147
+ ImaginaryVoxels(): LuaTuple<[Array<ImaginaryVoxel>, Array<BasePart>]>;
148
+ /** Welds the hitbox to follow a part */
149
+ WeldTo(part: BasePart): void;
150
+ /** Unwelds the hitbox */
151
+ Unweld(): void;
152
+ /** Starts continuous destruction */
153
+ Start(): void;
154
+ /** Stops continuous destruction */
155
+ Stop(): void;
156
+ /** Destroys the hitbox and disconnects all connections */
157
+ DestroyHitbox(): void;
158
+ }
159
+
160
+ export type EffectHook = (voxel: BasePart, info: DestroyedVoxelInfo) => void;
161
+
162
+ export type ClientEffectName<T extends string> = 'Default' | 'Rough' | T
163
+ export type ServerEffectName<T extends string> = 'Default' | 'MappingVoxelSpace' | 'BumpyFloorBreakWalls' | T
164
+
165
+
166
+ /// <reference types="@rbxts/types" />
167
+
168
+ /**
169
+ * Function type for testing if a point is contained within a part shape
170
+ */
171
+ type PartTypeContainsPoint = (cframe: CFrame, size: Vector3, p: Vector3) => boolean;
172
+
173
+ /**
174
+ * Function type for getting vertices of a part shape
175
+ */
176
+ type GetVertsFunction = (cframe: CFrame, size: Vector3) => Array<Vector3>;
177
+
178
+ /**
179
+ * Function type for getting normals of a part shape
180
+ */
181
+ type GetNormalsFunction = (cframe: CFrame, size?: Vector3) => Array<Vector3>;
182
+
183
+ /**
184
+ * Collection of vertex calculation functions for different part shapes
185
+ */
186
+ interface GetVertsFunctions {
187
+ /**
188
+ * Gets the 8 corner vertices of a block part
189
+ */
190
+ Block: GetVertsFunction;
191
+
192
+ /**
193
+ * Gets the 6 vertices of a wedge part
194
+ */
195
+ Wedge: GetVertsFunction;
196
+
197
+ /**
198
+ * Gets the 5 vertices of a corner wedge part
199
+ */
200
+ CornerWedge: GetVertsFunction;
201
+ }
202
+
203
+ /**
204
+ * Collection of normal calculation functions for different part shapes
205
+ */
206
+ interface GetNormalsFunctions {
207
+ /**
208
+ * Gets the 6 face normals of a block part
209
+ * @param cframe The CFrame of the block
210
+ */
211
+ Block: (cframe: CFrame) => Array<Vector3>;
212
+
213
+ /**
214
+ * Gets the face normals of a wedge part
215
+ * @param cframe The CFrame of the wedge
216
+ * @param size The size of the wedge
217
+ */
218
+ Wedge: (cframe: CFrame, size: Vector3) => Array<Vector3>;
219
+
220
+ /**
221
+ * Gets the face normals of a corner wedge part
222
+ * @param cframe The CFrame of the corner wedge
223
+ * @param size The size of the corner wedge
224
+ */
225
+ CornerWedge: (cframe: CFrame, size: Vector3) => Array<Vector3>;
226
+ }
227
+
228
+ /**
229
+ * VertexMath - Geometric intersection and containment testing utilities
230
+ *
231
+ * Provides efficient algorithms for:
232
+ * - Vertex and normal calculations for different part shapes
233
+ * - Separating Axis Theorem (SAT) intersection tests
234
+ * - Point-in-shape containment tests
235
+ * - Shape-shape intersection tests
236
+ *
237
+ * @example
238
+ * ```ts
239
+ * import VertexMath from "./VertexMath";
240
+ *
241
+ * // Get vertices of a block
242
+ * const blockVerts = VertexMath.GetVerts.Block(
243
+ * new CFrame(0, 10, 0),
244
+ * new Vector3(10, 5, 8)
245
+ * );
246
+ *
247
+ * // Test if a part contains all vertices
248
+ * const part = game.Workspace.WaitForChild("TestPart") as Part;
249
+ * const contained = VertexMath.PartContainsAllVerts(part, blockVerts);
250
+ * ```
251
+ */
252
+ export interface VertexMath {
253
+ /**
254
+ * Collection of functions to get vertices for different part shapes.
255
+ *
256
+ * Each function returns an array of Vector3 positions representing
257
+ * the corners of the shape in world space.
258
+ *
259
+ * @example
260
+ * ```ts
261
+ * // Get block vertices
262
+ * const blockVerts = VertexMath.GetVerts.Block(
263
+ * part.CFrame,
264
+ * part.Size
265
+ * );
266
+ *
267
+ * // Get wedge vertices
268
+ * const wedgeVerts = VertexMath.GetVerts.Wedge(
269
+ * wedgePart.CFrame,
270
+ * wedgePart.Size
271
+ * );
272
+ * ```
273
+ */
274
+ GetVerts: GetVertsFunctions;
275
+
276
+ /**
277
+ * Collection of functions to get normal vectors for different part shapes.
278
+ *
279
+ * Each function returns an array of Vector3 unit vectors representing
280
+ * the outward-facing normals of the shape's faces.
281
+ *
282
+ * @example
283
+ * ```ts
284
+ * // Get block normals (6 face normals)
285
+ * const blockNormals = VertexMath.GetNormals.Block(part.CFrame);
286
+ *
287
+ * // Get wedge normals (requires size for slope calculation)
288
+ * const wedgeNormals = VertexMath.GetNormals.Wedge(
289
+ * wedgePart.CFrame,
290
+ * wedgePart.Size
291
+ * );
292
+ * ```
293
+ */
294
+ GetNormals: GetNormalsFunctions;
295
+
296
+ /**
297
+ * Separating Axis Theorem (SAT) intersection test.
298
+ *
299
+ * Tests whether two convex shapes intersect by checking if there exists
300
+ * a separating axis between them. This is the fundamental algorithm used
301
+ * for collision detection in Shatterbox.
302
+ *
303
+ * The algorithm tests separation on all candidate axes:
304
+ * - Face normals of shape A
305
+ * - Face normals of shape B
306
+ * - Cross products of edge directions from both shapes
307
+ *
308
+ * @param VA Vertices of shape A
309
+ * @param NA Normal vectors of shape A
310
+ * @param VB Vertices of shape B
311
+ * @param NB Normal vectors of shape B
312
+ * @returns True if shapes intersect, false if separated
313
+ *
314
+ * @example
315
+ * ```ts
316
+ * const partA = game.Workspace.PartA as Part;
317
+ * const partB = game.Workspace.PartB as Part;
318
+ *
319
+ * const vertsA = VertexMath.GetVerts.Block(partA.CFrame, partA.Size);
320
+ * const normalsA = VertexMath.GetNormals.Block(partA.CFrame);
321
+ * const vertsB = VertexMath.GetVerts.Block(partB.CFrame, partB.Size);
322
+ * const normalsB = VertexMath.GetNormals.Block(partB.CFrame);
323
+ *
324
+ * if (VertexMath.SAT(vertsA, normalsA, vertsB, normalsB)) {
325
+ * print("Parts are intersecting!");
326
+ * }
327
+ * ```
328
+ */
329
+ SAT(VA: Array<Vector3>, NA: Array<Vector3>, VB: Array<Vector3>, NB: Array<Vector3>): boolean;
330
+
331
+ /**
332
+ * Tests if a part fully encapsulates a block part.
333
+ *
334
+ * This is an optimized check that tests if all 8 corners of a block
335
+ * are contained within another part of any shape.
336
+ *
337
+ * @param part The container part (can be Block, Ball, Cylinder, Wedge, or CornerWedge)
338
+ * @param blockCFrame CFrame of the block to test
339
+ * @param blockSize Size of the block to test
340
+ * @returns True if the block is fully inside the part
341
+ *
342
+ * @example
343
+ * ```ts
344
+ * const container = game.Workspace.Container as Part;
345
+ * const blockCF = new CFrame(0, 10, 0);
346
+ * const blockSize = new Vector3(5, 5, 5);
347
+ *
348
+ * if (VertexMath.PartEncapsulatesBlockPart(container, blockCF, blockSize)) {
349
+ * print("Block is fully inside container");
350
+ * }
351
+ * ```
352
+ */
353
+ PartEncapsulatesBlockPart(part: Part, blockCFrame: CFrame, blockSize: Vector3): boolean;
354
+
355
+ /**
356
+ * Tests if a part contains all given vertices.
357
+ *
358
+ * Works with any part shape (Block, Ball, Cylinder, Wedge, CornerWedge).
359
+ * Uses shape-specific containment algorithms for efficiency.
360
+ *
361
+ * @param part The part to test against
362
+ * @param verts Array of vertex positions in world space
363
+ * @returns True if all vertices are inside the part
364
+ *
365
+ * @example
366
+ * ```ts
367
+ * const part = game.Workspace.TestPart as Part;
368
+ * const testVerts = [
369
+ * new Vector3(0, 10, 0),
370
+ * new Vector3(1, 10, 1),
371
+ * new Vector3(-1, 10, -1)
372
+ * ];
373
+ *
374
+ * if (VertexMath.PartContainsAllVerts(part, testVerts)) {
375
+ * print("All vertices are inside the part");
376
+ * }
377
+ * ```
378
+ */
379
+ PartContainsAllVerts(part: Part, verts: Array<Vector3>): boolean;
380
+
381
+ /**
382
+ * Tests if a part contains at least one vertex.
383
+ *
384
+ * Returns the index (1-based) of the first contained vertex,
385
+ * or false if no vertices are contained.
386
+ *
387
+ * @param part The part to test against
388
+ * @param verts Array of vertex positions in world space
389
+ * @returns Index of first contained vertex (1-based), or false if none
390
+ *
391
+ * @example
392
+ * ```ts
393
+ * const part = game.Workspace.TestPart as Part;
394
+ * const verts = VertexMath.GetVerts.Block(
395
+ * new CFrame(0, 10, 0),
396
+ * new Vector3(10, 10, 10)
397
+ * );
398
+ *
399
+ * const result = VertexMath.PartContainsAVert(part, verts);
400
+ * if (typeIs(result, "number")) {
401
+ * print(`Vertex ${result} is inside the part`);
402
+ * verts.remove(result - 1); // Convert to 0-based for array access
403
+ * }
404
+ * ```
405
+ */
406
+ PartContainsAVert(part: Part, verts: Array<Vector3>): number | false;
407
+
408
+ /**
409
+ * Tests if a ball (sphere) intersects with a block.
410
+ *
411
+ * Uses an optimized algorithm that finds the closest point on the block
412
+ * to the sphere center, then checks if that point is within the sphere radius.
413
+ *
414
+ * @param sphereCFrame CFrame of the sphere center
415
+ * @param sphereSize Size of the sphere (uses minimum dimension as diameter)
416
+ * @param boxCFrame CFrame of the box
417
+ * @param boxSize Size of the box
418
+ * @returns True if the sphere and box intersect
419
+ *
420
+ * @example
421
+ * ```ts
422
+ * const explosionPos = new CFrame(0, 10, 0);
423
+ * const explosionSize = new Vector3(8, 8, 8); // 4-stud radius
424
+ *
425
+ * const wall = game.Workspace.Wall as Part;
426
+ *
427
+ * if (VertexMath.BallIntersectsBlock(
428
+ * explosionPos,
429
+ * explosionSize,
430
+ * wall.CFrame,
431
+ * wall.Size
432
+ * )) {
433
+ * print("Explosion hits wall!");
434
+ * }
435
+ * ```
436
+ */
437
+ BallIntersectsBlock(
438
+ sphereCFrame: CFrame,
439
+ sphereSize: Vector3,
440
+ boxCFrame: CFrame,
441
+ boxSize: Vector3
442
+ ): boolean;
443
+
444
+ /**
445
+ * Tests if a cylinder intersects with a block.
446
+ *
447
+ * This is a complex intersection test that combines multiple algorithms:
448
+ * - Line segment vs cylinder tests for all block edges
449
+ * - SAT test using cylinder bounding box
450
+ * - Plane vs cylinder tests for all block faces
451
+ *
452
+ * The cylinder is oriented along its X-axis (right vector).
453
+ *
454
+ * @param cylinderCFrame CFrame of the cylinder
455
+ * @param cylinderSize Size of the cylinder (X = length, Y/Z = diameter)
456
+ * @param cylinderBoxVerts Pre-calculated bounding box vertices of cylinder
457
+ * @param cylinderBoxNormals Pre-calculated bounding box normals of cylinder
458
+ * @param boxCFrame CFrame of the box
459
+ * @param boxSize Size of the box
460
+ * @param boxVerts Vertices of the box
461
+ * @param boxNormals Normals of the box
462
+ * @returns True if the cylinder and box intersect
463
+ *
464
+ * @example
465
+ * ```ts
466
+ * const cylinder = game.Workspace.Cylinder as Part;
467
+ * const block = game.Workspace.Block as Part;
468
+ *
469
+ * // Pre-calculate cylinder bounding box
470
+ * const cylinderVerts = VertexMath.GetVerts.Block(
471
+ * cylinder.CFrame,
472
+ * cylinder.Size
473
+ * );
474
+ * const cylinderNormals = VertexMath.GetNormals.Block(cylinder.CFrame);
475
+ *
476
+ * // Get block geometry
477
+ * const blockVerts = VertexMath.GetVerts.Block(block.CFrame, block.Size);
478
+ * const blockNormals = VertexMath.GetNormals.Block(block.CFrame);
479
+ *
480
+ * if (VertexMath.CylinderIntersectsBlock(
481
+ * cylinder.CFrame,
482
+ * cylinder.Size,
483
+ * cylinderVerts,
484
+ * cylinderNormals,
485
+ * block.CFrame,
486
+ * block.Size,
487
+ * blockVerts,
488
+ * blockNormals
489
+ * )) {
490
+ * print("Cylinder intersects block!");
491
+ * }
492
+ * ```
493
+ */
494
+ CylinderIntersectsBlock(
495
+ cylinderCFrame: CFrame,
496
+ cylinderSize: Vector3,
497
+ cylinderBoxVerts: Array<Vector3>,
498
+ cylinderBoxNormals: Array<Vector3>,
499
+ boxCFrame: CFrame,
500
+ boxSize: Vector3,
501
+ boxVerts: Array<Vector3>,
502
+ boxNormals: Array<Vector3>
503
+ ): boolean;
504
+ }
@@ -0,0 +1,101 @@
1
+ --!strict
2
+ --!native
3
+ --!optimize 2
4
+ --#selene: allow(multiple_statements)
5
+
6
+ --[[ USAGE
7
+
8
+
9
+ local visualizer = HitboxVisualizer.Create(hitbox)
10
+
11
+ -- set properties (visualizer.property = someValue)
12
+
13
+
14
+ -- shows the hitbox once
15
+ visualizer:ShowOnce(dur, useFade)
16
+
17
+ -- shows the hitbox constantly
18
+ visualizer:StartShowing()
19
+
20
+ -- stops showing the hitbox constantly
21
+ visualizer:StopShowing()
22
+ ]]
23
+
24
+ local RunService = game:GetService("RunService")
25
+ local TweenService = game:GetService("TweenService")
26
+ local FadeParams = { Transparency = 1 }
27
+
28
+ local HitboxVisualizer = {}
29
+
30
+ type HitboxType = { CFrame : CFrame, Size : Vector3, Shape : Enum.PartType }
31
+
32
+ function HitboxVisualizer.Create(hitbox : HitboxType)
33
+
34
+ local visualizer = {}
35
+ visualizer.Transparency = 0.5 :: number
36
+ visualizer.Color = Color3.new(1) :: Color3
37
+
38
+ visualizer.DefualtOnceDuration = 2
39
+ visualizer.DefaultFade = true
40
+
41
+ local vPart = Instance.new("Part")
42
+ vPart.Material = Enum.Material.SmoothPlastic
43
+ vPart.Anchored = true
44
+ vPart.CanCollide = false
45
+ vPart.CanTouch = false
46
+ vPart.CanQuery = false
47
+ vPart.Name = "VisualizedHitbox"
48
+
49
+ function visualizer:ShowOnce(duration : number?, useFade : boolean?)
50
+ if not duration then duration = visualizer.DefualtOnceDuration end
51
+ if useFade == nil then useFade = visualizer.DefaultFade end
52
+
53
+ local v = vPart:Clone()
54
+ v.Color = visualizer.Color
55
+ v.Transparency = visualizer.Transparency
56
+ v.Shape = hitbox.Shape
57
+ v.Size = hitbox.Size
58
+ v.CFrame = hitbox.CFrame
59
+ v.Parent = workspace
60
+
61
+ if not useFade then return end
62
+
63
+ local tween = TweenService:Create(v, TweenInfo.new(duration, Enum.EasingStyle.Linear), FadeParams)
64
+
65
+ tween.Completed:Connect(function() v:Destroy(); end)
66
+
67
+ tween:Play()
68
+ end
69
+
70
+
71
+ local ccon : RBXScriptConnection
72
+
73
+
74
+ function visualizer:StartShowing()
75
+ if ccon then return end
76
+ vPart.Parent = workspace
77
+ ccon = RunService.PreSimulation:Connect(function()
78
+ vPart.Color = visualizer.Color
79
+ vPart.Transparency = visualizer.Transparency
80
+ vPart.Shape = hitbox.Shape
81
+ vPart.Size = hitbox.Size
82
+ vPart.CFrame = hitbox.CFrame
83
+ end)
84
+ end
85
+
86
+
87
+ function visualizer:StopShowing()
88
+ if not ccon then return end
89
+ ccon:Disconnect()
90
+ ccon = nil
91
+ vPart.Parent = nil
92
+ end
93
+
94
+
95
+ return visualizer
96
+ end
97
+
98
+
99
+
100
+
101
+ return HitboxVisualizer
@@ -0,0 +1,66 @@
1
+ --!strict
2
+ --!native
3
+ --!optimize 2
4
+
5
+ --#selene: allow(shadowing)
6
+
7
+ --[[ USAGE:
8
+
9
+ Call the module with the tag you want. Example: TaggedArray("Destructibles") returns a continuously updating array of destructibles.
10
+
11
+ Call TaggedArray.Destroy(tag) to stop the continous updates associated with the array.
12
+
13
+ Caching is used, so separate calls requesting the same tagged array will recieve the same tagged array (instead of new arrays and connections each time)
14
+ ]]
15
+
16
+
17
+ local CollectionService = game:GetService("CollectionService")
18
+
19
+ type TaggedArrayInternal = {
20
+ Array : {Instance},
21
+ Connections : {
22
+ Added : RBXScriptConnection,
23
+ Removed : RBXScriptConnection
24
+ }
25
+ }
26
+
27
+ local TaggedArrays = {} :: { [string] : TaggedArrayInternal }
28
+
29
+ local function GetTaggedArray(_, tag : string) : {Instance}
30
+ local found = TaggedArrays[tag]
31
+ if found then return found.Array end
32
+
33
+ local taggedArray = CollectionService:GetTagged(tag)
34
+
35
+ TaggedArrays[tag] = {
36
+ Array = taggedArray,
37
+
38
+ Connections = {
39
+ Added =
40
+ CollectionService:GetInstanceAddedSignal(tag):Connect(function(i : Instance)
41
+ table.insert(taggedArray, i)
42
+ end),
43
+ Removed =
44
+ CollectionService:GetInstanceRemovedSignal(tag):Connect(function(i : Instance)
45
+ local found = table.find(taggedArray, i)
46
+ if not found then return end
47
+ table.remove(taggedArray, found)
48
+ end)
49
+ }
50
+ }
51
+
52
+ return taggedArray
53
+ end
54
+
55
+ local TaggedArray = setmetatable({}, { __call = GetTaggedArray })
56
+
57
+ function TaggedArray.Destroy(tag : string)
58
+ local found = TaggedArrays[tag]
59
+ if not found then return end
60
+ local connections = found.Connections
61
+ connections.Added:Disconnect()
62
+ connections.Removed:Disconnect()
63
+ TaggedArrays[tag] = nil
64
+ end
65
+
66
+ return TaggedArray