aaa0 = Instance.new("Part")
aaa0.CanCollide = true
aaa0.Transparency = 0
aaa0.Size = Vector3.new(13.72901725769043, 9.729999542236328, 0.911344051361084)
aaa0.Shape = Enum.PartType.Block
aaa0.Reflectance = 0
aaa0.Name = "aaa"
aaa0.BottomSurface = Enum.SurfaceType.Smooth
aaa0.BrickColor = BrickColor.new("Medium stone grey")
aaa0.TopSurface = Enum.SurfaceType.Smooth
aaa0.RightSurface = Enum.SurfaceType.Smooth
aaa0.Material = Enum.Material.Plastic
aaa0.FrontSurface = Enum.SurfaceType.Smooth
aaa0.Rotation = Vector3.new(0, 0, 0)
aaa0.CFrame = CFrame.new(-8.51, 5.39, -0.71) * CFrame.Angles(math.rad(0), math.rad(0), math.rad(0))
aaa0.Anchored = true
aaa0.Color = Color3.fromRGB(163, 162, 165)
aaa0.Locked = false
aaa0.Parent = script
aaa0.LeftSurface = Enum.SurfaceType.Smooth
aaa0.BackSurface = Enum.SurfaceType.Smooth
aaa0.Position = Vector3.new(-8.511962890625, 5.386419296264648, -0.7108142375946045)
-- Gui to Lua
-- Version: 3.2
-- Instances:
local SurfaceGui = Instance.new("SurfaceGui")
local Frame = Instance.new("Frame")
--Properties:
SurfaceGui.Parent = aaa0
SurfaceGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
Frame.Parent = SurfaceGui
Frame.BackgroundColor3 = Color3.fromRGB(50, 50, 50)
Frame.BorderSizePixel = 0
Frame.Size = UDim2.new(1, 0, 1, 0)
local floor = Instance.new("Frame", SurfaceGui)
floor.Size = UDim2.fromScale(1, 0.1)
floor.BackgroundColor3 = Color3.new()
floor.BorderSizePixel = 0
floor.Position = UDim2.fromScale(0, 0.9)
-- Converted using Mokiros's Model to Script Version 3
-- Converted string size: 580 characters
local ScriptFunctions = {
function(script, require)
return require(script.Engine)
end,
function(script,require)
-- List of commonly used variables all across the library
return {
engineInit = {
gravity = Vector2.new(0, .3),
friction = 0.9,
airfriction = 0.98,
bounce = 0.8,
timeSteps = 1,
canvas = {
topLeft = Vector2.new(0, 0),
size = workspace.CurrentCamera.ViewportSize,
},
},
universalMass = 1,
speed = 55,
properties = {
"gravity",
"friction",
"collisionmultiplier",
"airfriction",
"universalmass"
},
rigidbody = {
props = {
"Object",
"Collidable",
"Anchored",
"LifeSpan",
"KeepInCanvas",
"Gravity",
"Friction",
"AirFriction",
"Structure",
"Mass",
"CanRotate"
},
must_have = {
"Object"
}
},
constraint = {
color = Color3.new(1, 1, 1),
thickness = 4,
types = {
"rope",
"spring",
"rod"
},
props = {
"Type",
"Point1",
"Point2",
"Visible",
"Thickness",
"RestLength",
"SpringConstant",
"Color",
},
must_have = {
"Type",
"Point1",
"Point2",
}
},
point = {
radius = 2.5,
color = Color3.new(1),
uicRadius = UDim.new(1, 0),
props = {
"Position",
"Visible",
"Snap",
"KeepInCanvas",
"Radius",
"Color"
},
must_have = {
"Position"
}
},
offset = Vector2.new(0, 36),
VALID_OBJECT_PROPS = {
"Position",
"Visible",
"Snap",
"KeepInCanvas",
"Radius",
"Color",
"Type",
"Point1",
"Point2",
"Thickness",
"RestLength",
"SpringConstant",
"Object",
"Collidable",
"Anchored",
"LifeSpan",
"Gravity",
"Friction",
"AirFriction",
"Structure",
"Mass",
"CanRotate"
},
OBJECT_PROPS_TYPES = {
Position = "Vector2",
Visible = "boolean",
Snap = "boolean",
KeepInCanvas = "boolean",
Radius = "number",
Color = "Color3",
Type = "string",
Thickness = "number",
RestLength = "number",
SpringConstant = "number",
Object = "Instance",
Collidable = "boolean",
Anchored = "boolean",
LifeSpan = "number",
Gravity = "Vector2",
Friction = "number",
AirFriction = "number",
Structure = "table",
Mass = "number",
CanRotate = "boolean"
},
}
end,
function(script,require)
-- Handling type errors
return function (arg: string, param, pos: number, expected: string)
if typeof(param) ~= expected then
error(
string.format(
"[Nature2D]: Invalid Argument #%s. Expected type %q for %s, got %q",
tostring(pos),
expected,
arg,
typeof(param)
),
2
)
end
end
end,
function(script,require)
-- Handling exceptions
local TYPES = {
NO_CANVAS_FOUND = "No canvas found, initialize the engine's canvas using Engine:CreateCanvas().",
NO_RIGIDBODIES_FOUND = "No rigid bodies found on start.",
PROPERTY_NOT_FOUND = "Invalid Argument #1. Property not found.",
INVALID_CONSTRAINT_TYPE = "Received Invalid Constraint Type.",
INVALID_CONSTRAINT_LENGTH = "Received Invalid Constraint Length.",
INVALID_CONSTRAINT_THICKNESS = "Received Invalid Constraint Thickness.",
SAME_ID = "Cannot ignore collisions for the same RigidBodies.",
INVALID_RIGIDBODY = "Received Invalid RigidBody.",
INVALID_OBJECT = "Received an Invalid Object. Valid objects - RigidBody, Point and Constraint.",
INVALID_PROPERTY = "Received an Invalid Object Property.",
MUST_HAVE_PROPERTY = "Missing must-have properties.",
CANVAS_FRAME_NOT_FOUND = "No canvas frame found, initialize the canvas's frame to render custom Points and Constraints!",
INVALID_TIME = "Received invalid time to apply force for.",
ALREADY_STARTED = "Engine is already running.",
CANNOT_SET_COLLISION_ITERATIONS = "Cannot set collision iterations! You must turn on quadtree usage using Engine:UseQuadtrees(true)."
}
return function (TASK: string, TYPE: string, details: string?)
if TYPES[TYPE] then
local exception = string.format("[Nature2D]: %s%s", TYPES[TYPE], if details then " "..details else "")
if TASK == "warn" then
warn(exception)
elseif TASK == "error" then
error(exception, 2)
end
end
end
end,
function(script,require)
return function (custom)
if custom then
error("[Nature2D]: This method cannot be used with custom RigidBodies", 2)
end
end
end,
function(script,require)
-- RigidBodies are formed by Constraints, Points and UI Elements.
-- Services and utilities
local Point = require(script.Parent.Point)
local Constraint = require(script.Parent.Constraint)
local Globals = require(script.Parent.Parent.Constants.Globals)
local Signal = require(script.Parent.Parent.Utilities.Signal)
local Types = require(script.Parent.Parent.Types)
local Janitor = require(script.Parent.Parent.Utilities.Janitor)
local throwTypeError = require(script.Parent.Parent.Debugging.TypeErrors)
local throwException = require(script.Parent.Parent.Debugging.Exceptions)
local restrict = require(script.Parent.Parent.Debugging.Restrict)
local HttpService = game:GetService("HttpService")
local RigidBody = {}
RigidBody.__index = RigidBody
-- [PRIVATE]
-- This method is used to fetch the positions of the 4 corners of UI element.
local function GetCorners(frame: GuiObject, engine)
local pos, size = frame.AbsolutePosition, frame.AbsoluteSize
local rotation = math.rad(frame.Rotation)
local center = pos + size/2
local temp = math.sqrt((size.X/2)^2+(size.Y/2)^2)
local offset = (engine.path and false) and Globals.offset or Vector2.new(0, 0)
-- Calculate and return all 4 corners of the GuiObject
-- Also adheres to the Rotation of the GuiObject
local t = math.atan2(size.Y, size.X)
local a = rotation + t
local b = rotation - t
return {
center - temp * Vector2.new(math.cos(a), math.sin(a)) + offset, -- topleft
center + temp * Vector2.new(math.cos(b), math.sin(b)) + offset, -- topright
center - temp * Vector2.new(math.cos(b), math.sin(b)) + offset, -- bottomleft
center + temp * Vector2.new(math.cos(a), math.sin(a)) + offset, -- bottomright
}
end
-- This method is used to calculate the depth/penetration of a collision
local function CalculatePenetration(minA: number, maxA: number, minB: number, maxB: number) : number
if minA < minB then
return minB - maxA
else
return minA - maxB
end
end
local function CalculateOffset(pos, anchorPoint, size)
return (Vector2.new(.5, .5) - anchorPoint) * size
end
-- This method is used to calculate the center position of a UI element
local function CalculateCenter(vertices) : Vector2
local center = Vector2.new(0, 0)
local minX = math.huge
local minY = math.huge
local maxX = -math.huge
local maxY = -math.huge
for _, v in ipairs(vertices) do
center += v.pos
minX = math.min(minX, v.pos.x)
minY = math.min(minY, v.pos.y)
maxX = math.max(maxX, v.pos.x)
maxY = math.max(maxY, v.pos.y)
end
center /= #vertices
return center
end
-- Used to calculate the AbsoluteSize for custom RigidBodies
local function CalculateSize(vertices)
local minX = math.huge
local minY = math.huge
local maxX = -math.huge
local maxY = -math.huge
for _, v in ipairs(vertices) do
minX = math.min(minX, v.pos.x)
minY = math.min(minY, v.pos.y)
maxX = math.max(maxX, v.pos.x)
maxY = math.max(maxY, v.pos.y)
end
return Vector2.new(maxX - minX, maxY - minY)
end
local function CreateRotationCache(cache, center, vertices)
table.clear(cache)
for _, p in ipairs(vertices) do
local r = (p.pos - center).Magnitude
local theta = math.atan2(p.pos.Y - center.Y, p.pos.X - center.X)
table.insert(cache, { r, theta })
end
end
-- This method is used to update the positions of each point of a rigidbody to the corners of a UI element.
local function UpdateVertices(frame: GuiObject, vertices, engine)
local corners = GetCorners(frame, engine)
for i, vertex in ipairs(vertices) do
vertex:SetPosition(corners[i].X, corners[i].Y)
end
end
-- [PUBLIC]
-- This method is used to initialize a new RigidBody.
function RigidBody.new(frame: GuiObject?, m: number, collidable: boolean?, anchored: boolean?, engine, custom: Types.Custom?, structure)
local isCustom = false
if custom then
isCustom = true
end
local vertices = isCustom and custom.Vertices or {}
local edges = isCustom and custom.Edges or {}
-- Configurations
local pointConfig = {
snap = anchored,
selectable = false,
render = false,
keepInCanvas = true
}
local constraintConfig = {
restLength = nil,
render = false,
thickness = 4,
support = false,
TYPE = "ROD"
}
-- Point creation method
local function addPoint(pos)
local newPoint = Point.new(pos, engine.canvas, engine, pointConfig)
vertices[#vertices + 1] = newPoint
return newPoint
end
-- Constraint creation method
local function addConstraint(p1, p2, support)
constraintConfig.support = support
local newConstraint = Constraint.new(p1, p2, engine.canvas, constraintConfig)
edges[#edges + 1] = newConstraint
return newConstraint
end
if not isCustom then
-- Create Points
local corners = GetCorners(frame, engine)
local topleft = addPoint(corners[1])
local topright = addPoint(corners[2])
local bottomleft = addPoint(corners[3])
local bottomright = addPoint(corners[4])
-- Connect points with constraints
addConstraint(topleft, topright, false)
addConstraint(topleft, bottomleft, false)
addConstraint(topright, bottomright, false)
addConstraint(bottomleft, bottomright, false)
addConstraint(topleft, bottomright, true)
addConstraint(topright, bottomleft, true)
end
local self = setmetatable({
id = HttpService:GenerateGUID(false),
custom = isCustom,
_janitor = Janitor.new(), structure = structure,
vertices = vertices,
edges = edges,
frame = isCustom and nil or frame,
size = isCustom and CalculateSize(vertices) or nil,
anchored = anchored,
mass = m,
collidable = collidable,
canRotate = true,
rotationCache = {},
center = isCustom and CalculateCenter(vertices) or frame.AbsolutePosition + frame.AbsoluteSize/2,
engine = engine,
spawnedAt = os.clock(),
lifeSpan = nil,
anchorRotation = (anchored and not isCustom) and frame.Rotation or nil,
anchorPos = (anchored and not isCustom) and frame.AbsolutePosition + frame.AbsoluteSize/2 or nil,
Touched = nil,
TouchEnded = nil,
CanvasEdgeTouched = nil,
Collisions = {
Body = false,
CanvasEdge = false,
Other = {}
},
States = {},
filtered = {},
}, RigidBody)
-- Apply offsets if ScreenGui's IgnoreGuiInset property is set to true
-- Offset = Vector2.new(0, 36)
if engine.path and false then
self.anchorPos = self.anchorPos and self.anchorPos + Globals.offset or nil
if not self.custom then
self.center += Globals.offset
end
end
if #self.rotationCache < 1 then
CreateRotationCache(self.rotationCache, self.center, self.vertices)
end
-- Create events
self.Touched = Signal.new()
self.TouchEnded = Signal.new()
self.CanvasEdgeTouched = Signal.new()
-- Set parents of points and constraints
for _, edge in ipairs(edges) do
edge.Parent = self
edge._janitor:Add(edge.Parent, "Destroy")
self._janitor:Add(edge, "Destroy")
end
self._janitor:Add(self.Touched, "Destroy")
self._janitor:Add(self.TouchEnded, "Destroy")
self._janitor:Add(self.CanvasEdgeTouched, "Destroy")
if not self.custom then
self._janitor:Add(self.frame, "Destroy")
self._janitor:LinkToInstance(self.frame)
end
return self
end
-- This method projects the RigidBody on an axis. Used for collision detection.
function RigidBody:CreateProjection(Axis: Vector2, Min: number, Max: number) : (number, number)
local DotP = Axis:Dot(self.vertices[1].pos)
Min, Max = DotP, DotP
for _, v in ipairs(self.vertices) do
DotP = Axis:Dot(v.pos)
Min = math.min(DotP, Min)
Max = math.max(DotP, Max)
end
return Min, Max
end
-- This method detects collision between two RigidBodies.
function RigidBody:DetectCollision(other)
if not self.custom and (not self.frame and not other.frame) then
return { false, {} }
end
-- Calculate center of the Body
self.center = CalculateCenter(self.vertices)
-- Initialize collision information
local minDist = math.huge
local collision: Types.Collision = {
axis = nil,
depth = nil,
edge = nil,
vertex = nil
}
-- Loop throught both bodies' edges (excluding support edges)
-- Calculate an axis and then project both bodies to the axis
-- Assign axis and edge of collision to the collision information dictionary
-- Calculate the penetration/depth of the collision
-- Find the vertex that collided with the edge
-- If a collision took place, return the collision information
for i = 1, #self.edges + #other.edges, 1 do
local edge = i <= #self.edges and self.edges[i] or other.edges[i - #self.edges]
if not edge.support then
local axis = Vector2.new(
edge.point1.pos.Y - edge.point2.pos.Y,
edge.point2.pos.X - edge.point1.pos.X
).Unit
local MinA, MinB, MaxA, MaxB
MinA, MaxA = self:CreateProjection(axis, MinA, MaxA)
MinB, MaxB = other:CreateProjection(axis, MinB, MaxB)
local dist = CalculatePenetration(MinA, MaxA, MinB, MaxB)
if dist > 0 then
return { false, {} }
elseif math.abs(dist) < minDist then
minDist = math.abs(dist)
collision.axis = axis
collision.edge = edge
end
end
end
collision.depth = minDist
if collision.edge and collision.edge.Parent ~= other then
local Temp = other
other = self
self = Temp
end
local centerDif = self.center - other.center
local dot = collision.axis:Dot(centerDif)
if dot < 0 then
collision.axis *= -1
end
local minMag = math.huge
for i = 1, #self.vertices, 1 do
local dif = self.vertices[i].pos - other.center
local dist = collision.axis:Dot(dif)
if dist < minMag then
minMag = dist
collision.vertex = self.vertices[i]
end
end
return { true, collision }
end
-- This method is used to apply an external force on the rigid body.
function RigidBody:ApplyForce(force: Vector2, t: number)
throwTypeError("force", force, 1, "Vector2")
if t then
throwTypeError("time", t, 2, "number")
if t <= 0 then
throwException("error", "INVALID_TIME")
end
end
for _, v in ipairs(self.vertices) do
v:ApplyForce(force, t)
end
end
-- This method updates the positions of the RigidBody's points and constraints.
function RigidBody:Update(dt: number)
self.center = CalculateCenter(self.vertices)
for i, vertex in ipairs(self.vertices) do
if not self.canRotate then
local info = self.rotationCache[i]
local r = info[1]
local t = info[2]
vertex:ApplyForce((self.center + Vector2.new(math.cos(t), math.sin(t)) * r) - vertex.pos)
end
vertex:Update(dt)
vertex:Render()
end
for _, edge in ipairs(self.edges) do
for i = 1, self.engine.iterations.constraint do
edge:Constrain()
end
edge:Render()
end
end
-- This method updates the positions and appearance of the RigidBody on screen.
function RigidBody:Render()
-- If the RigidBody exceeds its life span, it is destroyed.
if self.lifeSpan and os.clock() - self.spawnedAt >= self.lifeSpan then
self:Destroy()
end
if self.custom then return end
-- Apply rotations and update positions
-- Respects the anchor point of the GuiObject
if self.anchored then
local anchorPos = self.anchorPos - CalculateOffset(self.anchorPos, self.frame.AnchorPoint, self.frame.AbsoluteSize)
self.frame.Position = UDim2.fromOffset(anchorPos.X, anchorPos.Y)
if self.canRotate then
self:Rotate(self.anchorRotation)
end
else
local center = self.center - CalculateOffset(self.center, self.frame.AnchorPoint, self.frame.AbsoluteSize)
local dif: Vector2 = self.vertices[2].pos - self.vertices[1].pos
self.frame.Position = UDim2.new(0, center.X, 0, center.Y)
if self.canRotate then
self.frame.Rotation = math.deg(math.atan2(dif.Y, dif.X))
end
end
end
-- This method is used to clone the RigidBody while keeping the original one intact.
function RigidBody:Clone(deepCopy: boolean)
if not self.custom and not self.frame then return end
if not self.engine then return end
local frame
if not self.custom then
frame = self.frame:Clone()
frame.Parent = self.frame.Parent
end
local copy = self.engine:Create("RigidBody", {
Mass = self.mass,
Object = frame,
Structure = self.custom and self.structure or nil,
Anchored = self.anchored,
Collidable = self.collidable
})
-- Copy lifespan, states and filtered RigidBodies
if deepCopy == true then
copy.States = self.States
if self.lifeSpan then
copy:SetLifeSpan(self.lifeSpan)
end
for _, body in ipairs(self.filtered) do
copy:FilterCollisionsWith(body)
end
end
return copy
end
-- This method is used to destroy the RigidBody.
-- The body's UI element is destroyed, its connections are disconnected and the body is removed from the engine.
function RigidBody:Destroy(keepFrame: boolean)
self._janitor:Cleanup()
for i, body in ipairs(self.engine.bodies) do
if self.id == body.id then
table.clear(self.Collisions.Other)
table.remove(self.engine.bodies, i)
self.engine.ObjectRemoved:Fire(self)
break
end
end
table.clear(self.vertices)
table.clear(self.edges)
end
-- This method is used to rotate the RigidBody's UI element.
-- After rotation the positions of its points and constraints are automatically updated.
function RigidBody:Rotate(newRotation: number)
throwTypeError("newRotation", newRotation, 1, "number")
-- Update anchorRotation if the body is anchored
if self.anchored and self.anchorRotation then
self.anchorRotation = newRotation
end
-- Apply rotation and update positions
-- Update the RigidBody's points
local oldRotation
if self.custom then
-- Will need to cache oldRotation somewhere.
-- This method will result in weird oldRotations for some custom rigid bodies.
local dif = self.vertices[2].pos - self.vertices[1].pos
oldRotation = math.deg(math.atan2(dif.Y, dif.X))
local tempRotationCache = {}
CreateRotationCache(tempRotationCache, self.center, self.vertices)
for i, info in ipairs(tempRotationCache) do
local r = info[1]
local t = info[2] + math.rad(newRotation)
local v = self.vertices[i]
v.pos = self.center + Vector2.new(math.cos(t), math.sin(t)) * r
v.oldPos = v.pos
end
else
oldRotation = self.frame.Rotation
local offset = CalculateOffset(self.anchorPos, self.frame.AnchorPoint, self.frame.AbsoluteSize)
local position = self.anchorPos - offset
self.frame.Position = self.anchored and UDim2.fromOffset(position.X, position.Y) or UDim2.fromOffset(self.center.x, self.center.y)
self.frame.Rotation = newRotation
UpdateVertices(self.frame, self.vertices, self.engine)
end
return oldRotation, newRotation
end
-- This method is used to set a new position of the RigidBody's UI element.
function RigidBody:SetPosition(PositionX: number, PositionY: number)
--restrict(self.custom)
throwTypeError("PositionX", PositionX, 1, "number")
throwTypeError("PositionY", PositionY, 2, "number")
-- Update anchorPos if the body is anchored
if self.anchored and self.anchorPos then
self.anchorPos = Vector2.new(PositionX, PositionY)
end
local oldPosition
-- Update position
-- Update the RigidBody's points
if self.custom then
oldPosition = UDim2.fromOffset(self.center.X, self.center.Y)
local tempRotationCache = {}
CreateRotationCache(tempRotationCache, self.center, self.vertices)
self.center = Vector2.new(PositionX, PositionY)
for i, info in ipairs(tempRotationCache) do
local r = info[1]
local t = info[2]
local v = self.vertices[i]
v.pos = self.center + Vector2.new(math.cos(t), math.sin(t)) * r
v.oldPos = v.pos
end
else
oldPosition = self.frame.Position
self.frame.Position = UDim2.fromOffset(PositionX, PositionY)
UpdateVertices(self.frame, self.vertices, self.engine)
end
return oldPosition, UDim2.fromOffset(PositionX, PositionY)
end
-- This method is used to set a new size of the RigidBody's UI element.
function RigidBody:SetSize(SizeX: number, SizeY: number)
restrict(self.custom)
throwTypeError("SizeX", SizeX, 1, "number")
throwTypeError("SizeY", SizeY, 2, "number")
-- Update size
-- Update the RigidBody's points
local oldSize = self.frame.Size
self.frame.Size = UDim2.fromOffset(SizeX, SizeY)
UpdateVertices(self.frame, self.vertices, self.engine)
for _, edge in ipairs(self.edges) do
edge.restLength = (edge.point2.pos - edge.point1.pos).Magnitude
end
return oldSize, UDim2.fromOffset(SizeX, SizeY)
end
function RigidBody:SetScale(scale: number)
if not self.custom then return end
throwTypeError("scale", scale, 1, "number")
scale = math.max(0.00001, scale)
for i, info in ipairs(self.rotationCache) do
local r = info[1] * scale
local t = info[2]
local v = self.vertices[i]
v.pos = self.center + Vector2.new(math.cos(t), math.sin(t)) * r
v.oldPos = v.pos
end
for _, edge in ipairs(self.edges) do
edge.restLength = (edge.point2.pos - edge.point1.pos).Magnitude
end
end
-- This method is used to anchor the RigidBody.
-- Its position will no longer change.
function RigidBody:Anchor()
self.anchored = true
self.anchorRotation = self.frame and self.frame.Rotation or nil
self.anchorPos = self.center
for _, vertex in ipairs(self.vertices) do
if not vertex.selectable then vertex.snap = self.anchored end
end
end
-- This method is used to unachor and anchored RigidBody.
function RigidBody:Unanchor()
self.anchored = false
self.anchorRotation = nil
self.anchorPos = nil
for _, vertex in ipairs(self.vertices) do
if not vertex.selectable then vertex.snap = self.anchored end
end
end
-- This method is used to determine whether the RigidBody will collide with other RigidBodies.
function RigidBody:CanCollide(collidable: boolean)
throwTypeError("collidable", collidable, 1, "boolean")
self.collidable = collidable
end
function RigidBody:CanRotate(canRotate: boolean)
restrict(self.custom)
throwTypeError("canRotate", canRotate, 1, "boolean")
self.canRotate = canRotate
CreateRotationCache(self.rotationCache, self.center, self.vertices)
end
-- The RigidBody's UI Element can be fetched using this method.
function RigidBody:GetFrame() : GuiObject
return self.frame
end
-- The RigidBody's unique ID can be fetched using this method.
function RigidBody:GetId() : string
return self.id
end
-- The RigidBody's Points can be fetched using this method.
function RigidBody:GetVertices()
return self.vertices
end
-- The RigidBody's Constraints can be fetched using this method.
function RigidBody:GetConstraints()
return self.edges
end
--vThis method is used to set the RigidBody's life span.
-- Life span is determined by 'seconds'.
-- After this time in seconds has been passed after the RigidBody is created, the RigidBody is automatically destroyed and removed from the engine.
function RigidBody:SetLifeSpan(seconds: number)
throwTypeError("seconds", seconds, 1, "number")
self.lifeSpan = seconds
end
-- This method determines if the RigidBody stays inside the engine's canvas at all times.
function RigidBody:KeepInCanvas(keepInCanvas: boolean)
throwTypeError("keepInCanvas", keepInCanvas, 1, "boolean")
for _, p in ipairs(self.vertices) do
p.keepInCanvas = keepInCanvas
end
end
-- This method sets a custom frictional damp value just for the RigidBody.
function RigidBody:SetFriction(friction: number)
throwTypeError("friction", friction, 1, "number")
for _, p in ipairs(self.vertices) do
p.friction = math.clamp(1 - friction, 0, 1)
end
end
-- This method sets a custom air frictional damp value just for the RigidBody.
function RigidBody:SetAirFriction(friction: number)
throwTypeError("friction", friction, 1, "number")
for _, p in ipairs(self.vertices) do
p.airfriction = math.clamp(1 - friction, 0, 1)
end
end
-- This method sets a custom gravitational force just for the RigidBody.
function RigidBody:SetGravity(force: Vector2)
throwTypeError("force", force, 1, "Vector2")
for _, p in ipairs(self.vertices) do
p.gravity = force
end
end
-- Sets a new mass for the RigidBody
function RigidBody:SetMass(mass: number)
if self.mass ~= mass and mass >= 1 then
self.mass = mass
end
end
-- Returns true if the RigidBody lies within the boundaries of the canvas, else false.
function RigidBody:IsInBounds() : boolean
local canvas = self.engine.canvas
if not canvas then return false end
-- Check if all vertices lie within the canvas.
for _, v in ipairs(self.vertices) do
local pos = v.pos
if not ((pos.X >= canvas.topLeft.X and pos.X <= canvas.topLeft.X + canvas.size.X) and (pos.Y >= canvas.topLeft.Y and pos.Y <= canvas.topLeft.Y + canvas.size.Y)) then
return false
end
end
return true
end
-- Returns the average of all the velocities of the RigidBody's points
function RigidBody:AverageVelocity() : Vector2
local sum = Vector2.new(0, 0)
for _, v in ipairs(self.vertices) do
sum += v:Velocity()
end
-- Return average
return sum/#self.vertices
end
-- STATE MANAGEMENT
-- Used to initialize or update states of a RigidBody
function RigidBody:SetState(state: string, value: any)
throwTypeError("state", state, 1, "string")
if self.States[state] == value then return end
self.States[state] = value
end
-- Used to fetch an already existing state
function RigidBody:GetState(state: string) : any
throwTypeError("state", state, 1, "string")
return self.States[state]
end
-- Used to fetch the center position of the RigidBody
function RigidBody:GetCenter()
return self.center
end
-- Used to ignore/filter any collisions with the other RigidBody.
function RigidBody:FilterCollisionsWith(otherBody)
if not otherBody.id or not typeof(otherBody.id) == "string" or not otherBody.filtered then
throwException("error", "INVALID_RIGIDBODY")
end
if otherBody.id == self.id then throwException("error", "SAME_ID") end
-- Insert the ids into their respective places
if not table.find(self.filtered, otherBody.id) then
table.insert(self.filtered, otherBody.id)
table.insert(otherBody.filtered, self.id)
end
end
-- Used to unfilter collisions with the other RigidBody.
-- The two bodies will now collide with each other.
function RigidBody:UnfilterCollisionsWith(otherBody)
if not otherBody.id or not typeof(otherBody.id) == "string" or not otherBody.filtered then
throwException("error", "INVALID_RIGIDBODY")
end
if otherBody.id == self.id then throwException("error", "SAME_ID") end
local i1 = table.find(self.filtered, otherBody.id)
local i2 = table.find(otherBody.filtered, self.id)
-- Remove the ids from their respective places
if i1 and i2 then
table.remove(self.filtered, i1)
table.remove(otherBody.filtered, i2)
end
end
-- Returns all filtered RigidBodies.
function RigidBody:GetFilteredRigidBodies()
return self.filtered
end
-- Returns an array of all RigidBodies that are in collision with the current
function RigidBody:GetTouchingRigidBodies()
return self.Collisions.Other
end
-- Determines the max force that can be aoplied to the RigidBody.
function RigidBody:SetMaxForce(maxForce: number)
throwTypeError("maxForce", maxForce, 1, "number")
for _, p in ipairs(self.vertices) do
p:SetMaxForce(maxForce)
end
end
return RigidBody
end,
function(script,require)
-- Points are what make the rigid bodies behave like real world entities.
-- Points are responsible for the movement of the RigidBodies and Constraints.
-- Services and utilities
local Globals = require(script.Parent.Parent.Constants.Globals)
local Types = require(script.Parent.Parent.Types)
local throwTypeError = require(script.Parent.Parent.Debugging.TypeErrors)
local throwException = require(script.Parent.Parent.Debugging.Exceptions)
local Janitor = require(script.Parent.Parent.Utilities.Janitor)
local HttpService = game:GetService("HttpService")
local Point = {}
Point.__index = Point
-- This method is used to initialize a new Point.
function Point.new(pos: Vector2, canvas: Types.Canvas, engine: Types.EngineConfig, config: Types.PointConfig, parent)
local self = setmetatable({
id = HttpService:GenerateGUID(false),
Parent = parent,
frame = nil,
_janitor = nil,
engine = engine,
canvas = canvas,
oldPos = pos,
pos = pos,
oldForces = Vector2.new(),
forces = Vector2.new(),
maxForce = nil,
gravity = engine.gravity,
friction = engine.friction,
airfriction = engine.airfriction,
bounce = engine.bounce,
snap = config.snap,
selectable = config.selectable,
render = config.render,
keepInCanvas = config.keepInCanvas,
color = nil,
radius = Globals.point.radius,
timed = {
start = nil,
t = nil,
force = Vector2.new()
}
}, Point)
local janitor = Janitor.new()
janitor:Add(self, "Destroy")
if self.Parent then
janitor:Add(self.Parent, "Destroy")
end
self._janitor = janitor
return self
end
-- This method is used to apply a force to the Point.
function Point:ApplyForce(force: Vector2, t: number)
throwTypeError("force", force, 1, "Vector2")
self.forces += force
if t then
throwTypeError("time", t, 2, "number")
if t <= 0 then
throwException("error", "INVALID_TIME")
end
self.timed.start = os.clock()
self.timed.t = t
self.timed.force = force
end
end
-- This method is used to apply external forces like gravity and is responsible for moving the point.
function Point:Update(dt: number)
if not self.snap then
self:ApplyForce(self.gravity)
if self.timed.start then
if os.clock() - self.timed.start < self.timed.t then
self:ApplyForce(self.timed.force)
else
self.timed.start = nil
self.timed.t = nil
self.timed.force = Vector2.new()
end
end
-- Calculate velocity
local velocity = self.pos
velocity -= self.oldPos
velocity += self.forces
local body = self.Parent
-- Apply friction
if body and body.Parent then
local mass = body.Parent.mass
if mass then
self.forces /= mass
end
if body.Parent.Collisions.CanvasEdge or body.Parent.Collisions.Body then
velocity *= self.friction
else
velocity *= self.airfriction
end
else
velocity *= self.friction
end
-- clamp velocity
if self.maxForce then
velocity = velocity.Unit * math.min(velocity.Magnitude, self.maxForce)
end
-- Update point positions
self.oldPos = self.pos
self.pos += velocity
self.oldForces = self.forces
self.forces *= 0
end
end
-- This method is used to keep the point in the engine's canvas.
-- Any point that goes past the canvas, is positioned correctly and the direction of its flipped is reversed accordingly.
function Point:KeepInCanvas()
-- vx = velocity.X
-- vy = velocity.Y
local vx = self.pos.X - self.oldPos.X
local vy = self.pos.Y - self.oldPos.Y
local boundX = self.canvas.topLeft.X + self.canvas.size.X
local boundY = self.canvas.topLeft.Y + self.canvas.size.Y
local collision = false
local edge
if self.pos.Y > boundY then
self.pos = Vector2.new(self.pos.X, boundY)
self.oldPos = Vector2.new(self.oldPos.X, self.pos.Y + vy * self.bounce)
collision = true
edge = "Bottom"
elseif self.pos.Y < self.canvas.topLeft.Y then
self.pos = Vector2.new(self.pos.X, self.canvas.topLeft.Y)
self.oldPos = Vector2.new(self.oldPos.X, self.pos.Y - vy * self.bounce)
collision = true
edge = "Top"
end
if self.pos.X < self.canvas.topLeft.X then
self.pos = Vector2.new(self.canvas.topLeft.X, self.pos.Y)
self.oldPos = Vector2.new(self.pos.X + vx * self.bounce, self.oldPos.Y)
collision = true
edge = "Left"
elseif self.pos.X > boundX then
self.pos = Vector2.new(boundX, self.pos.Y)
self.oldPos = Vector2.new(self.pos.X - vx * self.bounce, self.oldPos.Y)
collision = true
edge = "Right"
end
local body = self.Parent
-- Fire CanvasEdgeTouched event
if body and body.Parent then
if collision then
local prev = body.Parent.Collisions.CanvasEdge
body.Parent.Collisions.CanvasEdge = true
if prev == false then
body.Parent.CanvasEdgeTouched:Fire(edge)
end
else
body.Parent.Collisions.CanvasEdge = false
end
end
end
-- This method is used to update the position and appearance of the Point on screen.
function Point:Render()
if self.render then
if not self.canvas.frame then
throwException("error", "CANVAS_FRAME_NOT_FOUND")
end
if not self.frame then
-- Create new instance for the point
local p = Instance.new("Frame")
local border = Instance.new("UICorner")
local r = self.radius or Globals.point.radius
p.AnchorPoint = Vector2.new(.5, .5)
p.BackgroundColor3 = self.color or Globals.point.color
p.Size = UDim2.new(0, r * 2, 0, r * 2)
p.Parent = self.canvas.frame
border.CornerRadius = Globals.point.uicRadius
border.Parent = p
self.frame = p
self._janitor:Add(self.frame, "Destroy")
end
-- Update the point's instance
self.frame.Position = UDim2.new(0, self.pos.x, 0, self.pos.y)
end
if self.keepInCanvas then
self:KeepInCanvas()
end
end
function Point:Destroy()
self._janitor:Cleanup()
if not self.Parent then
for i, c in ipairs(self.engine.points) do
if c.id == self.id then
table.remove(self.engine.points, i)
self.engine.ObjectRemoved:Fire(self)
break
end
end
end
end
-- This method is used to determine the radius of the point.
function Point:SetRadius(radius: number)
throwTypeError("radius", radius, 1, "number")
self.radius = radius
end
-- his method is used to determine the color of the point on screen.
-- By default this is set to (RED) Color3.new(1, 0, 0).
function Point:Stroke(color: Color3)
throwTypeError("color", color, 1, "Color3")
self.color = color
end
-- This method determines if the point remains anchored.
-- If set to false, the point is unanchored.
function Point:Snap(snap: boolean)
throwTypeError("snap", snap, 1, "boolean")
self.snap = snap
end
-- Returns the velocity of the Point
function Point:Velocity() : Vector2
return self.pos - self.oldPos
end
function Point:GetNetForce() : Vector2
return self.oldForces
end
-- Returns the Parent (Constraint) of the Point if any.
function Point:GetParent()
return self.Parent
end
-- Used to set a new position for the point
function Point:SetPosition(x: number, y: number)
throwTypeError("x", x, 1, "number")
throwTypeError("y", y, 2, "number")
local newPosition = Vector2.new(x, y)
self.oldPos = newPosition
self.pos = newPosition
end
-- Determines the max force that can be aoplied to the Point.
function Point:SetMaxForce(maxForce: number)
throwTypeError("maxForce", maxForce, 1, "number")
self.maxForce = math.abs(maxForce)
end
return Point
end,
function(script,require)
-- Constraints keep two points together in place and maintain uniform distance between the two.
-- Constraints and Points together join to keep a RigidBody in place hence making both Points and Constraints a vital part of the library.
-- Custom constraints such as Ropes, Rods, Bridges and chains can also be made.
-- Points of two rigid bodies can be connected with constraints, two individual points can also be connected with constraints to form Ropes etc.
-- Services and utilities
local line = require(script.Parent.Parent.Utilities.Line)
local Globals = require(script.Parent.Parent.Constants.Globals)
local throwTypeError = require(script.Parent.Parent.Debugging.TypeErrors)
local throwException = require(script.Parent.Parent.Debugging.Exceptions)
local Janitor = require(script.Parent.Parent.Utilities.Janitor)
local Types = require(script.Parent.Parent.Types)
local https = game:GetService("HttpService")
local Constraint = {}
Constraint.__index = Constraint
-- This method is used to initialize a constraint.
function Constraint.new(p1: Types.Point, p2: Types.Point, canvas: Types.Canvas, config: Types.SegmentConfig, engine, parent)
local self = setmetatable({
id = https:GenerateGUID(false),
_janitor = nil,
engine = engine,
Parent = parent,
frame = nil,
canvas = canvas,
point1 = p1,
point2 = p2,
restLength = config.restLength or (p2.pos - p1.pos).Magnitude,
render = config.render,
thickness = config.thickness or Globals.constraint.thickness,
support = config.support,
_TYPE = config.TYPE,
k = 0.1,
color = nil,
}, Constraint)
local janitor = Janitor.new()
janitor:Add(self, "Destroy")
janitor:Add(self.point1, "Destroy")
janitor:Add(self.point2, "Destroy")
if self.Parent then
janitor:Add(self.Parent, "Destroy")
end
self._janitor = janitor
self.point1.Parent = self
self.point2.Parent = self
self.point1._janitor:Add(self.point1.Parent, "Destroy")
self.point2._janitor:Add(self.point2.Parent, "Destroy")
return self
end
-- This method is used to keep uniform distance between the constraint's points, i.e. constrain.
function Constraint:Constrain()
local cur = (self.point2.pos - self.point1.pos).Magnitude
local force
-- Validate constraint types
if self._TYPE == "ROPE" then
local restLength = self.restLength
if cur < self.thickness then
restLength = self.thickness
end
if cur > self.restLength or self.restLength < self.thickness then
-- Solve rope constraint force
local offset = ((restLength - cur)/restLength)/2
force = self.point2.pos - self.point1.pos
force *= offset
end
elseif self._TYPE == "ROD" then
-- Solve rod constraint force
local offset = self.restLength - cur
local dif = self.point2.pos - self.point1.pos
dif = dif.Unit
force = (dif * offset)/2
elseif self._TYPE == "SPRING" then
-- Solve spring constraint force
force = self.point2.pos - self.point1.pos
local mag = force.Magnitude - self.restLength
force = force.Unit
force *= -1 * self.k * mag
else
return
end
-- Apply forces to constraint's points
if force then
if not self.point1.snap then self.point1.pos -= force end
if not self.point2.snap then self.point2.pos += force end
end
end
-- This method is used to update the position and appearance of the constraint on screen.
function Constraint:Render()
if self.render and not self.support then
if not self.canvas.frame then
throwException("error", "CANVAS_FRAME_NOT_FOUND")
end
local thickness = self.thickness or Globals.constraint.thickness
local color = self.color or Globals.constraint.color
local image = self._TYPE == "SPRING" and "rbxassetid://8404350124" or nil
if not self.frame then
self.frame = line(self.point1.pos, self.point2.pos, self.canvas.frame, thickness, color, nil, image)
self._janitor:Add(self.frame, "Destroy")
end
-- Draw constraint on screen
line(self.point1.pos, self.point2.pos, self.canvas.frame, thickness, color, self.frame, image)
end
end
-- Used to set the minimum constrained distance between two points.
-- By default, the initial distance between the two points.
function Constraint:SetLength(newLength: number)
throwTypeError("length", newLength, 1, "number")
if newLength <= 0 then
throwException("error", "INVALID_CONSTRAINT_LENGTH")
end
self.restLength = newLength
end
-- This method returns the current distance between the two points of a constraint.
function Constraint:GetLength() : number
return (self.point2.pos - self.point1.pos).Magnitude
end
-- This method is used to change the color of a constraint.
-- By default a constraint's color is set to the default value of (WHITE) Color3.new(1, 1, 1).
function Constraint:Stroke(color: Color3)
throwTypeError("color", color, 1, "Color3")
self.color = color
end
-- This method destroys the constraint.
-- Its UI element is no longer rendered on screen and the constraint is removed from the engine.
-- This is irreversible.
function Constraint:Destroy()
self._janitor:Cleanup()
if not self.Parent then
for i, c in ipairs(self.engine.constraints) do
if c.id == self.id then
table.remove(self.engine.constraints, i)
self.engine.ObjectRemoved:Fire(self)
break
end
end
end
self.point1 = nil
self.point2 = nil
end
-- Returns the constraints points.
function Constraint:GetPoints()
return self.point1, self.point2
end
-- Returns the UI element for the constrained IF rendered.
function Constraint:GetFrame() : Frame?
return self.frame
end
-- This method is used to update the Spring constant (by default 0.1) used for spring constraint calculations.
function Constraint:SetSpringConstant(k: number)
throwTypeError("springConstant", k, 1, "number")
self.k = k
end
-- The constraints's unique ID can be fetched using this method.
function Constraint:GetId() : string
return self.id
end
-- Returns the Parent (RigidBody) of the Constraint if any.
function Constraint:GetParent()
return self.Parent
end
return Constraint
end,
function(script,require)
local Types = require(script.Parent.Parent.Types)
local Quadtree = require(script.Parent.Parent.Utilities.Quadtree)
-- Search and return an element from a table using a lambda function
local function SearchTable(t: { any }, a: any, lambda: (a: any, b: any) -> boolean) : any
for _, v in ipairs(t) do
if lambda(a, v) then
return v
end
end
return nil
end
local Runner = {}
-- This method is responsible for separating two rigidbodies if they collide with each other.
function Runner.CollisionResponse(body: Types.RigidBody, other: Types.RigidBody, isColliding: boolean, Collision: Types.Collision, dt: number, oldCollidingWith, iteration: number)
if not isColliding then return end
-- Fire the touched event
if iteration == 1 and body.Touched._handlerListHead and body.Touched._handlerListHead.Connected then
if not SearchTable(oldCollidingWith, other, function(a, b) return a.id == b.id end) then
body.Touched:Fire(other.id, Collision)
end
end
-- Calculate penetration in 2 dimensions
local penetration: Vector2 = Collision.axis * Collision.depth
local p1: Types.Point = Collision.edge.point1
local p2: Types.Point = Collision.edge.point2
-- Calculate a t alpha value
local t
if math.abs(p1.pos.X - p2.pos.X) > math.abs(p1.pos.Y - p2.pos.Y) then
t = (Collision.vertex.pos.X - penetration.X - p1.pos.X)/(p2.pos.X - p1.pos.X)
else
t = (Collision.vertex.pos.Y - penetration.Y - p1.pos.Y)/(p2.pos.Y - p1.pos.Y)
end
-- Create a lambda
local factor: number = 1 / (t^2 + (1 - t)^2)
-- Calculate masses
local bodyMass = Collision.edge.Parent.mass
local m = t * bodyMass + (1 - t) * bodyMass
local cMass = 1 / (m + Collision.vertex.Parent.Parent.mass)
-- Calculate ratios of collision effects
local r1 = Collision.vertex.Parent.Parent.mass * cMass
local r2 = m * cMass
-- If the body is not anchored, apply forces to the constraint
if not Collision.edge.Parent.anchored then
p1.pos -= penetration * ((1 - t) * factor * r1)
p2.pos -= penetration * (t * factor * r1)
end
-- If the body is not anchored, apply forces to the point
if not Collision.vertex.Parent.Parent.anchored then
Collision.vertex.pos += penetration * r2
end
end
function Runner.Update(self, dt)
local tree;
-- Create a quadtree and insert bodies if neccesary
if self.quadtrees then
tree = Quadtree.new(self.canvas.topLeft, self.canvas.size, 4)
for _, body in ipairs(self.bodies) do
if body.collidable then
tree:Insert(body)
end
end
else
if self.iterations.collision ~= 1 then
self.iterations.collision = 1
end
end
-- Loop through each body
-- Update the body
-- Calculate the closest RigidBodies to a given body if neccesary
for _, body in ipairs(self.bodies) do
body:Update(dt)
local OldCollidingWith = body.Collisions.Other
local CollidingWith = {}
if body.collidable then
local filtered = self.bodies
if self.quadtrees then
local abs = body.custom and body.size or body.frame.AbsoluteSize
local side = abs.X > abs.Y and abs.X or abs.Y
local range = {
position = body.center - Vector2.new(side * 1.5, side * 1.5),
size = Vector2.new(side * 3, side * 3)
}
filtered = tree:Search(range, {})
end
-- Loop through the filtered RigidBodies
-- Detect collisions
-- Process collision response
for _, other in ipairs(filtered) do
if body.id ~= other.id and other.collidable and not table.find(body.filtered, other.id) then
local result, isColliding, Collision, didCollide
for i = 1, self.iterations.collision do
result = body:DetectCollision(other)
isColliding = result[1]
Collision = result[2]
if i == 1 and not isColliding then
break
end
didCollide = true
Runner.CollisionResponse(body, other, isColliding, Collision, dt, OldCollidingWith, i)
end
if didCollide then
body.Collisions.Body = true
other.Collisions.Body = true
table.insert(CollidingWith, other)
else
body.Collisions.Body = false
other.Collisions.Body = false
-- Fire TouchEnded event
if body.TouchEnded._handlerListHead and body.TouchEnded._handlerListHead.Connected then
if SearchTable(OldCollidingWith, other, function (a, b) return a.id == b.id end) then
body.TouchEnded:Fire(other.id)
end
end
end
end
end
end
body.Collisions.Other = CollidingWith
end
if #self.points > 0 then
for _, point in ipairs(self.points) do
point:Update(dt)
end
end
if #self.constraints > 0 then
for _, constraint in ipairs(self.constraints) do
if constraint._TYPE ~= "SPRING" then
for i = 1, self.iterations.constraint do
constraint:Constrain()
end
else
constraint:Constrain()
end
end
end
self.Updated:Fire()
end
function Runner.Render(self)
for _, body in ipairs(self.bodies) do
body:Render()
end
for _, point in ipairs(self.points) do
point:Render()
end
for _, constraint in ipairs(self.constraints) do
constraint:Render()
end
end
return Runner
end,
function(script,require)
-- -----------------------------------------------------------------------------
-- Batched Yield-Safe Signal Implementation --
-- This is a Signal class which has effectively identical behavior to a --
-- normal RBXScriptSignal, with the only difference being a couple extra --
-- stack frames at the bottom of the stack trace when an error is thrown. --
-- This implementation caches runner coroutines, so the ability to yield in --
-- the signal handlers comes at minimal extra cost over a naive signal --
-- implementation that either always or never spawns a thread. --
-- --
-- API: --
-- local Signal = require(THIS MODULE) --
-- local sig = Signal.new() --
-- local connection = sig:Connect(function(arg1, arg2, ...) ... end) --
-- sig:Fire(arg1, arg2, ...) --
-- connection:Disconnect() --
-- sig:DisconnectAll() --
-- local arg1, arg2, ... = sig:Wait() --
-- --
-- Licence: --
-- Licenced under the MIT licence. --
-- --
-- Authors: --
-- stravant - July 31st, 2021 - Created the file. --
-- sleitnick - August 3rd, 2021 - Modified for Knit. --
-- -----------------------------------------------------------------------------
-- The currently idle thread to run the next handler on
local freeRunnerThread = nil
-- Function which acquires the currently idle handler runner thread, runs the
-- function fn on it, and then releases the thread, returning it to being the
-- currently idle one.
-- If there was a currently idle runner thread already, that's okay, that old
-- one will just get thrown and eventually GCed.
local function acquireRunnerThreadAndCallEventHandler(fn, ...)
local acquiredRunnerThread = freeRunnerThread
freeRunnerThread = nil
fn(...)
-- The handler finished running, this runner thread is free again.
freeRunnerThread = acquiredRunnerThread
end
-- Coroutine runner that we create coroutines of. The coroutine can be
-- repeatedly resumed with functions to run followed by the argument to run
-- them with.
local function runEventHandlerInFreeThread(...)
acquireRunnerThreadAndCallEventHandler(...)
while true do
acquireRunnerThreadAndCallEventHandler(coroutine.yield())
end
end
-- Connection class
local Connection = {}
Connection.__index = Connection
function Connection.new(signal, fn)
return setmetatable({
Connected = true,
_signal = signal,
_fn = fn,
_next = false,
}, Connection)
end
function Connection:Disconnect()
if not self.Connected then
return
end
self.Connected = false
-- Unhook the node, but DON'T clear it. That way any fire calls that are
-- currently sitting on this node will be able to iterate forwards off of
-- it, but any subsequent fire calls will not hit it, and it will be GCed
-- when no more fire calls are sitting on it.
if self._signal._handlerListHead == self then
self._signal._handlerListHead = self._next
else
local prev = self._signal._handlerListHead
while prev and prev._next ~= self do
prev = prev._next
end
if prev then
prev._next = self._next
end
end
end
Connection.Destroy = Connection.Disconnect
-- Make Connection strict
setmetatable(Connection, {
__index = function(_tb, key)
if key ~= "_handlerListHead" then
error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2)
end
end,
__newindex = function(_tb, key, _value)
error(("Attempt to set Connection::%s (not a valid member)"):format(tostring(key)), 2)
end
})
--[=[
@class Signal
Signals allow events to be dispatched and handled.
For example:
```lua
local signal = Signal.new()
signal:Connect(function(msg)
print("Got message:", msg)
end)
signal:Fire("Hello world!")
```
]=]
local Signal = {}
Signal.__index = Signal
--[=[
Constructs a new Signal
@return Signal
]=]
function Signal.new()
local self = setmetatable({
_handlerListHead = false,
_proxyHandler = nil,
}, Signal)
return self
end
--[=[
Constructs a new Signal that wraps around an RBXScriptSignal.
@param rbxScriptSignal RBXScriptSignal -- Existing RBXScriptSignal to wrap
@return Signal
For example:
```lua
local signal = Signal.Wrap(workspace.ChildAdded)
signal:Connect(function(part) print(part.Name .. " added") end)
Instance.new("Part").Parent = workspace
```
]=]
function Signal.Wrap(rbxScriptSignal)
assert(typeof(rbxScriptSignal) == "RBXScriptSignal", "Argument #1 to Signal.Wrap must be a RBXScriptSignal; got " .. typeof(rbxScriptSignal))
local signal = Signal.new()
signal._proxyHandler = rbxScriptSignal:Connect(function(...)
signal:Fire(...)
end)
return signal
end
--[=[
Checks if the given object is a Signal.
@param obj any -- Object to check
@return boolean -- `true` if the object is a Signal.
]=]
function Signal.Is(obj)
return type(obj) == "table" and getmetatable(obj) == Signal
end
--[=[
Connects a function to the signal, which will be called anytime the signal is fired.
@param fn (...any) -> nil
@return Connection -- A connection to the signal
]=]
function Signal:Connect(fn: (...any) -> ())
local connection = Connection.new(self, fn)
if self._handlerListHead then
connection._next = self._handlerListHead
self._handlerListHead = connection
else
self._handlerListHead = connection
end
return connection
end
function Signal:GetConnections()
local items = {}
local item = self._handlerListHead
while item do
table.insert(items, item)
item = item._next
end
return items
end
--[=[
Disconnects all connections from the signal.
]=]
function Signal:DisconnectAll()
local item = self._handlerListHead
while item do
item.Connected = false
item = item._next
end
self._handlerListHead = false
end
-- Signal:Fire(...) implemented by running the handler functions on the
-- coRunnerThread, and any time the resulting thread yielded without returning
-- to us, that means that it yielded to the Roblox scheduler and has been taken
-- over by Roblox scheduling, meaning we have to make a new coroutine runner.
--[=[
Fire the signal, which will call all of the connected functions with the given arguments.
@param ... any -- Arguments to pass to the connected functions
]=]
function Signal:Fire(...)
local item = self._handlerListHead
while item do
if item.Connected then
if not freeRunnerThread then
freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
end
task.spawn(freeRunnerThread, item._fn, ...)
end
item = item._next
end
end
--[=[
Same as `Fire`, but uses `task.defer` internally & doesn't take advantage of thread reuse.
@param ... any -- Arguments to pass to the connected functions
]=]
function Signal:FireDeferred(...)
local item = self._handlerListHead
while item do
task.defer(item._fn, ...)
item = item._next
end
end
--[=[
Yields the current thread until the signal is fired, and returns the arguments fired from the signal.
@return ... any -- Arguments passed to the signal when it was fired
@yields
]=]
function Signal:Wait(): (...any)
local waitingCoroutine = coroutine.running()
local cn
cn = self:Connect(function(...)
cn:Disconnect()
task.spawn(waitingCoroutine, ...)
end)
return coroutine.yield()
end
--[=[
Cleans up the signal.
]=]
function Signal:Destroy()
self:DisconnectAll()
local proxyHandler = rawget(self, "_proxyHandler")
if proxyHandler then
proxyHandler:Disconnect()
end
end
-- Make signal strict
setmetatable(Signal, {
__index = function(_tb, key)
if key ~= "Connected" then
error(("Attempt to get Signal::%s (not a valid member)"):format(tostring(key)), 2)
end
end,
__newindex = function(_tb, key, _value)
error(("Attempt to set Signal::%s (not a valid member)"):format(tostring(key)), 2)
end
})
return Signal
end,
function(script,require)
-- This utility is used in Collision Detection
-- Quadtree data structure
-- Services and utilities
local Types = require(script.Parent.Parent.Types)
local Quadtree = {}
Quadtree.__index = Quadtree
-- Calculate sub-divisions of a node
local function GetDivisions(position: Vector2, size: Vector2)
return {
position,
position + Vector2.new(size.X/2, 0),
position + Vector2.new(0, size.Y/2),
position + Vector2.new(size.X/2, size.Y/2),
}
end
-- Check if a range overlaps a node of the quadtree
local function RangeOverlapsNode(node: Types.Quadtree<Types.RigidBody>, range: Types.Range) : boolean
local ap1 = range.position
local as1 = range.size
local sum = ap1 + as1
local ap2 = node.position
local as2 = node.size
local sum2 = ap2 + as2
-- Detect overlapping
return (ap1.x < sum2.x and sum.x > ap2.x) and (ap1.y < sum2.y and sum.y > ap2.y)
end
-- Check if a point lies within a range
local function RangeHasPoint(range: Types.Range, obj: Types.RigidBody) : boolean
local p = obj.center
return (
(p.X > range.position.X) and (p.X < (range.position.X + range.size.X)) and
(p.Y > range.position.Y) and (p.Y < (range.position.Y + range.size.Y))
)
end
-- Merge two arrays
local function merge<T>(array1: {T}, array2: {T}) : {T}
if #array2 > 0 then
for _, v in ipairs(array2) do
table.insert(array1, v)
end
end
return array1
end
-- Initialize a new quadtree
function Quadtree.new(_position: Vector2, _size: Vector2, _capacity: number)
return setmetatable({
position = _position,
size = _size,
capacity = _capacity,
objects = {},
divided = false,
}, Quadtree)
end
-- Insert a RigidBody in the quadtree
function Quadtree:Insert(body: Types.RigidBody)
if not self:HasObject(body.center) then return end
if #self.objects < self.capacity then
self.objects[#self.objects + 1] = body
else
-- Subdivide if not already
if not self.divided then
self:SubDivide()
self.divided = true
end
-- Insert the RigidBody in the subdivisions if possible
self.topLeft:Insert(body)
self.topRight:Insert(body)
self.bottomLeft:Insert(body)
self.bottomRight:Insert(body)
end
end
function Quadtree:HasObject(p: Vector2) : boolean
return (
(p.X > self.position.X) and (p.X < (self.position.X + self.size.X)) and
(p.Y > self.position.Y) and (p.Y < (self.position.Y + self.size.Y))
)
end
-- Create subdivisions of a node
function Quadtree:SubDivide()
local divisions = GetDivisions(self.position, self.size)
self.topLeft = Quadtree.new(divisions[1], self.size/2, self.capacity)
self.topRight = Quadtree.new(divisions[2], self.size/2, self.capacity)
self.bottomLeft = Quadtree.new(divisions[3], self.size/2, self.capacity)
self.bottomRight = Quadtree.new(divisions[4], self.size/2, self.capacity)
end
-- Search through the nodes, given a range query.
-- Returns any rigidbody that lies within the range.
function Quadtree:Search(range: Types.Range, objects: { Types.RigidBody })
if not objects then
objects = {}
end
if not RangeOverlapsNode(self, range) then
return objects
end
for _, obj in ipairs(self.objects) do
if RangeHasPoint(range, obj) then
objects[#objects + 1] = obj
end
end
if self.divided then
self.topLeft:Search(range, objects)
self.topRight:Search(range, objects)
self.bottomLeft:Search(range, objects)
self.bottomRight:Search(range, objects)
end
return objects
end
return Quadtree
end,
function(script,require)
-- This utility is used to render a constraint on the screen.
-- Services and utilities
local Globals = require(script.Parent.Parent.Constants.Globals)
-- Create the constraint's instance and apply properties
local function draw(hyp: number, origin: Vector2, thickness: number, parent: Instance, color: Color3, l: Frame?, image: string?) : Frame
local line = l or (image and Instance.new("ImageLabel") or Instance.new("Frame"))
line.Name = "Constraint"
line.AnchorPoint = Vector2.new(.5, .5)
line.Size = UDim2.new(0, hyp, 0, (thickness or Globals.constraint.thickness) + (image and 15 or 0))
line.BackgroundTransparency = image and 1 or 0
line.BorderSizePixel = 0
line.Position = UDim2.fromOffset(origin.X, origin.Y)
line.ZIndex = 1
if image then
line.Image = image
line.ImageColor3 = color or Globals.constraint.color
else
line.BackgroundColor3 = color or Globals.constraint.color
end
line.Parent = parent
return line
end
return function (origin: Vector2, endpoint: Vector2, parent: Instance, thickness: number, color: Color3, l: Frame?, image: string?) : Frame
-- Calculate magnitude between the constraint's points
-- Draw the constraint
-- Calculate rotation
local hyp = (endpoint - origin).Magnitude
local line = draw(hyp, origin, thickness, parent, color, l, image)
local mid = (origin + endpoint)/2
local theta = math.atan2((origin - endpoint).Y, (origin - endpoint).X)
-- Apply rotation and update position
line.Position = UDim2.fromOffset(mid.x, mid.y)
line.Rotation = math.deg(theta)
return line
end
end,
function(script,require)
-- Janitor
-- Original by Validark
-- Modifications by pobammer
-- roblox-ts support by OverHash and Validark
-- LinkToInstance fixed by Elttob.
-- Cleanup edge cases fixed by codesenseAye.
local GetPromiseLibrary = require(script.GetPromiseLibrary)
local Symbol = require(script.Symbol)
local FoundPromiseLibrary, Promise = GetPromiseLibrary()
local IndicesReference = Symbol("IndicesReference")
local LinkToInstanceIndex = Symbol("LinkToInstanceIndex")
local METHOD_NOT_FOUND_ERROR = "Object %s doesn't have method %s, are you sure you want to add it? Traceback: %s"
local NOT_A_PROMISE = "Invalid argument #1 to 'Janitor:AddPromise' (Promise expected, got %s (%s))"
--[=[
Janitor is a light-weight, flexible object for cleaning up connections, instances, or anything. This implementation covers all use cases,
as it doesn't force you to rely on naive typechecking to guess how an instance should be cleaned up.
Instead, the developer may specify any behavior for any object.
@class Janitor
]=]
local Janitor = {}
Janitor.ClassName = "Janitor"
Janitor.CurrentlyCleaning = true
Janitor[IndicesReference] = nil
Janitor.__index = Janitor
local TypeDefaults = {
["function"] = true;
RBXScriptConnection = "Disconnect";
}
--[=[
Determines if the passed object is a Janitor. This checks the metatable directly.
@param Object any -- The object you are checking.
@return boolean -- `true` if `Object` is a Janitor.
]=]
function Janitor.Is(Object: any): boolean
return type(Object) == "table" and getmetatable(Object) == Janitor
end
type StringOrTrue = string | boolean
--[=[
Adds an `Object` to Janitor for later cleanup, where `MethodName` is the key of the method within `Object` which should be called at cleanup time.
If the `MethodName` is `true` the `Object` itself will be called instead. If passed an index it will occupy a namespace which can be `Remove()`d or overwritten.
Returns the `Object`.
:::info
Objects not given an explicit `MethodName` will be passed into the `typeof` function for a very naive typecheck.
RBXConnections will be assigned to "Disconnect", functions will be assigned to `true`, and everything else will default to "Destroy".
Not recommended, but hey, you do you.
:::
```lua
local Workspace = game:GetService("Workspace")
local TweenService = game:GetService("TweenService")
local Obliterator = Janitor.new()
local Part = Workspace.Part
-- Queue the Part to be Destroyed at Cleanup time
Obliterator:Add(Part, "Destroy")
-- Queue function to be called with `true` MethodName
Obliterator:Add(print, true)
-- This implementation allows you to specify behavior for any object
Obliterator:Add(TweenService:Create(Part, TweenInfo.new(1), {Size = Vector3.new(1, 1, 1)}), "Cancel")
-- By passing an Index, the Object will occupy a namespace
-- If "CurrentTween" already exists, it will call :Remove("CurrentTween") before writing
Obliterator:Add(TweenService:Create(Part, TweenInfo.new(1), {Size = Vector3.new(1, 1, 1)}), "Destroy", "CurrentTween")
```
```ts
import { Workspace, TweenService } from "@rbxts/services";
import { Janitor } from "@rbxts/janitor";
const Obliterator = new Janitor<{ CurrentTween: Tween }>();
const Part = Workspace.FindFirstChild("Part") as Part;
// Queue the Part to be Destroyed at Cleanup time
Obliterator.Add(Part, "Destroy");
// Queue function to be called with `true` MethodName
Obliterator.Add(print, true);
// This implementation allows you to specify behavior for any object
Obliterator.Add(TweenService.Create(Part, new TweenInfo(1), {Size: new Vector3(1, 1, 1)}), "Cancel");
// By passing an Index, the Object will occupy a namespace
// If "CurrentTween" already exists, it will call :Remove("CurrentTween") before writing
Obliterator.Add(TweenService.Create(Part, new TweenInfo(1), {Size: new Vector3(1, 1, 1)}), "Destroy", "CurrentTween");
```
@param Object T -- The object you want to clean up.
@param MethodName? string|true -- The name of the method that will be used to clean up. If not passed, it will first check if the object's type exists in TypeDefaults, and if that doesn't exist, it assumes `Destroy`.
@param Index? any -- The index that can be used to clean up the object manually.
@return T -- The object that was passed as the first argument.
]=]
function Janitor:Add(Object: any, MethodName: StringOrTrue?, Index: any?): any
if Index then
self:Remove(Index)
local This = self[IndicesReference]
if not This then
This = {}
self[IndicesReference] = This
end
This[Index] = Object
end
MethodName = MethodName or TypeDefaults[typeof(Object)] or "Destroy"
if type(Object) ~= "function" and not Object[MethodName] then
warn(string.format(METHOD_NOT_FOUND_ERROR, tostring(Object), tostring(MethodName), debug.traceback(nil :: any, 2)))
end
self[Object] = MethodName
return Object
end
--[=[
Adds a [Promise](https://github.com/evaera/roblox-lua-promise) to the Janitor. If the Janitor is cleaned up and the Promise is not completed, the Promise will be cancelled.
```lua
local Obliterator = Janitor.new()
Obliterator:AddPromise(Promise.delay(3)):andThenCall(print, "Finished!"):catch(warn)
task.wait(1)
Obliterator:Cleanup()
```
```ts
import { Janitor } from "@rbxts/janitor";
const Obliterator = new Janitor();
Obliterator.AddPromise(Promise.delay(3)).andThenCall(print, "Finished!").catch(warn);
task.wait(1);
Obliterator.Cleanup();
```
@param PromiseObject Promise -- The promise you want to add to the Janitor.
@return Promise
]=]
function Janitor:AddPromise(PromiseObject)
if FoundPromiseLibrary then
if not Promise.is(PromiseObject) then
error(string.format(NOT_A_PROMISE, typeof(PromiseObject), tostring(PromiseObject)))
end
if PromiseObject:getStatus() == Promise.Status.Started then
local Id = newproxy(false)
local NewPromise = self:Add(Promise.new(function(Resolve, _, OnCancel)
if OnCancel(function()
PromiseObject:cancel()
end) then
return
end
Resolve(PromiseObject)
end), "cancel", Id)
NewPromise:finallyCall(self.Remove, self, Id)
return NewPromise
else
return PromiseObject
end
else
return PromiseObject
end
end
--[=[
Cleans up whatever `Object` was set to this namespace by the 3rd parameter of [Janitor.Add](#Add).
```lua
local Obliterator = Janitor.new()
Obliterator:Add(workspace.Baseplate, "Destroy", "Baseplate")
Obliterator:Remove("Baseplate")
```
```ts
import { Workspace } from "@rbxts/services";
import { Janitor } from "@rbxts/janitor";
const Obliterator = new Janitor<{ Baseplate: Part }>();
Obliterator.Add(Workspace.FindFirstChild("Baseplate") as Part, "Destroy", "Baseplate");
Obliterator.Remove("Baseplate");
```
@param Index any -- The index you want to remove.
@return Janitor
]=]
function Janitor:Remove(Index: any)
local This = self[IndicesReference]
if This then
local Object = This[Index]
if Object then
local MethodName = self[Object]
if MethodName then
if MethodName == true then
Object()
else
local ObjectMethod = Object[MethodName]
if ObjectMethod then
ObjectMethod(Object)
end
end
self[Object] = nil
end
This[Index] = nil
end
end
return self
end
--[=[
Gets whatever object is stored with the given index, if it exists. This was added since Maid allows getting the task using `__index`.
```lua
local Obliterator = Janitor.new()
Obliterator:Add(workspace.Baseplate, "Destroy", "Baseplate")
print(Obliterator:Get("Baseplate")) -- Returns Baseplate.
```
```ts
import { Workspace } from "@rbxts/services";
import { Janitor } from "@rbxts/janitor";
const Obliterator = new Janitor<{ Baseplate: Part }>();
Obliterator.Add(Workspace.FindFirstChild("Baseplate") as Part, "Destroy", "Baseplate");
print(Obliterator.Get("Baseplate")); // Returns Baseplate.
```
@param Index any -- The index that the object is stored under.
@return any? -- This will return the object if it is found, but it won't return anything if it doesn't exist.
]=]
function Janitor:Get(Index: any): any?
local This = self[IndicesReference]
if This then
return This[Index]
else
return nil
end
end
local function GetFenv(self)
return function()
for Object, MethodName in pairs(self) do
if Object ~= IndicesReference then
return Object, MethodName
end
end
end
end
--[=[
Calls each Object's `MethodName` (or calls the Object if `MethodName == true`) and removes them from the Janitor. Also clears the namespace.
This function is also called when you call a Janitor Object (so it can be used as a destructor callback).
```lua
Obliterator:Cleanup() -- Valid.
Obliterator() -- Also valid.
```
```ts
Obliterator.Cleanup()
```
]=]
function Janitor:Cleanup()
if not self.CurrentlyCleaning then
self.CurrentlyCleaning = nil
local Get = GetFenv(self)
local Object, MethodName = Get()
while Object and MethodName do -- changed to a while loop so that if you add to the janitor inside of a callback it doesn't get untracked (instead it will loop continuously which is a lot better than a hard to pindown edgecase)
if MethodName == true then
Object()
else
local ObjectMethod = Object[MethodName]
if ObjectMethod then
ObjectMethod(Object)
end
end
self[Object] = nil
Object, MethodName = Get()
end
local This = self[IndicesReference]
if This then
table.clear(This)
self[IndicesReference] = {}
end
self.CurrentlyCleaning = false
end
end
--[=[
Calls [Janitor.Cleanup](#Cleanup) and renders the Janitor unusable.
:::warning
Running this will make any attempts to call a function of Janitor error.
:::
]=]
function Janitor:Destroy()
self:Cleanup()
table.clear(self)
setmetatable(self, nil)
end
Janitor.__call = Janitor.Cleanup
--[=[
A wrapper for an `RBXScriptConnection`. Makes the Janitor clean up when the instance is destroyed. This was created by Corecii.
@class RbxScriptConnection
@__index RbxScriptConnection
]=]
local RbxScriptConnection = {}
RbxScriptConnection.Connected = true
RbxScriptConnection.__index = RbxScriptConnection
--[=[
Disconnects the Signal.
]=]
function RbxScriptConnection:Disconnect()
if self.Connected then
self.Connected = false
self.Connection:Disconnect()
end
end
function RbxScriptConnection._new(RBXScriptConnection: RBXScriptConnection)
return setmetatable({
Connection = RBXScriptConnection;
}, RbxScriptConnection)
end
function RbxScriptConnection:__tostring()
return "RbxScriptConnection<" .. tostring(self.Connected) .. ">"
end
type RbxScriptConnection = typeof(RbxScriptConnection._new(game:GetPropertyChangedSignal("ClassName"):Connect(function() end)))
--[=[
"Links" this Janitor to an Instance, such that the Janitor will `Cleanup` when the Instance is `Destroyed()` and garbage collected.
A Janitor may only be linked to one instance at a time, unless `AllowMultiple` is true. When called with a truthy `AllowMultiple` parameter,
the Janitor will "link" the Instance without overwriting any previous links, and will also not be overwritable.
When called with a falsy `AllowMultiple` parameter, the Janitor will overwrite the previous link which was also called with a falsy `AllowMultiple` parameter, if applicable.
```lua
local Obliterator = Janitor.new()
Obliterator:Add(function()
print("Cleaning up!")
end, true)
do
local Folder = Instance.new("Folder")
Obliterator:LinkToInstance(Folder)
Folder:Destroy()
end
```
```ts
import { Janitor } from "@rbxts/janitor";
const Obliterator = new Janitor();
Obliterator.Add(() => print("Cleaning up!"), true);
{
const Folder = new Instance("Folder");
Obliterator.LinkToInstance(Folder, false);
Folder.Destroy();
}
```
This returns a mock `RBXScriptConnection` (see: [RbxScriptConnection](#RbxScriptConnection)).
@param Object Instance -- The instance you want to link the Janitor to.
@param AllowMultiple? boolean -- Whether or not to allow multiple links on the same Janitor.
@return RbxScriptConnection -- A pseudo RBXScriptConnection that can be disconnected to prevent the cleanup of LinkToInstance.
]=]
function Janitor:LinkToInstance(Object: Instance, AllowMultiple: boolean?): RbxScriptConnection
local Connection
local IndexToUse = AllowMultiple and newproxy(false) or LinkToInstanceIndex
local IsNilParented = Object.Parent == nil
local ManualDisconnect = setmetatable({}, RbxScriptConnection)
local function ChangedFunction(_DoNotUse, NewParent)
if ManualDisconnect.Connected then
_DoNotUse = nil
IsNilParented = NewParent == nil
if IsNilParented then
task.defer(function()
if not ManualDisconnect.Connected then
return
elseif not Connection.Connected then
self:Cleanup()
else
while IsNilParented and Connection.Connected and ManualDisconnect.Connected do
task.wait()
end
if ManualDisconnect.Connected and IsNilParented then
self:Cleanup()
end
end
end)
end
end
end
Connection = Object.AncestryChanged:Connect(ChangedFunction)
ManualDisconnect.Connection = Connection
if IsNilParented then
ChangedFunction(nil, Object.Parent)
end
Object = nil :: any
return self:Add(ManualDisconnect, "Disconnect", IndexToUse)
end
--[=[
Links several instances to a new Janitor, which is then returned.
@param ... Instance -- All the Instances you want linked.
@return Janitor -- A new Janitor that can be used to manually disconnect all LinkToInstances.
]=]
function Janitor:LinkToInstances(...: Instance)
local ManualCleanup = Janitor.new()
for _, Object in ipairs({...}) do
ManualCleanup:Add(self:LinkToInstance(Object, true), "Disconnect")
end
return ManualCleanup
end
--[=[
Instantiates a new Janitor object.
@return Janitor
]=]
function Janitor.new()
return setmetatable({
CurrentlyCleaning = false;
[IndicesReference] = nil;
}, Janitor)
end
export type Janitor = typeof(Janitor.new())
return Janitor
end,
function(script,require)
-- This only exists because the LSP warns Key `__tostring` not found in type `table?`.
local function Symbol(Name: string)
local self = newproxy(true)
local Metatable = getmetatable(self)
function Metatable.__tostring()
return Name
end
return self
end
return Symbol
end,
function(script,require)
-- TODO: When Promise is on Wally, remove this in favor of just `script.Parent.Parent:FindFirstChild("Promise")`.
local ReplicatedFirst = game:GetService("ReplicatedFirst")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local ServerStorage = game:GetService("ServerStorage")
local LOCATIONS_TO_SEARCH = {script.Parent.Parent, ReplicatedFirst, ReplicatedStorage, ServerScriptService, ServerStorage}
local function FindFirstDescendantWithNameAndClassName(Parent: Instance, Name: string, ClassName: string)
for _, Descendant in ipairs(Parent:GetDescendants()) do
if Descendant:IsA(ClassName) and Descendant.Name == Name then
return Descendant
end
end
return nil
end
local function GetPromiseLibrary()
-- I'm not too keen on how this is done.
-- It's better than the multiple if statements (probably).
local Plugin = script:FindFirstAncestorOfClass("Plugin")
if Plugin then
local Promise = FindFirstDescendantWithNameAndClassName(Plugin, "Promise", "ModuleScript")
if Promise then
return true, require(Promise)
else
return false
end
end
local Promise
for _, Location in ipairs(LOCATIONS_TO_SEARCH) do
Promise = FindFirstDescendantWithNameAndClassName(Location, "Promise", "ModuleScript")
if Promise then
break
end
end
if Promise then
return true, require(Promise)
else
return false
end
end
return GetPromiseLibrary
end,
function(script,require)
-- The Engine or the core of the library handles all the RigidBodies, constraints and points.
-- It's responsible for the simulation of these elements and handling all tasks related to the library.
-- Services and utilities
local RigidBody = require(script.Parent.Physics.RigidBody)
local Point = require(script.Parent.Physics.Point)
local Constraint = require(script.Parent.Physics.Constraint)
local PhysicsRunner = require(script.Parent.Physics.Runner)
local Globals = require(script.Parent.Constants.Globals)
local Signal = require(script.Parent.Utilities.Signal)
local Quadtree = require(script.Parent.Utilities.Quadtree)
local Janitor = require(script.Parent.Utilities.Janitor)
local Types = require(script.Parent.Types)
local throwException = require(script.Parent.Debugging.Exceptions)
local throwTypeError = require(script.Parent.Debugging.TypeErrors)
local RunService = game:GetService("RunService")
local function SearchTable(t: { any }, a: any, lambda: (a: any, b: any) -> boolean) : any
for _, v in ipairs(t) do
if lambda(a, v) then
return v
end
end
return nil
end
local Engine = {}
Engine.__index = Engine
-- This method is used to initialize basic configurations of the engine and allocate memory for future tasks.
function Engine.init(screengui: Instance)
if not typeof(screengui) == "Instance" or not screengui:IsA("Instance") then
error("Invalid Argument #1. 'screengui' must be a ScreenGui.", 2)
end
local self = setmetatable({
bodies = {},
constraints = {},
points = {},
connection = nil,
_janitor = nil,
gravity = Globals.engineInit.gravity,
friction = Globals.engineInit.friction,
airfriction = Globals.engineInit.airfriction,
bounce = Globals.engineInit.bounce,
timeSteps = Globals.engineInit.timeSteps,
mass = Globals.universalMass,
path = screengui,
speed = Globals.speed,
quadtrees = false,
independent = true,
canvas = {
frame = nil,
topLeft = Globals.engineInit.canvas.topLeft,
size = Globals.engineInit.canvas.size
},
iterations = {
constraint = 1,
collision = 1,
},
Started = Signal.new(),
Stopped = Signal.new(),
ObjectAdded = Signal.new(),
ObjectRemoved = Signal.new(),
Updated = Signal.new(),
}, Engine)
local janitor = Janitor.new()
janitor:Add(self.Started, "Destroy")
janitor:Add(self.Stopped, "Destroy")
janitor:Add(self.ObjectAdded, "Destroy")
janitor:Add(self.ObjectRemoved, "Destroy")
janitor:Add(self.Updated, "Destroy")
self._janitor = janitor
return self
end
-- This method is used to start simulating rigid bodies and constraints.
function Engine:Start()
if not self.canvas then throwException("error", "NO_CANVAS_FOUND") end
if #self.bodies == 0 then throwException("warn", "NO_RIGIDBODIES_FOUND") end
if self.connection then throwException("warn", "ALREADY_STARTED") return end
-- Fire Engine.Started event
self.Started:Fire()
local fixedDeltaTime = 1/60
local epsilon = 1/1000
local accumulator = 0
--local framesRenderedBeforeStep = 0
local connection;
connection = RunService.Stepped:Connect(function(deltaTime)
--[[if self.independent then
accumulator += deltaTime
while accumulator > 0 do
accumulator -= fixedDeltaTime
PhysicsRunner.Update(self, deltaTime)
PhysicsRunner.Render(self)
end
if accumulator >= -epsilon then
accumulator = 0
end
else]]
accumulator = 0
PhysicsRunner.Update(self, deltaTime)
PhysicsRunner.Render(self)
--end
end)
self.connection = connection
self._janitor:Add(self.connection, "Disconnect", "MainConnection")
end
-- This method is used to stop simulating rigid bodies and constraints.
function Engine:Stop()
-- Fire Engine.Stopped event
-- Disconnect all connections
if self.connection then
self.Stopped:Fire()
self._janitor:Remove("MainConnection")
self.connection = nil
end
end
-- This method is used to create RigidBodies, Constraints and Points
function Engine:Create(object: string, properties: Types.Properties)
-- Validate types of the object and property table
throwTypeError("object", object, 1, "string")
throwTypeError("properties", properties, 2, "table")
-- Validate object
if object ~= "Constraint" and object ~= "Point" and object ~= "RigidBody" then
throwException("error", "INVALID_OBJECT")
end
-- Validate property table
for prop, value in pairs(properties) do
if not table.find(Globals.VALID_OBJECT_PROPS, prop) then
throwException("error", "INVALID_PROPERTY", string.format("%q is not a valid property!", prop))
return
end
if not table.find(Globals[string.lower(object)].props, prop) then
throwException("error", "INVALID_PROPERTY", string.format("%q is not a valid property for a %s!", prop, object))
return
end
if Globals.OBJECT_PROPS_TYPES[prop] and typeof(value) ~= Globals.OBJECT_PROPS_TYPES[prop] then
error(
string.format(
"[Nature2D]: Invalid Property type for %q. Expected %q got %q.",
prop,
Globals.OBJECT_PROPS_TYPES[prop],
typeof(value)
),
2
)
end
end
-- Check if must-have properties exist in the property table
for _, prop in ipairs(Globals[string.lower(object)].must_have) do
if not properties[prop] then
local throw = true
if prop == "Object" and properties.Structure then
throw = false
end
if throw then
throwException("error", "MUST_HAVE_PROPERTY", string.format("You must specify the %q property for a %s!", prop, object))
return
end
end
end
local newObject
-- Create the Point object
if object == "Point" then
local newPoint = Point.new(properties.Position or Vector2.new(), self.canvas, self, {
snap = properties.Snap,
selectable = false,
render = properties.Visible,
keepInCanvas = properties.KeepInCanvas or true
}, nil)
-- Apply properties
if properties.Radius then newPoint:SetRadius(properties.Radius) end
if properties.Color then newPoint:Stroke(properties.Color) end
table.insert(self.points, newPoint)
newObject = newPoint
-- Create the constraint object
elseif object == "Constraint" then
if not table.find(Globals.constraint.types, string.lower(properties.Type or "")) then
throwException("error", "INVALID_CONSTRAINT_TYPE")
end
-- Validate restlength and thickness of the constraint
if properties.RestLength and properties.RestLength <= 0 then
throwException("error", "INVALID_CONSTRAINT_LENGTH")
end
if properties.Thickness and properties.Thickness <= 0 then
throwException("error", "INVALID_CONSTRAINT_THICKNESS")
end
if properties.Point1 and properties.Point2 and properties.Type then
-- Calculate distance
local dist = (properties.Point1.pos - properties.Point2.pos).Magnitude
local newConstraint = Constraint.new(properties.Point1, properties.Point2, self.canvas, {
restLength = properties.RestLength or dist,
render = properties.Visible,
thickness = properties.Thickness,
support = false,
TYPE = string.upper(properties.Type)
}, self)
-- Apply properties
if properties.SpringConstant then newConstraint:SetSpringConstant(properties.SpringConstant) end
if properties.Color then newConstraint:Stroke(properties.Color) end
table.insert(self.constraints, newConstraint)
newObject = newConstraint
end
-- Create the RigidBody object
elseif object == "RigidBody" then
-- Validate custom RigidBody structure
if properties.Object and not properties.Object:IsA("GuiObject") and not properties.Structure then
error("'Object' must be a GuiObject", 2)
end
local obj = nil
if not properties.Structure then
obj = properties.Object
end
local custom: Types.Custom = {
Vertices = {},
Edges = {}
}
if properties.Structure then
if not self.canvas.frame then
throwException("error", "CANVAS_FRAME_NOT_FOUND")
end
for _, c in ipairs(properties.Structure) do
local a = c[1]
local b = c[2]
local support = c[3]
if typeof(a) ~= "Vector2" or typeof(b) ~= "Vector2" then
error("[Nature2D]: Invalid point positions for custom RigidBody structure.", 2)
end
if support and typeof(support) ~= "boolean" then error("[Nature2D]: 'support' must be a boolean or nil") end
if a == b then error("[Nature2D]: A constraint cannot have the same points.", 2) end
local PointA = SearchTable(custom.Vertices, a, function(i, v) return i == v.pos end)
local PointB = SearchTable(custom.Vertices, b, function(i, v) return i == v.pos end)
if not PointA then
PointA = Point.new(a, self.canvas, self, {
snap = properties.Anchored,
selectable = false,
render = false,
keepInCanvas = properties.KeepInCanvas or true
})
table.insert(custom.Vertices, PointA)
end
if not PointB then
PointB = Point.new(b, self.canvas, self, {
snap = properties.Anchored,
selectable = false,
render = false,
keepInCanvas = properties.KeepInCanvas or true
})
table.insert(custom.Vertices, PointB)
end
local edge = Constraint.new(PointA, PointB, self.canvas, {
render = support and false or true,
thickness = 2,
support = support,
TYPE = "ROD"
}, self)
table.insert(custom.Edges, edge)
end
end
local newBody = RigidBody.new(
obj,
properties.Mass or self.mass,
properties.Collidable,
properties.Anchored,
self,
properties.Structure and custom or nil,
properties.Structure
)
--Apply properties
if properties.LifeSpan then newBody:SetLifeSpan(properties.LifeSpan) end
if properties.KeepInCanvas and typeof(properties.KeepInCanvas) == "boolean" then newBody:KeepInCanvas(properties.KeepInCanvas) end
if properties.Gravity then newBody:SetGravity(properties.Gravity) end
if properties.Friction then newBody:SetFriction(properties.Friction) end
if properties.AirFriction then newBody:SetAirFriction(properties.AirFriction) end
if properties.CanRotate and typeof(properties.CanRotate) == "boolean" and not properties.Structure then newBody:CanRotate(properties.CanRotate) end
table.insert(self.bodies, newBody)
newObject = newBody
end
self._janitor:Add(newObject, "Destroy")
self.ObjectAdded:Fire(newObject)
return newObject
end
-- This method is used to fetch all RigidBodies that have been created.
-- Ones that have been destroyed, won't be fetched.
function Engine:GetBodies()
return self.bodies
end
-- This method is used to fetch all Constraints that have been created.
-- Ones that have been destroyed, won't be fetched.
function Engine:GetConstraints()
return self.constraints
end
-- This method is used to fetch all Points that have been created.
function Engine:GetPoints()
return self.points
end
-- This function is used to initialize boundaries to which all bodies and constraints obey.
-- An object cannot go past this boundary.
function Engine:CreateCanvas(topLeft: Vector2, size: Vector2, frame: Frame)
throwTypeError("topLeft", topLeft, 1, "Vector2")
throwTypeError("size", size, 2, "Vector2")
self.canvas.topLeft = topLeft
self.canvas.size = size
if frame and frame:IsA("Frame") then
self.canvas.frame = frame
end
end
-- This method is used to determine the simulation speed of the engine.
-- By default the simulation speed is set to 55.
function Engine:SetSimulationSpeed(speed: number)
throwTypeError("speed", speed, 1, "number")
self.speed = speed
end
-- This method is used to configure universal physical properties possessed by all rigid bodies and constraints.
function Engine:SetPhysicalProperty(property: string, value: Vector2 | number)
throwTypeError("property", property, 1, "string")
local properties = Globals.properties
-- Update properties of the Engine
local function Update(object)
if string.lower(property) == "collisionmultiplier" then
throwTypeError("value", value, 2, "number")
object.bounce = value
elseif string.lower(property) == "gravity" then
throwTypeError("value", value, 2, "Vector2")
object.gravity = value
elseif string.lower(property) == "friction" then
throwTypeError("value", value, 2, "number")
object.friction = math.clamp(1 - value, 0, 1)
elseif string.lower(property) == "airfriction" then
throwTypeError("value", value, 2, "number")
object.airfriction = math.clamp(1 - value, 0, 1)
elseif string.lower(property) == "universalmass" then
throwTypeError("value", value, 2, "number")
object.mass = math.max(0, value)
end
end
-- Validate and update properties
if table.find(properties, string.lower(property)) then
if #self.bodies < 1 then
Update(self)
else
Update(self)
for _, b in ipairs(self.bodies) do
for _, v in ipairs(b:GetVertices()) do
Update(v)
end
end
end
else
throwException("error", "PROPERTY_NOT_FOUND")
end
end
-- This method is used to fetch an individual rigid body from its ID.
function Engine:GetBodyById(id: string)
throwTypeError("id", id, 1, "string")
for _, b in ipairs(self.bodies) do
if b.id == id then
return b
end
end
return
end
-- This method is used to fetch an individual constraint body from its ID.
function Engine:GetConstraintById(id: string)
throwTypeError("id", id, 1, "string")
for _, c in ipairs(self.constraints) do
if c.id == id then
return c
end
end
return
end
function Engine:GetDebugInfo() : Types.DebugInfo
return {
Objects = {
RigidBodies = #self.bodies,
Constraints = #self.constraints,
Points = #self.points
},
Running = not not (self.connection),
Physics = {
Gravity = self.gravity,
Friction = 1 - self.friction,
AirFriction = 1 - self.airfriction,
CollisionMultiplier = self.bounce,
TimeSteps = self.timeSteps,
SimulationSpeed = self.speed,
UsingQuadtrees = self.quadtrees,
FramerateIndependent = self.independent
},
Path = self.path,
Canvas = {
Frame = self.canvas.frame,
TopLeft = self.canvas.topLeft,
Size = self.canvas.size
}
}
end
-- Determines if Quadtrees will be used in collision deteWction.
-- By default this is set to false
function Engine:UseQuadtrees(use: boolean)
throwTypeError("useQuadtrees", use, 1, "boolean")
self.quadtrees = use
end
-- Determines if Frame rate does not affect the simulation speed.
-- By default set to true.
function Engine:FrameRateIndependent(independent: boolean)
throwTypeError("independent", independent, 1, "boolean")
self.independent = independent
end
function Engine:SetConstraintIterations(iterations: number)
throwTypeError("iterations", iterations, 1, "number")
self.iterations.constraint = math.floor(math.clamp(iterations, 1, 10))
end
function Engine:SetCollisionIterations(iterations: number)
throwTypeError("iterations", iterations, 1, "number")
if self.quadtrees then
self.iterations.collision = math.floor(math.clamp(iterations, 1, 10))
else
throwException("warn", "CANNOT_SET_COLLISION_ITERATIONS")
end
end
function Engine:Destroy()
self._janitor:Destroy()
setmetatable(self, nil)
end
return Engine
end,
function(script,require)
-- Type Definitions
export type Quadtree<T> = {
position: Vector2,
size: Vector2,
capacity: number,
objects: {T},
divided: boolean,
}
export type Canvas = {
topLeft: Vector2,
size: Vector2,
frame: Frame?
}
export type Point = {
Parent: any,
frame: Frame?,
engine: { any },
canvas: Canvas,
oldPos: Vector2,
pos: Vector2,
forces: Vector2,
gravity: Vector2,
friction: number,
airfriction: number,
bounce: number,
snap: boolean,
selectable: boolean,
render: boolean,
keepInCanvas: boolean,
color: Color3?,
radius: number
}
export type RigidBody = {
CreateProjection: (Axis: Vector2, Min: number, Max: number) -> (number, number),
SetState: (state: string, value: any) -> (),
GetState: (state: string) -> any,
id: string,
vertices: { Point },
edges: { any },
frame: GuiObject?,
anchored: boolean,
mass: number,
collidable: boolean,
center: Vector2,
engine: { any },
spawnedAt: number,
lifeSpan: number?,
anchorRotation: number?,
anchorPos: Vector2?,
Touched: any,
CanvasEdgeTouched: any,
States: { any }
}
export type SegmentConfig = {
restLength: number?,
render: boolean,
thickness: number?,
support: boolean,
TYPE: string,
}
export type EngineConfig = {
gravity: Vector2,
friction: number,
bounce: number,
speed: number,
airfriction: number,
}
export type PointConfig = {
snap: boolean,
selectable: boolean,
render: boolean,
keepInCanvas: boolean
}
export type Collision = {
axis: Vector2,
depth: number,
edge: any,
vertex: Point
}
export type Range = {
position: Vector2,
size: Vector2
}
export type Properties = {
Position: Vector2?,
Visible: boolean?,
Snap: boolean?,
KeepInCanvas: boolean?,
Radius: number?,
Color: Color3?,
Type: string?,
Point1: Point?,
Point2: Point?,
Thickness: number?,
RestLength: number?,
SpringConstant: number?,
Object: GuiObject?,
Collidable: boolean?,
Anchored: boolean?,
LifeSpan: number?,
Gravity: Vector2?,
Friction: number?,
AirFriction: number?,
Structure: {}?,
Mass: number?,
CanRotate: boolean
}
export type Custom = {
Vertices: { any },
Edges: { any }
}
export type Plugins = {
Triangle: (a: Vector2, b: Vector2, c: Vector2) -> (),
Quad: (a: Vector2, b: Vector2, c: Vector2, d: Vector2) -> (),
MouseConstraint: (engine: { any }, range: number, rigidbodies: { any }) -> ()
}
export type DebugInfo = {
Objects: {
RigidBodies: number,
Constraints: number,
Points: number
},
Running: boolean,
Physics: {
Gravity: Vector2,
Friction: number,
AirFriction: number,
CollisionMultiplier: number,
TimeSteps: number,
SimulationSpeed: number,
UsingQuadtrees: boolean,
FramerateIndependent: boolean
},
Path: ScreenGui,
Canvas: {
Frame: GuiObject,
TopLeft: Vector2,
Size: Vector2
}
}
return nil
end,
function(script,require)
return {
MouseConstraint = require(script.MouseConstraint),
Quad = require(script.Quad),
Triangle = require(script.Triangle)
}
end,
function(script,require)
local UserInputService = game:GetService("UserInputService")
return function (engine: { any }, range: number)
local held = nil
local connections = {}
connections.InputBegan = UserInputService.InputBegan:Connect(function(input, processedEvent)
if processedEvent then return end
if input.UserInputType == Enum.UserInputType.MouseButton1 and not held then
for _, b in ipairs(engine.bodies) do
for _, p in ipairs(b.vertices) do
if (p.pos - UserInputService:GetMouseLocation()).Magnitude <= range then
p.selectable = true
p.snap = true
held = p
break
end
end
if held then
break
end
end
end
end)
connections.InputEnded = UserInputService.InputEnded:Connect(function(input, processedEvent)
if processedEvent then return end
if input.UserInputType == Enum.UserInputType.MouseButton1 and held then
held.selectable = false
held.snap = false
held = nil
end
end)
connections.InputChanged = UserInputService.InputChanged:Connect(function(input, processedEvent)
if processedEvent then return end
if input.UserInputType == Enum.UserInputType.MouseMovement and held then
local mouse = UserInputService:GetMouseLocation()
held:SetPosition(mouse.X, mouse.Y)
end
end)
return function ()
held.snap = false
held = nil
connections.InputBegan:Disconnect()
connections.InputEnded:Disconnect()
connections.InputChanged:Disconnect()
end
end
end,
function(script,require)
-- Returns a quadrilateral structure for Custom RigidBodies given 4 points
return function (a: Vector2, b: Vector2, c: Vector2, d: Vector2)
return {
{ a, b, false },
{ b, c, false },
{ c, d, false },
{ d, a, false },
{ a, c, true },
{ b, d, true }
}
end
end,
function(script,require)
-- Returns a triangular structure for Custom RigidBodies given 3 points
return function (a: Vector2, b: Vector2, c: Vector2)
return {
{ a, b, false },
{ a, c, false },
{ b, c, false }
}
end
end
}
local ScriptIndex = 0
local Scripts,ModuleScripts,ModuleCache = {},{},{}
local _require = require
function require(obj,...)
local index = ModuleScripts[obj]
if not index then
local a,b = pcall(_require,obj,...)
return not a and error(b,2) or b
end
local res = ModuleCache[index]
if res then return res end
res = ScriptFunctions[index](obj,require)
ModuleCache[index] = res
return res
end
local function Script(obj,ismodule)
ScriptIndex = ScriptIndex + 1
local t = ismodule and ModuleScripts or Scripts
t[obj] = ScriptIndex
end
function RunScripts()
for script,index in pairs(Scripts) do
coroutine.wrap(ScriptFunctions[index])(script,require)
end
end
local function Decode(str)
local StringLength = #str
-- Base64 decoding
do
local decoder = {}
for b64code, char in pairs(('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='):split('')) do
decoder[char:byte()] = b64code-1
end
local n = StringLength
local t,k = table.create(math.floor(n/4)+1),1
local padding = str:sub(-2) == '==' and 2 or str:sub(-1) == '=' and 1 or 0
for i = 1, padding > 0 and n-4 or n, 4 do
local a, b, c, d = str:byte(i,i+3)
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d]
t[k] = string.char(bit32.extract(v,16,8),bit32.extract(v,8,8),bit32.extract(v,0,8))
k = k + 1
end
if padding == 1 then
local a, b, c = str:byte(n-3,n-1)
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40
t[k] = string.char(bit32.extract(v,16,8),bit32.extract(v,8,8))
elseif padding == 2 then
local a, b = str:byte(n-3,n-2)
local v = decoder[a]*0x40000 + decoder[b]*0x1000
t[k] = string.char(bit32.extract(v,16,8))
end
str = table.concat(t)
end
local Position = 1
local function Parse(fmt)
local Values = {string.unpack(fmt,str,Position)}
Position = table.remove(Values)
return table.unpack(Values)
end
local Settings = Parse('B')
local Flags = Parse('B')
Flags = {
--[[ValueIndexByteLength]] bit32.extract(Flags,6,2)+1,
--[[InstanceIndexByteLength]] bit32.extract(Flags,4,2)+1,
--[[ConnectionsIndexByteLength]] bit32.extract(Flags,2,2)+1,
--[[MaxPropertiesLengthByteLength]] bit32.extract(Flags,0,2)+1,
--[[Use Double instead of Float]] bit32.band(Settings,0b1) > 0
}
local ValueFMT = ('I'..Flags[1])
local InstanceFMT = ('I'..Flags[2])
local ConnectionFMT = ('I'..Flags[3])
local PropertyLengthFMT = ('I'..Flags[4])
local ValuesLength = Parse(ValueFMT)
local Values = table.create(ValuesLength)
local CFrameIndexes = {}
local ValueDecoders = {
--!!Start
[1] = function(Modifier)
return Parse('s'..Modifier)
end,
--!!Split
[2] = function(Modifier)
return Modifier ~= 0
end,
--!!Split
[3] = function()
return Parse('d')
end,
--!!Split
[4] = function(_,Index)
table.insert(CFrameIndexes,{Index,Parse(('I'..Flags[1]):rep(3))})
end,
--!!Split
[5] = {CFrame.new,Flags[5] and 'dddddddddddd' or 'ffffffffffff'},
--!!Split
[6] = {Color3.fromRGB,'BBB'},
--!!Split
[7] = {BrickColor.new,'I2'},
--!!Split
[8] = function(Modifier)
local len = Parse('I'..Modifier)
local kpts = table.create(len)
for i = 1,len do
kpts[i] = ColorSequenceKeypoint.new(Parse('f'),Color3.fromRGB(Parse('BBB')))
end
return ColorSequence.new(kpts)
end,
--!!Split
[9] = function(Modifier)
local len = Parse('I'..Modifier)
local kpts = table.create(len)
for i = 1,len do
kpts[i] = NumberSequenceKeypoint.new(Parse(Flags[5] and 'ddd' or 'fff'))
end
return NumberSequence.new(kpts)
end,
--!!Split
[10] = {Vector3.new,Flags[5] and 'ddd' or 'fff'},
--!!Split
[11] = {Vector2.new,Flags[5] and 'dd' or 'ff'},
--!!Split
[12] = {UDim2.new,Flags[5] and 'di2di2' or 'fi2fi2'},
--!!Split
[13] = {Rect.new,Flags[5] and 'dddd' or 'ffff'},
--!!Split
[14] = function()
local flags = Parse('B')
local ids = {"Top","Bottom","Left","Right","Front","Back"}
local t = {}
for i = 0,5 do
if bit32.extract(flags,i,1)==1 then
table.insert(t,Enum.NormalId[ids[i+1]])
end
end
return Axes.new(unpack(t))
end,
--!!Split
[15] = function()
local flags = Parse('B')
local ids = {"Top","Bottom","Left","Right","Front","Back"}
local t = {}
for i = 0,5 do
if bit32.extract(flags,i,1)==1 then
table.insert(t,Enum.NormalId[ids[i+1]])
end
end
return Faces.new(unpack(t))
end,
--!!Split
[16] = {PhysicalProperties.new,Flags[5] and 'ddddd' or 'fffff'},
--!!Split
[17] = {NumberRange.new,Flags[5] and 'dd' or 'ff'},
--!!Split
[18] = {UDim.new,Flags[5] and 'di2' or 'fi2'},
--!!Split
[19] = function()
return Ray.new(Vector3.new(Parse(Flags[5] and 'ddd' or 'fff')),Vector3.new(Parse(Flags[5] and 'ddd' or 'fff')))
end
--!!End
}
for i = 1,ValuesLength do
local TypeAndModifier = Parse('B')
local Type = bit32.band(TypeAndModifier,0b11111)
local Modifier = (TypeAndModifier - Type) / 0b100000
local Decoder = ValueDecoders[Type]
if type(Decoder)=='function' then
Values[i] = Decoder(Modifier,i)
else
Values[i] = Decoder[1](Parse(Decoder[2]))
end
end
for i,t in pairs(CFrameIndexes) do
Values[t[1]] = CFrame.fromMatrix(Values[t[2]],Values[t[3]],Values[t[4]])
end
local InstancesLength = Parse(InstanceFMT)
local Instances = {}
local NoParent = {}
for i = 1,InstancesLength do
local ClassName = Values[Parse(ValueFMT)]
local obj
local MeshPartMesh,MeshPartScale
if ClassName == "UnionOperation" then
obj = DecodeUnion(Values,Flags,Parse)
obj.UsePartColor = true
elseif ClassName:find("Script") then
obj = Instance.new("Folder")
Script(obj,ClassName=='ModuleScript')
elseif ClassName == "MeshPart" then
obj = Instance.new("Part")
MeshPartMesh = Instance.new("SpecialMesh")
MeshPartMesh.MeshType = Enum.MeshType.FileMesh
MeshPartMesh.Parent = obj
else
obj = Instance.new(ClassName)
end
local Parent = Instances[Parse(InstanceFMT)]
local PropertiesLength = Parse(PropertyLengthFMT)
local AttributesLength = Parse(PropertyLengthFMT)
Instances[i] = obj
for i = 1,PropertiesLength do
local Prop,Value = Values[Parse(ValueFMT)],Values[Parse(ValueFMT)]
-- ok this looks awful
if MeshPartMesh then
if Prop == "MeshId" then
MeshPartMesh.MeshId = Value
continue
elseif Prop == "TextureID" then
MeshPartMesh.TextureId = Value
continue
elseif Prop == "Size" then
if not MeshPartScale then
MeshPartScale = Value
else
MeshPartMesh.Scale = Value / MeshPartScale
end
elseif Prop == "MeshSize" then
if not MeshPartScale then
MeshPartScale = Value
MeshPartMesh.Scale = obj.Size / Value
else
MeshPartMesh.Scale = MeshPartScale / Value
end
continue
end
end
obj[Prop] = Value
end
if MeshPartMesh then
if MeshPartMesh.MeshId=='' then
if MeshPartMesh.TextureId=='' then
MeshPartMesh.TextureId = 'rbxasset://textures/meshPartFallback.png'
end
MeshPartMesh.Scale = obj.Size
end
end
for i = 1,AttributesLength do
obj:SetAttribute(Values[Parse(ValueFMT)],Values[Parse(ValueFMT)])
end
if not Parent then
table.insert(NoParent,obj)
else
obj.Parent = Parent
end
end
local ConnectionsLength = Parse(ConnectionFMT)
for i = 1,ConnectionsLength do
local a,b,c = Parse(InstanceFMT),Parse(ValueFMT),Parse(InstanceFMT)
Instances[a][Values[b]] = Instances[c]
end
return NoParent
end
local Objects = Decode('AAAcIQxNb2R1bGVTY3JpcHQhBE5hbWUhCE5hdHVyZTJEIQZGb2xkZXIhCUNvbnN0YW50cyEHR2xvYmFscyEJRGVidWdnaW5nIQpUeXBlRXJyb3JzIQpFeGNlcHRpb25zIQhSZXN0cmljdCEHUGh5c2ljcyEJUmlnaWRCb2R5IQVQb2ludCEKQ29uc3RyYWludCEGUnVu'
..'bmVyIQlVdGlsaXRpZXMhBlNpZ25hbCEIUXVhZHRyZWUhBExpbmUhB0phbml0b3IhBlN5bWJvbCERR2V0UHJvbWlzZUxpYnJhcnkhBkVuZ2luZSEFVHlwZXMhB1BsdWdpbnMhD01vdXNlQ29uc3RyYWludCEEUXVhZCEIVHJpYW5nbGUZAQABAAIDBAEBAAIFAQIBAAIG'
..'BAEBAAIHAQQBAAIIAQQBAAIJAQQBAAIKBAEBAAILAQgBAAIMAQgBAAINAQgBAAIOAQgBAAIPBAEBAAIQAQ0BAAIRAQ0BAAISAQ0BAAITAQ0BAAIUAREBAAIVAREBAAIWAQEBAAIXAQEBAAIYAQEBAAIZARYBAAIaARYBAAIbARYBAAIcAA==')
for _,obj in pairs(Objects) do
obj.Parent = script or workspace
end
local engine = require(script.Nature2D).init(SurfaceGui)
engine.timeSteps = 1
engine:CreateCanvas(Vector2.new(), SurfaceGui.AbsoluteSize)
print(engine:Create("RigidBody", {
Object = floor,
Collidable = true,
Anchored = true
}))
task.defer(function()
while true do
local b = Instance.new("ImageLabel", SurfaceGui)
b.Image = "rbxassetid://5938078827"
b.Size = UDim2.fromOffset(50, 50)
b.AnchorPoint = Vector2.one/2
b.Position = UDim2.fromScale(0.5, 0.5)
b.BorderSizePixel = 0
b.BackgroundTransparency = 1
local e = engine:Create("RigidBody", {
Object = b,
Collidable = true,
Anchored = false
})
task.delay(4, e.Destroy, e)
task.wait(0.8)
end
end)
engine:Start()
-- Converted using Mokiros's Model to Script Version 3
-- Converted string size: 580 characters
local ScriptFunctions = {
function(script, require)
return require(script.Engine)
end,
function(script,require)
-- List of commonly used variables all across the library
return {
engineInit = {
gravity = Vector2.new(0, .3),
friction = 0.9,
airfriction = 0.98,
bounce = 0.8,
timeSteps = 1,
canvas = {
topLeft = Vector2.new(0, 0),
size = workspace.CurrentCamera.ViewportSize,
},
},
universalMass = 1,
speed = 55,
properties = {
"gravity",
"friction",
"collisionmultiplier",
"airfriction",
"universalmass"
},
rigidbody = {
props = {
"Object",
"Collidable",
"Anchored",
"LifeSpan",
"KeepInCanvas",
"Gravity",
"Friction",
"AirFriction",
"Structure",
"Mass",
"CanRotate"
},
must_have = {
"Object"
}
},
constraint = {
color = Color3.new(1, 1, 1),
thickness = 4,
types = {
"rope",
"spring",
"rod"
},
props = {
"Type",
"Point1",
"Point2",
"Visible",
"Thickness",
"RestLength",
"SpringConstant",
"Color",
},
must_have = {
"Type",
"Point1",
"Point2",
}
},
point = {
radius = 2.5,
color = Color3.new(1),
uicRadius = UDim.new(1, 0),
props = {
"Position",
"Visible",
"Snap",
"KeepInCanvas",
"Radius",
"Color"
},
must_have = {
"Position"
}
},
offset = Vector2.new(0, 36),
VALID_OBJECT_PROPS = {
"Position",
"Visible",
"Snap",
"KeepInCanvas",
"Radius",
"Color",
"Type",
"Point1",
"Point2",
"Thickness",
"RestLength",
"SpringConstant",
"Object",
"Collidable",
"Anchored",
"LifeSpan",
"Gravity",
"Friction",
"AirFriction",
"Structure",
"Mass",
"CanRotate"
},
OBJECT_PROPS_TYPES = {
Position = "Vector2",
Visible = "boolean",
Snap = "boolean",
KeepInCanvas = "boolean",
Radius = "number",
Color = "Color3",
Type = "string",
Thickness = "number",
RestLength = "number",
SpringConstant = "number",
Object = "Instance",
Collidable = "boolean",
Anchored = "boolean",
LifeSpan = "number",
Gravity = "Vector2",
Friction = "number",
AirFriction = "number",
Structure = "table",
Mass = "number",
CanRotate = "boolean"
},
}
end,
function(script,require)
-- Handling type errors
return function (arg: string, param, pos: number, expected: string)
if typeof(param) ~= expected then
error(
string.format(
"[Nature2D]: Invalid Argument #%s. Expected type %q for %s, got %q",
tostring(pos),
expected,
arg,
typeof(param)
),
2
)
end
end
end,
function(script,require)
-- Handling exceptions
local TYPES = {
NO_CANVAS_FOUND = "No canvas found, initialize the engine's canvas using Engine:CreateCanvas().",
NO_RIGIDBODIES_FOUND = "No rigid bodies found on start.",
PROPERTY_NOT_FOUND = "Invalid Argument #1. Property not found.",
INVALID_CONSTRAINT_TYPE = "Received Invalid Constraint Type.",
INVALID_CONSTRAINT_LENGTH = "Received Invalid Constraint Length.",
INVALID_CONSTRAINT_THICKNESS = "Received Invalid Constraint Thickness.",
SAME_ID = "Cannot ignore collisions for the same RigidBodies.",
INVALID_RIGIDBODY = "Received Invalid RigidBody.",
INVALID_OBJECT = "Received an Invalid Object. Valid objects - RigidBody, Point and Constraint.",
INVALID_PROPERTY = "Received an Invalid Object Property.",
MUST_HAVE_PROPERTY = "Missing must-have properties.",
CANVAS_FRAME_NOT_FOUND = "No canvas frame found, initialize the canvas's frame to render custom Points and Constraints!",
INVALID_TIME = "Received invalid time to apply force for.",
ALREADY_STARTED = "Engine is already running.",
CANNOT_SET_COLLISION_ITERATIONS = "Cannot set collision iterations! You must turn on quadtree usage using Engine:UseQuadtrees(true)."
}
return function (TASK: string, TYPE: string, details: string?)
if TYPES[TYPE] then
local exception = string.format("[Nature2D]: %s%s", TYPES[TYPE], if details then " "..details else "")
if TASK == "warn" then
warn(exception)
elseif TASK == "error" then
error(exception, 2)
end
end
end
end,
function(script,require)
return function (custom)
if custom then
error("[Nature2D]: This method cannot be used with custom RigidBodies", 2)
end
end
end,
function(script,require)
-- RigidBodies are formed by Constraints, Points and UI Elements.
-- Services and utilities
local Point = require(script.Parent.Point)
local Constraint = require(script.Parent.Constraint)
local Globals = require(script.Parent.Parent.Constants.Globals)
local Signal = require(script.Parent.Parent.Utilities.Signal)
local Types = require(script.Parent.Parent.Types)
local Janitor = require(script.Parent.Parent.Utilities.Janitor)
local throwTypeError = require(script.Parent.Parent.Debugging.TypeErrors)
local throwException = require(script.Parent.Parent.Debugging.Exceptions)
local restrict = require(script.Parent.Parent.Debugging.Restrict)
local HttpService = game:GetService("HttpService")
local RigidBody = {}
RigidBody.__index = RigidBody
-- [PRIVATE]
-- This method is used to fetch the positions of the 4 corners of UI element.
local function GetCorners(frame: GuiObject, engine)
local pos, size = frame.AbsolutePosition, frame.AbsoluteSize
local rotation = math.rad(frame.Rotation)
local center = pos + size/2
local temp = math.sqrt((size.X/2)^2+(size.Y/2)^2)
local offset = (engine.path and false) and Globals.offset or Vector2.new(0, 0)
-- Calculate and return all 4 corners of the GuiObject
-- Also adheres to the Rotation of the GuiObject
local t = math.atan2(size.Y, size.X)
local a = rotation + t
local b = rotation - t
return {
center - temp * Vector2.new(math.cos(a), math.sin(a)) + offset, -- topleft
center + temp * Vector2.new(math.cos(b), math.sin(b)) + offset, -- topright
center - temp * Vector2.new(math.cos(b), math.sin(b)) + offset, -- bottomleft
center + temp * Vector2.new(math.cos(a), math.sin(a)) + offset, -- bottomright
}
end
-- This method is used to calculate the depth/penetration of a collision
local function CalculatePenetration(minA: number, maxA: number, minB: number, maxB: number) : number
if minA < minB then
return minB - maxA
else
return minA - maxB
end
end
local function CalculateOffset(pos, anchorPoint, size)
return (Vector2.new(.5, .5) - anchorPoint) * size
end
-- This method is used to calculate the center position of a UI element
local function CalculateCenter(vertices) : Vector2
local center = Vector2.new(0, 0)
local minX = math.huge
local minY = math.huge
local maxX = -math.huge
local maxY = -math.huge
for _, v in ipairs(vertices) do
center += v.pos
minX = math.min(minX, v.pos.x)
minY = math.min(minY, v.pos.y)
maxX = math.max(maxX, v.pos.x)
maxY = math.max(maxY, v.pos.y)
end
center /= #vertices
return center
end
-- Used to calculate the AbsoluteSize for custom RigidBodies
local function CalculateSize(vertices)
local minX = math.huge
local minY = math.huge
local maxX = -math.huge
local maxY = -math.huge
for _, v in ipairs(vertices) do
minX = math.min(minX, v.pos.x)
minY = math.min(minY, v.pos.y)
maxX = math.max(maxX, v.pos.x)
maxY = math.max(maxY, v.pos.y)
end
return Vector2.new(maxX - minX, maxY - minY)
end
local function CreateRotationCache(cache, center, vertices)
table.clear(cache)
for _, p in ipairs(vertices) do
local r = (p.pos - center).Magnitude
local theta = math.atan2(p.pos.Y - center.Y, p.pos.X - center.X)
table.insert(cache, { r, theta })
end
end
-- This method is used to update the positions of each point of a rigidbody to the corners of a UI element.
local function UpdateVertices(frame: GuiObject, vertices, engine)
local corners = GetCorners(frame, engine)
for i, vertex in ipairs(vertices) do
vertex:SetPosition(corners[i].X, corners[i].Y)
end
end
-- [PUBLIC]
-- This method is used to initialize a new RigidBody.
function RigidBody.new(frame: GuiObject?, m: number, collidable: boolean?, anchored: boolean?, engine, custom: Types.Custom?, structure)
local isCustom = false
if custom then
isCustom = true
end
local vertices = isCustom and custom.Vertices or {}
local edges = isCustom and custom.Edges or {}
-- Configurations
local pointConfig = {
snap = anchored,
selectable = false,
render = false,
keepInCanvas = true
}
local constraintConfig = {
restLength = nil,
render = false,
thickness = 4,
support = false,
TYPE = "ROD"
}
-- Point creation method
local function addPoint(pos)
local newPoint = Point.new(pos, engine.canvas, engine, pointConfig)
vertices[#vertices + 1] = newPoint
return newPoint
end
-- Constraint creation method
local function addConstraint(p1, p2, support)
constraintConfig.support = support
local newConstraint = Constraint.new(p1, p2, engine.canvas, constraintConfig)
edges[#edges + 1] = newConstraint
return newConstraint
end
if not isCustom then
-- Create Points
local corners = GetCorners(frame, engine)
local topleft = addPoint(corners[1])
local topright = addPoint(corners[2])
local bottomleft = addPoint(corners[3])
local bottomright = addPoint(corners[4])
-- Connect points with constraints
addConstraint(topleft, topright, false)
addConstraint(topleft, bottomleft, false)
addConstraint(topright, bottomright, false)
addConstraint(bottomleft, bottomright, false)
addConstraint(topleft, bottomright, true)
addConstraint(topright, bottomleft, true)
end
local self = setmetatable({
id = HttpService:GenerateGUID(false),
custom = isCustom,
_janitor = Janitor.new(), structure = structure,
vertices = vertices,
edges = edges,
frame = isCustom and nil or frame,
size = isCustom and CalculateSize(vertices) or nil,
anchored = anchored,
mass = m,
collidable = collidable,
canRotate = true,
rotationCache = {},
center = isCustom and CalculateCenter(vertices) or frame.AbsolutePosition + frame.AbsoluteSize/2,
engine = engine,
spawnedAt = os.clock(),
lifeSpan = nil,
anchorRotation = (anchored and not isCustom) and frame.Rotation or nil,
anchorPos = (anchored and not isCustom) and frame.AbsolutePosition + frame.AbsoluteSize/2 or nil,
Touched = nil,
TouchEnded = nil,
CanvasEdgeTouched = nil,
Collisions = {
Body = false,
CanvasEdge = false,
Other = {}
},
States = {},
filtered = {},
}, RigidBody)
-- Apply offsets if ScreenGui's IgnoreGuiInset property is set to true
-- Offset = Vector2.new(0, 36)
if engine.path and false then
self.anchorPos = self.anchorPos and self.anchorPos + Globals.offset or nil
if not self.custom then
self.center += Globals.offset
end
end
if #self.rotationCache < 1 then
CreateRotationCache(self.rotationCache, self.center, self.vertices)
end
-- Create events
self.Touched = Signal.new()
self.TouchEnded = Signal.new()
self.CanvasEdgeTouched = Signal.new()
-- Set parents of points and constraints
for _, edge in ipairs(edges) do
edge.Parent = self
edge._janitor:Add(edge.Parent, "Destroy")
self._janitor:Add(edge, "Destroy")
end
self._janitor:Add(self.Touched, "Destroy")
self._janitor:Add(self.TouchEnded, "Destroy")
self._janitor:Add(self.CanvasEdgeTouched, "Destroy")
if not self.custom then
self._janitor:Add(self.frame, "Destroy")
self._janitor:LinkToInstance(self.frame)
end
return self
end
-- This method projects the RigidBody on an axis. Used for collision detection.
function RigidBody:CreateProjection(Axis: Vector2, Min: number, Max: number) : (number, number)
local DotP = Axis:Dot(self.vertices[1].pos)
Min, Max = DotP, DotP
for _, v in ipairs(self.vertices) do
DotP = Axis:Dot(v.pos)
Min = math.min(DotP, Min)
Max = math.max(DotP, Max)
end
return Min, Max
end
-- This method detects collision between two RigidBodies.
function RigidBody:DetectCollision(other)
if not self.custom and (not self.frame and not other.frame) then
return { false, {} }
end
-- Calculate center of the Body
self.center = CalculateCenter(self.vertices)
-- Initialize collision information
local minDist = math.huge
local collision: Types.Collision = {
axis = nil,
depth = nil,
edge = nil,
vertex = nil
}
-- Loop throught both bodies' edges (excluding support edges)
-- Calculate an axis and then project both bodies to the axis
-- Assign axis and edge of collision to the collision information dictionary
-- Calculate the penetration/depth of the collision
-- Find the vertex that collided with the edge
-- If a collision took place, return the collision information
for i = 1, #self.edges + #other.edges, 1 do
local edge = i <= #self.edges and self.edges[i] or other.edges[i - #self.edges]
if not edge.support then
local axis = Vector2.new(
edge.point1.pos.Y - edge.point2.pos.Y,
edge.point2.pos.X - edge.point1.pos.X
).Unit
local MinA, MinB, MaxA, MaxB
MinA, MaxA = self:CreateProjection(axis, MinA, MaxA)
MinB, MaxB = other:CreateProjection(axis, MinB, MaxB)
local dist = CalculatePenetration(MinA, MaxA, MinB, MaxB)
if dist > 0 then
return { false, {} }
elseif math.abs(dist) < minDist then
minDist = math.abs(dist)
collision.axis = axis
collision.edge = edge
end
end
end
collision.depth = minDist
if collision.edge and collision.edge.Parent ~= other then
local Temp = other
other = self
self = Temp
end
local centerDif = self.center - other.center
local dot = collision.axis:Dot(centerDif)
if dot < 0 then
collision.axis *= -1
end
local minMag = math.huge
for i = 1, #self.vertices, 1 do
local dif = self.vertices[i].pos - other.center
local dist = collision.axis:Dot(dif)
if dist < minMag then
minMag = dist
collision.vertex = self.vertices[i]
end
end
return { true, collision }
end
-- This method is used to apply an external force on the rigid body.
function RigidBody:ApplyForce(force: Vector2, t: number)
throwTypeError("force", force, 1, "Vector2")
if t then
throwTypeError("time", t, 2, "number")
if t <= 0 then
throwException("error", "INVALID_TIME")
end
end
for _, v in ipairs(self.vertices) do
v:ApplyForce(force, t)
end
end
-- This method updates the positions of the RigidBody's points and constraints.
function RigidBody:Update(dt: number)
self.center = CalculateCenter(self.vertices)
for i, vertex in ipairs(self.vertices) do
if not self.canRotate then
local info = self.rotationCache[i]
local r = info[1]
local t = info[2]
vertex:ApplyForce((self.center + Vector2.new(math.cos(t), math.sin(t)) * r) - vertex.pos)
end
vertex:Update(dt)
vertex:Render()
end
for _, edge in ipairs(self.edges) do
for i = 1, self.engine.iterations.constraint do
edge:Constrain()
end
edge:Render()
end
end
-- This method updates the positions and appearance of the RigidBody on screen.
function RigidBody:Render()
-- If the RigidBody exceeds its life span, it is destroyed.
if self.lifeSpan and os.clock() - self.spawnedAt >= self.lifeSpan then
self:Destroy()
end
if self.custom then return end
-- Apply rotations and update positions
-- Respects the anchor point of the GuiObject
if self.anchored then
local anchorPos = self.anchorPos - CalculateOffset(self.anchorPos, self.frame.AnchorPoint, self.frame.AbsoluteSize)
self.frame.Position = UDim2.fromOffset(anchorPos.X, anchorPos.Y)
if self.canRotate then
self:Rotate(self.anchorRotation)
end
else
local center = self.center - CalculateOffset(self.center, self.frame.AnchorPoint, self.frame.AbsoluteSize)
local dif: Vector2 = self.vertices[2].pos - self.vertices[1].pos
self.frame.Position = UDim2.new(0, center.X, 0, center.Y)
if self.canRotate then
self.frame.Rotation = math.deg(math.atan2(dif.Y, dif.X))
end
end
end
-- This method is used to clone the RigidBody while keeping the original one intact.
function RigidBody:Clone(deepCopy: boolean)
if not self.custom and not self.frame then return end
if not self.engine then return end
local frame
if not self.custom then
frame = self.frame:Clone()
frame.Parent = self.frame.Parent
end
local copy = self.engine:Create("RigidBody", {
Mass = self.mass,
Object = frame,
Structure = self.custom and self.structure or nil,
Anchored = self.anchored,
Collidable = self.collidable
})
-- Copy lifespan, states and filtered RigidBodies
if deepCopy == true then
copy.States = self.States
if self.lifeSpan then
copy:SetLifeSpan(self.lifeSpan)
end
for _, body in ipairs(self.filtered) do
copy:FilterCollisionsWith(body)
end
end
return copy
end
-- This method is used to destroy the RigidBody.
-- The body's UI element is destroyed, its connections are disconnected and the body is removed from the engine.
function RigidBody:Destroy(keepFrame: boolean)
self._janitor:Cleanup()
for i, body in ipairs(self.engine.bodies) do
if self.id == body.id then
table.clear(self.Collisions.Other)
table.remove(self.engine.bodies, i)
self.engine.ObjectRemoved:Fire(self)
break
end
end
table.clear(self.vertices)
table.clear(self.edges)
end
-- This method is used to rotate the RigidBody's UI element.
-- After rotation the positions of its points and constraints are automatically updated.
function RigidBody:Rotate(newRotation: number)
throwTypeError("newRotation", newRotation, 1, "number")
-- Update anchorRotation if the body is anchored
if self.anchored and self.anchorRotation then
self.anchorRotation = newRotation
end
-- Apply rotation and update positions
-- Update the RigidBody's points
local oldRotation
if self.custom then
-- Will need to cache oldRotation somewhere.
-- This method will result in weird oldRotations for some custom rigid bodies.
local dif = self.vertices[2].pos - self.vertices[1].pos
oldRotation = math.deg(math.atan2(dif.Y, dif.X))
local tempRotationCache = {}
CreateRotationCache(tempRotationCache, self.center, self.vertices)
for i, info in ipairs(tempRotationCache) do
local r = info[1]
local t = info[2] + math.rad(newRotation)
local v = self.vertices[i]
v.pos = self.center + Vector2.new(math.cos(t), math.sin(t)) * r
v.oldPos = v.pos
end
else
oldRotation = self.frame.Rotation
local offset = CalculateOffset(self.anchorPos, self.frame.AnchorPoint, self.frame.AbsoluteSize)
local position = self.anchorPos - offset
self.frame.Position = self.anchored and UDim2.fromOffset(position.X, position.Y) or UDim2.fromOffset(self.center.x, self.center.y)
self.frame.Rotation = newRotation
UpdateVertices(self.frame, self.vertices, self.engine)
end
return oldRotation, newRotation
end
-- This method is used to set a new position of the RigidBody's UI element.
function RigidBody:SetPosition(PositionX: number, PositionY: number)
--restrict(self.custom)
throwTypeError("PositionX", PositionX, 1, "number")
throwTypeError("PositionY", PositionY, 2, "number")
-- Update anchorPos if the body is anchored
if self.anchored and self.anchorPos then
self.anchorPos = Vector2.new(PositionX, PositionY)
end
local oldPosition
-- Update position
-- Update the RigidBody's points
if self.custom then
oldPosition = UDim2.fromOffset(self.center.X, self.center.Y)
local tempRotationCache = {}
CreateRotationCache(tempRotationCache, self.center, self.vertices)
self.center = Vector2.new(PositionX, PositionY)
for i, info in ipairs(tempRotationCache) do
local r = info[1]
local t = info[2]
local v = self.vertices[i]
v.pos = self.center + Vector2.new(math.cos(t), math.sin(t)) * r
v.oldPos = v.pos
end
else
oldPosition = self.frame.Position
self.frame.Position = UDim2.fromOffset(PositionX, PositionY)
UpdateVertices(self.frame, self.vertices, self.engine)
end
return oldPosition, UDim2.fromOffset(PositionX, PositionY)
end
-- This method is used to set a new size of the RigidBody's UI element.
function RigidBody:SetSize(SizeX: number, SizeY: number)
restrict(self.custom)
throwTypeError("SizeX", SizeX, 1, "number")
throwTypeError("SizeY", SizeY, 2, "number")
-- Update size
-- Update the RigidBody's points
local oldSize = self.frame.Size
self.frame.Size = UDim2.fromOffset(SizeX, SizeY)
UpdateVertices(self.frame, self.vertices, self.engine)
for _, edge in ipairs(self.edges) do
edge.restLength = (edge.point2.pos - edge.point1.pos).Magnitude
end
return oldSize, UDim2.fromOffset(SizeX, SizeY)
end
function RigidBody:SetScale(scale: number)
if not self.custom then return end
throwTypeError("scale", scale, 1, "number")
scale = math.max(0.00001, scale)
for i, info in ipairs(self.rotationCache) do
local r = info[1] * scale
local t = info[2]
local v = self.vertices[i]
v.pos = self.center + Vector2.new(math.cos(t), math.sin(t)) * r
v.oldPos = v.pos
end
for _, edge in ipairs(self.edges) do
edge.restLength = (edge.point2.pos - edge.point1.pos).Magnitude
end
end
-- This method is used to anchor the RigidBody.
-- Its position will no longer change.
function RigidBody:Anchor()
self.anchored = true
self.anchorRotation = self.frame and self.frame.Rotation or nil
self.anchorPos = self.center
for _, vertex in ipairs(self.vertices) do
if not vertex.selectable then vertex.snap = self.anchored end
end
end
-- This method is used to unachor and anchored RigidBody.
function RigidBody:Unanchor()
self.anchored = false
self.anchorRotation = nil
self.anchorPos = nil
for _, vertex in ipairs(self.vertices) do
if not vertex.selectable then vertex.snap = self.anchored end
end
end
-- This method is used to determine whether the RigidBody will collide with other RigidBodies.
function RigidBody:CanCollide(collidable: boolean)
throwTypeError("collidable", collidable, 1, "boolean")
self.collidable = collidable
end
function RigidBody:CanRotate(canRotate: boolean)
restrict(self.custom)
throwTypeError("canRotate", canRotate, 1, "boolean")
self.canRotate = canRotate
CreateRotationCache(self.rotationCache, self.center, self.vertices)
end
-- The RigidBody's UI Element can be fetched using this method.
function RigidBody:GetFrame() : GuiObject
return self.frame
end
-- The RigidBody's unique ID can be fetched using this method.
function RigidBody:GetId() : string
return self.id
end
-- The RigidBody's Points can be fetched using this method.
function RigidBody:GetVertices()
return self.vertices
end
-- The RigidBody's Constraints can be fetched using this method.
function RigidBody:GetConstraints()
return self.edges
end
--vThis method is used to set the RigidBody's life span.
-- Life span is determined by 'seconds'.
-- After this time in seconds has been passed after the RigidBody is created, the RigidBody is automatically destroyed and removed from the engine.
function RigidBody:SetLifeSpan(seconds: number)
throwTypeError("seconds", seconds, 1, "number")
self.lifeSpan = seconds
end
-- This method determines if the RigidBody stays inside the engine's canvas at all times.
function RigidBody:KeepInCanvas(keepInCanvas: boolean)
throwTypeError("keepInCanvas", keepInCanvas, 1, "boolean")
for _, p in ipairs(self.vertices) do
p.keepInCanvas = keepInCanvas
end
end
-- This method sets a custom frictional damp value just for the RigidBody.
function RigidBody:SetFriction(friction: number)
throwTypeError("friction", friction, 1, "number")
for _, p in ipairs(self.vertices) do
p.friction = math.clamp(1 - friction, 0, 1)
end
end
-- This method sets a custom air frictional damp value just for the RigidBody.
function RigidBody:SetAirFriction(friction: number)
throwTypeError("friction", friction, 1, "number")
for _, p in ipairs(self.vertices) do
p.airfriction = math.clamp(1 - friction, 0, 1)
end
end
-- This method sets a custom gravitational force just for the RigidBody.
function RigidBody:SetGravity(force: Vector2)
throwTypeError("force", force, 1, "Vector2")
for _, p in ipairs(self.vertices) do
p.gravity = force
end
end
-- Sets a new mass for the RigidBody
function RigidBody:SetMass(mass: number)
if self.mass ~= mass and mass >= 1 then
self.mass = mass
end
end
-- Returns true if the RigidBody lies within the boundaries of the canvas, else false.
function RigidBody:IsInBounds() : boolean
local canvas = self.engine.canvas
if not canvas then return false end
-- Check if all vertices lie within the canvas.
for _, v in ipairs(self.vertices) do
local pos = v.pos
if not ((pos.X >= canvas.topLeft.X and pos.X <= canvas.topLeft.X + canvas.size.X) and (pos.Y >= canvas.topLeft.Y and pos.Y <= canvas.topLeft.Y + canvas.size.Y)) then
return false
end
end
return true
end
-- Returns the average of all the velocities of the RigidBody's points
function RigidBody:AverageVelocity() : Vector2
local sum = Vector2.new(0, 0)
for _, v in ipairs(self.vertices) do
sum += v:Velocity()
end
-- Return average
return sum/#self.vertices
end
-- STATE MANAGEMENT
-- Used to initialize or update states of a RigidBody
function RigidBody:SetState(state: string, value: any)
throwTypeError("state", state, 1, "string")
if self.States[state] == value then return end
self.States[state] = value
end
-- Used to fetch an already existing state
function RigidBody:GetState(state: string) : any
throwTypeError("state", state, 1, "string")
return self.States[state]
end
-- Used to fetch the center position of the RigidBody
function RigidBody:GetCenter()
return self.center
end
-- Used to ignore/filter any collisions with the other RigidBody.
function RigidBody:FilterCollisionsWith(otherBody)
if not otherBody.id or not typeof(otherBody.id) == "string" or not otherBody.filtered then
throwException("error", "INVALID_RIGIDBODY")
end
if otherBody.id == self.id then throwException("error", "SAME_ID") end
-- Insert the ids into their respective places
if not table.find(self.filtered, otherBody.id) then
table.insert(self.filtered, otherBody.id)
table.insert(otherBody.filtered, self.id)
end
end
-- Used to unfilter collisions with the other RigidBody.
-- The two bodies will now collide with each other.
function RigidBody:UnfilterCollisionsWith(otherBody)
if not otherBody.id or not typeof(otherBody.id) == "string" or not otherBody.filtered then
throwException("error", "INVALID_RIGIDBODY")
end
if otherBody.id == self.id then throwException("error", "SAME_ID") end
local i1 = table.find(self.filtered, otherBody.id)
local i2 = table.find(otherBody.filtered, self.id)
-- Remove the ids from their respective places
if i1 and i2 then
table.remove(self.filtered, i1)
table.remove(otherBody.filtered, i2)
end
end
-- Returns all filtered RigidBodies.
function RigidBody:GetFilteredRigidBodies()
return self.filtered
end
-- Returns an array of all RigidBodies that are in collision with the current
function RigidBody:GetTouchingRigidBodies()
return self.Collisions.Other
end
-- Determines the max force that can be aoplied to the RigidBody.
function RigidBody:SetMaxForce(maxForce: number)
throwTypeError("maxForce", maxForce, 1, "number")
for _, p in ipairs(self.vertices) do
p:SetMaxForce(maxForce)
end
end
return RigidBody
end,
function(script,require)
-- Points are what make the rigid bodies behave like real world entities.
-- Points are responsible for the movement of the RigidBodies and Constraints.
-- Services and utilities
local Globals = require(script.Parent.Parent.Constants.Globals)
local Types = require(script.Parent.Parent.Types)
local throwTypeError = require(script.Parent.Parent.Debugging.TypeErrors)
local throwException = require(script.Parent.Parent.Debugging.Exceptions)
local Janitor = require(script.Parent.Parent.Utilities.Janitor)
local HttpService = game:GetService("HttpService")
local Point = {}
Point.__index = Point
-- This method is used to initialize a new Point.
function Point.new(pos: Vector2, canvas: Types.Canvas, engine: Types.EngineConfig, config: Types.PointConfig, parent)
local self = setmetatable({
id = HttpService:GenerateGUID(false),
Parent = parent,
frame = nil,
_janitor = nil,
engine = engine,
canvas = canvas,
oldPos = pos,
pos = pos,
oldForces = Vector2.new(),
forces = Vector2.new(),
maxForce = nil,
gravity = engine.gravity,
friction = engine.friction,
airfriction = engine.airfriction,
bounce = engine.bounce,
snap = config.snap,
selectable = config.selectable,
render = config.render,
keepInCanvas = config.keepInCanvas,
color = nil,
radius = Globals.point.radius,
timed = {
start = nil,
t = nil,
force = Vector2.new()
}
}, Point)
local janitor = Janitor.new()
janitor:Add(self, "Destroy")
if self.Parent then
janitor:Add(self.Parent, "Destroy")
end
self._janitor = janitor
return self
end
-- This method is used to apply a force to the Point.
function Point:ApplyForce(force: Vector2, t: number)
throwTypeError("force", force, 1, "Vector2")
self.forces += force
if t then
throwTypeError("time", t, 2, "number")
if t <= 0 then
throwException("error", "INVALID_TIME")
end
self.timed.start = os.clock()
self.timed.t = t
self.timed.force = force
end
end
-- This method is used to apply external forces like gravity and is responsible for moving the point.
function Point:Update(dt: number)
if not self.snap then
self:ApplyForce(self.gravity)
if self.timed.start then
if os.clock() - self.timed.start < self.timed.t then
self:ApplyForce(self.timed.force)
else
self.timed.start = nil
self.timed.t = nil
self.timed.force = Vector2.new()
end
end
-- Calculate velocity
local velocity = self.pos
velocity -= self.oldPos
velocity += self.forces
local body = self.Parent
-- Apply friction
if body and body.Parent then
local mass = body.Parent.mass
if mass then
self.forces /= mass
end
if body.Parent.Collisions.CanvasEdge or body.Parent.Collisions.Body then
velocity *= self.friction
else
velocity *= self.airfriction
end
else
velocity *= self.friction
end
-- clamp velocity
if self.maxForce then
velocity = velocity.Unit * math.min(velocity.Magnitude, self.maxForce)
end
-- Update point positions
self.oldPos = self.pos
self.pos += velocity
self.oldForces = self.forces
self.forces *= 0
end
end
-- This method is used to keep the point in the engine's canvas.
-- Any point that goes past the canvas, is positioned correctly and the direction of its flipped is reversed accordingly.
function Point:KeepInCanvas()
-- vx = velocity.X
-- vy = velocity.Y
local vx = self.pos.X - self.oldPos.X
local vy = self.pos.Y - self.oldPos.Y
local boundX = self.canvas.topLeft.X + self.canvas.size.X
local boundY = self.canvas.topLeft.Y + self.canvas.size.Y
local collision = false
local edge
if self.pos.Y > boundY then
self.pos = Vector2.new(self.pos.X, boundY)
self.oldPos = Vector2.new(self.oldPos.X, self.pos.Y + vy * self.bounce)
collision = true
edge = "Bottom"
elseif self.pos.Y < self.canvas.topLeft.Y then
self.pos = Vector2.new(self.pos.X, self.canvas.topLeft.Y)
self.oldPos = Vector2.new(self.oldPos.X, self.pos.Y - vy * self.bounce)
collision = true
edge = "Top"
end
if self.pos.X < self.canvas.topLeft.X then
self.pos = Vector2.new(self.canvas.topLeft.X, self.pos.Y)
self.oldPos = Vector2.new(self.pos.X + vx * self.bounce, self.oldPos.Y)
collision = true
edge = "Left"
elseif self.pos.X > boundX then
self.pos = Vector2.new(boundX, self.pos.Y)
self.oldPos = Vector2.new(self.pos.X - vx * self.bounce, self.oldPos.Y)
collision = true
edge = "Right"
end
local body = self.Parent
-- Fire CanvasEdgeTouched event
if body and body.Parent then
if collision then
local prev = body.Parent.Collisions.CanvasEdge
body.Parent.Collisions.CanvasEdge = true
if prev == false then
body.Parent.CanvasEdgeTouched:Fire(edge)
end
else
body.Parent.Collisions.CanvasEdge = false
end
end
end
-- This method is used to update the position and appearance of the Point on screen.
function Point:Render()
if self.render then
if not self.canvas.frame then
throwException("error", "CANVAS_FRAME_NOT_FOUND")
end
if not self.frame then
-- Create new instance for the point
local p = Instance.new("Frame")
local border = Instance.new("UICorner")
local r = self.radius or Globals.point.radius
p.AnchorPoint = Vector2.new(.5, .5)
p.BackgroundColor3 = self.color or Globals.point.color
p.Size = UDim2.new(0, r * 2, 0, r * 2)
p.Parent = self.canvas.frame
border.CornerRadius = Globals.point.uicRadius
border.Parent = p
self.frame = p
self._janitor:Add(self.frame, "Destroy")
end
-- Update the point's instance
self.frame.Position = UDim2.new(0, self.pos.x, 0, self.pos.y)
end
if self.keepInCanvas then
self:KeepInCanvas()
end
end
function Point:Destroy()
self._janitor:Cleanup()
if not self.Parent then
for i, c in ipairs(self.engine.points) do
if c.id == self.id then
table.remove(self.engine.points, i)
self.engine.ObjectRemoved:Fire(self)
break
end
end
end
end
-- This method is used to determine the radius of the point.
function Point:SetRadius(radius: number)
throwTypeError("radius", radius, 1, "number")
self.radius = radius
end
-- his method is used to determine the color of the point on screen.
-- By default this is set to (RED) Color3.new(1, 0, 0).
function Point:Stroke(color: Color3)
throwTypeError("color", color, 1, "Color3")
self.color = color
end
-- This method determines if the point remains anchored.
-- If set to false, the point is unanchored.
function Point:Snap(snap: boolean)
throwTypeError("snap", snap, 1, "boolean")
self.snap = snap
end
-- Returns the velocity of the Point
function Point:Velocity() : Vector2
return self.pos - self.oldPos
end
function Point:GetNetForce() : Vector2
return self.oldForces
end
-- Returns the Parent (Constraint) of the Point if any.
function Point:GetParent()
return self.Parent
end
-- Used to set a new position for the point
function Point:SetPosition(x: number, y: number)
throwTypeError("x", x, 1, "number")
throwTypeError("y", y, 2, "number")
local newPosition = Vector2.new(x, y)
self.oldPos = newPosition
self.pos = newPosition
end
-- Determines the max force that can be aoplied to the Point.
function Point:SetMaxForce(maxForce: number)
throwTypeError("maxForce", maxForce, 1, "number")
self.maxForce = math.abs(maxForce)
end
return Point
end,
function(script,require)
-- Constraints keep two points together in place and maintain uniform distance between the two.
-- Constraints and Points together join to keep a RigidBody in place hence making both Points and Constraints a vital part of the library.
-- Custom constraints such as Ropes, Rods, Bridges and chains can also be made.
-- Points of two rigid bodies can be connected with constraints, two individual points can also be connected with constraints to form Ropes etc.
-- Services and utilities
local line = require(script.Parent.Parent.Utilities.Line)
local Globals = require(script.Parent.Parent.Constants.Globals)
local throwTypeError = require(script.Parent.Parent.Debugging.TypeErrors)
local throwException = require(script.Parent.Parent.Debugging.Exceptions)
local Janitor = require(script.Parent.Parent.Utilities.Janitor)
local Types = require(script.Parent.Parent.Types)
local https = game:GetService("HttpService")
local Constraint = {}
Constraint.__index = Constraint
-- This method is used to initialize a constraint.
function Constraint.new(p1: Types.Point, p2: Types.Point, canvas: Types.Canvas, config: Types.SegmentConfig, engine, parent)
local self = setmetatable({
id = https:GenerateGUID(false),
_janitor = nil,
engine = engine,
Parent = parent,
frame = nil,
canvas = canvas,
point1 = p1,
point2 = p2,
restLength = config.restLength or (p2.pos - p1.pos).Magnitude,
render = config.render,
thickness = config.thickness or Globals.constraint.thickness,
support = config.support,
_TYPE = config.TYPE,
k = 0.1,
color = nil,
}, Constraint)
local janitor = Janitor.new()
janitor:Add(self, "Destroy")
janitor:Add(self.point1, "Destroy")
janitor:Add(self.point2, "Destroy")
if self.Parent then
janitor:Add(self.Parent, "Destroy")
end
self._janitor = janitor
self.point1.Parent = self
self.point2.Parent = self
self.point1._janitor:Add(self.point1.Parent, "Destroy")
self.point2._janitor:Add(self.point2.Parent, "Destroy")
return self
end
-- This method is used to keep uniform distance between the constraint's points, i.e. constrain.
function Constraint:Constrain()
local cur = (self.point2.pos - self.point1.pos).Magnitude
local force
-- Validate constraint types
if self._TYPE == "ROPE" then
local restLength = self.restLength
if cur < self.thickness then
restLength = self.thickness
end
if cur > self.restLength or self.restLength < self.thickness then
-- Solve rope constraint force
local offset = ((restLength - cur)/restLength)/2
force = self.point2.pos - self.point1.pos
force *= offset
end
elseif self._TYPE == "ROD" then
-- Solve rod constraint force
local offset = self.restLength - cur
local dif = self.point2.pos - self.point1.pos
dif = dif.Unit
force = (dif * offset)/2
elseif self._TYPE == "SPRING" then
-- Solve spring constraint force
force = self.point2.pos - self.point1.pos
local mag = force.Magnitude - self.restLength
force = force.Unit
force *= -1 * self.k * mag
else
return
end
-- Apply forces to constraint's points
if force then
if not self.point1.snap then self.point1.pos -= force end
if not self.point2.snap then self.point2.pos += force end
end
end
-- This method is used to update the position and appearance of the constraint on screen.
function Constraint:Render()
if self.render and not self.support then
if not self.canvas.frame then
throwException("error", "CANVAS_FRAME_NOT_FOUND")
end
local thickness = self.thickness or Globals.constraint.thickness
local color = self.color or Globals.constraint.color
local image = self._TYPE == "SPRING" and "rbxassetid://8404350124" or nil
if not self.frame then
self.frame = line(self.point1.pos, self.point2.pos, self.canvas.frame, thickness, color, nil, image)
self._janitor:Add(self.frame, "Destroy")
end
-- Draw constraint on screen
line(self.point1.pos, self.point2.pos, self.canvas.frame, thickness, color, self.frame, image)
end
end
-- Used to set the minimum constrained distance between two points.
-- By default, the initial distance between the two points.
function Constraint:SetLength(newLength: number)
throwTypeError("length", newLength, 1, "number")
if newLength <= 0 then
throwException("error", "INVALID_CONSTRAINT_LENGTH")
end
self.restLength = newLength
end
-- This method returns the current distance between the two points of a constraint.
function Constraint:GetLength() : number
return (self.point2.pos - self.point1.pos).Magnitude
end
-- This method is used to change the color of a constraint.
-- By default a constraint's color is set to the default value of (WHITE) Color3.new(1, 1, 1).
function Constraint:Stroke(color: Color3)
throwTypeError("color", color, 1, "Color3")
self.color = color
end
-- This method destroys the constraint.
-- Its UI element is no longer rendered on screen and the constraint is removed from the engine.
-- This is irreversible.
function Constraint:Destroy()
self._janitor:Cleanup()
if not self.Parent then
for i, c in ipairs(self.engine.constraints) do
if c.id == self.id then
table.remove(self.engine.constraints, i)
self.engine.ObjectRemoved:Fire(self)
break
end
end
end
self.point1 = nil
self.point2 = nil
end
-- Returns the constraints points.
function Constraint:GetPoints()
return self.point1, self.point2
end
-- Returns the UI element for the constrained IF rendered.
function Constraint:GetFrame() : Frame?
return self.frame
end
-- This method is used to update the Spring constant (by default 0.1) used for spring constraint calculations.
function Constraint:SetSpringConstant(k: number)
throwTypeError("springConstant", k, 1, "number")
self.k = k
end
-- The constraints's unique ID can be fetched using this method.
function Constraint:GetId() : string
return self.id
end
-- Returns the Parent (RigidBody) of the Constraint if any.
function Constraint:GetParent()
return self.Parent
end
return Constraint
end,
function(script,require)
local Types = require(script.Parent.Parent.Types)
local Quadtree = require(script.Parent.Parent.Utilities.Quadtree)
-- Search and return an element from a table using a lambda function
local function SearchTable(t: { any }, a: any, lambda: (a: any, b: any) -> boolean) : any
for _, v in ipairs(t) do
if lambda(a, v) then
return v
end
end
return nil
end
local Runner = {}
-- This method is responsible for separating two rigidbodies if they collide with each other.
function Runner.CollisionResponse(body: Types.RigidBody, other: Types.RigidBody, isColliding: boolean, Collision: Types.Collision, dt: number, oldCollidingWith, iteration: number)
if not isColliding then return end
-- Fire the touched event
if iteration == 1 and body.Touched._handlerListHead and body.Touched._handlerListHead.Connected then
if not SearchTable(oldCollidingWith, other, function(a, b) return a.id == b.id end) then
body.Touched:Fire(other.id, Collision)
end
end
-- Calculate penetration in 2 dimensions
local penetration: Vector2 = Collision.axis * Collision.depth
local p1: Types.Point = Collision.edge.point1
local p2: Types.Point = Collision.edge.point2
-- Calculate a t alpha value
local t
if math.abs(p1.pos.X - p2.pos.X) > math.abs(p1.pos.Y - p2.pos.Y) then
t = (Collision.vertex.pos.X - penetration.X - p1.pos.X)/(p2.pos.X - p1.pos.X)
else
t = (Collision.vertex.pos.Y - penetration.Y - p1.pos.Y)/(p2.pos.Y - p1.pos.Y)
end
-- Create a lambda
local factor: number = 1 / (t^2 + (1 - t)^2)
-- Calculate masses
local bodyMass = Collision.edge.Parent.mass
local m = t * bodyMass + (1 - t) * bodyMass
local cMass = 1 / (m + Collision.vertex.Parent.Parent.mass)
-- Calculate ratios of collision effects
local r1 = Collision.vertex.Parent.Parent.mass * cMass
local r2 = m * cMass
-- If the body is not anchored, apply forces to the constraint
if not Collision.edge.Parent.anchored then
p1.pos -= penetration * ((1 - t) * factor * r1)
p2.pos -= penetration * (t * factor * r1)
end
-- If the body is not anchored, apply forces to the point
if not Collision.vertex.Parent.Parent.anchored then
Collision.vertex.pos += penetration * r2
end
end
function Runner.Update(self, dt)
local tree;
-- Create a quadtree and insert bodies if neccesary
if self.quadtrees then
tree = Quadtree.new(self.canvas.topLeft, self.canvas.size, 4)
for _, body in ipairs(self.bodies) do
if body.collidable then
tree:Insert(body)
end
end
else
if self.iterations.collision ~= 1 then
self.iterations.collision = 1
end
end
-- Loop through each body
-- Update the body
-- Calculate the closest RigidBodies to a given body if neccesary
for _, body in ipairs(self.bodies) do
body:Update(dt)
local OldCollidingWith = body.Collisions.Other
local CollidingWith = {}
if body.collidable then
local filtered = self.bodies
if self.quadtrees then
local abs = body.custom and body.size or body.frame.AbsoluteSize
local side = abs.X > abs.Y and abs.X or abs.Y
local range = {
position = body.center - Vector2.new(side * 1.5, side * 1.5),
size = Vector2.new(side * 3, side * 3)
}
filtered = tree:Search(range, {})
end
-- Loop through the filtered RigidBodies
-- Detect collisions
-- Process collision response
for _, other in ipairs(filtered) do
if body.id ~= other.id and other.collidable and not table.find(body.filtered, other.id) then
local result, isColliding, Collision, didCollide
for i = 1, self.iterations.collision do
result = body:DetectCollision(other)
isColliding = result[1]
Collision = result[2]
if i == 1 and not isColliding then
break
end
didCollide = true
Runner.CollisionResponse(body, other, isColliding, Collision, dt, OldCollidingWith, i)
end
if didCollide then
body.Collisions.Body = true
other.Collisions.Body = true
table.insert(CollidingWith, other)
else
body.Collisions.Body = false
other.Collisions.Body = false
-- Fire TouchEnded event
if body.TouchEnded._handlerListHead and body.TouchEnded._handlerListHead.Connected then
if SearchTable(OldCollidingWith, other, function (a, b) return a.id == b.id end) then
body.TouchEnded:Fire(other.id)
end
end
end
end
end
end
body.Collisions.Other = CollidingWith
end
if #self.points > 0 then
for _, point in ipairs(self.points) do
point:Update(dt)
end
end
if #self.constraints > 0 then
for _, constraint in ipairs(self.constraints) do
if constraint._TYPE ~= "SPRING" then
for i = 1, self.iterations.constraint do
constraint:Constrain()
end
else
constraint:Constrain()
end
end
end
self.Updated:Fire()
end
function Runner.Render(self)
for _, body in ipairs(self.bodies) do
body:Render()
end
for _, point in ipairs(self.points) do
point:Render()
end
for _, constraint in ipairs(self.constraints) do
constraint:Render()
end
end
return Runner
end,
function(script,require)
-- -----------------------------------------------------------------------------
-- Batched Yield-Safe Signal Implementation --
-- This is a Signal class which has effectively identical behavior to a --
-- normal RBXScriptSignal, with the only difference being a couple extra --
-- stack frames at the bottom of the stack trace when an error is thrown. --
-- This implementation caches runner coroutines, so the ability to yield in --
-- the signal handlers comes at minimal extra cost over a naive signal --
-- implementation that either always or never spawns a thread. --
-- --
-- API: --
-- local Signal = require(THIS MODULE) --
-- local sig = Signal.new() --
-- local connection = sig:Connect(function(arg1, arg2, ...) ... end) --
-- sig:Fire(arg1, arg2, ...) --
-- connection:Disconnect() --
-- sig:DisconnectAll() --
-- local arg1, arg2, ... = sig:Wait() --
-- --
-- Licence: --
-- Licenced under the MIT licence. --
-- --
-- Authors: --
-- stravant - July 31st, 2021 - Created the file. --
-- sleitnick - August 3rd, 2021 - Modified for Knit. --
-- -----------------------------------------------------------------------------
-- The currently idle thread to run the next handler on
local freeRunnerThread = nil
-- Function which acquires the currently idle handler runner thread, runs the
-- function fn on it, and then releases the thread, returning it to being the
-- currently idle one.
-- If there was a currently idle runner thread already, that's okay, that old
-- one will just get thrown and eventually GCed.
local function acquireRunnerThreadAndCallEventHandler(fn, ...)
local acquiredRunnerThread = freeRunnerThread
freeRunnerThread = nil
fn(...)
-- The handler finished running, this runner thread is free again.
freeRunnerThread = acquiredRunnerThread
end
-- Coroutine runner that we create coroutines of. The coroutine can be
-- repeatedly resumed with functions to run followed by the argument to run
-- them with.
local function runEventHandlerInFreeThread(...)
acquireRunnerThreadAndCallEventHandler(...)
while true do
acquireRunnerThreadAndCallEventHandler(coroutine.yield())
end
end
-- Connection class
local Connection = {}
Connection.__index = Connection
function Connection.new(signal, fn)
return setmetatable({
Connected = true,
_signal = signal,
_fn = fn,
_next = false,
}, Connection)
end
function Connection:Disconnect()
if not self.Connected then
return
end
self.Connected = false
-- Unhook the node, but DON'T clear it. That way any fire calls that are
-- currently sitting on this node will be able to iterate forwards off of
-- it, but any subsequent fire calls will not hit it, and it will be GCed
-- when no more fire calls are sitting on it.
if self._signal._handlerListHead == self then
self._signal._handlerListHead = self._next
else
local prev = self._signal._handlerListHead
while prev and prev._next ~= self do
prev = prev._next
end
if prev then
prev._next = self._next
end
end
end
Connection.Destroy = Connection.Disconnect
-- Make Connection strict
setmetatable(Connection, {
__index = function(_tb, key)
if key ~= "_handlerListHead" then
error(("Attempt to get Connection::%s (not a valid member)"):format(tostring(key)), 2)
end
end,
__newindex = function(_tb, key, _value)
error(("Attempt to set Connection::%s (not a valid member)"):format(tostring(key)), 2)
end
})
--[=[
@class Signal
Signals allow events to be dispatched and handled.
For example:
```lua
local signal = Signal.new()
signal:Connect(function(msg)
print("Got message:", msg)
end)
signal:Fire("Hello world!")
```
]=]
local Signal = {}
Signal.__index = Signal
--[=[
Constructs a new Signal
@return Signal
]=]
function Signal.new()
local self = setmetatable({
_handlerListHead = false,
_proxyHandler = nil,
}, Signal)
return self
end
--[=[
Constructs a new Signal that wraps around an RBXScriptSignal.
@param rbxScriptSignal RBXScriptSignal -- Existing RBXScriptSignal to wrap
@return Signal
For example:
```lua
local signal = Signal.Wrap(workspace.ChildAdded)
signal:Connect(function(part) print(part.Name .. " added") end)
Instance.new("Part").Parent = workspace
```
]=]
function Signal.Wrap(rbxScriptSignal)
assert(typeof(rbxScriptSignal) == "RBXScriptSignal", "Argument #1 to Signal.Wrap must be a RBXScriptSignal; got " .. typeof(rbxScriptSignal))
local signal = Signal.new()
signal._proxyHandler = rbxScriptSignal:Connect(function(...)
signal:Fire(...)
end)
return signal
end
--[=[
Checks if the given object is a Signal.
@param obj any -- Object to check
@return boolean -- `true` if the object is a Signal.
]=]
function Signal.Is(obj)
return type(obj) == "table" and getmetatable(obj) == Signal
end
--[=[
Connects a function to the signal, which will be called anytime the signal is fired.
@param fn (...any) -> nil
@return Connection -- A connection to the signal
]=]
function Signal:Connect(fn: (...any) -> ())
local connection = Connection.new(self, fn)
if self._handlerListHead then
connection._next = self._handlerListHead
self._handlerListHead = connection
else
self._handlerListHead = connection
end
return connection
end
function Signal:GetConnections()
local items = {}
local item = self._handlerListHead
while item do
table.insert(items, item)
item = item._next
end
return items
end
--[=[
Disconnects all connections from the signal.
]=]
function Signal:DisconnectAll()
local item = self._handlerListHead
while item do
item.Connected = false
item = item._next
end
self._handlerListHead = false
end
-- Signal:Fire(...) implemented by running the handler functions on the
-- coRunnerThread, and any time the resulting thread yielded without returning
-- to us, that means that it yielded to the Roblox scheduler and has been taken
-- over by Roblox scheduling, meaning we have to make a new coroutine runner.
--[=[
Fire the signal, which will call all of the connected functions with the given arguments.
@param ... any -- Arguments to pass to the connected functions
]=]
function Signal:Fire(...)
local item = self._handlerListHead
while item do
if item.Connected then
if not freeRunnerThread then
freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
end
task.spawn(freeRunnerThread, item._fn, ...)
end
item = item._next
end
end
--[=[
Same as `Fire`, but uses `task.defer` internally & doesn't take advantage of thread reuse.
@param ... any -- Arguments to pass to the connected functions
]=]
function Signal:FireDeferred(...)
local item = self._handlerListHead
while item do
task.defer(item._fn, ...)
item = item._next
end
end
--[=[
Yields the current thread until the signal is fired, and returns the arguments fired from the signal.
@return ... any -- Arguments passed to the signal when it was fired
@yields
]=]
function Signal:Wait(): (...any)
local waitingCoroutine = coroutine.running()
local cn
cn = self:Connect(function(...)
cn:Disconnect()
task.spawn(waitingCoroutine, ...)
end)
return coroutine.yield()
end
--[=[
Cleans up the signal.
]=]
function Signal:Destroy()
self:DisconnectAll()
local proxyHandler = rawget(self, "_proxyHandler")
if proxyHandler then
proxyHandler:Disconnect()
end
end
-- Make signal strict
setmetatable(Signal, {
__index = function(_tb, key)
if key ~= "Connected" then
error(("Attempt to get Signal::%s (not a valid member)"):format(tostring(key)), 2)
end
end,
__newindex = function(_tb, key, _value)
error(("Attempt to set Signal::%s (not a valid member)"):format(tostring(key)), 2)
end
})
return Signal
end,
function(script,require)
-- This utility is used in Collision Detection
-- Quadtree data structure
-- Services and utilities
local Types = require(script.Parent.Parent.Types)
local Quadtree = {}
Quadtree.__index = Quadtree
-- Calculate sub-divisions of a node
local function GetDivisions(position: Vector2, size: Vector2)
return {
position,
position + Vector2.new(size.X/2, 0),
position + Vector2.new(0, size.Y/2),
position + Vector2.new(size.X/2, size.Y/2),
}
end
-- Check if a range overlaps a node of the quadtree
local function RangeOverlapsNode(node: Types.Quadtree<Types.RigidBody>, range: Types.Range) : boolean
local ap1 = range.position
local as1 = range.size
local sum = ap1 + as1
local ap2 = node.position
local as2 = node.size
local sum2 = ap2 + as2
-- Detect overlapping
return (ap1.x < sum2.x and sum.x > ap2.x) and (ap1.y < sum2.y and sum.y > ap2.y)
end
-- Check if a point lies within a range
local function RangeHasPoint(range: Types.Range, obj: Types.RigidBody) : boolean
local p = obj.center
return (
(p.X > range.position.X) and (p.X < (range.position.X + range.size.X)) and
(p.Y > range.position.Y) and (p.Y < (range.position.Y + range.size.Y))
)
end
-- Merge two arrays
local function merge<T>(array1: {T}, array2: {T}) : {T}
if #array2 > 0 then
for _, v in ipairs(array2) do
table.insert(array1, v)
end
end
return array1
end
-- Initialize a new quadtree
function Quadtree.new(_position: Vector2, _size: Vector2, _capacity: number)
return setmetatable({
position = _position,
size = _size,
capacity = _capacity,
objects = {},
divided = false,
}, Quadtree)
end
-- Insert a RigidBody in the quadtree
function Quadtree:Insert(body: Types.RigidBody)
if not self:HasObject(body.center) then return end
if #self.objects < self.capacity then
self.objects[#self.objects + 1] = body
else
-- Subdivide if not already
if not self.divided then
self:SubDivide()
self.divided = true
end
-- Insert the RigidBody in the subdivisions if possible
self.topLeft:Insert(body)
self.topRight:Insert(body)
self.bottomLeft:Insert(body)
self.bottomRight:Insert(body)
end
end
function Quadtree:HasObject(p: Vector2) : boolean
return (
(p.X > self.position.X) and (p.X < (self.position.X + self.size.X)) and
(p.Y > self.position.Y) and (p.Y < (self.position.Y + self.size.Y))
)
end
-- Create subdivisions of a node
function Quadtree:SubDivide()
local divisions = GetDivisions(self.position, self.size)
self.topLeft = Quadtree.new(divisions[1], self.size/2, self.capacity)
self.topRight = Quadtree.new(divisions[2], self.size/2, self.capacity)
self.bottomLeft = Quadtree.new(divisions[3], self.size/2, self.capacity)
self.bottomRight = Quadtree.new(divisions[4], self.size/2, self.capacity)
end
-- Search through the nodes, given a range query.
-- Returns any rigidbody that lies within the range.
function Quadtree:Search(range: Types.Range, objects: { Types.RigidBody })
if not objects then
objects = {}
end
if not RangeOverlapsNode(self, range) then
return objects
end
for _, obj in ipairs(self.objects) do
if RangeHasPoint(range, obj) then
objects[#objects + 1] = obj
end
end
if self.divided then
self.topLeft:Search(range, objects)
self.topRight:Search(range, objects)
self.bottomLeft:Search(range, objects)
self.bottomRight:Search(range, objects)
end
return objects
end
return Quadtree
end,
function(script,require)
-- This utility is used to render a constraint on the screen.
-- Services and utilities
local Globals = require(script.Parent.Parent.Constants.Globals)
-- Create the constraint's instance and apply properties
local function draw(hyp: number, origin: Vector2, thickness: number, parent: Instance, color: Color3, l: Frame?, image: string?) : Frame
local line = l or (image and Instance.new("ImageLabel") or Instance.new("Frame"))
line.Name = "Constraint"
line.AnchorPoint = Vector2.new(.5, .5)
line.Size = UDim2.new(0, hyp, 0, (thickness or Globals.constraint.thickness) + (image and 15 or 0))
line.BackgroundTransparency = image and 1 or 0
line.BorderSizePixel = 0
line.Position = UDim2.fromOffset(origin.X, origin.Y)
line.ZIndex = 1
if image then
line.Image = image
line.ImageColor3 = color or Globals.constraint.color
else
line.BackgroundColor3 = color or Globals.constraint.color
end
line.Parent = parent
return line
end
return function (origin: Vector2, endpoint: Vector2, parent: Instance, thickness: number, color: Color3, l: Frame?, image: string?) : Frame
-- Calculate magnitude between the constraint's points
-- Draw the constraint
-- Calculate rotation
local hyp = (endpoint - origin).Magnitude
local line = draw(hyp, origin, thickness, parent, color, l, image)
local mid = (origin + endpoint)/2
local theta = math.atan2((origin - endpoint).Y, (origin - endpoint).X)
-- Apply rotation and update position
line.Position = UDim2.fromOffset(mid.x, mid.y)
line.Rotation = math.deg(theta)
return line
end
end,
function(script,require)
-- Janitor
-- Original by Validark
-- Modifications by pobammer
-- roblox-ts support by OverHash and Validark
-- LinkToInstance fixed by Elttob.
-- Cleanup edge cases fixed by codesenseAye.
local GetPromiseLibrary = require(script.GetPromiseLibrary)
local Symbol = require(script.Symbol)
local FoundPromiseLibrary, Promise = GetPromiseLibrary()
local IndicesReference = Symbol("IndicesReference")
local LinkToInstanceIndex = Symbol("LinkToInstanceIndex")
local METHOD_NOT_FOUND_ERROR = "Object %s doesn't have method %s, are you sure you want to add it? Traceback: %s"
local NOT_A_PROMISE = "Invalid argument #1 to 'Janitor:AddPromise' (Promise expected, got %s (%s))"
--[=[
Janitor is a light-weight, flexible object for cleaning up connections, instances, or anything. This implementation covers all use cases,
as it doesn't force you to rely on naive typechecking to guess how an instance should be cleaned up.
Instead, the developer may specify any behavior for any object.
@class Janitor
]=]
local Janitor = {}
Janitor.ClassName = "Janitor"
Janitor.CurrentlyCleaning = true
Janitor[IndicesReference] = nil
Janitor.__index = Janitor
local TypeDefaults = {
["function"] = true;
RBXScriptConnection = "Disconnect";
}
--[=[
Determines if the passed object is a Janitor. This checks the metatable directly.
@param Object any -- The object you are checking.
@return boolean -- `true` if `Object` is a Janitor.
]=]
function Janitor.Is(Object: any): boolean
return type(Object) == "table" and getmetatable(Object) == Janitor
end
type StringOrTrue = string | boolean
--[=[
Adds an `Object` to Janitor for later cleanup, where `MethodName` is the key of the method within `Object` which should be called at cleanup time.
If the `MethodName` is `true` the `Object` itself will be called instead. If passed an index it will occupy a namespace which can be `Remove()`d or overwritten.
Returns the `Object`.
:::info
Objects not given an explicit `MethodName` will be passed into the `typeof` function for a very naive typecheck.
RBXConnections will be assigned to "Disconnect", functions will be assigned to `true`, and everything else will default to "Destroy".
Not recommended, but hey, you do you.
:::
```lua
local Workspace = game:GetService("Workspace")
local TweenService = game:GetService("TweenService")
local Obliterator = Janitor.new()
local Part = Workspace.Part
-- Queue the Part to be Destroyed at Cleanup time
Obliterator:Add(Part, "Destroy")
-- Queue function to be called with `true` MethodName
Obliterator:Add(print, true)
-- This implementation allows you to specify behavior for any object
Obliterator:Add(TweenService:Create(Part, TweenInfo.new(1), {Size = Vector3.new(1, 1, 1)}), "Cancel")
-- By passing an Index, the Object will occupy a namespace
-- If "CurrentTween" already exists, it will call :Remove("CurrentTween") before writing
Obliterator:Add(TweenService:Create(Part, TweenInfo.new(1), {Size = Vector3.new(1, 1, 1)}), "Destroy", "CurrentTween")
```
```ts
import { Workspace, TweenService } from "@rbxts/services";
import { Janitor } from "@rbxts/janitor";
const Obliterator = new Janitor<{ CurrentTween: Tween }>();
const Part = Workspace.FindFirstChild("Part") as Part;
// Queue the Part to be Destroyed at Cleanup time
Obliterator.Add(Part, "Destroy");
// Queue function to be called with `true` MethodName
Obliterator.Add(print, true);
// This implementation allows you to specify behavior for any object
Obliterator.Add(TweenService.Create(Part, new TweenInfo(1), {Size: new Vector3(1, 1, 1)}), "Cancel");
// By passing an Index, the Object will occupy a namespace
// If "CurrentTween" already exists, it will call :Remove("CurrentTween") before writing
Obliterator.Add(TweenService.Create(Part, new TweenInfo(1), {Size: new Vector3(1, 1, 1)}), "Destroy", "CurrentTween");
```
@param Object T -- The object you want to clean up.
@param MethodName? string|true -- The name of the method that will be used to clean up. If not passed, it will first check if the object's type exists in TypeDefaults, and if that doesn't exist, it assumes `Destroy`.
@param Index? any -- The index that can be used to clean up the object manually.
@return T -- The object that was passed as the first argument.
]=]
function Janitor:Add(Object: any, MethodName: StringOrTrue?, Index: any?): any
if Index then
self:Remove(Index)
local This = self[IndicesReference]
if not This then
This = {}
self[IndicesReference] = This
end
This[Index] = Object
end
MethodName = MethodName or TypeDefaults[typeof(Object)] or "Destroy"
if type(Object) ~= "function" and not Object[MethodName] then
warn(string.format(METHOD_NOT_FOUND_ERROR, tostring(Object), tostring(MethodName), debug.traceback(nil :: any, 2)))
end
self[Object] = MethodName
return Object
end
--[=[
Adds a [Promise](https://github.com/evaera/roblox-lua-promise) to the Janitor. If the Janitor is cleaned up and the Promise is not completed, the Promise will be cancelled.
```lua
local Obliterator = Janitor.new()
Obliterator:AddPromise(Promise.delay(3)):andThenCall(print, "Finished!"):catch(warn)
task.wait(1)
Obliterator:Cleanup()
```
```ts
import { Janitor } from "@rbxts/janitor";
const Obliterator = new Janitor();
Obliterator.AddPromise(Promise.delay(3)).andThenCall(print, "Finished!").catch(warn);
task.wait(1);
Obliterator.Cleanup();
```
@param PromiseObject Promise -- The promise you want to add to the Janitor.
@return Promise
]=]
function Janitor:AddPromise(PromiseObject)
if FoundPromiseLibrary then
if not Promise.is(PromiseObject) then
error(string.format(NOT_A_PROMISE, typeof(PromiseObject), tostring(PromiseObject)))
end
if PromiseObject:getStatus() == Promise.Status.Started then
local Id = newproxy(false)
local NewPromise = self:Add(Promise.new(function(Resolve, _, OnCancel)
if OnCancel(function()
PromiseObject:cancel()
end) then
return
end
Resolve(PromiseObject)
end), "cancel", Id)
NewPromise:finallyCall(self.Remove, self, Id)
return NewPromise
else
return PromiseObject
end
else
return PromiseObject
end
end
--[=[
Cleans up whatever `Object` was set to this namespace by the 3rd parameter of [Janitor.Add](#Add).
```lua
local Obliterator = Janitor.new()
Obliterator:Add(workspace.Baseplate, "Destroy", "Baseplate")
Obliterator:Remove("Baseplate")
```
```ts
import { Workspace } from "@rbxts/services";
import { Janitor } from "@rbxts/janitor";
const Obliterator = new Janitor<{ Baseplate: Part }>();
Obliterator.Add(Workspace.FindFirstChild("Baseplate") as Part, "Destroy", "Baseplate");
Obliterator.Remove("Baseplate");
```
@param Index any -- The index you want to remove.
@return Janitor
]=]
function Janitor:Remove(Index: any)
local This = self[IndicesReference]
if This then
local Object = This[Index]
if Object then
local MethodName = self[Object]
if MethodName then
if MethodName == true then
Object()
else
local ObjectMethod = Object[MethodName]
if ObjectMethod then
ObjectMethod(Object)
end
end
self[Object] = nil
end
This[Index] = nil
end
end
return self
end
--[=[
Gets whatever object is stored with the given index, if it exists. This was added since Maid allows getting the task using `__index`.
```lua
local Obliterator = Janitor.new()
Obliterator:Add(workspace.Baseplate, "Destroy", "Baseplate")
print(Obliterator:Get("Baseplate")) -- Returns Baseplate.
```
```ts
import { Workspace } from "@rbxts/services";
import { Janitor } from "@rbxts/janitor";
const Obliterator = new Janitor<{ Baseplate: Part }>();
Obliterator.Add(Workspace.FindFirstChild("Baseplate") as Part, "Destroy", "Baseplate");
print(Obliterator.Get("Baseplate")); // Returns Baseplate.
```
@param Index any -- The index that the object is stored under.
@return any? -- This will return the object if it is found, but it won't return anything if it doesn't exist.
]=]
function Janitor:Get(Index: any): any?
local This = self[IndicesReference]
if This then
return This[Index]
else
return nil
end
end
local function GetFenv(self)
return function()
for Object, MethodName in pairs(self) do
if Object ~= IndicesReference then
return Object, MethodName
end
end
end
end
--[=[
Calls each Object's `MethodName` (or calls the Object if `MethodName == true`) and removes them from the Janitor. Also clears the namespace.
This function is also called when you call a Janitor Object (so it can be used as a destructor callback).
```lua
Obliterator:Cleanup() -- Valid.
Obliterator() -- Also valid.
```
```ts
Obliterator.Cleanup()
```
]=]
function Janitor:Cleanup()
if not self.CurrentlyCleaning then
self.CurrentlyCleaning = nil
local Get = GetFenv(self)
local Object, MethodName = Get()
while Object and MethodName do -- changed to a while loop so that if you add to the janitor inside of a callback it doesn't get untracked (instead it will loop continuously which is a lot better than a hard to pindown edgecase)
if MethodName == true then
Object()
else
local ObjectMethod = Object[MethodName]
if ObjectMethod then
ObjectMethod(Object)
end
end
self[Object] = nil
Object, MethodName = Get()
end
local This = self[IndicesReference]
if This then
table.clear(This)
self[IndicesReference] = {}
end
self.CurrentlyCleaning = false
end
end
--[=[
Calls [Janitor.Cleanup](#Cleanup) and renders the Janitor unusable.
:::warning
Running this will make any attempts to call a function of Janitor error.
:::
]=]
function Janitor:Destroy()
self:Cleanup()
table.clear(self)
setmetatable(self, nil)
end
Janitor.__call = Janitor.Cleanup
--[=[
A wrapper for an `RBXScriptConnection`. Makes the Janitor clean up when the instance is destroyed. This was created by Corecii.
@class RbxScriptConnection
@__index RbxScriptConnection
]=]
local RbxScriptConnection = {}
RbxScriptConnection.Connected = true
RbxScriptConnection.__index = RbxScriptConnection
--[=[
Disconnects the Signal.
]=]
function RbxScriptConnection:Disconnect()
if self.Connected then
self.Connected = false
self.Connection:Disconnect()
end
end
function RbxScriptConnection._new(RBXScriptConnection: RBXScriptConnection)
return setmetatable({
Connection = RBXScriptConnection;
}, RbxScriptConnection)
end
function RbxScriptConnection:__tostring()
return "RbxScriptConnection<" .. tostring(self.Connected) .. ">"
end
type RbxScriptConnection = typeof(RbxScriptConnection._new(game:GetPropertyChangedSignal("ClassName"):Connect(function() end)))
--[=[
"Links" this Janitor to an Instance, such that the Janitor will `Cleanup` when the Instance is `Destroyed()` and garbage collected.
A Janitor may only be linked to one instance at a time, unless `AllowMultiple` is true. When called with a truthy `AllowMultiple` parameter,
the Janitor will "link" the Instance without overwriting any previous links, and will also not be overwritable.
When called with a falsy `AllowMultiple` parameter, the Janitor will overwrite the previous link which was also called with a falsy `AllowMultiple` parameter, if applicable.
```lua
local Obliterator = Janitor.new()
Obliterator:Add(function()
print("Cleaning up!")
end, true)
do
local Folder = Instance.new("Folder")
Obliterator:LinkToInstance(Folder)
Folder:Destroy()
end
```
```ts
import { Janitor } from "@rbxts/janitor";
const Obliterator = new Janitor();
Obliterator.Add(() => print("Cleaning up!"), true);
{
const Folder = new Instance("Folder");
Obliterator.LinkToInstance(Folder, false);
Folder.Destroy();
}
```
This returns a mock `RBXScriptConnection` (see: [RbxScriptConnection](#RbxScriptConnection)).
@param Object Instance -- The instance you want to link the Janitor to.
@param AllowMultiple? boolean -- Whether or not to allow multiple links on the same Janitor.
@return RbxScriptConnection -- A pseudo RBXScriptConnection that can be disconnected to prevent the cleanup of LinkToInstance.
]=]
function Janitor:LinkToInstance(Object: Instance, AllowMultiple: boolean?): RbxScriptConnection
local Connection
local IndexToUse = AllowMultiple and newproxy(false) or LinkToInstanceIndex
local IsNilParented = Object.Parent == nil
local ManualDisconnect = setmetatable({}, RbxScriptConnection)
local function ChangedFunction(_DoNotUse, NewParent)
if ManualDisconnect.Connected then
_DoNotUse = nil
IsNilParented = NewParent == nil
if IsNilParented then
task.defer(function()
if not ManualDisconnect.Connected then
return
elseif not Connection.Connected then
self:Cleanup()
else
while IsNilParented and Connection.Connected and ManualDisconnect.Connected do
task.wait()
end
if ManualDisconnect.Connected and IsNilParented then
self:Cleanup()
end
end
end)
end
end
end
Connection = Object.AncestryChanged:Connect(ChangedFunction)
ManualDisconnect.Connection = Connection
if IsNilParented then
ChangedFunction(nil, Object.Parent)
end
Object = nil :: any
return self:Add(ManualDisconnect, "Disconnect", IndexToUse)
end
--[=[
Links several instances to a new Janitor, which is then returned.
@param ... Instance -- All the Instances you want linked.
@return Janitor -- A new Janitor that can be used to manually disconnect all LinkToInstances.
]=]
function Janitor:LinkToInstances(...: Instance)
local ManualCleanup = Janitor.new()
for _, Object in ipairs({...}) do
ManualCleanup:Add(self:LinkToInstance(Object, true), "Disconnect")
end
return ManualCleanup
end
--[=[
Instantiates a new Janitor object.
@return Janitor
]=]
function Janitor.new()
return setmetatable({
CurrentlyCleaning = false;
[IndicesReference] = nil;
}, Janitor)
end
export type Janitor = typeof(Janitor.new())
return Janitor
end,
function(script,require)
-- This only exists because the LSP warns Key `__tostring` not found in type `table?`.
local function Symbol(Name: string)
local self = newproxy(true)
local Metatable = getmetatable(self)
function Metatable.__tostring()
return Name
end
return self
end
return Symbol
end,
function(script,require)
-- TODO: When Promise is on Wally, remove this in favor of just `script.Parent.Parent:FindFirstChild("Promise")`.
local ReplicatedFirst = game:GetService("ReplicatedFirst")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ServerScriptService = game:GetService("ServerScriptService")
local ServerStorage = game:GetService("ServerStorage")
local LOCATIONS_TO_SEARCH = {script.Parent.Parent, ReplicatedFirst, ReplicatedStorage, ServerScriptService, ServerStorage}
local function FindFirstDescendantWithNameAndClassName(Parent: Instance, Name: string, ClassName: string)
for _, Descendant in ipairs(Parent:GetDescendants()) do
if Descendant:IsA(ClassName) and Descendant.Name == Name then
return Descendant
end
end
return nil
end
local function GetPromiseLibrary()
-- I'm not too keen on how this is done.
-- It's better than the multiple if statements (probably).
local Plugin = script:FindFirstAncestorOfClass("Plugin")
if Plugin then
local Promise = FindFirstDescendantWithNameAndClassName(Plugin, "Promise", "ModuleScript")
if Promise then
return true, require(Promise)
else
return false
end
end
local Promise
for _, Location in ipairs(LOCATIONS_TO_SEARCH) do
Promise = FindFirstDescendantWithNameAndClassName(Location, "Promise", "ModuleScript")
if Promise then
break
end
end
if Promise then
return true, require(Promise)
else
return false
end
end
return GetPromiseLibrary
end,
function(script,require)
-- The Engine or the core of the library handles all the RigidBodies, constraints and points.
-- It's responsible for the simulation of these elements and handling all tasks related to the library.
-- Services and utilities
local RigidBody = require(script.Parent.Physics.RigidBody)
local Point = require(script.Parent.Physics.Point)
local Constraint = require(script.Parent.Physics.Constraint)
local PhysicsRunner = require(script.Parent.Physics.Runner)
local Globals = require(script.Parent.Constants.Globals)
local Signal = require(script.Parent.Utilities.Signal)
local Quadtree = require(script.Parent.Utilities.Quadtree)
local Janitor = require(script.Parent.Utilities.Janitor)
local Types = require(script.Parent.Types)
local throwException = require(script.Parent.Debugging.Exceptions)
local throwTypeError = require(script.Parent.Debugging.TypeErrors)
local RunService = game:GetService("RunService")
local function SearchTable(t: { any }, a: any, lambda: (a: any, b: any) -> boolean) : any
for _, v in ipairs(t) do
if lambda(a, v) then
return v
end
end
return nil
end
local Engine = {}
Engine.__index = Engine
-- This method is used to initialize basic configurations of the engine and allocate memory for future tasks.
function Engine.init(screengui: Instance)
if not typeof(screengui) == "Instance" or not screengui:IsA("Instance") then
error("Invalid Argument #1. 'screengui' must be a ScreenGui.", 2)
end
local self = setmetatable({
bodies = {},
constraints = {},
points = {},
connection = nil,
_janitor = nil,
gravity = Globals.engineInit.gravity,
friction = Globals.engineInit.friction,
airfriction = Globals.engineInit.airfriction,
bounce = Globals.engineInit.bounce,
timeSteps = Globals.engineInit.timeSteps,
mass = Globals.universalMass,
path = screengui,
speed = Globals.speed,
quadtrees = false,
independent = true,
canvas = {
frame = nil,
topLeft = Globals.engineInit.canvas.topLeft,
size = Globals.engineInit.canvas.size
},
iterations = {
constraint = 1,
collision = 1,
},
Started = Signal.new(),
Stopped = Signal.new(),
ObjectAdded = Signal.new(),
ObjectRemoved = Signal.new(),
Updated = Signal.new(),
}, Engine)
local janitor = Janitor.new()
janitor:Add(self.Started, "Destroy")
janitor:Add(self.Stopped, "Destroy")
janitor:Add(self.ObjectAdded, "Destroy")
janitor:Add(self.ObjectRemoved, "Destroy")
janitor:Add(self.Updated, "Destroy")
self._janitor = janitor
return self
end
-- This method is used to start simulating rigid bodies and constraints.
function Engine:Start()
if not self.canvas then throwException("error", "NO_CANVAS_FOUND") end
if #self.bodies == 0 then throwException("warn", "NO_RIGIDBODIES_FOUND") end
if self.connection then throwException("warn", "ALREADY_STARTED") return end
-- Fire Engine.Started event
self.Started:Fire()
local fixedDeltaTime = 1/60
local epsilon = 1/1000
local accumulator = 0
--local framesRenderedBeforeStep = 0
local connection;
connection = RunService.Stepped:Connect(function(deltaTime)
--[[if self.independent then
accumulator += deltaTime
while accumulator > 0 do
accumulator -= fixedDeltaTime
PhysicsRunner.Update(self, deltaTime)
PhysicsRunner.Render(self)
end
if accumulator >= -epsilon then
accumulator = 0
end
else]]
accumulator = 0
PhysicsRunner.Update(self, deltaTime)
PhysicsRunner.Render(self)
--end
end)
self.connection = connection
self._janitor:Add(self.connection, "Disconnect", "MainConnection")
end
-- This method is used to stop simulating rigid bodies and constraints.
function Engine:Stop()
-- Fire Engine.Stopped event
-- Disconnect all connections
if self.connection then
self.Stopped:Fire()
self._janitor:Remove("MainConnection")
self.connection = nil
end
end
-- This method is used to create RigidBodies, Constraints and Points
function Engine:Create(object: string, properties: Types.Properties)
-- Validate types of the object and property table
throwTypeError("object", object, 1, "string")
throwTypeError("properties", properties, 2, "table")
-- Validate object
if object ~= "Constraint" and object ~= "Point" and object ~= "RigidBody" then
throwException("error", "INVALID_OBJECT")
end
-- Validate property table
for prop, value in pairs(properties) do
if not table.find(Globals.VALID_OBJECT_PROPS, prop) then
throwException("error", "INVALID_PROPERTY", string.format("%q is not a valid property!", prop))
return
end
if not table.find(Globals[string.lower(object)].props, prop) then
throwException("error", "INVALID_PROPERTY", string.format("%q is not a valid property for a %s!", prop, object))
return
end
if Globals.OBJECT_PROPS_TYPES[prop] and typeof(value) ~= Globals.OBJECT_PROPS_TYPES[prop] then
error(
string.format(
"[Nature2D]: Invalid Property type for %q. Expected %q got %q.",
prop,
Globals.OBJECT_PROPS_TYPES[prop],
typeof(value)
),
2
)
end
end
-- Check if must-have properties exist in the property table
for _, prop in ipairs(Globals[string.lower(object)].must_have) do
if not properties[prop] then
local throw = true
if prop == "Object" and properties.Structure then
throw = false
end
if throw then
throwException("error", "MUST_HAVE_PROPERTY", string.format("You must specify the %q property for a %s!", prop, object))
return
end
end
end
local newObject
-- Create the Point object
if object == "Point" then
local newPoint = Point.new(properties.Position or Vector2.new(), self.canvas, self, {
snap = properties.Snap,
selectable = false,
render = properties.Visible,
keepInCanvas = properties.KeepInCanvas or true
}, nil)
-- Apply properties
if properties.Radius then newPoint:SetRadius(properties.Radius) end
if properties.Color then newPoint:Stroke(properties.Color) end
table.insert(self.points, newPoint)
newObject = newPoint
-- Create the constraint object
elseif object == "Constraint" then
if not table.find(Globals.constraint.types, string.lower(properties.Type or "")) then
throwException("error", "INVALID_CONSTRAINT_TYPE")
end
-- Validate restlength and thickness of the constraint
if properties.RestLength and properties.RestLength <= 0 then
throwException("error", "INVALID_CONSTRAINT_LENGTH")
end
if properties.Thickness and properties.Thickness <= 0 then
throwException("error", "INVALID_CONSTRAINT_THICKNESS")
end
if properties.Point1 and properties.Point2 and properties.Type then
-- Calculate distance
local dist = (properties.Point1.pos - properties.Point2.pos).Magnitude
local newConstraint = Constraint.new(properties.Point1, properties.Point2, self.canvas, {
restLength = properties.RestLength or dist,
render = properties.Visible,
thickness = properties.Thickness,
support = false,
TYPE = string.upper(properties.Type)
}, self)
-- Apply properties
if properties.SpringConstant then newConstraint:SetSpringConstant(properties.SpringConstant) end
if properties.Color then newConstraint:Stroke(properties.Color) end
table.insert(self.constraints, newConstraint)
newObject = newConstraint
end
-- Create the RigidBody object
elseif object == "RigidBody" then
-- Validate custom RigidBody structure
if properties.Object and not properties.Object:IsA("GuiObject") and not properties.Structure then
error("'Object' must be a GuiObject", 2)
end
local obj = nil
if not properties.Structure then
obj = properties.Object
end
local custom: Types.Custom = {
Vertices = {},
Edges = {}
}
if properties.Structure then
if not self.canvas.frame then
throwException("error", "CANVAS_FRAME_NOT_FOUND")
end
for _, c in ipairs(properties.Structure) do
local a = c[1]
local b = c[2]
local support = c[3]
if typeof(a) ~= "Vector2" or typeof(b) ~= "Vector2" then
error("[Nature2D]: Invalid point positions for custom RigidBody structure.", 2)
end
if support and typeof(support) ~= "boolean" then error("[Nature2D]: 'support' must be a boolean or nil") end
if a == b then error("[Nature2D]: A constraint cannot have the same points.", 2) end
local PointA = SearchTable(custom.Vertices, a, function(i, v) return i == v.pos end)
local PointB = SearchTable(custom.Vertices, b, function(i, v) return i == v.pos end)
if not PointA then
PointA = Point.new(a, self.canvas, self, {
snap = properties.Anchored,
selectable = false,
render = false,
keepInCanvas = properties.KeepInCanvas or true
})
table.insert(custom.Vertices, PointA)
end
if not PointB then
PointB = Point.new(b, self.canvas, self, {
snap = properties.Anchored,
selectable = false,
render = false,
keepInCanvas = properties.KeepInCanvas or true
})
table.insert(custom.Vertices, PointB)
end
local edge = Constraint.new(PointA, PointB, self.canvas, {
render = support and false or true,
thickness = 2,
support = support,
TYPE = "ROD"
}, self)
table.insert(custom.Edges, edge)
end
end
local newBody = RigidBody.new(
obj,
properties.Mass or self.mass,
properties.Collidable,
properties.Anchored,
self,
properties.Structure and custom or nil,
properties.Structure
)
--Apply properties
if properties.LifeSpan then newBody:SetLifeSpan(properties.LifeSpan) end
if properties.KeepInCanvas and typeof(properties.KeepInCanvas) == "boolean" then newBody:KeepInCanvas(properties.KeepInCanvas) end
if properties.Gravity then newBody:SetGravity(properties.Gravity) end
if properties.Friction then newBody:SetFriction(properties.Friction) end
if properties.AirFriction then newBody:SetAirFriction(properties.AirFriction) end
if properties.CanRotate and typeof(properties.CanRotate) == "boolean" and not properties.Structure then newBody:CanRotate(properties.CanRotate) end
table.insert(self.bodies, newBody)
newObject = newBody
end
self._janitor:Add(newObject, "Destroy")
self.ObjectAdded:Fire(newObject)
return newObject
end
-- This method is used to fetch all RigidBodies that have been created.
-- Ones that have been destroyed, won't be fetched.
function Engine:GetBodies()
return self.bodies
end
-- This method is used to fetch all Constraints that have been created.
-- Ones that have been destroyed, won't be fetched.
function Engine:GetConstraints()
return self.constraints
end
-- This method is used to fetch all Points that have been created.
function Engine:GetPoints()
return self.points
end
-- This function is used to initialize boundaries to which all bodies and constraints obey.
-- An object cannot go past this boundary.
function Engine:CreateCanvas(topLeft: Vector2, size: Vector2, frame: Frame)
throwTypeError("topLeft", topLeft, 1, "Vector2")
throwTypeError("size", size, 2, "Vector2")
self.canvas.topLeft = topLeft
self.canvas.size = size
if frame and frame:IsA("Frame") then
self.canvas.frame = frame
end
end
-- This method is used to determine the simulation speed of the engine.
-- By default the simulation speed is set to 55.
function Engine:SetSimulationSpeed(speed: number)
throwTypeError("speed", speed, 1, "number")
self.speed = speed
end
-- This method is used to configure universal physical properties possessed by all rigid bodies and constraints.
function Engine:SetPhysicalProperty(property: string, value: Vector2 | number)
throwTypeError("property", property, 1, "string")
local properties = Globals.properties
-- Update properties of the Engine
local function Update(object)
if string.lower(property) == "collisionmultiplier" then
throwTypeError("value", value, 2, "number")
object.bounce = value
elseif string.lower(property) == "gravity" then
throwTypeError("value", value, 2, "Vector2")
object.gravity = value
elseif string.lower(property) == "friction" then
throwTypeError("value", value, 2, "number")
object.friction = math.clamp(1 - value, 0, 1)
elseif string.lower(property) == "airfriction" then
throwTypeError("value", value, 2, "number")
object.airfriction = math.clamp(1 - value, 0, 1)
elseif string.lower(property) == "universalmass" then
throwTypeError("value", value, 2, "number")
object.mass = math.max(0, value)
end
end
-- Validate and update properties
if table.find(properties, string.lower(property)) then
if #self.bodies < 1 then
Update(self)
else
Update(self)
for _, b in ipairs(self.bodies) do
for _, v in ipairs(b:GetVertices()) do
Update(v)
end
end
end
else
throwException("error", "PROPERTY_NOT_FOUND")
end
end
-- This method is used to fetch an individual rigid body from its ID.
function Engine:GetBodyById(id: string)
throwTypeError("id", id, 1, "string")
for _, b in ipairs(self.bodies) do
if b.id == id then
return b
end
end
return
end
-- This method is used to fetch an individual constraint body from its ID.
function Engine:GetConstraintById(id: string)
throwTypeError("id", id, 1, "string")
for _, c in ipairs(self.constraints) do
if c.id == id then
return c
end
end
return
end
function Engine:GetDebugInfo() : Types.DebugInfo
return {
Objects = {
RigidBodies = #self.bodies,
Constraints = #self.constraints,
Points = #self.points
},
Running = not not (self.connection),
Physics = {
Gravity = self.gravity,
Friction = 1 - self.friction,
AirFriction = 1 - self.airfriction,
CollisionMultiplier = self.bounce,
TimeSteps = self.timeSteps,
SimulationSpeed = self.speed,
UsingQuadtrees = self.quadtrees,
FramerateIndependent = self.independent
},
Path = self.path,
Canvas = {
Frame = self.canvas.frame,
TopLeft = self.canvas.topLeft,
Size = self.canvas.size
}
}
end
-- Determines if Quadtrees will be used in collision deteWction.
-- By default this is set to false
function Engine:UseQuadtrees(use: boolean)
throwTypeError("useQuadtrees", use, 1, "boolean")
self.quadtrees = use
end
-- Determines if Frame rate does not affect the simulation speed.
-- By default set to true.
function Engine:FrameRateIndependent(independent: boolean)
throwTypeError("independent", independent, 1, "boolean")
self.independent = independent
end
function Engine:SetConstraintIterations(iterations: number)
throwTypeError("iterations", iterations, 1, "number")
self.iterations.constraint = math.floor(math.clamp(iterations, 1, 10))
end
function Engine:SetCollisionIterations(iterations: number)
throwTypeError("iterations", iterations, 1, "number")
if self.quadtrees then
self.iterations.collision = math.floor(math.clamp(iterations, 1, 10))
else
throwException("warn", "CANNOT_SET_COLLISION_ITERATIONS")
end
end
function Engine:Destroy()
self._janitor:Destroy()
setmetatable(self, nil)
end
return Engine
end,
function(script,require)
-- Type Definitions
export type Quadtree<T> = {
position: Vector2,
size: Vector2,
capacity: number,
objects: {T},
divided: boolean,
}
export type Canvas = {
topLeft: Vector2,
size: Vector2,
frame: Frame?
}
export type Point = {
Parent: any,
frame: Frame?,
engine: { any },
canvas: Canvas,
oldPos: Vector2,
pos: Vector2,
forces: Vector2,
gravity: Vector2,
friction: number,
airfriction: number,
bounce: number,
snap: boolean,
selectable: boolean,
render: boolean,
keepInCanvas: boolean,
color: Color3?,
radius: number
}
export type RigidBody = {
CreateProjection: (Axis: Vector2, Min: number, Max: number) -> (number, number),
SetState: (state: string, value: any) -> (),
GetState: (state: string) -> any,
id: string,
vertices: { Point },
edges: { any },
frame: GuiObject?,
anchored: boolean,
mass: number,
collidable: boolean,
center: Vector2,
engine: { any },
spawnedAt: number,
lifeSpan: number?,
anchorRotation: number?,
anchorPos: Vector2?,
Touched: any,
CanvasEdgeTouched: any,
States: { any }
}
export type SegmentConfig = {
restLength: number?,
render: boolean,
thickness: number?,
support: boolean,
TYPE: string,
}
export type EngineConfig = {
gravity: Vector2,
friction: number,
bounce: number,
speed: number,
airfriction: number,
}
export type PointConfig = {
snap: boolean,
selectable: boolean,
render: boolean,
keepInCanvas: boolean
}
export type Collision = {
axis: Vector2,
depth: number,
edge: any,
vertex: Point
}
export type Range = {
position: Vector2,
size: Vector2
}
export type Properties = {
Position: Vector2?,
Visible: boolean?,
Snap: boolean?,
KeepInCanvas: boolean?,
Radius: number?,
Color: Color3?,
Type: string?,
Point1: Point?,
Point2: Point?,
Thickness: number?,
RestLength: number?,
SpringConstant: number?,
Object: GuiObject?,
Collidable: boolean?,
Anchored: boolean?,
LifeSpan: number?,
Gravity: Vector2?,
Friction: number?,
AirFriction: number?,
Structure: {}?,
Mass: number?,
CanRotate: boolean
}
export type Custom = {
Vertices: { any },
Edges: { any }
}
export type Plugins = {
Triangle: (a: Vector2, b: Vector2, c: Vector2) -> (),
Quad: (a: Vector2, b: Vector2, c: Vector2, d: Vector2) -> (),
MouseConstraint: (engine: { any }, range: number, rigidbodies: { any }) -> ()
}
export type DebugInfo = {
Objects: {
RigidBodies: number,
Constraints: number,
Points: number
},
Running: boolean,
Physics: {
Gravity: Vector2,
Friction: number,
AirFriction: number,
CollisionMultiplier: number,
TimeSteps: number,
SimulationSpeed: number,
UsingQuadtrees: boolean,
FramerateIndependent: boolean
},
Path: ScreenGui,
Canvas: {
Frame: GuiObject,
TopLeft: Vector2,
Size: Vector2
}
}
return nil
end,
function(script,require)
return {
MouseConstraint = require(script.MouseConstraint),
Quad = require(script.Quad),
Triangle = require(script.Triangle)
}
end,
function(script,require)
local UserInputService = game:GetService("UserInputService")
return function (engine: { any }, range: number)
local held = nil
local connections = {}
connections.InputBegan = UserInputService.InputBegan:Connect(function(input, processedEvent)
if processedEvent then return end
if input.UserInputType == Enum.UserInputType.MouseButton1 and not held then
for _, b in ipairs(engine.bodies) do
for _, p in ipairs(b.vertices) do
if (p.pos - UserInputService:GetMouseLocation()).Magnitude <= range then
p.selectable = true
p.snap = true
held = p
break
end
end
if held then
break
end
end
end
end)
connections.InputEnded = UserInputService.InputEnded:Connect(function(input, processedEvent)
if processedEvent then return end
if input.UserInputType == Enum.UserInputType.MouseButton1 and held then
held.selectable = false
held.snap = false
held = nil
end
end)
connections.InputChanged = UserInputService.InputChanged:Connect(function(input, processedEvent)
if processedEvent then return end
if input.UserInputType == Enum.UserInputType.MouseMovement and held then
local mouse = UserInputService:GetMouseLocation()
held:SetPosition(mouse.X, mouse.Y)
end
end)
return function ()
held.snap = false
held = nil
connections.InputBegan:Disconnect()
connections.InputEnded:Disconnect()
connections.InputChanged:Disconnect()
end
end
end,
function(script,require)
-- Returns a quadrilateral structure for Custom RigidBodies given 4 points
return function (a: Vector2, b: Vector2, c: Vector2, d: Vector2)
return {
{ a, b, false },
{ b, c, false },
{ c, d, false },
{ d, a, false },
{ a, c, true },
{ b, d, true }
}
end
end,
function(script,require)
-- Returns a triangular structure for Custom RigidBodies given 3 points
return function (a: Vector2, b: Vector2, c: Vector2)
return {
{ a, b, false },
{ a, c, false },
{ b, c, false }
}
end
end
}
local ScriptIndex = 0
local Scripts,ModuleScripts,ModuleCache = {},{},{}
local _require = require
function require(obj,...)
local index = ModuleScripts[obj]
if not index then
local a,b = pcall(_require,obj,...)
return not a and error(b,2) or b
end
local res = ModuleCache[index]
if res then return res end
res = ScriptFunctions[index](obj,require)
ModuleCache[index] = res
return res
end
local function Script(obj,ismodule)
ScriptIndex = ScriptIndex + 1
local t = ismodule and ModuleScripts or Scripts
t[obj] = ScriptIndex
end
function RunScripts()
for script,index in pairs(Scripts) do
coroutine.wrap(ScriptFunctions[index])(script,require)
end
end
local function Decode(str)
local StringLength = #str
-- Base64 decoding
do
local decoder = {}
for b64code, char in pairs(('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='):split('')) do
decoder[char:byte()] = b64code-1
end
local n = StringLength
local t,k = table.create(math.floor(n/4)+1),1
local padding = str:sub(-2) == '==' and 2 or str:sub(-1) == '=' and 1 or 0
for i = 1, padding > 0 and n-4 or n, 4 do
local a, b, c, d = str:byte(i,i+3)
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d]
t[k] = string.char(bit32.extract(v,16,8),bit32.extract(v,8,8),bit32.extract(v,0,8))
k = k + 1
end
if padding == 1 then
local a, b, c = str:byte(n-3,n-1)
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40
t[k] = string.char(bit32.extract(v,16,8),bit32.extract(v,8,8))
elseif padding == 2 then
local a, b = str:byte(n-3,n-2)
local v = decoder[a]*0x40000 + decoder[b]*0x1000
t[k] = string.char(bit32.extract(v,16,8))
end
str = table.concat(t)
end
local Position = 1
local function Parse(fmt)
local Values = {string.unpack(fmt,str,Position)}
Position = table.remove(Values)
return table.unpack(Values)
end
local Settings = Parse('B')
local Flags = Parse('B')
Flags = {
--[[ValueIndexByteLength]] bit32.extract(Flags,6,2)+1,
--[[InstanceIndexByteLength]] bit32.extract(Flags,4,2)+1,
--[[ConnectionsIndexByteLength]] bit32.extract(Flags,2,2)+1,
--[[MaxPropertiesLengthByteLength]] bit32.extract(Flags,0,2)+1,
--[[Use Double instead of Float]] bit32.band(Settings,0b1) > 0
}
local ValueFMT = ('I'..Flags[1])
local InstanceFMT = ('I'..Flags[2])
local ConnectionFMT = ('I'..Flags[3])
local PropertyLengthFMT = ('I'..Flags[4])
local ValuesLength = Parse(ValueFMT)
local Values = table.create(ValuesLength)
local CFrameIndexes = {}
local ValueDecoders = {
--!!Start
[1] = function(Modifier)
return Parse('s'..Modifier)
end,
--!!Split
[2] = function(Modifier)
return Modifier ~= 0
end,
--!!Split
[3] = function()
return Parse('d')
end,
--!!Split
[4] = function(_,Index)
table.insert(CFrameIndexes,{Index,Parse(('I'..Flags[1]):rep(3))})
end,
--!!Split
[5] = {CFrame.new,Flags[5] and 'dddddddddddd' or 'ffffffffffff'},
--!!Split
[6] = {Color3.fromRGB,'BBB'},
--!!Split
[7] = {BrickColor.new,'I2'},
--!!Split
[8] = function(Modifier)
local len = Parse('I'..Modifier)
local kpts = table.create(len)
for i = 1,len do
kpts[i] = ColorSequenceKeypoint.new(Parse('f'),Color3.fromRGB(Parse('BBB')))
end
return ColorSequence.new(kpts)
end,
--!!Split
[9] = function(Modifier)
local len = Parse('I'..Modifier)
local kpts = table.create(len)
for i = 1,len do
kpts[i] = NumberSequenceKeypoint.new(Parse(Flags[5] and 'ddd' or 'fff'))
end
return NumberSequence.new(kpts)
end,
--!!Split
[10] = {Vector3.new,Flags[5] and 'ddd' or 'fff'},
--!!Split
[11] = {Vector2.new,Flags[5] and 'dd' or 'ff'},
--!!Split
[12] = {UDim2.new,Flags[5] and 'di2di2' or 'fi2fi2'},
--!!Split
[13] = {Rect.new,Flags[5] and 'dddd' or 'ffff'},
--!!Split
[14] = function()
local flags = Parse('B')
local ids = {"Top","Bottom","Left","Right","Front","Back"}
local t = {}
for i = 0,5 do
if bit32.extract(flags,i,1)==1 then
table.insert(t,Enum.NormalId[ids[i+1]])
end
end
return Axes.new(unpack(t))
end,
--!!Split
[15] = function()
local flags = Parse('B')
local ids = {"Top","Bottom","Left","Right","Front","Back"}
local t = {}
for i = 0,5 do
if bit32.extract(flags,i,1)==1 then
table.insert(t,Enum.NormalId[ids[i+1]])
end
end
return Faces.new(unpack(t))
end,
--!!Split
[16] = {PhysicalProperties.new,Flags[5] and 'ddddd' or 'fffff'},
--!!Split
[17] = {NumberRange.new,Flags[5] and 'dd' or 'ff'},
--!!Split
[18] = {UDim.new,Flags[5] and 'di2' or 'fi2'},
--!!Split
[19] = function()
return Ray.new(Vector3.new(Parse(Flags[5] and 'ddd' or 'fff')),Vector3.new(Parse(Flags[5] and 'ddd' or 'fff')))
end
--!!End
}
for i = 1,ValuesLength do
local TypeAndModifier = Parse('B')
local Type = bit32.band(TypeAndModifier,0b11111)
local Modifier = (TypeAndModifier - Type) / 0b100000
local Decoder = ValueDecoders[Type]
if type(Decoder)=='function' then
Values[i] = Decoder(Modifier,i)
else
Values[i] = Decoder[1](Parse(Decoder[2]))
end
end
for i,t in pairs(CFrameIndexes) do
Values[t[1]] = CFrame.fromMatrix(Values[t[2]],Values[t[3]],Values[t[4]])
end
local InstancesLength = Parse(InstanceFMT)
local Instances = {}
local NoParent = {}
for i = 1,InstancesLength do
local ClassName = Values[Parse(ValueFMT)]
local obj
local MeshPartMesh,MeshPartScale
if ClassName == "UnionOperation" then
obj = DecodeUnion(Values,Flags,Parse)
obj.UsePartColor = true
elseif ClassName:find("Script") then
obj = Instance.new("Folder")
Script(obj,ClassName=='ModuleScript')
elseif ClassName == "MeshPart" then
obj = Instance.new("Part")
MeshPartMesh = Instance.new("SpecialMesh")
MeshPartMesh.MeshType = Enum.MeshType.FileMesh
MeshPartMesh.Parent = obj
else
obj = Instance.new(ClassName)
end
local Parent = Instances[Parse(InstanceFMT)]
local PropertiesLength = Parse(PropertyLengthFMT)
local AttributesLength = Parse(PropertyLengthFMT)
Instances[i] = obj
for i = 1,PropertiesLength do
local Prop,Value = Values[Parse(ValueFMT)],Values[Parse(ValueFMT)]
-- ok this looks awful
if MeshPartMesh then
if Prop == "MeshId" then
MeshPartMesh.MeshId = Value
continue
elseif Prop == "TextureID" then
MeshPartMesh.TextureId = Value
continue
elseif Prop == "Size" then
if not MeshPartScale then
MeshPartScale = Value
else
MeshPartMesh.Scale = Value / MeshPartScale
end
elseif Prop == "MeshSize" then
if not MeshPartScale then
MeshPartScale = Value
MeshPartMesh.Scale = obj.Size / Value
else
MeshPartMesh.Scale = MeshPartScale / Value
end
continue
end
end
obj[Prop] = Value
end
if MeshPartMesh then
if MeshPartMesh.MeshId=='' then
if MeshPartMesh.TextureId=='' then
MeshPartMesh.TextureId = 'rbxasset://textures/meshPartFallback.png'
end
MeshPartMesh.Scale = obj.Size
end
end
for i = 1,AttributesLength do
obj:SetAttribute(Values[Parse(ValueFMT)],Values[Parse(ValueFMT)])
end
if not Parent then
table.insert(NoParent,obj)
else
obj.Parent = Parent
end
end
local ConnectionsLength = Parse(ConnectionFMT)
for i = 1,ConnectionsLength do
local a,b,c = Parse(InstanceFMT),Parse(ValueFMT),Parse(InstanceFMT)
Instances[a][Values[b]] = Instances[c]
end
return NoParent
end
local Objects = Decode('AAAcIQxNb2R1bGVTY3JpcHQhBE5hbWUhCE5hdHVyZTJEIQZGb2xkZXIhCUNvbnN0YW50cyEHR2xvYmFscyEJRGVidWdnaW5nIQpUeXBlRXJyb3JzIQpFeGNlcHRpb25zIQhSZXN0cmljdCEHUGh5c2ljcyEJUmlnaWRCb2R5IQVQb2ludCEKQ29uc3RyYWludCEGUnVu'
..'bmVyIQlVdGlsaXRpZXMhBlNpZ25hbCEIUXVhZHRyZWUhBExpbmUhB0phbml0b3IhBlN5bWJvbCERR2V0UHJvbWlzZUxpYnJhcnkhBkVuZ2luZSEFVHlwZXMhB1BsdWdpbnMhD01vdXNlQ29uc3RyYWludCEEUXVhZCEIVHJpYW5nbGUZAQABAAIDBAEBAAIFAQIBAAIG'
..'BAEBAAIHAQQBAAIIAQQBAAIJAQQBAAIKBAEBAAILAQgBAAIMAQgBAAINAQgBAAIOAQgBAAIPBAEBAAIQAQ0BAAIRAQ0BAAISAQ0BAAITAQ0BAAIUAREBAAIVAREBAAIWAQEBAAIXAQEBAAIYAQEBAAIZARYBAAIaARYBAAIbARYBAAIcAA==')
for _,obj in pairs(Objects) do
obj.Parent = script or workspace
end
return require(script.Nature2D)