Plugin Developer Guide

How to Make
Custom Shapes

A developer guide to creating shapes for Project Gravity using the plugin system.

01 Step 1: File Location

Project Gravity loads shapes from the GravityShapes folder inside your executor's workspace. Drop any .lua or .txt file here to automatically add it to the UI dropdown.

Overwriting

Files with the exact same name as official shapes (e.g., Meteor Shower.lua) will completely replace the official GitHub version.

Errors

Syntax errors will prevent the shape from loading and trigger a warning in the F9 console. Other shapes will continue running normally.

02 Step 2: Barebones Template

Use this minimal boilerplate code structure to build a new shape module:

template.lua
local M = {}

function M.px(t, c, x6, x9, x1)
end

function M.f2(p, cen, d, t, c, x1, x6, x9)
    return (cen - p.Position) * (x1.k10 * x9.c1), cen
end

M.Controls = {}

return M

03 Step 3: Variable Reference

A description of variables available in your shape calculation functions:

p
The Roblox part object currently being manipulated.
cen
The Vector3 center of gravity point (usually the player character's RootPart).
t
Elapsed running time in seconds. Useful for creating oscillating or rotating movements.
c
UI controls table. Custom settings defined in M.Controls can be read via their key (e.g., c.k11).
d
A persistent table unique to each part. Use this to store offsets or state values. Persists until the part is dropped.
x6.pre
Global runtime memory cache. Used to save computations once per frame in M.px for all parts.
x1
Global configuration settings table (e.g., x1.MaxSpeed, x1.k10 the strength multiplier). Avoid modifying.
x9
Internal multipliers and constants used by the physics engine. Safely ignore.

04 Step 4: The Functions

Every shape returns a module table containing these three members:

M.px Pre-Computation

Runs exactly once per frame prior to physics loop. Used to pre-calculate global offsets and save to x6.pre to optimize performance.

M.f2 Physics Loop

Runs for every individual part in the gravity zone, every frame. Must return a Vector3 target velocity vector representing the push to apply to the part.

Anti-Jitter (Important)

To prevent jitter when the system skips frames to optimize high part counts, return your raw target_position as a second argument. The engine uses this for feed-forward trajectory smoothing.

return (target_position - p.Position) * (x1.k10 * x9.c1), target_position
M.Controls UI Layout

Defines interactive settings in the UI dropdown. Supports Slider (with Min, Max, optional Div and Default), Toggle, and TextBox (with Default, Desc, MaxChars) controls. Toggles return true or false in the c table.

M.Controls = {
    { Type = "Slider", Name = "Radius", Min = 5, Max = 100, Key = "k11" },
    { Type = "Toggle", Name = "Cut In Half", Key = "k12" },
    { Type = "TextBox", Name = "Label", Key = "k13", Default = "hello" }
}

To use a toggle (e.g., "Cut In Half"), check its key in your physics loop:

if c.k12 then
    -- Toggle is ON: Apply the effect
    radius = radius / 2
end

05 Important Rules

06 Full Example: "Floating Ring"

Here is a complete, working template containing custom controls, pre-computation, and orbital part offsets. Save this as Floating Ring.lua in your shapes directory.

Floating Ring.lua
local M = {}

function M.px(t, c, x6, x9, x1)
    if not x6.pre["Floating Ring"] then 
        x6.pre["Floating Ring"] = {} 
    end
    
    local memory = x6.pre["Floating Ring"]
    
    local dt = t - (memory.last_t or t)
    memory.last_t = t
    
    local speed = c.k13 or 10 
    memory.rotation = (memory.rotation or 0) + (dt * speed)
end

function M.f2(p, cen, d, t, c, x1, x6, x9)
    local memory = x6.pre["Floating Ring"] or {}
    local rotation = memory.rotation or 0
    
    local radius = c.k11 or 20
    local height = c.k12 or 5
    
    if not d.my_spot then
        d.my_spot = math.random() * (math.pi * 2)
    end
    
    local x = math.cos(rotation + d.my_spot) * radius
    local z = math.sin(rotation + d.my_spot) * radius
    local target_position = cen + Vector3.new(x, height, z)
    
    return (target_position - p.Position) * (x1.k10 * x9.c1), target_position 
end

M.Controls = {
    { Type = "Slider", Name = "Ring Radius", Min = 5, Max = 100, Key = "k11" },
    { Type = "Slider", Name = "Ring Height", Min = -50, Max = 50, Key = "k12" },
    { Type = "Slider", Name = "Spin Speed", Min = 0, Max = 100, Key = "k13" }
}

return M