PhysicsCharacterController | Mover Constraints based character controller

Just a small project that I initially made in 2-4 + hours purely for learning purposes that I wanted to share since I see not a lot of example projects for character controllers here on the dev forum the only one other being @EgoMoose platformer controller.

However soon later found out it was a lot of fun to play with and thought to modularize it further into a full blown project.

Objectives:

  1. Similar to default humanoid controller movement running

  2. Humanoid autorotate

  3. Controllable friction (ground and air)

  4. OOP Composition based, tried to make it as modular as possible

Cons:

  1. No truss climbing, swimming, and maximum slope height control
  2. Hipheight is a bit bouncy and not rigid like Humanoid

Showcase:


Physics based movement

Has acceleration and is adjustable, also now has default animations from the default animate script.

Slide Component: Press and hold F to slide:

Similar to tf2 trimping
(Pretty simple apply impulse force, disable drag force)
No animations or state supported so it does a funny little static run animations bug,

Mostly intended as an example for the concept on how to add additional components to the PhysicsCharacterController object beyond the run and jump CoreComponents.

Assets


Uncopylocked place file:

GitHub, Rojo project, has a MIT license for simplicity:

Credits:

lua fsm for managing state


Usage:


Example local script usage on the API:

--StarterPlayerScripts, runs once
local PhysicsCharacterController = require(script.PhysicsCharacterController)

local player = game.Players.LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()

local AnimateFunction = require(script.Parent.AnimateModule)

local initChar = function(newCharacter : Model)
	local rootPart = newCharacter:WaitForChild("HumanoidRootPart")
	local humanoid : Humanoid = newCharacter:WaitForChild("Humanoid")
	humanoid.PlatformStand = true

    --Object auto destroys input when character is not in workspace (Destroyed)
	--Within InitUpdateDefaultControls()
    --Dirty but works
	repeat task.wait() 
		-- warn("Waiting until workspace")
	until newCharacter.Parent == workspace

	local characterController = PhysicsCharacterController.new(rootPart)
	characterController:AddDefaultComponents()
	characterController:AddComponent("Slide")
	-- characterController:AddComponent("AirStrafe") --Component is WIP

	characterController:Run()
	characterController:ConnectComponentsToInput()
	task.spawn(AnimateFunction, newCharacter, characterController) --Init animation based on default animate script
end

player.CharacterAdded:Connect(initChar)

initChar(character)

Additionally there are attributes that are created by the script onto the character model which control the movement the explanation is listed below:

Summary
  • Bounce → Dampening on the spring based standing force, increased bounce value causes decreased dampening value causing more bounce

  • Suspension → More suspension means more suspension force, 11000 was the max value I found before the springs were too strong and shot itself to space.

  • HipHeight → Self explanatory default humanoid hipheight calculations with an added + 0.5 for the spring system

  • FlatFriction → Friction opposite of velocity direction while moving, always a constant value of 500 and slowly stabilizes at 0 when not moving allowing for responsive stops.

  • WalkSpeed → Maximum velocity when holding onto a key.

  • XZDragFactorVSquared → Coefficient for the drag forces, more value == more drag, also in the calculation for walk speed do not set to zero or else the movement force will be zero, To note setting it to a low value such as 0.1 will cause the acceleration to decrease.

image

Old single script code in GitHub for reference before OOP was implemented

105 Likes
Recreate this type of movement?
New Humanoid Physics Controller - Beta
Help With Sliding Mechanic In My Game
Drifting skis issue
Velocity+Walking
How to make humanoids auto rotate, rotate faster?
Make Player Stay Upright
New Humanoid Physics Controller - Beta
Player gets pulled down before even landing?
Aircraft glitching at high speeds or low FPS
Help me achieve this Movement System
Where to start with Custom Capsule / Sphere Character Controller?
How to make character not loose momentum mid air
Can't turn around my character without stopping their movement
What are you working on currently? (2023)
Making player skid?
How do i make a movement system
(Game-breaking) Recent update breaks RigidConstraints
Addition of delta time destroys drag and thrust equations
What are you working on currently? (2023)
Script Structure and Communication Advice
Help on making character turn slower
Creating smooth terrain and part terrain planet gravity. edit: for a physics based car now
OOP Combat System/State Combat System
Working On A Movement System
Raycast suspension, the friction force makes car fling and and applies a high number forces to the car
How would I implement Source Engine movement into my game?
Raycast based vehicle sliding down slope
Character decelerate after stopping
How would I use the force that a SpringConstraint calculates?
How to make a momentum change
Moving character along with a spring
VectorForce acting weird
Controlling aerial player movement
Creating the opposite of humanoid auto-rotate?
Is it possible to get attribute on player with separated script?
Trouble Creating Dash/Roll
Custom Character Consistent Turn Speed
How to keep momentum if you touch the ground?

is this compatible with mobile? I’ll prob replace with the old ones i have if it does.

6 Likes

Should be for the movement and jumping as it uses humanoid.MoveDirection and UIS.JumpRequest which shouls be cross platform able.

The sliding though uses the F key so you might have to rebind it on your own.

2 Likes

i kinda cannot edit the place as it’s having Team Create on, can i have the place file instead?
also i don’t wanna copy the code since it has Attributes so it would be easier for me to edit values
image

1 Like

You can copy the code since the attributes are created via the local script itself and given to the character model.

For the team create a lot has changed since I created a place I’ll check it out thanks.

Edit: I disabled team create it should work now.

Update:

  • Default animation script support

Old prototype for reference:

  • Reorganisation

  • Post overhaul

  • Uploaded to GitHub

Project should be good for basic usage now. However there should be problems with the hip height as the current method doesn’t seem to cling to the ground like a Humanoid. Please let me know if you know an alternate method of doing so.

2 Likes

I love this, actual momentum! There’s one glitch I’ve encountered, though:

There isn’t seemingly support for sitting, either.


In this video, everything’s working fine until I sit. Standing up, my character falls over then starts briefly vanishing, even freezing Studio if I stand still too long.

If StreamingEnabled is on, the “gameplay paused” message appears each time the character disappears, and even completely freezes the game if I don’t move.

It looks like your script disables the built-in character movement code, but after I jump out of a seat, it somehow gets re-enabled.

Is there a way to fix this, or a planned update to add support for sitting? I’d like character momentum in my experience, and also chairs (for restaurants and stuff).

3 Likes

Oh yeah thanks, never ocurred to me to test sitting when I was mostly testing the movement :sweat_smile:

The dissapearing glitch is probably a NaN error with move direction and velocity, seems to be an error with the humanoid (causing problems again) being reenabled after sitting as you mentioned probably in Running.lua component.

Will check it out when I get the time, thanks for the support.

3 Likes

Hmm… I thought of something right before you replied; When looking through the scripts to change the default attribute values to fit my game, I noticed you enable PlatformStand on the Humanoid; Maybe sitting overrides that, so I’d have to re-enable PlatformStand upon exiting a seat.

No problem; I saw your post in the topic about Roblox’s upcoming new character controller and from what I’ve tested of it, I feel your script does basically everything I’d like Roblox’s official one to do.

I’ll keep your movement script in my game since it’s an improvement over the default to me.

One last question, though. My game uses a custom Animate script replacement; How easy would it be to convert it to a compatible ModuleScript to use in place of this movement system’s AnimateModule? Apologies if this is getting too off-topic.

My custom Animate script
-- Player Animation LocalScript replacement, initially based on GnomeCode's custom character animation script but more fleshed-out.

-- SERVICES
local Players			= game:GetService("Players")
local ReplicatedStorage	= game:GetService("ReplicatedStorage")

-- REFERENCES
local LocalPlayer		= Players.LocalPlayer
local AnimationRemote	= ReplicatedStorage.Remotes.ManageAnimations

local rCurrentSeat		= nil	-- Chair LocalPlayer is currently sitting in.
local rSeatWeld			= nil	-- The seat's "SeatWeld" weld, created by sitting on it.
local SeatWeldPlayer	= nil	-- Player object reference, gotten using GetPlayerFromCharacter on the weld's second part.

while not LocalPlayer:GetAttribute("Loaded_LoadScreen") do
	task.wait(0.5)
end
-- Animation references. (Don't reference their Value or the animations won't be changable mid-game!)
local rStandAnim		= script:WaitForChild("StandAnim",3)
local rSitPose			= script:WaitForChild("SitPose",3)
local rToolHoldPose		= script:WaitForChild("ToolHoldPose",3)
local rRunAnim			= script:WaitForChild("RunAnim",3)
local rClimbAnim		= script:WaitForChild("ClimbAnim",3)
local rJumpAnim			= script:WaitForChild("JumpAnim",3)
local rFallAnim			= script:WaitForChild("FallAnim",3)
local rLandAnim			= script:WaitForChild("LandAnim",3)
local LocalHumanoid		= LocalPlayer.Character.Humanoid

-- Constantly monitor this player's Character for a tool; If they carry a tool with a handle, show the "holding tool" pose.
-- TODO: Re-submit the "holding tool pose" animation with Movement priority, so it just "works" with any sitting animation.
task.spawn(function()
	local PlayerTool = nil
	local PlayerToolHandle = nil
	local DefaultToolPoseActive = false
	while LocalPlayer.Character.Humanoid do
		PlayerTool = LocalPlayer.Character:FindFirstChildOfClass("Tool")
		if PlayerTool then
			PlayerToolHandle = PlayerTool:FindFirstChild("Handle")	-- Find the tool's Handle and make sure it's a descendant of BasePart.
			if PlayerToolHandle and PlayerToolHandle:IsA("BasePart") then
				if not DefaultToolPoseActive then
					DefaultToolPoseActive = true
					AnimationRemote.PlayAnimation:FireServer(nil, rToolHoldPose.Value, nil, 0.333, 21)	-- Show the tool-holding pose.
				end
			end
		elseif DefaultToolPoseActive then	-- If the player isn't holding a tool, lower the player's arm.
			DefaultToolPoseActive = false
			AnimationRemote.StopAnimation:FireServer(nil, 21, nil, nil, 0.25)
		end
		
		task.wait()
	end
end)

local CurrentSpeedAnimation = 0
LocalHumanoid.Running:Connect(function(speed)
	if speed > 0 then	-- If running, stop any standing animations then play the running loop.
		AnimationRemote.StopAnimation:FireServer(nil, 13, nil, nil, 0.25)	-- Standing
		AnimationRemote.StopAnimation:FireServer(nil, 14, nil, nil, 0.25)	-- Idle
		
		AnimationRemote.PlayAnimation:FireServer(nil, rRunAnim.Value, nil, 0.2, 16)
	else	-- Otherwise, do the opposite, and stop all movement-related animations before playing the standing loop.
		AnimationRemote.StopAnimation:FireServer(nil, 15, nil, nil, 0.25)	-- Walking
		AnimationRemote.StopAnimation:FireServer(nil, 16, nil, nil, 0.25)	-- Running
		
		AnimationRemote.PlayAnimation:FireServer(nil, rStandAnim.Value, nil, 0.15, 13)
	end
end)

-- Plays and stops player animations using ManageAnimations based on built-in state transitions.
LocalHumanoid.StateChanged:Connect(function(_oldState, _newState)
	-- First, check what state the player was in before this, and stop that animation.
	if _oldState == Enum.HumanoidStateType.Seated then
		AnimationRemote.StopAnimation:FireServer(nil, 12, nil, nil, 0.175)
		SeatWeldPlayer = nil	-- Invalidate seat-related variables.
		rCurrentSeat = nil
		rSeatWeld = nil
	elseif _oldState == Enum.HumanoidStateType.Running then	-- Running is used for both standing AND moving, confusingly.
		AnimationRemote.StopAnimation:FireServer(nil, 13, nil, nil, 0.25)	-- Standing
		AnimationRemote.StopAnimation:FireServer(nil, 14, nil, nil, 0.25)	-- Idle
		AnimationRemote.StopAnimation:FireServer(nil, 15, nil, nil, 0.25)	-- Walking
		AnimationRemote.StopAnimation:FireServer(nil, 16, nil, nil, 0.25)	-- Running
		AnimationRemote.StopAnimation:FireServer(nil, 19, nil, nil, 0.125)	-- Landing

	elseif _oldState == Enum.HumanoidStateType.Freefall then
		AnimationRemote.StopAnimation:FireServer(nil, 18, nil, nil, 0.25)	-- Falling
	elseif _oldState == Enum.HumanoidStateType.Climbing then
		AnimationRemote.StopAnimation:FireServer(nil, 20, nil, nil, 0.25)	-- Climbing
	end
	
	if _newState == Enum.HumanoidStateType.Seated then
		-- Now, this is a bit unnecessarily complex, but check the entire workspace looking for a SeatWeld that links to this player.
		-- If so, check if the seat the player's in is supposed to use a custom sitting animation, showing that instead of the default
		-- pose if it's requested.
		print("Sitting started! Scanning workspace for SeatWeld instances.")
		for _, _instance in ipairs(workspace:GetDescendants()) do
			if _instance:IsA("Weld") and _instance.Name == "SeatWeld" then	-- If this is a Weld named SeatWeld, try to grab its player.
				print("Valid SeatWeld found at ", _instance:GetFullName(), ". Trying to find out what player 'owns' it.")
				rCurrentSeat = _instance.Parent
				rSeatWeld = _instance
				SeatWeldPlayer = Players:GetPlayerFromCharacter(_instance.Part1.Parent)
				
				print("Player associated with this weld should be", _instance.Part1.Parent:GetFullName(), ".")
				if SeatWeldPlayer then
					if SeatWeldPlayer.UserId == LocalPlayer.UserId then	-- If the detected player has the same ID, we've found the Weld we're looking for!
						print("Found you! " .. SeatWeldPlayer.DisplayName .. "'s sitting on " .. rCurrentSeat:GetFullName() .. "!")
						break
					else	-- If it isn't, nil the variable so it isn't detected when this for-loop ends.
						print(rCurrentSeat:GetFullName() .. " isn't being sat on by you.")
						SeatWeldPlayer = nil
					end
				end
			end
		end
		
		-- If this player's seat weld was found and this chair has a custom sitting animation, decide what animation to play based on that.
		local rSitAnim	= rCurrentSeat:FindFirstChild("SitAnimation")
		if SeatWeldPlayer and rSitAnim then
			local SitAnimList = {}
			-- Check if this is an array (comma-separated list with no spaces) or single animation ID (integer).
			-- Though this is always an array, it only contains a single index if this is an IntValue. StringValues use more indices, to be
			-- chosen randomly when firing the RemoteEvent below.
			if rSitAnim:IsA("IntValue") then SitAnimList[1] = rSitAnim.Value
			elseif rSitAnim:IsA("StringValue") then
			local temp_commaSeparatedAnimList = string.split(rSitAnim.Value,",")
				-- Split the string value into parts, then add each ID to the animation list array. A random animation will be chosen to play.
				for i = 1, #temp_commaSeparatedAnimList, 1 do
					SitAnimList[i] = tonumber(temp_commaSeparatedAnimList[i])
				end
			end
			
			AnimationRemote.PlayAnimation:FireServer(nil, SitAnimList[(math.random(0,255)%#SitAnimList)+1], nil, 0.25, 12)	-- Pick an animation to use.
		else
			AnimationRemote.PlayAnimation:FireServer(nil, rSitPose.Value, nil, 0.25, 12)	-- Show the default sitting pose.
		end
	elseif _newState == Enum.HumanoidStateType.Jumping then
		AnimationRemote.PlayAnimation:FireServer(nil, rJumpAnim.Value, nil, 0, 17)
	elseif _newState == Enum.HumanoidStateType.Freefall then
		AnimationRemote.PlayAnimation:FireServer(nil, rFallAnim.Value, nil, 0.125, 18)
	elseif _newState == Enum.HumanoidStateType.Landed then
		AnimationRemote.PlayAnimation:FireServer(nil, rLandAnim.Value, nil, 0.075, 19)
	end
end)
2 Likes

For the animations its pretty simple just needs the object passed and get the signals connected like so:

--Hook up 
    local stateSignals = characterController._StateSignals
    local connection1 = stateSignals.Running:Connect(onRunning)
    local connection2 = stateSignals.Jumping:Connect(onJumping)
    local connection3 = stateSignals.FreeFalling:Connect(onFreeFall)
    local connections = {connection1, connection2, connection3}
    local stateMachine = characterController._StateMachine
    stateMachine.on_land = function()
        pose = "Running"
        onRunning(0) --Reset to standing idle animation
    end

Oh! That sounds easier than I was thinking. I’m going to have to try “porting” the script over. If sitting is fixed, everything could be brought over.

Speaking of sitting, indeed, it disables PlatformStand the moment a character sits. I tried adding a line to my script that forcibly enables it again after transitioning from the Seated humanoid state, but it doesn’t prevent the NaN bug.
NaN_bug.png
If I jump almost immediately after sitting, it may not become NaN; One time, it just combined both of the movement scripts. If the NaN glitch starts, movement also begins to feel laggy, almost as if network ownership of the player was given to the server.

2 Likes

Update


Alright seating and NaN should be fixed for the most part now.

Unfortunately this came with a new quirk for seating as this still uses the default Humanoid seating system (needs to create seat weld like original Humanoid) so it does this:

  1. default humanoid.Jump to get out the seat
  2. original jump impulse of the controller.

This causes the character to jump slightly higher, I tried decreasing the jump power on this special case but it seems the value needs adjusting.

--HumanoidOnSeat component
--240 less impulse force to account for this odd humanoid unseat behavior
        self._PlatformStanding = humanoid.PlatformStanding:Connect(function(active)
            if active == false and humanoid.SeatPart == nil then
                local debounceTime = data._Model:GetAttribute("JumpDebounceTime")
                local Jump = data:GetComponent("Jump")
        
                humanoid.PlatformStand = true
                if not debounce then
                    debounce = true
--240 less impulse force to account for the humanoid unseating behavior
                    Jump:_RawJump(data, 240)

                    task.wait(debounceTime)
                    debounce = false
                end
            end
        end)

Also the delay in seating is due to me testing it with studio network lab enabled:

Also now JumpPower and JumpDebounceTime has been added as additional attributes for playing around with if needed.

Updates should be both in the place file and GitHub

3 Likes

Thanks for the quick update! I just updated the script used in my game and seats indeed now work! This is almost perfect, but there’s two things that aren’t:

  • No CanCollide ray check - It seems collision detection is done using a downward raycast from the player. This mostly works, but it doesn’t ignore invisible technical parts that have CanQuery set but not CanCollide. I fixed this by changing line 109 of HipHeight to “if raycastResult and raycastResult.Instance.CanCollide then”.

  • VR controller support? - This is probably way out-of-scope of this script since it involves someone else’s system, but I use Nexus VR Character Model, which seems to just send the usual humanoid-moving commands to move the player around when in smooth locomotion mode.

    When I tested out this script in VR, the player couldn’t move at all, so I just edited the main script so it doesn’t load the physics-based movement code if VR is detected.

    Momentum in VR would be neat, but I suppose it isn’t required.

Still, tough, your script works very well as-is, and will hold me over until Roblox’s potential new character controller is added to the engine. :woot:

2 Likes

Well, I brought over my Animate script, and got it to work with this script. I’ve found one oddity with it in the process, though. Functions like Humanoid.Jumping and Humanoid.FreeFalling normally receive an argument that’s true if entering the state and false if exiting it, but PhysicsCharacterController seems to send “false” to it when the state is entered, and only then.

I was trying to fix my script earlier, even adding print statements to see when my functions were running, and that led to this discovery. In my copy of the script, I edited line 117 of the main script to be:

			signal:Fire(true)

Now my jump and fall animations play, and mostly everything works. I just wanted to update with my findings about the state machine giving the connections a different value than default Roblox logic.

EDIT: Oh, yes, and an update about the whole “PhysicsCharacterController doesn’t support VR controllers” thing that I said: It actually does, which I found out when Nexus VR Character Model randomly stopped initializing properly. For some reason, Nexus VR is what breaks PhysicsCharacterController, not controllers specifically.

2 Likes

Getting used to it but I’ve found a bug where player character is possible to enter a part that were collided, also somehow collision group makes your character assigning the position like climbing on a mesh in the test place if humanoids were walking towards.

3 Likes

With Roblox’s new beta character controller announced, I’ve returned to say that as of now, I still think PhysicsCharacterController, even with its lack of swimming and climbing noted in the first post, is better than it for satisfyingly smooth movement. :woot: This video shows how smoothly I can curve around obstacles, which I can’t do with Roblox’s official physics-based movement (as it slows me down any time I change directions, unlike here, where turns smoothly transfer momentum).

My settings for PhysicsCharacterController
  • Bounce: 15
  • FlatFriction: 150
  • HipHeight: 3.306 (edited calculation in scripts somewhere for better foot alignment)
  • JumpDebounceTime: 0.175
  • JumpPower: 425
  • Suspension: 1000
  • WalkSpeed: 45.25
  • XZDragFactorVSquared: 0.177

In case this isn’t worthy of a reply to this topic, I have a bug to report; If your game or Roblox Studio lags for any reason, the physics completely break, usually making my character bounce very high into the air, becoming impossible to recover from if enough lag happens.

If merely moving your mouse cursor around the 3D viewport makes Studio lag like my computer unfortunately does, try shaking it around constantly and as the framerate stays low, your character may go flying… This reminds me of a very iffy game engine named CopperCube, where the framerate dropping also made physics unstable. When someone tried my baseplate, I guess they had a really bad frame rate, as they bounced like this constantly while I had PhysicsCharacterController on.

4 Likes

Apologies for being a little late. A lot of the concepts to do with ModuleScripts are new to me. I’m attempting to add a sliding animation for the slide mechanic. No matter what I try I can’t seem to connect the Slide module to the Animation module. Do you mind walking me through how you would go about adding animations for any modules in the AddonComponents folder?

Apologies for the delays, currently to implement slide animation you would have to edit with the state machine in the main module script, then switch the state in the slide module, then go to the animate script to add in your custom play animation on state events.

It’s a bit messy which is why I’m trying to think of a better way to handle state and such perhaps if the state machine can be edited from the additional component modules instead.

Also with sliding I encountered some collision issues as the hip height is not rigid, however I found a solution which is to use a rolling sphere as the collision box which is what EgoMoose and X_O has done for their character controllers.

I 100% recommend discarding the player module
In my opinion
It just has so many limitations ;-;

i like how u used attributes tho :smiley:

nice work btw!

1 Like

does humanoid.Running or humanoid events work on this in general? ive tried connect the Running event and nothing happens
also, will animations play if you just local anim = animator:LoadAnimation(animation) anim:Play()

1 Like