[Release] Custom character controller

So I reworked my CameraScript to integrate the character controller instead of using an independent part with AlignPosition and AlignOrientation. This required some modifying of the controller’s script to hold a little extra information and place some of its data in some ObjectValues, namely the lastHit and fakeLastHit values. I made sure to stay organized by commenting the modifications starting in pkg and ending with an identifier.

pkg LastPart

I created a relationship between one part’s cframe in fakeWorld and the other one in the real workspace using this formula:

(part.CFrame - part.Position) * (fakePart.CFrame - fakePart.Position):Inverse()

and I used this as the root angle for the camera. Usually, the lastHit part and its fake counterpart is used, and it falls back to the HumanoidRootPart if the lastHit part doesn’t exist. This is kept track of using ObjectValues parented to the controller.

I figured out the ControlScript as well, so have fun moving freely between platforms and not getting frustrated because you’re at some weird angle.

I listed some pros and cons compared to the file in my last post:

Pros:

  • The camera now rotates with parts you are standing on that are spinning on their Y-axis
  • The controls synchronize perfectly with the camera’s movement, which means no more frustration with moving on spinning platforms.
  • No extra part / aligning overhead, if that matters at all

Cons/New Bugs:

  • the camera now spins by 180 degrees whenever you switch between a slope pointing towards the X axis and one pointing towards the Z axis and vice versa, but only while upside-down.
    – This was what my last implementation was able to fix, by being independent from the character controllers.
  • This causes the camera to spin on meshes while upside-down as well.
  • There is still a camera spin whenever there is a transition between an object staying still and an object spinning on its Y-axis, I think I only saw this while standing upside-down as well.

pkg CameraOffset (doesn’t work, addressing a bug)

I haven’t figured out a fix to this upside-down spin bug, but it will probably have to deal with more CFrame manipulation. I will have to understand how to detect when this spin happens though. I almost solved it inside the controller itself (with this package), but it only corrects itself when the character is completely upside-down.

Here is the playground file with the updated ControlScript:

playgroundCamera.rbxl (361.0 KB)

I have added a few keybinds for convenience, that being Tab for resetting movement and Left Ctrl for flipping gravity, which is a really fun feature by itself.
I also added just a few extra obstacles here and there to test out stuff, like stairs.

12 Likes

the long awaited for…

pkg CameraOffset

So I have fixed this upside-down bug, very proud of myself for this one. Mainly because I now know how to use cross product and dot product better. What I did was:

  • I looked in the method for controller.setPart, as that is the relevant method
  • I took the math.atan2 between the X and Z coordinates of private.lastAxis,
  • did the same for the current cross product,
  • took the difference,
  • made sure that the change in angle was never greater than pi.
  • fired an event with this number as the parameter
  • CameraScript’s x value changed accordingly
  • I also copied this over to an extraRotation variable so that the humanoid’s movement did not need to catch up with the camera

Whenever the event fired, the value passed in is added to the x value in the CameraScript, and it makes transitions between slopes nearly seamless.

pkg rPartRot

I have also partially fixed the camera spin bug from rotating parts. It is dependent on the LookVector of the rPart, which means it would be hard to orient these parts correctly if they are asymmetrical, or if the part spins on it’s LookVector. This platform is an example:

Other than this, pkg rPartRot also works when transitioning between two sets of rotating parts at the same time. This could be useful for obbies if you make the gravity independent of the rotating part when you jump through the setNormal method.

My method of implementation for this was limited: it uses very similar functions as the code for pkg CameraOffset. Expansion on this will be next on my to-do list.

Keep in mind that the trigger for this function is dependent on the fact that every rotating part is named “rPart,” just like the autoRotate function that called it.

Controller Comments:

I annotated the controller quite a bit if you didn’t know how it works, albeit you probably still won’t know when you’ve read over it for 20 minutes straight. So, I’ll give the rundown of what happens in the playground file:

  1. The controller is instantiated with the character as the parameter.
  • A fake, or phys,version of the character is created, which controls the physics of the real one. This is known as the physCharacter in the script.

– The physics are manipulated through an AlignOrientation and AlignPosition object, called forces in the script, and the real character’s humanoid is placed in the PlatformStand state.

  • A fake version of the world is also created at WorldCenter, which defaults to 5000 studs away in the X direction. This is the internal physics engine that determines the orientation of your character in the real world.
  1. Every frame, or when autoRotate is called:
  • getFilter is called

– The controller gathers every object in the workspace.World folder that intersects with a 10 stud length Region3 placed at the center of the character’s HumanoidRootPart. This collection of objects is known as a filter in the script.

  • castFeet is called.

– This method casts a cylinder of rays below the character’s feet onto this filter. These rays expose information about the land the character is directly standing on, such as normals, whether it’s complex geometry, etc.

  1. Based on the info gathered from castFeet:
  • If there is a new part, setPart is called.

– This is the method that sets the cframe of your character relative to the world, and the “cframe of the world” relative to your physCharacter in the fakeWorld.

  • updateFakeWorld and updateCharacter are unconditionally called.

updateFakeWorld makes sure to acknowledge any parts that were picked up by the filter, and places any new ones in the fakeWorld, and destroying any old ones.

  • updateCharacter is called

– This sets the Attachment1 property’s CFrame of the AlignPosition and AlignOrientation that is applied to your character. It also makes the physCharacter move in the same direction you are moving

Hopefully, this betters your understanding of what the controller does, along with the existing comments.

Here is the normal file:

playgroundCamera.rbxl (96.4 KB)

This is the debug file:

playgroundCamera.rbxl (106.2 KB)

I don’t understand how these files are different sizes… all I did was change two variables and move the spawn location…

Since it is now possible to walk on spheres and other nice things with good camera and character stability, it might be a cool idea to create some kind of game with planetary gravity mechanics.

30 Likes

Holy smokes dude this is absolutely amazing! I really appreciate that you put the time into making this all the better. Mad respect!

10 Likes

Thanks for the praise!
Honestly, I can see something big coming from this character controller if it gets optimized enough. It could be some big game in the future, or maybe it could inspire something in the Roblox development pipeline.
Even more honestly, it’s all thanks to you for creating this. I have never seen something like this ever on Roblox, and I just want to see someone use it in their game.

Speaking of, I realized I posted that playgroundCamera file in a sort of debug state. I was bit too excited… I will update the post to have that fixed, I guess along with the debug version if anyone is curious, like right now.

4 Likes

Hello! Wanted to ask about the possibility of making a Pathfinders off it. What I mean is that the play can be followed by a mob on a sphere and such.

So first, a quick fix to pkg rPartRot to only rotate if the rotation between when you first got on an rPart and when you got off is large enough, that way the axis created from those two states doesn’t get inaccurate.

playgroundCamera.rbxl (96.5 KB)

I literally just came up with an idea to fix to pkg rPartRot only using LookVectors while writing this post. I guess you should look out for that.

and @AlvinPolys ,

The first hurdle this controller would have to overcome is creating a controller for npc’s. This seems relatively simple to do, just create the controller server-side, and offset the WorldCenter every time a new npc is created.

The second hurdle is the actual pathfinding portion of the problem:

I’ve been doing some research on this, and, through A* pathfinding, it could be possible IF:

  • There is a way to map the surface of a part with nodes,

  • There is a way to compute which nodes neighbor the previous node,
    – This would probably rely on a normal from the part and a position, with neighboring nodes being only those that:
    — create an angle between normals less than maybe 45 or 70 degrees and
    — are a suitable distance away from it laterally and a vertical distance less than the humanoid’s hip height,

  • And lastly, it depends on if the humanoid of the NPC can be programmed to follow these points, probably through a similar formula as the one used in pkg LastPart

If there is a way to figure all that out, then you (or I) can build a pathfinder for this controller. I can tell you right now that I have no idea how to figure out the first requirement, the rest is simply trial and error.

3 Likes

I’m gonna make a poll here about how I should release these controllers:

  • Post controller updates as places in this thread (like I am doing right now)
  • Create a Package for the controller, and post update details here
  • Fork the controller from EgoMoose’s GitHub page, and post the fork here.
  • Maybe something else…

0 voters

If you do have an idea about where to post new controllers, please reply with it!

1 Like

Thanks for replying! About the mapping, I thinking of working out an API transforming the sphere into a 2d map, plotting 3d locations into the map, maybe finding the path on 2d and recalling it into 3d dimensions. (Just a concept)

2 Likes

I made a small fix to pkg LastPart so that it does not change the x value on the camera when the change in angle equals pi. This comes from using flips by pressing the Ctrl key, as well as transitions between flat land and slopes, surprisingly from the cylinder.

I also made another small fix to pkg rPartRot to let a part rotate on any singular vector of the CFrame and still fix the camera accordingly.

This does not fix everything in the character controller. There are still more bugs I haven’t seen and subsequently ones I would love to fix.

I have been experimenting a bit with a pathfinding module. You won’t find it on this new file, but I have been tinkering with the node mapping portion of it. You probably will not see even a prototype of it any time soon, but it has caught my interest.

Here is the new playground file, according to the poll:

playgroundCamera.rbxl (97.9 KB)

Edit: I feel that I am finished with updating this controller’s camera, as it seems it is now in a state where it is viable to use in other people’s games. Thanks for the support, everyone!

6 Likes

sorry for the late bump, but upon testing I have found if you hold w/a/s/d and then open the chat bar, you get stuck moving in whatever direction you were holding and you become forced to reset. I’ve played around with the code for a while but haven’t managed to fix anything, maybe you could possibly help me with this? (I don’t usually code physics-based things)

This has to deal with the ControlScript in StarterPlayerScripts not recording reverting a the movedirection back to what it was, which can be easily fixed.

import this file: ControlScript.lua (2.6 KB)

into your workspace, and replace that ControlScript in StarterPlayerScripts with this.

The only thing I really changed was using an addition of all move vectors at runtime instead of using just one vector the entire time:

--[[ BEFORE: line 5
MoveVector = Vector3.new() 
-- AFTER: line 5-10 ]]
MoveVector = {
	[Enum.KeyCode.W] = Vector3.new(),
	[Enum.KeyCode.A] = Vector3.new(),
	[Enum.KeyCode.S] = Vector3.new(),
	[Enum.KeyCode.D] = Vector3.new()
}
-----------------------------------------------------------------
--[[ BEFORE: line 44-49
				if state == Enum.UserInputState.Begin then
					MoveVector = MoveVector + tab.V
					tab.d = true
				elseif state == Enum.UserInputState.End and tab.d then
					MoveVector = MoveVector - tab.V
				end
-- AFTER: line 49-54 ]]
				if state == Enum.UserInputState.Begin then
					MoveVector[input.KeyCode] = tab.V
					tab.d = true
				elseif state == Enum.UserInputState.End and tab.d then
					MoveVector[input.KeyCode] = Vector3.new()
				end
-----------------------------------------------------------------
--[[ BEFORE: line 85-87
	conns[2] = RunService.Heartbeat:Connect(function()
		player:Move(CFrame.Angles(0,camX.Value,0) * MoveVector)
	end)
-- AFTER: line 90-94 ]]
	conns[2] = RunService.Heartbeat:Connect(function()
		player:Move(CFrame.Angles(0,camX.Value,0) * 
			(MoveVector[Enum.KeyCode.W] + MoveVector[Enum.KeyCode.A] + MoveVector[Enum.KeyCode.S] + MoveVector[Enum.KeyCode.D])
		)
	end

Sorry about that! :smiley:

Wow, thanks a ton! I was doing it all wrong and adding textbox detectors etc but this does the job very well! :smiley:

I was just wondering if it’s possible to remove the lerp from zooming and moving the camera but keeping the smoothening on walking up walls etc? I tried messing around for around half an hour and got nowhere, I ended up removing smoothing altogether and not keeping it on walking walls and making the camera go crazy

There are two tweens that pertain to zooming and moving the camera respectively:

Zoom

In the UISTable of CameraScript, there is an extra variable I use to differentiate between the actual value I use for zoom and the value formed from tweening this value, known as zoomTween.
If you want to remove tweening from this, the simplest fix is just using zoom in the camera.CFrame/point definition instead of zoomTween

Camera Movement

Just before the definition of point in the RunService:BindToRenderStep event binding, I steeply lerp angle between its initial angle and intended angle so transitions between slopes smooth over without appearing disruptive.
I believe it’s definition looks like this:

local camFocus = (lastPart.CFrame - lastPart.Position) * 
    (fakeLastPart.CFrame - fakeLastPart.Position):Inverse()
--...
angle = angle:Lerp(
    camFocus * 
    CFrame.Angles(0,x,0) * CFrame.Angles(y,0,0),
    0.5
) --0.5 could be larger or smth

The solution would be to lerp the CFrame that comes from the camFocus definition most likely, and ignore the one made by angle if you wanted. Something like this:

camFocus = camFocus:Lerp(
    (lastPart.CFrame - lastPart.Position) * 
    (lastFakePart.CFrame - lastFakePart.Position):Inverse(),
    0.5
)
--...
angle = camFocus * 
    CFrame.Angles(0,x,0) * CFrame.Angles(y,0,0)

Since the camFocus does not interfere with the position of the camera and only the orientation, this should not look glitchy at all. However, I have not looked at my codebase in a long time and I may be wrong in how RunService:BindToRenderStep is utilized or how the variables are defined, but lerping (lP.CFrame - lP.Position) * (fLP.CFrame - fLP.Position):Inverse() is how you can smooth over slope transitions without tweening camera movement.

1 Like

Wondering if there’s a way other than invisible wedges to make it easier to go from floor directly to a straight wall

They were suppose to on an old trello roadmap. They never did implement it.

1 Like

The controller has a setNormal method for custom changes in gravity, you could use this in combination with .Touched events and getNormal checks to create a custom gravity system.
E.g: say you have a floor + wall combo that you want to script to change normals.
You could probably have a setup like this, except each part covers the entire wall/floor:
image
Part1 would set the character’s normal to the floor on touch, and Part2 would set the character’s normal to the wall on touch, all with a debounce of maybe 0.5 seconds.
A script like this would suffice:

-- add the character/controller to the table 
-- every time you make a new controller
local Interfaces = {
 -- [Character] = {Controller = controller, Debounce = false}
}

local GravParts = {
 -- [hitbox] = normal
    [Part1] = Vector3.new(0,1,0),
    [Part2] = Vector3.new(1,0,0)
}

for hitbox, normal in pairs(GravParts)
    hitbox.Touched:Connect(function(hit)
        local i = Interfaces[hit.Parent]
        if i and not i.Debounce and i.Controller:getNormal() ~= normal then
            i.Debounce = true
            i.Controller:setNormal(normal)
            wait(0.5)
            i.Debounce = false
        end
    end)
end
3 Likes

I uploaded the place files to Roblox as Uncopylocked Games for ease of accessibility and play testing

Hope you don’t mind.

5 Likes

I might as well post my version of the place on here as well:

I’m not sure if it will always be public, but it is here for now. Also keep in mind that I have not created mobile controls for this yet. For right now, only keyboard and mouse is supported.

4 Likes

I figured out how to get the camera movement specifically to tween instead of the entire angle, if you were stuck on that. I just made an extra xOffset value since the offset and part:fakePart relationship is basically coupled together.

This also bugged out the ControlScript, so I also made an extra camXSum variable under CameraScript to fix it.

Here are the individual scripts if you want to replace it, I also updated gravity swords up there to show this:

CameraScript.rbxm (3.6 KB)
ControlScript.lua (1.9 KB)

2 Likes