Untitled

Run Settings
LanguageLua
Language Version
Run Command
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)
Editor Settings
Theme
Key bindings
Full width
Lines