Wall stick/Gravity Controller

Also, I wanted to ask a couple of questions

  1. Is there a way to toggle this ON and OFF inside a live game. It would be nice to have a this ‘wall walk’ only active if a player has a certain item, or is a certain morph, or is using a certain spell, etc…

  2. Is there an easy way to change the code to where a ‘jump’ will always set gravity back to normal no matter what surface or orientation you are on.
    For example, if I am on a ceiling, instead of jumping making me land back onto the ceiling, jumping makes me fall to the floor

1 Like

Is this possible to make something like that but the gravity only change on special parts


Yes, you could do this. It wouldn’t even be that hard. You could go about this several ways. You could edit the raycasting logic so that it only set the gravity direction when the dot product of the surface normal and current up direction is withing a certain range. For only 45 degree angles or below, this would be dotProduct <= 0.707. Another method you could do is to monitor the last part the player was touching. Then use CollectionService to tag certain parts as being traversable with the gravity controller. Then, if the current or last part is traversable, allow the gravity to be set.

1 Like

Seems like they updated the CameraModule. Now, it returns {} instead of the actual module. Brilliant.

1 Like

Glad someone figured this out…I spent hours trying to figure out what was going on…Not sure how to get around this without forking it :man_facepalming:

1 Like

I’ve been questioning if it’s reasonable to keep making the camera injector work every time a roblox update breaks it vs fork the scripts and update those manually every time roblox rolls out new ones.

But I got the CameraInjector to work anyway.

I replaced:

TransparencyController.Enable = function(self, ...)
	copy(self, ...)
	local env = getfenv(3)
	env.UserSettings = FakeUserSettingsFunc
	local f = setfenv(3, env)
	TransparencyController.Enable = copy
	result = f()
	bind.Event:Wait() -- infinite wait so no more connections can be made


local CameraModuleSelf = nil
local CameraModuleMeta = nil
local metasetmetatable = function(newTable:{}, newMeta:{})
	local env = getfenv(2)
	if env.script==CameraModule then
		CameraModuleSelf = newTable
		CameraModuleMeta = newMeta
	return setmetatable(newTable, newMeta)

local phaseTwoEnable = function(self, ...)
	local env = getfenv(3)
	env.setmetatable = nil
	setfenv(3, env)

	TransparencyController.Enable = copy
	return copy(self, ...)

TransparencyController.Enable = function(self, ...)
	copy(self, ...)
	local env = getfenv(3)
	env.UserSettings = FakeUserSettingsFunc
	env.setmetatable = metasetmetatable
	local f = setfenv(3, env)
	TransparencyController.Enable = phaseTwoEnable
	result = f()
	if result.ActivateCameraController==nil 
		and typeof(CameraModuleSelf)=="table"
		and typeof(CameraModuleSelf.ActivateCameraController)=="function" then
		result = CameraModuleSelf	
	bind.Event:Wait() -- infinite wait so no more connections can be made

This way, when CameraModule.new() is called the second time, I snag the self instance passed into setmetatable and replace the empty table returned by require(CameraModule) with that.

Also, within Camera:Update, you want to get rid of the call to the removed ApplyVRTransform


and, as pointed out by @SelDraken above, in Camera:Update, add the dt parameter

		if self.activeTransparencyController then

I expected there might be other trouble because of all the VR Camera changes, but I couldn’t find any issues with WallStick or GravityController after that.


Do you think this will end up breaking again with new roblox camera updates in the future?

I can promise you it will.

I will probably keep using this technique anyway. The new for Fall 2022 public CameraModule API workaround will probably continue to work for quite a while, but the specific details of CameraModule:Update will probably keep changing and introduce new bugs.

I will probably look into ways it can degrade or fail more gracefully. The big issue with the public CameraModule api workaround as it has been for a while is it depends on setfenv() which may affect performance because luau marks the affected environment as tainted and cannot optimize lua in that context safely. I’m not sure if that just affects CameraModule.new() or actually any of the functions called that may have inherited the context.

With how the camera controller scripts have changed over the last 2 years, I think it may finally be possible to have a custom UpVector for camera control by only changing the various camera controllers and not the CameraModule itself.

Long term, maybe it could be a little more reliable. Maybe.
If you want fixes now, you can see my changes on github


Actually, I may have found a better way. I don’t think the CameraInjector is likely to stop working this time, but most of the problems that came up in the last 2 years-3 years have been from changes in CameraModule.Update that we can’t predict.

I got the WallStick controller to work without modifying the CameraModule, and modifying BaseCamera instead. I had BaseCamera.new() wait to get called, then it wraps the CameraController.Update method instead. The unmodified CameraModule.Update calls my camera controller UpdateWrapper, which calls the controller .Update, then adjusts the cframe and focus to match the custom UpVector.

It also avoids using setfenv, so theoretically this avoids turning off the luau performance optimizations in CameraModule.

IDK how many people are still using this, but if people are interesting, I can create a branch using this technique that I’m pretty sure is less likely to break in the future.

demo code for clarity:

local cameraControllers = {}
local BaseCamera:BaseCamera = require(CameraModule:WaitForChild("BaseCamera"))
local BaseCamera_new = BaseCamera.new
function BaseCamera:UpdateWithUpVector(dt,newCameraCFrame, newCameraFocus)
	return newCameraCFrame, newCameraFocus
function BaseCamera.new()
	local env = getfenv(2)
	local camScript:ModuleScript = env.script

	local newSelfTable:BaseCamera = BaseCamera_new()
	-- camera controller implementation (such as ClassicCamera) has called BaseCamera.new
	-- newSelfTable will be returned to actual camera controller implementation to be initialized
	-- actual controller module table will be set as the metatable of newSelfTable
	-- we can wrap any calls to the actual controller methods before we return newSelfTable:
	if typeof(camScript)=="Instance" and camScript:IsA("ModuleScript") then -- sanity check
		print("BaseCamera.new called for camera controller: ", camScript:GetFullName())
		local controllerInfo = {
			Name = camScript.Name,
			Script = camScript,
			ControllerSelf = newSelfTable,
		cameraControllers[controllerInfo.Name] = controllerInfo
		function newSelfTable:Update(dt)
			local cameraControllerMeta:BaseCamera = getmetatable(self) -- this is the underlying camera controller __index and metatable
			local newCameraCFrame, newCameraFocus 
				=  cameraControllerMeta.Update(self, dt) -- get cframe and focus from underlying camera controller update
			return self:UpdateWithUpVector(dt, newCameraCFrame, newCameraFocus)
		-- sanity check:
		function newSelfTable:Enable(enable: boolean)
			if self~=newSelfTable then
				print("Something is wrong. expected self==baseTable. There will be problems")
			local cameraControllerMeta:BaseCamera = getmetatable(self)
			print("Enabling camera controller:", controllerInfo.Name)
			cameraControllerMeta.Enable(self, enable)
		print("Can't find script calling BaseCamera.new", env)
	return newSelfTable

local Camera = require(CameraModule)

After that, you can wrap the return values from whateverCameraController.Update(dt) by doing this

BaseCamera.UpdateWithUpVector = function(self, dt, newCameraCFrame, newCameraFocus)
	newCameraFocus = CFrame.new(newCameraFocus.p) -- vehicle camera fix

	calculateUpCFrame(Camera, dt)

	local lockOffset = Vector3.new(0, 0, 0)
	if Camera.activeMouseLockController and Camera.activeMouseLockController:GetIsMouseLocked() then
		lockOffset = Camera.activeMouseLockController:GetMouseLockOffset()

	local offset = newCameraFocus:ToObjectSpace(newCameraCFrame)
	local camRotation = upCFrame * twistCFrame * offset
	newCameraFocus = newCameraFocus - newCameraCFrame:VectorToWorldSpace(lockOffset) + camRotation:VectorToWorldSpace(lockOffset)
	newCameraCFrame = newCameraFocus * camRotation
	return newCameraCFrame, newCameraFocus

Yeah this would be extremely helpful as I’ve been trying to make a game using the gravity controller, but the camera breaking has set me back for months in progress :sweat_smile:. It would be a tragedy for this to break in the future.

@EgoMoose, this controller is just incredible, I made a whole game based on it, I just want to say thank you and congratulations on your amazing work.
This is the game if you want to check it out:


Woah, wait this module is compatible with pathfinding? How’d you do that?

It’s not pathfinding, I record each frame crossing the obstacles, I just make dummy repeat them

Awesome work, I have one question though. How did you do the force gravity change and are you using wallstick of gravity controller.

1 Like

Thanks, I’m using gravity controller.
What exactly do you mean by the force of gravity?

At first I thought you were using wall stick because when the play falls off they immediately fall back to normal gravity. I want to know how you managed that because it doesn’t work with normal gravity controller from the copied game.

What do you mean by “repeat them?” can you give a more detailed explanation?

That’s part of the controller, just change the drop distance at which gravity is restored. It’s this part here:

	local height = Controller:GetFallHeight()
	if height < -50 then
		Controller:ResetGravity(Vector3.new(0, 1, 0))

1 Like

I used heartbeat to create a table that contains the position and orientation of each part of the player while I am crossing the obstacle, this table is saved in DataStore. When the obstacle first appears in the game the server downloads it and stores it in a cache for use it every time the obstacle reappears. When the player talks to the dummy the server sends the table to the client and I use heartbeat again to read it and give the dummy a fluid movement, basically it’s a replay.
I made my own code to do this, but there is a module that does all this, it didn’t work for me, but the idea definitely came from there


Very much interested also. My spider morphs need to be able to climb walls again. Thanks