Features:
- Reliably orients with the ground including when entity is on top of two (or more) different surfaces
This allows for a smooth transition from surface to surface which also makes it less “bumpy”.
A common method is to use the surface normal at one point which doesn’t account for cases such as when half of your entity’s body is on a ramp and the other half isn’t where you now want to orient sort-of “halfway” between both surfaces. This works around this by raycasting at 3 points and determining the “plane / flat surface” which your entity naturally sits on with its legs.
- Freely moving legs with BallSocketConstraints
These help the entity get stuck less often and also give it some minor climbing power when an obstacle is encountered.
- MaxSlopeAngle which you can set to control how steep your entity can climb
Works by calculating the mass of the rig and adjusting BodyVelocity accordingly. Entity will also be slightly slower on steeper surfaces as expected and also slip down if moved over a surface too steep.
- Adjustable Speed / SprintSpeed / TurnSpeed / MaxJumpHeight / JumpCooldown
If you wish to use the same system. Can alternatively implement an acceleration of speed to a MaxSpeed.
TestRigJump.rbxl (146.2 KB) (Hit Play, not run Or hit F5 to demo)
Remember to set green parts to Transparency 1 and possibly attach a rig which can run animations.
(Recommend replacing UserInputService with ContextActionService to work on other devices)
Can be applied to non-player entities / animals throughout your game too.
Full source code (if just want to read):
--Remember to set NetworkOwnership in practice (See ServerScriptService.HorseServerScript which does this only for this demo)
--BodyGyro may need higher MaxTorque values for heavier rigs if more non-massless parts are joined with the rig
--//Settings//--
local Speed = 30 -- studs per sec
local SprintSpeed = 50 -- studs per sec
local TurnSpeed = 0.03 -- radians per frame
local MaxJumpHeight = 8 -- studs
local JumpCooldown = 1 -- seconds
local ResetOrientationMidair = true
--//
--//Main Variables//--
local player = game.Players.LocalPlayer
local cam = workspace.CurrentCamera
local model = workspace.Horse
local hrp = model.HumanoidRootPart
local bg = hrp.BodyGyro
local bv = hrp.BodyVelocity
local UserInputService = game:GetService("UserInputService")
local p1, p2, p3 = hrp.Attachment1, hrp.Attachment2, hrp.Attachment3 --attachments for determining orientation
local jumpTick = 0
--//
--//Initial Setup//--
player.Character = model
bg.CFrame = hrp.CFrame
model.Humanoid.PlatformStand = true
hrp.Anchored = false
--//
--//Gather input and set to a value of 1 while held, 0 otherwise//--
local w, a, s, d, shift, space = 0, 0, 0, 0, 0, 0
--Recommend using ContextActionService in practice to work with TouchScreens and GamePads
local function onInputBegan(input, gameProcessed)
if input.KeyCode == Enum.KeyCode.W then
w = 1
end
if input.KeyCode == Enum.KeyCode.A then
a = 1
end
if input.KeyCode == Enum.KeyCode.S then
s = 1
end
if input.KeyCode == Enum.KeyCode.D then
d = 1
end
if input.KeyCode == Enum.KeyCode.LeftShift then
shift = 1
end
if input.KeyCode == Enum.KeyCode.Space then
space = 1
end
end
local function onInputEnded(input,gameProcessed)
if input.KeyCode == Enum.KeyCode.W then
w = 0
end
if input.KeyCode == Enum.KeyCode.A then
a = 0
end
if input.KeyCode == Enum.KeyCode.S then
s = 0
end
if input.KeyCode == Enum.KeyCode.D then
d = 0
end
if input.KeyCode == Enum.KeyCode.LeftShift then
shift = 0
end
if input.KeyCode == Enum.KeyCode.Space then
space = 0
end
end
UserInputService.InputBegan:connect(onInputBegan)
UserInputService.InputEnded:connect(onInputEnded)
--//
game:GetService("RunService").RenderStepped:Connect(function ()
--//Update Movement//--
local vectorMoveDir = (Speed + shift*(SprintSpeed - Speed))*(w*Vector3.new(0, 0, -1) + s*Vector3.new(0, 0, 1)).Unit
local bool = vectorMoveDir.X == vectorMoveDir.X
vectorMoveDir = bool and vectorMoveDir or Vector3.new(0, 0, 0) --Avoid NaNs
bv.Velocity = hrp.CFrame:VectorToWorldSpace(vectorMoveDir)
--//
--//Ground orientation math//--
local down = -10*hrp.CFrame.upVector
local r1, r2, r3 = Ray.new(hrp.CFrame:PointToWorldSpace(p1.Position), down), Ray.new(hrp.CFrame:PointToWorldSpace(p2.Position), down), Ray.new(hrp.CFrame:PointToWorldSpace(p3.Position), down)
local o1, p1 = workspace:FindPartOnRay(r1, model)
local o2, p2 = workspace:FindPartOnRay(r2, model)
local o3, p3 = workspace:FindPartOnRay(r3, model)
local upVector = (o1 and o2 and o3) == nil and bg.CFrame.UpVector or -((p1-p2):Cross(p3-p2)).Unit
--//
--//Max slope angle math//--
local angleSlope = math.acos(upVector.Y)
local maxAngle = math.rad(model.MaxSlopeAngle.Value)
local mass = 0
for _, v in ipairs(model:GetDescendants()) do
if v:IsA('BasePart') then
mass = mass + v:GetMass()
end
end
if angleSlope > maxAngle then --limit rotation beyond maxAngle and reduce climbing power
bv.MaxForce = Vector3.new(0.5, 0, 0.5)*mass*workspace.Gravity*math.tan(maxAngle)/math.sqrt(2)
upVector = (math.sin(angleSlope-maxAngle)*Vector3.new(0,1,0) + math.sin(maxAngle)*upVector)/math.sin(angleSlope)
else --increase climbing power to a little beyond the required amount of force to reach maxAngle
bv.MaxForce = Vector3.new(1, 0, 1)*mass*workspace.Gravity*math.tan(maxAngle)/math.sqrt(2) + Vector3.new(500, 0, 500)
end
print(math.deg(angleSlope))
--//
--//Jumping//--
bv.MaxForce = bv.MaxForce*Vector3.new(1, 0, 1)
bv.Velocity = bv.Velocity*Vector3.new(1, 0, 1)
if space == 1 and tick() - jumpTick > JumpCooldown and (o1 or o2 or o3) then
bv.MaxForce = bv.MaxForce + Vector3.new(0, math.huge, 0)
bv.Velocity = bv.Velocity + Vector3.new(0, math.sqrt(2*workspace.Gravity*MaxJumpHeight), 0)
jumpTick = tick()
print("Jumped")
end
--//
if (o1 or o2 or o3) == nil and angleSlope > 0 and ResetOrientationMidair then
upVector = (math.sin(0.9*angleSlope)*upVector + math.sin(0.1*angleSlope)*Vector3.new(0, 1, 0))/math.sin(angleSlope)
end
local rightVector = (bg.CFrame.lookVector:Cross(upVector)).Unit
local pos = hrp.Position
bg.CFrame = CFrame.fromMatrix(pos, rightVector, upVector)*CFrame.Angles(0, TurnSpeed*(a-d), 0)
end)
EDIT: Working on making the demo easier to make into a ridable mount
Extras
If you use this in a creative way, I’d love to see it in a reply here or on Twitter:
https://twitter.com/quasiduck