Easy Entities: Free resource for entities that reorient with the ground reliably + other features


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

86 Likes

Thank you for this.
I took it for a spin and it seems to work nicely.
The adjustable speed you mentioned would be a nice addition. Being able to jump realiably without ever toppling over would also be a nice feature to have.

2 Likes

This looks really great! Thank you for this.

1 Like

I’ve now added a reliable jump, made the code neater with more comments, and added a feature that resets the horse orientation smoothly in mid-air which you can toggle.

1 Like

That’s awesome. Thank you for sharing this.
I can see myself having use for this in the future and make some fun project.

Was literally trying to make something for this a week ago, but failed miserably… Can’t believe a module for this comes out shortly after!

Kudos!

Do you think you could explain the math behind this?
I’m trying to do something similar for my hover car but I can’t wrap my head around how exactly this code works.

It works by rotating the model with the BodyGyro and applying velocity in the forward direction that the model is currently facing via BodyVelocity.

Any flat plane or surface can be made to pass through any three 3D points.

So for the CFrame I supply to the BodyGyro, I ray cast down from three different points on the main body to acquire three points below the body which I then use to approximate the surface that the entity is currently standing on.

Then, I find out the surface normal of the approximate surface that all three points sit on and supply this as the upvector for the BodyGyro CFrame.

I then cross the current bodygyro cframe lookvector with that upvector to get the rightvector and then use CFrame.fromMatrix(pos, rightVector, upVector).

It may be kind of overengineered as you can just use AlignOrientation constraints to turn and BodyVelocity to move which would instead just use Roblox’s physics engine to do most of the ground orientation.

2 Likes

That’s really amazing, thanks for all the open source that you have given to everyone!!

2 Likes

Hey! First of all, this is awesome. I’m using it for a horse in our game. But do you know what the reason could be that the horse doesn’t jump?
image
This is the jump code, and the animation runs every time I hit space outside of when on cooldown. I also printed the VEC(0, SQRT(2*GRAV*MAX_JUMP_HEIGHT), 0) and it came out as:
image

Video: https://gyazo.com/b64c3db67f41249382445175124116c5

It works sometimes, it’s just not consistent at all. Any ideas?