npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@rbxts/gravity-controller

v1.0.7

Published

TypeScript bindings for [EgoMoose's Rbx-Gravity-Controller](https://github.com/EgoMoose/Rbx-Gravity-Controller) with ground-normal-based wall walking by [EmilyBendsSpace](https://x.com/EmilyBendsSpace).

Readme

@rbxts/gravity-controller

TypeScript bindings for EgoMoose's Rbx-Gravity-Controller with ground-normal-based wall walking by EmilyBendsSpace.

Players can walk on walls, ceilings, and any arbitrary surface with smooth gravity transitions.

Installation

npm install @rbxts/gravity-controller

The package ships a GravityController.rbxmx model that contains the Lua runtime (camera, collider, state tracker, animations, and character sounds). The TypeScript wrapper in src/index.ts handles deploying those scripts at runtime and provides typed access to the controller.

How it works

Architecture

Server (onInit)                         Client (onStart)
─────────────────                       ──────────────────
installGravityControllerClass()         installGravityControllerClass()
  ├─ Copies Client/PlayerScriptsLoader    └─ require("GravityController")
  │  → StarterPlayerScripts                     from ReplicatedStorage
  ├─ Copies Client/RbxCharacterSounds           ↓
  │  → StarterPlayerScripts             new GravityControllerClass(player)
  ├─ Copies Client/Animate                ├─ Camera  (custom camera module)
  │  → StarterCharacterScripts            ├─ Control (input → move vector)
  └─ Moves GravityController module       ├─ Collider (physics body movers)
     → ReplicatedStorage                  └─ StateTracker (humanoid states)

installGravityControllerClass() must be called on both the server and the client. On the server it deploys the bundled scripts into StarterPlayerScripts, StarterCharacterScripts, and ReplicatedStorage. On the client it requires the GravityController ModuleScript from ReplicatedStorage and returns the class.

Permanent runtime modifications

The server-side installGravityControllerClass() replaces scripts in StarterPlayerScripts / StarterCharacterScripts:

  • PlayerScriptsLoader — modified version that monkey-patches BaseCamera, CameraUtils, Poppercam, and the CameraModule.Update loop to support arbitrary gravity up-vectors
  • Animate — custom version that works with PlatformStand = true
  • RbxCharacterSounds — custom version driven by the gravity controller's StateTracker rather than native Humanoid state

These replacements are permanent for the session — they survive GravityController:Destroy().

Key monkey-patch: BaseCamera:UpdateMouseBehavior()

The modified PlayerScriptsLoader overrides BaseCamera:UpdateMouseBehavior() to force UserGameSettings.RotationType from CameraRelative to MovementRelative. This is necessary while the gravity controller is active because CameraRelative rotation conflicts with the custom BodyGyro-driven character orientation.

This override is guarded by _G._gravityControllerActive so it only applies while a gravity controller instance is alive. Without this guard, any first-person camera system that depends on CameraRelative (such as Character-Realism's FpsCamera) will break permanently after gravity controller destruction.

What a GravityController instance does (per-player, temporary)

  • Sets Humanoid.PlatformStand = true (disables the default humanoid physics)
  • Creates collision proxy parts (Sphere, FloorDetector, JumpDetector) welded to HRP
  • Adds VectorForce, BodyGyro, BodyPosition to HRP
  • Binds "GravityStep" RenderStep at priority Camera - 1 (199)
  • Sets _G._gravityControllerActive = true

What GravityController:Destroy() does

  • Unbinds "GravityStep" from RenderStep
  • Maid:Sweep() — destroys all proxy parts, body movers, disconnects events
  • Sets Humanoid.PlatformStand = false
  • Sets _G._gravityControllerActive = false

It does not restore the Animate script or the PlayerScriptsLoader — those persist.

Gravity step (per frame)

Each render frame the controller runs onGravityStep:

  1. Query gravity direction — calls GetGravityUp(oldGravity) which you can override. By default it returns the previous gravity (no change). Assign getGravityControllerUp to enable surface-following wall walk.
  2. Lerp transition — spherically interpolates from the old gravity direction toward the new one, controlled by Transition (default 0.15).
  3. Compute world move vector — projects the camera-relative input onto the plane perpendicular to gravity so the character always moves along the surface.
  4. Compute forces — calculates a counter-gravity force (gForce) and a walk force (walkForce) that accelerates the character toward target velocity.
  5. Optional horizontal lock — when UseBodyPositionLock is enabled and the character is standing still on an aligned surface, a BodyPosition prevents micro-sliding.
  6. Update collider and state — applies the combined force to the character's body movers and updates the state tracker (running, jumping, freefall, etc.).

Interaction with first-person camera systems

If you use a first-person camera module (e.g. Character-Realism's FpsCamera) that depends on UserGameSettings.RotationType being CameraRelative:

  • While the gravity controller is active, onGravityStep handles character rotation via BodyGyro. The _G._gravityControllerActive flag ensures the monkey-patch forces MovementRelative only during this time.
  • After Destroy(), the flag clears and the monkey-patch becomes a no-op, allowing your first-person system to set AutoRotate = false and take over character rotation normally.

Ground normal detection (getGroundNormal)

The exported getGroundNormal function determines which direction is "up" by casting rays from the character's root part:

| Ray group | Count | Purpose | |---|---|---| | Center ray | 1 | Single downward ray (length 25) to find the surface directly below | | Down rays | 24 | Radial ring of rays angled slightly inward/outward, with alternating even/odd radii, to sample the surrounding surface normals | | Feeler rays | 9 | Shorter rays (length 2) fanning outward and downward to detect walls and edges the character is approaching |

All hit normals are weighted (front-facing rays weighted more heavily, feelers weighted 8x) and summed. The final unit vector becomes the new "up" direction. If no rays hit anything, the previous gravity direction is preserved.

GravityController.rbxmx file manifest

The .rbxmx model bundles all the Lua scripts needed at runtime. During installGravityControllerClass() these are deployed to the correct locations in the Roblox data model.

GravityController (Script) ← server entry point; deploys children at runtime
├── Client (Folder) ← scripts that get copied into StarterPlayer
│   ├── Animate (LocalScript) → StarterCharacterScripts
│   │   ├── Controller (ModuleScript) — bootstraps R6/R15 animation sets
│   │   ├── Loaded (BoolValue) — signals when animations are ready
│   │   ├── PlayEmote (BindableFunction) — emote playback hook
│   │   ├── R15 (ModuleScript) — full R15 animation state machine
│   │   ├── R6 (ModuleScript) — full R6 animation state machine
│   │   ├── ReplicatedHumanoid (ObjectValue) — humanoid reference for replication
│   │   └── VerifyAnims (ModuleScript) — validates animation assets on the character
│   ├── PlayerScriptsLoader (LocalScript) → StarterPlayerScripts
│   │   ├── CameraInjector (ModuleScript) — monkey-patches PlayerModule's CameraModule
│   │   │   to expose a public GetUpVector API for gravity-aware camera rotation
│   │   └── FakeUserSettings (ModuleScript) — shims UserSettings() to override feature
│   │       flags (e.g. disables UserRemoveTheCameraApi) during camera injection
│   └── RbxCharacterSounds (LocalScript) → StarterPlayerScripts
│       └── AnimationState (ModuleScript) — maps animation track names to
│           HumanoidStateTypes so footstep/jump/fall sounds play correctly
│           under custom gravity
└── GravityController (ModuleScript) → ReplicatedStorage
    ├── CharacterModules (Folder)
    │   ├── Camera (ModuleScript) — hooks into PlayerModule cameras to override
    │   │   GetUpVector, making the camera orbit around the custom gravity axis
    │   └── Control (ModuleScript) — wraps PlayerModule controls to read the
    │       move vector from keyboard/gamepad/touch input
    ├── Collider (ModuleScript) — creates an invisible Ball Part welded below
    │   the HRP for ground detection, plus VectorForce (gravity + walk),
    │   BodyGyro (orientation), and BodyPosition (optional anti-slide lock)
    ├── StateTracker (ModuleScript) — replaces Humanoid state detection with
    │   velocity-based Running/Jumping/Freefall tracking and fires the
    │   Animate script's callbacks (onRunning, onJumping, onFreeFall, etc.)
    └── Utility (Folder)
        ├── Maid (ModuleScript) — connection/instance cleanup utility
        └── Signal (ModuleScript) — lightweight event/signal implementation

Where each piece ends up at runtime:

| Script | Deployed to | Role | |---|---|---| | Client/PlayerScriptsLoader | StarterPlayerScripts | Replaces the default PlayerScriptsLoader to inject gravity-aware camera and control overrides into the stock PlayerModule | | Client/RbxCharacterSounds | StarterPlayerScripts | Replaces default character sounds so audio triggers are driven by the custom StateTracker instead of the native Humanoid state | | Client/Animate | StarterCharacterScripts | Replaces default Animate script; plays R6/R15 animations driven by StateTracker.Changed events rather than native humanoid states | | GravityController | ReplicatedStorage | The core module — required by both the TypeScript wrapper and the PlayerScriptsLoader at runtime |

API

installGravityControllerClass(config?)

Initializes the gravity system. Call on both server and client. Returns the GravityControllerClass constructor. Idempotent — calling it again returns the same class (and optionally applies new config).

GravityControllerClass

| Member | Type | Description | |---|---|---| | new(player) | constructor | Creates a controller for the given player's current character | | SetConstants(config) | static method | Updates physics constants globally (see Configuration below) |

GravityController (instance)

| Member | Type | Description | |---|---|---| | Player | Player | The owning player | | Character | Model | The player's character model | | Humanoid | Humanoid | The character's humanoid | | HRP | BasePart | HumanoidRootPart | | Maid | { Mark } | Cleanup helper — tracks connections for automatic teardown | | GetGravityUp(oldGravity) | method | Override this to control gravity direction each frame. Default returns oldGravity (no change). | | ResetGravity(direction) | method | Instantly sets the gravity-up vector and resets the fall tracker | | GetFallHeight() | method | Returns the signed distance fallen along the gravity axis while in freefall; 0 otherwise | | Destroy() | method | Unbinds the render step, sweeps all connections, and restores PlatformStand |

GravityManager

A plain TypeScript class (no framework dependencies) that manages the full lifecycle of a GravityController instance — waiting for the character and Animate script, constructing the controller, handling timeouts and retries, and tearing down cleanly.

const cls = installGravityControllerClass(config)
const manager = new GravityManager(cls, logger)

manager.enable(getGravityControllerUp)  // async 4-phase construction
manager.disable()                       // teardown + reset GravityUp attribute
manager.getController()                 // live instance or undefined
manager.getIsEnabling()                 // true while construction is in-flight

Constructor: new GravityManager(gravityControllerClass, logger?)

  • gravityControllerClass — the class returned by installGravityControllerClass()
  • logger — optional GravityLogger (see below). If omitted, logging is silently skipped.

enable(getGravityUp) — starts a 4-phase async construction:

  1. Waits for Players.LocalPlayer.Character to exist
  2. Waits for the Animate script and its Controller child (with timeout)
  3. Waits for the Animate module to finish loading (Loaded.Value = true)
  4. Constructs the GravityController instance and assigns GetGravityUp

Each call stores the requested getGravityUp function. If enable() is called again while construction is in-flight, the new function is saved and will be used when the current construction completes or on retry.

A generation counter invalidates stale constructions — if disable() is called while construction is running, the in-flight thread is cancelled and any completed-but-stale controller is destroyed immediately.

A watchdog fires after the timeout period and hard-cancels stuck construction threads, cleans up partial state (GravityStep RenderStep binding, PlatformStand), and triggers a retry.

disable() — tears down the active controller:

  • Increments the generation counter (invalidating any in-flight construction)
  • Cancels the construction thread if running
  • Calls Destroy() on the live controller
  • Sets the HRP GravityUp attribute to (0, 1, 0)

getController() — returns the live GravityController instance or undefined.

getIsEnabling() — returns true while construction is in-flight.

GravityLogger

Minimal logging interface so the package doesn't depend on any specific logging library. Any object with Info, Warn, and Error string methods satisfies it — including @rbxts/log's Logger.

interface GravityLogger {
  Info(message: string): void
  Warn(message: string): void
  Error(message: string): void
}

wrapGravityUpSaveAttribute(getGravityUp)

Higher-order function that wraps a GetGravityUp function to persist the current gravity direction as an HRP attribute (GravityUp). Only writes when the direction actually changes (magnitude delta > 0.001).

manager.enable(wrapGravityUpSaveAttribute(getGravityControllerUp))

GetGravityUp (type)

type GetGravityUp = (self: GravityController, oldGravityUp: Vector3) => Vector3

The signature for gravity direction functions. Passed to GravityManager.enable() or assigned to controller.GetGravityUp.

getGravityControllerUp(controller, oldGravityUp)

Convenience wrapper that calls getGroundNormal using the controller's HRP.CFrame with a rig-type-aware origin offset. Assign this to controller.GetGravityUp to enable wall walking.

getGroundNormal(cframe, originOffset, oldGravityUp)

Low-level raycast function that returns a unit Vector3 representing the surface normal beneath and around cframe. Useful if you want to build your own gravity logic.

Configuration

Pass a config table to installGravityControllerClass() or call SetConstants() at any time:

| Key | Type | Default | Description | |---|---|---|---| | Transition | number | 0.15 | Lerp alpha per frame for gravity direction changes. Lower = slower, smoother transitions. | | WalkForce | number | 66.67 | Horizontal acceleration multiplier. Increase for snappier movement. | | JumpModifier | number | 1.2 | Multiplier on Humanoid.JumpPower when jumping along the custom gravity axis. | | UseBodyPositionLock | boolean | false | When true, locks the character's horizontal position with a BodyPosition while idle on an aligned surface to prevent sliding. |

Usage

With GravityManager (recommended)

Server — install in a @Service:

import { OnInit, Service } from '@flamework/core'
import { installGravityControllerClass } from '@rbxts/gravity-controller'

@Service()
export class GravityService implements OnInit {
  onInit() {
    installGravityControllerClass()
  }
}

Client — use GravityManager to handle the full enable/disable lifecycle:

import { Controller, OnStart } from '@flamework/core'
import {
  getGravityControllerUp,
  GravityManager,
  installGravityControllerClass,
  wrapGravityUpSaveAttribute,
} from '@rbxts/gravity-controller'
import { Logger } from '@rbxts/log'

@Controller({})
export class PlayerGravityController implements OnStart {
  private gravityManager: GravityManager | undefined

  constructor(private logger: Logger) {}

  onStart() {
    const cls = installGravityControllerClass()
    this.gravityManager = new GravityManager(cls, this.logger)

    // Enable gravity with surface-following wall walk
    this.gravityManager.enable(
      wrapGravityUpSaveAttribute(getGravityControllerUp),
    )
  }

  disable() {
    this.gravityManager?.disable()
  }
}

Manual lifecycle (without GravityManager)

If you need full control over construction timing:

import {
  installGravityControllerClass,
  getGravityControllerUp,
} from '@rbxts/gravity-controller'
import { Players } from '@rbxts/services'

const GravityControllerClass = installGravityControllerClass()

Players.LocalPlayer.CharacterAdded.Connect(() => {
  const gc = new GravityControllerClass(Players.LocalPlayer)
  gc.GetGravityUp = getGravityControllerUp
})

Custom gravity direction

If you don't want surface-following wall walk, you can point gravity in any fixed direction:

const gc = new GravityControllerClass(Players.LocalPlayer)

// Gravity pulls toward -X (sideways)
gc.GetGravityUp = () => new Vector3(1, 0, 0)

Or reset gravity imperatively:

gc.ResetGravity(new Vector3(0, -1, 0)) // flip upside down

Credits

License

MIT