This commit is contained in:
Oscar
2025-05-25 18:16:55 +03:00
commit df4b259d17
551 changed files with 32089 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
[
"package.base",
"package.fish.scc"
]

Binary file not shown.

View File

@@ -0,0 +1,313 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>package.fish.scc</name>
</assembly>
<members>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.ManuallyUpdate">
<summary>
Manually update this by calling Move() or let it always be simulated
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.ScaleAgainstWalls">
<summary>
If pushing against a wall, scale the velocity based on the wall's angle (False is useful for NPCs that get stuck on corners)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.TraceWidth">
<summary>
Width of our trace
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.TraceHeight">
<summary>
Height of our trace
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.RotateWithGameObject">
<summary>
Rotate the trace with the gameobject
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.CylinderTrace">
<summary>
Use a cylinder trace instead of a box trace<br/>
[WARNING] This is a PHYSICAL TRACE, so it's more expensive than the normal box trace
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.IgnoreTags">
<summary>
Which tags it should ignore
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.MaxBounces">
<summary>
Max amount of trace calls whenever the simulation doesn't reach its target (Slide and collide bounces)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.GroundAcceleration">
<summary>
How fast you accelerate while on the ground (Units per second)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.GroundDeceleration">
<summary>
How fast you decelerate while on the ground (Units per second)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.AirAcceleration">
<summary>
How fast you accelerate while in the air (Units per second)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.AirDeceleration">
<summary>
How fast you decelerate while in the air (Units per second)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.IgnoreGroundSurface">
<summary>
Do we ignore the friction of the surface you're standing on or not?
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.IgnoreZ">
<summary>
Is this MoveHelper meant for horizontal grounded movement? (false = For flying or noclip)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.IgnoreZWhenZero">
<summary>
Do we ignore Z when it's near 0 (So that gravity affects you when not moving)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.WallTolerance">
<summary>
Tolerance from a 90° surface before it's considered a wall (Ex. Tolerance 1 = Between 89° and 91° can be a wall, 0.1 = 89.9° to 90.1°)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.GripFactorReduction">
<summary>
Player feels like it's gripping walls too much? Try more Grip Factor Reduction!
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.GroundStickEnabled">
<summary>
Stick the MoveHelper to the ground (IsOnGround will default to false if disabled)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.MaxGroundAngle">
<summary>
How steep terrain can be for you to stand on without slipping
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.GroundStickDistance">
<summary>
How far from the ground the MoveHelper is going to stick (Useful for going down stairs!)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.StepsEnabled">
<summary>
Enable steps climbing (+1 Trace call)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.StepHeight">
<summary>
How high steps can be for you to climb on
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.StepDepth">
<summary>
How deep it checks for steps (Minimum depth)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.StepTolerance">
<summary>
Tolerance from a 90° surface before it's considered a valid step (Ex. Tolerance 1 = Between 89° and 91° can be a step, 0.1 = 89.9° to 90.1°)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.PseudoStepsEnabled">
<summary>
Enable to ability to walk on a surface that's too steep if it's equal or smaller than a step (+1 Trace call when on steep terrain)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.PushEnabled">
<summary>
Instead of colliding with these tags the MoveHelper will be pushed away (Make sure the tags are in IgnoreTags as well!)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.PushTagsWeight">
<summary>
Which tags will push this MoveHelper away and with how much force (Make sure they are also included in IgnoreTags!) (+1 Trace call)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.GravityEnabled">
<summary>
Apply gravity to this MoveHelper when not on the ground
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.UseSceneGravity">
<summary>
Use the scene's gravity or our own
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.UseVectorGravity">
<summary>
Use a Vector3 gravity instead of a single float (Use this if you want to use a custom gravity)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.Gravity">
<summary>
Units per second squared (Default is -850f)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.VectorGravity">
<summary>
Units per second squared (Default is 0f, 0f, -850f)<br/>
Changes which way <see cref="P:ShrimpleCharacterController.ShrimpleCharacterController.GroundStickEnabled"/> sticks to the ground
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.UnstuckEnabled">
<summary>
Check if the MoveHelper is stuck and try to get it to unstuck (+Trace calls if stuck)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.MaxUnstuckTries">
<summary>
How many trace calls it will attempt to get the MoveHelper unstuck
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.WishVelocity">
<summary>
The simulated target velocity for our MoveHelper (Units per second, we apply Time.Delta inside)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.Velocity">
<summary>
The resulting velocity after the simulation is done (Units per second)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.IsOnGround">
<summary>
Is the MoveHelper currently touching the ground
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.GroundNormal">
<summary>
The current ground normal you're standing on (Always Vector3.Zero if IsOnGround false)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.GroundAngle">
<summary>
The current ground angle you're standing on (Always 0f if IsOnGround false)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.GroundSurface">
<summary>
The current surface you're standing on
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.GroundObject">
<summary>
The gameobject you're currently standing on
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.IsPushingAgainstWall">
<summary>
Is the MoveHelper currently pushing against a wall
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.WallNormal">
<summary>
The current wall normal you're pushing against (Always Vector3.Zero if IsPushingAgainstWall false)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.WallObject">
<summary>
The gameobject you're currently pushing on
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.IsSlipping">
<summary>
Is the MoveHelper standing on a terrain too steep to stand on (Always false if IsOnGround false)
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.IsStuck">
<summary>
The MoveHelper is stuck and we can't get it out
</summary>
</member>
<member name="F:ShrimpleCharacterController.ShrimpleCharacterController.SkinWidth">
<summary>
To avoid getting stuck due to imprecision we shrink the bounds before checking and compensate for it later
</summary>
</member>
<member name="P:ShrimpleCharacterController.ShrimpleCharacterController.Bounds">
<summary>
The bounds of this MoveHelper generated from the TraceWidth and TraceHeight
</summary>
</member>
<member name="F:ShrimpleCharacterController.ShrimpleCharacterController.UnstuckTarget">
<summary>
If another MoveHelper moved at the same time and they're stuck, let this one know that the other already unstuck for us
</summary>
</member>
<member name="M:ShrimpleCharacterController.ShrimpleCharacterController.BuildTrace(BBox,Vector3,Vector3)">
<summary>
Casts the current bounds from to and returns the scene trace result
</summary>
<param name="bounds"></param>
<param name="from"></param>
<param name="to"></param>
<returns></returns>
</member>
<member name="M:ShrimpleCharacterController.ShrimpleCharacterController.Punch(Vector3@)">
<summary>
Detach the MoveHelper from the ground and launch it somewhere (Units per second)
</summary>
<param name="amount"></param>
</member>
<member name="M:ShrimpleCharacterController.ShrimpleCharacterController.Move(System.Boolean)">
<summary>
Apply the WishVelocity, update the Velocity and the Position of the GameObject by simulating the MoveHelper
</summary>
<param name="manualUpdate">Just calculate but don't update position</param>
</member>
<member name="M:ShrimpleCharacterController.ShrimpleCharacterController.Move(System.Single,System.Boolean)">
<summary>
Apply the WishVelocity, update the Velocity and the Position of the GameObject by simulating the MoveHelper
</summary>
<param name="delta">The time step</param>
<param name="manualUpdate">Just calculate but don't update position</param>
</member>
<member name="T:ShrimpleCharacterController.ShrimpleCharacterController.MoveHelperResult">
<summary>
Sometimes we have to update only the position but not the velocity (Like when climbing steps or getting unstuck) so we can't have Position rely only on Velocity
</summary>
</member>
<member name="M:ShrimpleCharacterController.ShrimpleCharacterController.TestPosition(Vector3,System.String)">
<summary>
Debug don't use
</summary>
<param name="position"></param>
<param name="title"></param>
<returns></returns>
</member>
<member name="M:ShrimpleCharacterController.Vector3Extensions.MoveTowards(Vector3,Vector3,System.Single)">
<summary>
Move a vector3 towards a goal by a fixed distance
</summary>
<param name="value"></param>
<param name="target"></param>
<param name="travelSpeed"></param>
<returns></returns>
</member>
<member name="M:ShrimpleCharacterController.Vector3Extensions.ProjectAndScale(Vector3,Vector3)">
<summary>
Project a vector along a plane (normal) and scale it back to its original length
</summary>
<param name="value"></param>
<param name="normal"></param>
<returns></returns>
</member>
</members>
</doc>

Binary file not shown.

View File

@@ -0,0 +1,67 @@
{
"scene.lastopened": {
"Value": "[\u0022scenes/walker_example.scene\u0022,\u0022prefabs/debug_world.prefab\u0022]",
"Timeout": 1747778774,
"DeleteAt": 0
},
"gizmo.settings": {
"Value": "{\u0022EditMode\u0022:\u0022position\u0022,\u0022Selection\u0022:true,\u0022ViewMode\u0022:\u00223d\u0022,\u0022GizmosEnabled\u0022:true,\u0022GizmoScale\u0022:1,\u0022GridSpacing\u0022:8,\u0022SnapToGrid\u0022:true,\u0022SnapToAngles\u0022:true,\u0022AngleSpacing\u0022:15,\u0022GlobalSpace\u0022:false,\u0022DebugActionGraphs\u0022:false}",
"Timeout": 1747778773,
"DeleteAt": 0
},
"scenes/walker_example.scene.Viewport0": {
"Value": "{\u0022CameraPosition\u0022:\u0022-99.46711,-362.8735,485.0559\u0022,\u0022CameraRotation\u0022:\u0022-0.3022594,0.2325998,0.7326022,0.5637646\u0022,\u0022CameraOrthoHeight\u0022:1000}",
"Timeout": 1747780744,
"DeleteAt": 0
},
"AssetBrowser.Map.HistoryIdx": {
"Value": "2",
"Timeout": 1736783981,
"DeleteAt": 1745241658
},
"AssetBrowser.Map.History": {
"Value": "[\u0022C:\\\\Users\\\\uberm\\\\Desktop\\\\Projects\\\\shrimple_character_controller\\\\Assets\u0022,\u0022C:\\\\Users\\\\uberm\\\\Desktop\\\\Projects\\\\shrimple_character_controller\\\\Assets\u0022,\u0022C:\\\\Users\\\\uberm\\\\Desktop\\\\Projects\\\\shrimple_character_controller\\\\Assets\u0022]",
"Timeout": 1736783981,
"DeleteAt": 1745241658
},
"prefabs/debug_world.prefab.Viewport0": {
"Value": "{\u0022CameraPosition\u0022:\u0022-1075.94,-361.7078,194.5635\u0022,\u0022CameraRotation\u0022:\u00220.01240713,0.1013415,-0.1208866,0.9874019\u0022,\u0022CameraOrthoHeight\u0022:1000}",
"Timeout": 1747780744,
"DeleteAt": 0
},
".Viewport0": {
"Value": "{\u0022CameraPosition\u0022:\u002264.7494,-611.3881,145.0907\u0022,\u0022CameraRotation\u0022:\u0022-0.00140108,0.3086819,0.004317207,0.9511545\u0022,\u0022CameraOrthoHeight\u0022:1000}",
"Timeout": 1747780728,
"DeleteAt": 0
},
"SceneView.Viewport0.Settings": {
"Value": "{\u0022CameraPosition\u0022:\u0022-1075.94,-361.7078,194.5635\u0022,\u0022CameraRotation\u0022:\u00220.01240713,0.1013415,-0.1208866,0.9874019\u0022,\u0022View\u0022:0,\u0022RenderMode\u0022:0,\u0022WireframeMode\u0022:false,\u0022EnablePostProcessing\u0022:true,\u0022EnablePrefabLighting\u0022:true,\u0022ShowGrid\u0022:true,\u0022GridOpacity\u0022:0.2,\u0022GridAxis\u0022:0,\u0022CameraOrthoHeight\u0022:1000}",
"Timeout": 1747780744,
"DeleteAt": 0
},
"SceneView.Layout": {
"Value": "{\u0022Layout\u0022:0,\u0022SplitterState\u0022:[]}",
"Timeout": 1747780728,
"DeleteAt": 0
},
"folder-metadata": {
"Value": "{\u0022..\\\\..\\\\..\\\\..\\\\..\\\\Program Files (x86)\\\\Steam\\\\steamapps\\\\common\\\\sbox\\\\Recents\u0022:{\u0022Color\u0022:\u00220.46667,0.73333,1,1\u0022,\u0022Icon\u0022:\u0022\u0022},\u0022..\\\\..\\\\..\\\\..\\\\..\\\\Program Files (x86)\\\\Steam\\\\steamapps\\\\common\\\\sbox\\\\Everything\u0022:{\u0022Color\u0022:\u00220.46667,0.73333,1,1\u0022,\u0022Icon\u0022:\u0022\u0022},\u0022.\u0022:{\u0022Color\u0022:\u00220.46667,0.73333,1,1\u0022,\u0022Icon\u0022:\u0022\u0022},\u0022..\\\\..\\\\..\\\\..\\\\..\\\\Program Files (x86)\\\\Steam\\\\steamapps\\\\common\\\\sbox\\\\addons\\\\citizen\\\\assets\u0022:{\u0022Color\u0022:\u00220.46667,0.73333,1,1\u0022,\u0022Icon\u0022:\u0022\u0022},\u0022..\\\\..\\\\..\\\\..\\\\..\\\\Program Files (x86)\\\\Steam\\\\steamapps\\\\common\\\\sbox\\\\core\u0022:{\u0022Color\u0022:\u00220.46667,0.73333,1,1\u0022,\u0022Icon\u0022:\u0022\u0022},\u0022Assets\\\\prefabs\u0022:{\u0022Color\u0022:\u00220.46667,0.73333,1,1\u0022,\u0022Icon\u0022:\u0022\u0022},\u0022Assets\\\\scenes\u0022:{\u0022Color\u0022:\u00220.46667,0.73333,1,1\u0022,\u0022Icon\u0022:\u0022\u0022},\u0022Assets\u0022:{\u0022Color\u0022:\u00220.46667,0.73333,1,1\u0022,\u0022Icon\u0022:\u0022\u0022},\u0022code\u0022:{\u0022Color\u0022:\u00220.46667,0.73333,1,1\u0022,\u0022Icon\u0022:\u0022\u0022},\u0022Localization\u0022:{\u0022Color\u0022:\u00220.46667,0.73333,1,1\u0022,\u0022Icon\u0022:\u0022\u0022},\u0022ProjectSettings\u0022:{\u0022Color\u0022:\u00220.46667,0.73333,1,1\u0022,\u0022Icon\u0022:\u0022\u0022},\u0022code\\\\Examples\u0022:{\u0022Color\u0022:\u00220.46667,0.73333,1,1\u0022,\u0022Icon\u0022:\u0022\u0022},\u0022code\\\\Util\u0022:{\u0022Color\u0022:\u00220.46667,0.73333,1,1\u0022,\u0022Icon\u0022:\u0022\u0022}}",
"Timeout": 1737811230,
"DeleteAt": 1745241658
},
"AssetBrowser.History": {
"Value": "[\u0022C:\\\\Users\\\\uberm\\\\Desktop\\\\Projects\\\\shrimple_character_controller\\\\Assets\u0022]",
"Timeout": 1747778774,
"DeleteAt": 0
},
"AssetBrowser.HistoryIdx": {
"Value": "0",
"Timeout": 1747778774,
"DeleteAt": 0
},
"Window.SboxSceneEditor.Dock": {
"Value": "{\u0022floatingWindows\u0022:[],\u0022mainWrapper\u0022:{\u0022geometry\u0022:\u0022AdnQywADAAAAAAAAAAAAAAAAB9AAAAPRAAAAAAAAAAAAAAfQAAAD0QAAAAAAAAAACAAAAAAAAAAAAAAAB9AAAAPR\u0022,\u0022splitter\u0022:{\u0022items\u0022:[{\u0022currentIndex\u0022:0,\u0022objects\u0022:[{\u0022data\u0022:null,\u0022managedType\u0022:\u0022SceneTreeWidget\u0022,\u0022name\u0022:\u0022Hierarchy\u0022},{\u0022data\u0022:null,\u0022managedType\u0022:\u0022PanelInspectorWidget\u0022,\u0022name\u0022:\u0022UI Panels\u0022}],\u0022type\u0022:\u0022area\u0022},{\u0022items\u0022:[{\u0022items\u0022:[{\u0022currentIndex\u0022:0,\u0022objects\u0022:[{\u0022data\u0022:null,\u0022managedType\u0022:\u0022\u0022,\u0022name\u0022:\u0022GameFrame\u0022},{\u0022data\u0022:null,\u0022managedType\u0022:\u0022SceneDock\u0022,\u0022name\u0022:\u0022SceneDock:scenes/walker_example.scene\u0022},{\u0022data\u0022:null,\u0022managedType\u0022:\u0022SceneDock\u0022,\u0022name\u0022:\u0022SceneDock:prefabs/debug_world.prefab\u0022},{\u0022data\u0022:null,\u0022managedType\u0022:\u0022SceneDock\u0022,\u0022name\u0022:\u0022SceneDock:untitled\u0022}],\u0022type\u0022:\u0022area\u0022},{\u0022currentIndex\u0022:0,\u0022objects\u0022:[{\u0022data\u0022:null,\u0022managedType\u0022:\u0022ConsoleWidget\u0022,\u0022name\u0022:\u0022Console\u0022},{\u0022data\u0022:null,\u0022managedType\u0022:\u0022MainAssetBrowser\u0022,\u0022name\u0022:\u0022Asset Browser\u0022},{\u0022data\u0022:null,\u0022managedType\u0022:\u0022LibraryManagerDock\u0022,\u0022name\u0022:\u0022Library Manager\u0022},{\u0022data\u0022:null,\u0022managedType\u0022:\u0022MixerDock\u0022,\u0022name\u0022:\u0022Mixer\u0022}],\u0022type\u0022:\u0022area\u0022}],\u0022state\u0022:\u0022AAAA/wAAAAEAAAACAAACuAAAAsoA/////wEAAAACAA==\u0022,\u0022type\u0022:\u0022splitter\u0022},{\u0022items\u0022:[{\u0022currentIndex\u0022:0,\u0022objects\u0022:[{\u0022data\u0022:null,\u0022managedType\u0022:\u0022Inspector\u0022,\u0022name\u0022:\u0022Inspector\u0022},{\u0022data\u0022:null,\u0022managedType\u0022:\u0022UndoDock\u0022,\u0022name\u0022:\u0022Undo\u0022}],\u0022type\u0022:\u0022area\u0022},{\u0022currentIndex\u0022:0,\u0022objects\u0022:[{\u0022data\u0022:null,\u0022managedType\u0022:\u0022PerformanceDock\u0022,\u0022name\u0022:\u0022Performance\u0022}],\u0022type\u0022:\u0022area\u0022}],\u0022state\u0022:\u0022AAAA/wAAAAEAAAACAAAC2wAAAOcA/////wEAAAACAA==\u0022,\u0022type\u0022:\u0022splitter\u0022}],\u0022state\u0022:\u0022AAAA/wAAAAEAAAACAAAE/AAAAckA/////wEAAAABAA==\u0022,\u0022type\u0022:\u0022splitter\u0022}],\u0022state\u0022:\u0022AAAA/wAAAAEAAAACAAABBgAABskA/////wEAAAABAA==\u0022,\u0022type\u0022:\u0022splitter\u0022}},\u0022toolWindowManagerStateFormat\u0022:1}",
"Timeout": 1747778786,
"DeleteAt": 0
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1 @@
1.0.97283

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

View File

@@ -0,0 +1,121 @@
{
"__guid": "e78370eb-4e25-4b7f-bbf8-89f0fcbaa68b",
"GameObjects": [
{
"__guid": "048573b1-fa80-406a-a30d-93881e2b19c9",
"Flags": 0,
"Name": "debug_world",
"Enabled": true,
"__Prefab": "prefabs/debug_world.prefab",
"__PrefabVariables": {}
},
{
"__guid": "dad296d6-935e-4779-b5ae-67a3a0d28ab6",
"Flags": 0,
"Name": "Player",
"Position": "-171.0225,-113.5377,171.9933",
"Enabled": true,
"Components": [
{
"__type": "ShrimpleCharacterController.ShrimpleCharacterController",
"__guid": "fa127e8d-b05d-4e14-894d-d2977361591d",
"AirAcceleration": 1500,
"AirDeceleration": 700,
"Gravity": -850,
"GravityEnabled": false,
"GroundAcceleration": 1000,
"GroundDeceleration": 1500,
"GroundStickDistance": 12,
"GroundStickEnabled": false,
"IgnoreGroundSurface": false,
"IgnoreTags": "",
"IgnoreZ": false,
"IgnoreZWhenZero": true,
"ManuallyUpdate": true,
"MaxBounces": 5,
"MaxGroundAngle": 60,
"MaxUnstuckTries": 20,
"PushEnabled": false,
"PushTagsWeight": {
"player": 1
},
"ScaleAgainstWalls": true,
"StepDepth": 2,
"StepHeight": 12,
"StepsEnabled": false,
"TraceHeight": 72,
"TraceWidth": 16,
"UnstuckEnabled": true,
"UseSceneGravity": true
},
{
"__type": "ShrimpleCharacterController.ShrimpleFlyer",
"__guid": "e79bdc25-5a6f-4eb2-8a49-5f7aac27f8cd",
"RunSpeed": 2400,
"WalkSpeed": 800
},
{
"__type": "Sandbox.Citizen.CitizenAnimationHelper",
"__guid": "6bb6e1f8-1402-4b7b-9006-ea4866eb9e64",
"BodyWeight": 1,
"EyesWeight": 1,
"HeadWeight": 1,
"LookAtEnabled": false,
"Target": {
"_type": "component",
"component_id": "75a0dedf-07bc-4a29-9027-9e88625f7ba0",
"go": "768529ee-5ecc-4ddb-a81e-73fdb775baec",
"component_type": "SkinnedModelRenderer"
}
}
],
"Children": [
{
"__guid": "768529ee-5ecc-4ddb-a81e-73fdb775baec",
"Flags": 0,
"Name": "Model",
"Position": "0,0,0",
"Enabled": true,
"Components": [
{
"__type": "Sandbox.SkinnedModelRenderer",
"__guid": "75a0dedf-07bc-4a29-9027-9e88625f7ba0",
"BodyGroups": 341,
"CreateBoneObjects": false,
"Model": "models/citizen/citizen.vmdl",
"RenderType": "On",
"Tint": "1,1,1,1",
"UseAnimGraph": true
}
]
}
]
}
],
"SceneProperties": {
"FixedUpdateFrequency": 50,
"MaxFixedUpdates": 5,
"NetworkFrequency": 30,
"NetworkInterpolation": true,
"ThreadedAnimation": true,
"TimeScale": 1,
"UseFixedUpdate": true,
"NavMesh": {
"Enabled": false,
"IncludeStaticBodies": true,
"IncludeKeyframedBodies": true,
"EditorAutoUpdate": true,
"AgentHeight": 64,
"AgentRadius": 16,
"AgentStepSize": 18,
"AgentMaxSlope": 40,
"ExcludedBodies": "",
"IncludedBodies": ""
}
},
"Title": "walker_example",
"Description": "",
"ResourceVersion": 1,
"__references": [],
"__version": 1
}

View File

@@ -0,0 +1,146 @@
{
"__guid": "e78370eb-4e25-4b7f-bbf8-89f0fcbaa68b",
"GameObjects": [
{
"__guid": "048573b1-fa80-406a-a30d-93881e2b19c9",
"Flags": 0,
"Name": "debug_world",
"Enabled": true,
"__Prefab": "prefabs/debug_world.prefab",
"__PrefabVariables": {}
},
{
"__guid": "dad296d6-935e-4779-b5ae-67a3a0d28ab6",
"Flags": 0,
"Name": "Player",
"Position": "0,-49.46404,176.2773",
"Enabled": true,
"Components": [
{
"__type": "ShrimpleCharacterController.ShrimpleCharacterController",
"__guid": "fa127e8d-b05d-4e14-894d-d2977361591d",
"AirAcceleration": 250,
"AirDeceleration": 100,
"Gravity": -850,
"GravityEnabled": true,
"GroundAcceleration": 1000,
"GroundDeceleration": 2000,
"GroundStickDistance": 12,
"GroundStickEnabled": false,
"IgnoreGroundSurface": false,
"IgnoreTags": "",
"IgnoreZ": true,
"IgnoreZWhenZero": true,
"ManuallyUpdate": true,
"MaxBounces": 5,
"MaxGroundAngle": 60,
"MaxUnstuckTries": 20,
"PushEnabled": false,
"PushTagsWeight": {
"player": 1
},
"ScaleAgainstWalls": true,
"StepDepth": 2,
"StepHeight": 12,
"StepsEnabled": true,
"TraceHeight": 64,
"TraceWidth": 64,
"UnstuckEnabled": true,
"UseSceneGravity": true
},
{
"__type": "ShrimpleCharacterController.ShrimpleRoller",
"__guid": "c557f267-05dd-4185-be90-7d60757118ef",
"JumpStrength": 350,
"RunSpeed": 3000,
"WalkSpeed": 1000
}
],
"Children": [
{
"__guid": "768529ee-5ecc-4ddb-a81e-73fdb775baec",
"Flags": 0,
"Name": "Model",
"Position": "0,0,32",
"Enabled": true,
"Components": [
{
"__type": "Sandbox.ModelRenderer",
"__guid": "13202fec-761f-46b0-9517-21a8662c39f3",
"BodyGroups": 18446744073709551615,
"MaterialOverride": "materials/editor/black_grid_8_doublesided.vmat",
"Model": "models/dev/sphere.vmdl",
"RenderType": "On",
"Tint": "1,1,1,1"
},
{
"__type": "Sandbox.TrailRenderer",
"__guid": "0c954483-095d-48f1-bb1c-47d90bbb6ae6",
"BlendMode": "Normal",
"CastShadows": false,
"Color": {
"color": [
{
"c": "1,1,1,0.5"
},
{
"t": 1,
"c": "1,1,1,0"
}
],
"alpha": []
},
"LifeTime": 3,
"MaxPoints": 256,
"Opaque": false,
"PointDistance": 4,
"Texturing": {
"WorldSpace": true,
"UnitsPerTexture": 10,
"Scale": 1
},
"Width": {
"rangey": "0,64",
"frames": [
{
"y": 1
},
{
"x": 1
}
]
},
"Wireframe": false
}
]
}
]
}
],
"SceneProperties": {
"FixedUpdateFrequency": 50,
"MaxFixedUpdates": 5,
"NetworkFrequency": 30,
"NetworkInterpolation": true,
"ThreadedAnimation": true,
"TimeScale": 1,
"UseFixedUpdate": true,
"NavMesh": {
"Enabled": false,
"IncludeStaticBodies": true,
"IncludeKeyframedBodies": true,
"EditorAutoUpdate": true,
"AgentHeight": 64,
"AgentRadius": 16,
"AgentStepSize": 18,
"AgentMaxSlope": 40,
"ExcludedBodies": "",
"IncludedBodies": ""
}
},
"Title": "walker_example",
"Description": "",
"ResourceVersion": 1,
"__references": [],
"__version": 1
}

View File

@@ -0,0 +1,185 @@
{
"__guid": "e78370eb-4e25-4b7f-bbf8-89f0fcbaa68b",
"GameObjects": [
{
"__guid": "4a794eac-df05-4fff-b80b-f4f40e6b4e9e",
"Flags": 0,
"Name": "Scene Information",
"Enabled": true,
"Components": [
{
"__type": "Sandbox.SceneInformation",
"__guid": "f6aaec07-69f8-4387-b7d9-f94cbc0f362d",
"Author": null,
"Changes": null,
"Description": "",
"Group": null,
"OnComponentDestroy": null,
"OnComponentDisabled": null,
"OnComponentEnabled": null,
"OnComponentFixedUpdate": null,
"OnComponentStart": null,
"OnComponentUpdate": null,
"SceneTags": "",
"Title": "walker_example",
"Version": null
}
],
"Children": []
},
{
"__guid": "048573b1-fa80-406a-a30d-93881e2b19c9",
"Flags": 0,
"Name": "debug_world",
"Enabled": true,
"__Prefab": "prefabs/debug_world.prefab",
"__PrefabVariables": {}
},
{
"__guid": "dad296d6-935e-4779-b5ae-67a3a0d28ab6",
"Flags": 0,
"Name": "Player",
"Position": "-171.0225,-113.5377,171.9933",
"Enabled": true,
"Components": [
{
"__type": "ShrimpleCharacterController.ShrimpleCharacterController",
"__guid": "fa127e8d-b05d-4e14-894d-d2977361591d",
"__version": 1,
"AirAcceleration": 300,
"AirDeceleration": 0,
"CylinderTrace": true,
"Gravity": -850,
"GravityEnabled": true,
"GripFactorReduction": 1,
"GroundAcceleration": 1000,
"GroundDeceleration": 1500,
"GroundStickDistance": 12,
"GroundStickEnabled": true,
"IgnoreGroundSurface": false,
"IgnoreTags": "",
"IgnoreZ": true,
"IgnoreZWhenZero": true,
"ManuallyUpdate": true,
"MaxBounces": 5,
"MaxGroundAngle": 60,
"MaxUnstuckTries": 20,
"OnComponentDestroy": null,
"OnComponentDisabled": null,
"OnComponentEnabled": null,
"OnComponentFixedUpdate": null,
"OnComponentStart": null,
"OnComponentUpdate": null,
"PseudoStepsEnabled": true,
"PushEnabled": false,
"PushTagsWeight": {
"player": 1
},
"RotateWithGameObject": true,
"ScaleAgainstWalls": true,
"StepDepth": 2,
"StepHeight": 12,
"StepsEnabled": true,
"StepTolerance": 1,
"TraceHeight": 72,
"TraceWidth": 24,
"UnstuckEnabled": true,
"UseSceneGravity": true,
"UseVectorGravity": false,
"VectorGravity": "0,0,-850",
"WallTolerance": 1
},
{
"__type": "ShrimpleCharacterController.ShrimpleWalker",
"__guid": "21e48f2b-e808-42b7-83e6-83282972f187",
"DuckSpeed": 50,
"JumpStrength": 350,
"OnComponentDestroy": null,
"OnComponentDisabled": null,
"OnComponentEnabled": null,
"OnComponentFixedUpdate": null,
"OnComponentStart": null,
"OnComponentUpdate": null,
"RunSpeed": 300,
"WalkSpeed": 100
}
],
"Children": [
{
"__guid": "768529ee-5ecc-4ddb-a81e-73fdb775baec",
"Flags": 0,
"Name": "Model",
"Enabled": true,
"Components": [
{
"__type": "Sandbox.SkinnedModelRenderer",
"__guid": "75a0dedf-07bc-4a29-9027-9e88625f7ba0",
"AnimationGraph": null,
"BodyGroups": 341,
"BoneMergeTarget": null,
"CreateAttachments": false,
"CreateBoneObjects": false,
"MaterialGroup": null,
"MaterialOverride": null,
"Model": "models/citizen/citizen.vmdl",
"Morphs": {},
"OnComponentDestroy": null,
"OnComponentDisabled": null,
"OnComponentEnabled": null,
"OnComponentFixedUpdate": null,
"OnComponentStart": null,
"OnComponentUpdate": null,
"Parameters": {
"bools": {},
"ints": {},
"floats": {},
"vectors": {},
"rotations": {}
},
"PlaybackRate": 1,
"RenderOptions": {
"GameLayer": true,
"OverlayLayer": false,
"BloomLayer": false,
"AfterUILayer": false
},
"RenderType": "On",
"Sequence": {
"Name": null,
"Looping": true
},
"Tint": "1,1,1,1",
"UseAnimGraph": true
}
],
"Children": []
}
]
}
],
"SceneProperties": {
"NetworkInterpolation": true,
"TimeScale": 1,
"WantsSystemScene": true,
"Metadata": {
"Title": "walker_example"
},
"NavMesh": {
"Enabled": false,
"IncludeStaticBodies": true,
"IncludeKeyframedBodies": true,
"EditorAutoUpdate": true,
"AgentHeight": 64,
"AgentRadius": 16,
"AgentStepSize": 18,
"AgentMaxSlope": 40,
"ExcludedBodies": "",
"IncludedBodies": ""
}
},
"ResourceVersion": 2,
"Title": "walker_example",
"Description": null,
"__references": [],
"__version": 2
}

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Small Fish
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,34 @@
{
"Defaults": {
"solid": "Collide",
"trigger": "Trigger",
"ladder": "Ignore",
"water": "Trigger"
},
"Pairs": [
{
"a": "solid",
"b": "solid",
"r": "Collide"
},
{
"a": "trigger",
"b": "playerclip",
"r": "Ignore"
},
{
"a": "trigger",
"b": "solid",
"r": "Trigger"
},
{
"a": "playerclip",
"b": "solid",
"r": "Collide"
}
],
"__guid": "3f8027f2-5728-4178-a563-2be1da260457",
"__schema": "configdata",
"__type": "CollisionRules",
"__version": 1
}

View File

@@ -0,0 +1,194 @@
{
"Actions": [
{
"Name": "Forward",
"KeyboardCode": "W",
"GamepadCode": "None",
"GroupName": "Movement"
},
{
"Name": "Backward",
"KeyboardCode": "S",
"GamepadCode": "None",
"GroupName": "Movement"
},
{
"Name": "Left",
"KeyboardCode": "A",
"GamepadCode": "None",
"GroupName": "Movement"
},
{
"Name": "Right",
"KeyboardCode": "D",
"GamepadCode": "None",
"GroupName": "Movement"
},
{
"Name": "Jump",
"KeyboardCode": "space",
"GamepadCode": "A",
"GroupName": "Movement"
},
{
"Name": "Run",
"KeyboardCode": "shift",
"GamepadCode": "LeftJoystickButton",
"GroupName": "Movement"
},
{
"Name": "Walk",
"KeyboardCode": "alt",
"GamepadCode": "None",
"GroupName": "Movement"
},
{
"Name": "Duck",
"KeyboardCode": "ctrl",
"GamepadCode": "B",
"GroupName": "Movement"
},
{
"Name": "attack1",
"KeyboardCode": "mouse1",
"GamepadCode": "RightTrigger",
"GroupName": "Actions"
},
{
"Name": "attack2",
"KeyboardCode": "mouse2",
"GamepadCode": "LeftTrigger",
"GroupName": "Actions"
},
{
"Name": "reload",
"KeyboardCode": "r",
"GamepadCode": "X",
"GroupName": "Actions"
},
{
"Name": "use",
"KeyboardCode": "e",
"GamepadCode": "Y",
"GroupName": "Actions"
},
{
"Name": "Slot1",
"KeyboardCode": "1",
"GamepadCode": "DpadWest",
"GroupName": "Inventory"
},
{
"Name": "Slot2",
"KeyboardCode": "2",
"GamepadCode": "DpadEast",
"GroupName": "Inventory"
},
{
"Name": "Slot3",
"KeyboardCode": "3",
"GamepadCode": "DpadSouth",
"GroupName": "Inventory"
},
{
"Name": "Slot4",
"KeyboardCode": "4",
"GamepadCode": "None",
"GroupName": "Inventory"
},
{
"Name": "Slot5",
"KeyboardCode": "5",
"GamepadCode": "None",
"GroupName": "Inventory"
},
{
"Name": "Slot6",
"KeyboardCode": "6",
"GamepadCode": "None",
"GroupName": "Inventory"
},
{
"Name": "Slot7",
"KeyboardCode": "7",
"GamepadCode": "None",
"GroupName": "Inventory"
},
{
"Name": "Slot8",
"KeyboardCode": "8",
"GamepadCode": "None",
"GroupName": "Inventory"
},
{
"Name": "Slot9",
"KeyboardCode": "9",
"GamepadCode": "None",
"GroupName": "Inventory"
},
{
"Name": "Slot0",
"KeyboardCode": "0",
"GamepadCode": "None",
"GroupName": "Inventory"
},
{
"Name": "SlotPrev",
"KeyboardCode": "mouse4",
"GamepadCode": "SwitchLeftBumper",
"GroupName": "Inventory"
},
{
"Name": "SlotNext",
"KeyboardCode": "mouse5",
"GamepadCode": "SwitchRightBumper",
"GroupName": "Inventory"
},
{
"Name": "View",
"KeyboardCode": "C",
"GamepadCode": "RightJoystickButton",
"GroupName": "Other"
},
{
"Name": "Voice",
"KeyboardCode": "v",
"GamepadCode": "None",
"GroupName": "Other"
},
{
"Name": "Drop",
"KeyboardCode": "g",
"GamepadCode": "None",
"GroupName": "Other"
},
{
"Name": "Flashlight",
"KeyboardCode": "f",
"GamepadCode": "DpadNorth",
"GroupName": "Other"
},
{
"Name": "Score",
"KeyboardCode": "tab",
"GamepadCode": "SwitchLeftMenu",
"GroupName": "Other"
},
{
"Name": "Menu",
"KeyboardCode": "Q",
"GamepadCode": "SwitchRightMenu",
"GroupName": "Other"
},
{
"Name": "Chat",
"KeyboardCode": "enter",
"GamepadCode": "None",
"GroupName": "Other"
}
],
"__guid": "2b0cbf43-3f45-425d-8560-9eaecf2ed2a3",
"__schema": "configdata",
"__type": "InputSettings",
"__version": 1
}

View File

@@ -0,0 +1,41 @@
# Shrimple Character Controller
https://github.com/user-attachments/assets/374eea5b-7106-4c3e-8cb6-9bb56a1ff511
A shrimple yet versatile Character Controller/Move Helper that performs great.
Performs 60%-200% better than standard Character Controller.
You can find example scenes for the Walker, Roller, and Flyer here: [Controller Example Scenes](https://github.com/Small-Fish-Dev/shrimple_character_controller/tree/main/Assets/scenes)
Made from scratch using the classic [Collide and Slide](https://www.peroxide.dk/papers/collision/collision.pdf) method.
For any issues please report them here: [Github Issues](https://github.com/Small-Fish-Dev/shrimple_character_controller/issues)
# How to use
You can find code examples for the Walker, Roller, and Flyer here: [Example Controllers](https://github.com/Small-Fish-Dev/shrimple_character_controller/tree/main/code/Examples)
It all boils down to:
```csharp
Controller.WishVelocity = Vector3.Forward;
Controller.Move();
```
You'll have to call `.Move()` only if your Controller options "Manual Update" is set to true. Otherwise you can set it to false and it will always be simulated.
You are also able to manually update the GameObject's transform rather than letting the Controller do it:
```csharp
Controller.WishVelocity = wishDirection * wishSpeed;
var controllerResult = Controller.Move( false );
if ( controllerResult.Position.z <= 999f ) // Out of bounds!
{
Destroy();
}
else
{
Transform.Position = controllerResult.Position;
MyVelocity = controllerResult.Velocity;
}
```

View File

@@ -0,0 +1,5 @@
global using Sandbox;
global using System;
global using System.Collections.Generic;
global using System.Linq;
namespace ShrimpleCharacterController;

View File

@@ -0,0 +1,68 @@
using Sandbox.Citizen;
namespace ShrimpleCharacterController;
[Hide]
public sealed class ShrimpleFlyer : Component
{
[RequireComponent]
public ShrimpleCharacterController Controller { get; set; }
[RequireComponent]
public CitizenAnimationHelper AnimationHelper { get; set; }
public SkinnedModelRenderer Renderer { get; set; }
public GameObject Camera { get; set; }
[Property]
[Range(400f, 1600f, 20f)]
public float WalkSpeed { get; set; } = 800f;
[Property]
[Range(800f, 4000f, 80f)]
public float RunSpeed { get; set; } = 2400f;
public Angles EyeAngles { get; set; }
protected override void OnStart()
{
base.OnStart();
Renderer = AnimationHelper.Target;
Camera = new GameObject(true, "Camera");
Camera.SetParent(GameObject);
var cameraComponent = Camera.Components.Create<CameraComponent>();
cameraComponent.ZFar = 32768f;
}
protected override void OnFixedUpdate()
{
base.OnFixedUpdate();
var isDucking = Input.Down("Duck");
var isRunning = Input.Down("Run");
var ascending = Input.Down("Jump") ? 1f : 0f;
var descending = Input.Down("Duck") ? -1f : 0f;
var wishSpeed = isRunning ? RunSpeed : WalkSpeed;
var wishDirection = (Input.AnalogMove + Vector3.Up * (ascending + descending)).Normal * EyeAngles.ToRotation();
Controller.WishVelocity = wishDirection * wishSpeed;
Controller.Move();
AnimationHelper.WithWishVelocity(Controller.WishVelocity);
AnimationHelper.WithVelocity(Controller.Velocity);
AnimationHelper.IsGrounded = Controller.IsOnGround;
}
protected override void OnUpdate()
{
base.OnUpdate();
EyeAngles += Input.AnalogLook;
EyeAngles = EyeAngles.WithPitch(MathX.Clamp(EyeAngles.pitch, -10f, 40f));
Renderer.WorldRotation = Rotation.Slerp(Renderer.WorldRotation, Rotation.FromYaw(EyeAngles.yaw), Time.Delta * 5f);
var cameraOffset = Vector3.Up * 70f + Vector3.Backward * 220f;
Camera.WorldRotation = EyeAngles.ToRotation();
Camera.LocalPosition = cameraOffset * Camera.WorldRotation;
}
}

View File

@@ -0,0 +1,71 @@
namespace ShrimpleCharacterController;
[Hide]
public sealed class ShrimpleRoller : Component
{
[RequireComponent]
public ShrimpleCharacterController Controller { get; set; }
public ModelRenderer Renderer { get; set; }
public GameObject Camera { get; set; }
[Property]
[Range(500f, 2000f, 100f)]
public float WalkSpeed { get; set; } = 1000f;
[Property]
[Range(1000f, 5000f, 200f)]
public float RunSpeed { get; set; } = 3000f;
[Property]
[Range(200f, 500f, 20f)]
public float JumpStrength { get; set; } = 350f;
public Angles EyeAngles { get; set; }
protected override void OnStart()
{
base.OnStart();
Renderer = Components.Get<ModelRenderer>(FindMode.EnabledInSelfAndDescendants);
Camera = new GameObject(true, "Camera");
Camera.SetParent(GameObject);
var cameraComponent = Camera.Components.Create<CameraComponent>();
cameraComponent.ZFar = 32768f;
}
protected override void OnFixedUpdate()
{
base.OnFixedUpdate();
var wishDirection = Input.AnalogMove.Normal * Rotation.FromYaw(EyeAngles.yaw);
var isDucking = Input.Down("Duck");
var isRunning = Input.Down("Run");
var wishSpeed = isRunning ? RunSpeed : WalkSpeed;
Controller.WishVelocity = wishDirection * wishSpeed;
Controller.Move();
if (Input.Pressed("Jump") && Controller.IsOnGround)
Controller.Punch(Vector3.Up * JumpStrength);
}
protected override void OnUpdate()
{
base.OnUpdate();
EyeAngles += Input.AnalogLook;
EyeAngles = EyeAngles.WithPitch(MathX.Clamp(EyeAngles.pitch, -10f, 40f));
float pitchRotation = Controller.Velocity.x * Time.Delta;
float rollRotation = -Controller.Velocity.y * Time.Delta;
var ballPitch = Rotation.FromPitch(pitchRotation);
var ballRoll = Rotation.FromRoll(rollRotation);
Renderer.WorldRotation = ballPitch * ballRoll * Renderer.WorldRotation;
var cameraOffset = Vector3.Up * 70f + Vector3.Backward * 260f;
Camera.WorldRotation = EyeAngles.ToRotation();
Camera.LocalPosition = cameraOffset * Camera.WorldRotation;
}
}

View File

@@ -0,0 +1,84 @@
using Sandbox.Citizen;
namespace ShrimpleCharacterController;
[Hide]
public sealed class ShrimpleWalker : Component
{
[RequireComponent]
public ShrimpleCharacterController Controller { get; set; }
[RequireComponent]
public CitizenAnimationHelper AnimationHelper { get; set; }
public SkinnedModelRenderer Renderer { get; set; }
public GameObject Camera { get; set; }
[Property]
[Range(50f, 200f, 10f)]
public float WalkSpeed { get; set; } = 100f;
[Property]
[Range(100f, 500f, 20f)]
public float RunSpeed { get; set; } = 300f;
[Property]
[Range(25f, 100f, 5f)]
public float DuckSpeed { get; set; } = 50f;
[Property]
[Range(200f, 500f, 20f)]
public float JumpStrength { get; set; } = 350f;
public Angles EyeAngles { get; set; }
protected override void OnStart()
{
base.OnStart();
Renderer = Components.Get<SkinnedModelRenderer>(FindMode.EverythingInSelfAndDescendants);
Camera = new GameObject(true, "Camera");
Camera.SetParent(GameObject);
var cameraComponent = Camera.Components.Create<CameraComponent>();
cameraComponent.ZFar = 32768f;
}
protected override void OnFixedUpdate()
{
base.OnFixedUpdate();
var wishDirection = Input.AnalogMove.Normal * Rotation.FromYaw(EyeAngles.yaw) * GameObject.WorldRotation;
var isDucking = Input.Down("Duck");
var isRunning = Input.Down("Run");
var wishSpeed = isDucking ? DuckSpeed :
isRunning ? RunSpeed : WalkSpeed;
Controller.WishVelocity = wishDirection * wishSpeed;
Controller.Move();
if (Input.Pressed("Jump") && Controller.IsOnGround)
{
Controller.Punch(-Controller.AppliedGravity.Normal * JumpStrength);
AnimationHelper?.TriggerJump();
}
if (!AnimationHelper.IsValid()) return;
AnimationHelper.WithWishVelocity(Controller.WishVelocity);
AnimationHelper.WithVelocity(Controller.Velocity);
AnimationHelper.DuckLevel = isDucking ? 1f : 0f;
AnimationHelper.IsGrounded = Controller.IsOnGround;
}
protected override void OnUpdate()
{
base.OnUpdate();
EyeAngles += Input.AnalogLook;
EyeAngles = EyeAngles.WithPitch(MathX.Clamp(EyeAngles.pitch, -10f, 40f));
// Renderer.WorldRotation = Rotation.Slerp(Renderer.WorldRotation, Rotation.FromYaw(EyeAngles.yaw), Time.Delta * 5f);
var cameraOffset = Vector3.Up * 70f + Vector3.Backward * 220f;
Camera.LocalRotation = EyeAngles.ToRotation();
Camera.LocalPosition = cameraOffset * Camera.LocalRotation;
}
}

View File

@@ -0,0 +1,916 @@
using System.Text.Json.Nodes;
namespace ShrimpleCharacterController;
[Icon("nordic_walking")]
public class ShrimpleCharacterController : Component
{
/// <summary>
/// Manually update this by calling Move() or let it always be simulated
/// </summary>
[Property]
[Group("Options")]
public bool ManuallyUpdate { get; set; } = true;
/// <summary>
/// If pushing against a wall, scale the velocity based on the wall's angle (False is useful for NPCs that get stuck on corners)
/// </summary>
[Property]
[Group("Options")]
public bool ScaleAgainstWalls { get; set; } = true;
[Sync]
float _traceWidth { get; set; } = 16f;
/// <summary>
/// Width of our trace
/// </summary>
[Property]
[Group("Trace")]
[Range(1f, 64f, 1f, true, true)]
public float TraceWidth
{
get => _traceWidth;
set
{
_traceWidth = value;
Bounds = BuildBounds();
_shrunkenBounds = Bounds.Grow(-SkinWidth);
}
}
[Sync]
float _traceHeight { get; set; } = 72f;
/// <summary>
/// Height of our trace
/// </summary>
[Property]
[Group("Trace")]
[Range(1f, 256f, 1f, true, true)]
public float TraceHeight
{
get => _traceHeight;
set
{
_traceHeight = value;
Bounds = BuildBounds();
_shrunkenBounds = Bounds.Grow(-SkinWidth);
}
}
/// <summary>
/// Rotate the trace with the gameobject
/// </summary>
[Property]
[Group("Trace")]
public bool RotateWithGameObject { get; set; } = true;
/// <summary>
/// Use a cylinder trace instead of a box trace<br/>
/// [WARNING] This is a PHYSICAL TRACE, so it's more expensive than the normal box trace
/// </summary>
[Property]
[Group("Trace")]
public bool CylinderTrace { get; set; } = false;
/// <summary>
/// Which tags it should ignore
/// </summary>
[Property]
[Group("Trace")]
public TagSet IgnoreTags { get; set; } = new TagSet();
/// <summary>
/// Max amount of trace calls whenever the simulation doesn't reach its target (Slide and collide bounces)
/// </summary>
[Property]
[Group("Trace")]
[Range(1, 20, 1, true, true)]
public int MaxBounces { get; set; } = 5;
/// <summary>
/// How fast you accelerate while on the ground (Units per second)
/// </summary>
[Property]
[Group("Movement")]
[Range(0f, 3000f, 10f, false)]
[HideIf("GroundStickEnabled", false)]
public float GroundAcceleration { get; set; } = 1000f;
/// <summary>
/// How fast you decelerate while on the ground (Units per second)
/// </summary>
[Property]
[Group("Movement")]
[Range(0f, 3000f, 10f, false)]
[HideIf("GroundStickEnabled", false)]
public float GroundDeceleration { get; set; } = 1500f;
/// <summary>
/// How fast you accelerate while in the air (Units per second)
/// </summary>
[Property]
[Group("Movement")]
[Range(0f, 3000f, 10f, false)]
public float AirAcceleration { get; set; } = 300f;
/// <summary>
/// How fast you decelerate while in the air (Units per second)
/// </summary>
[Property]
[Group("Movement")]
[Range(0f, 3000f, 10f, false)]
public float AirDeceleration { get; set; } = 0f;
/// <summary>
/// Do we ignore the friction of the surface you're standing on or not?
/// </summary>
[Property]
[Group("Movement")]
public bool IgnoreGroundSurface { get; set; } = false;
/// <summary>
/// Is this MoveHelper meant for horizontal grounded movement? (false = For flying or noclip)
/// </summary>
[Property]
[Group("Movement")]
public bool IgnoreZ { get; set; } = true;
/// <summary>
/// Do we ignore Z when it's near 0 (So that gravity affects you when not moving)
/// </summary>
[Property]
[Title("Ignore Z When Zero")]
[Group("Movement")]
[HideIf("IgnoreZ", true)]
public bool IgnoreZWhenZero { get; set; } = true;
/// <summary>
/// Tolerance from a 90° surface before it's considered a wall (Ex. Tolerance 1 = Between 89° and 91° can be a wall, 0.1 = 89.9° to 90.1°)
/// </summary>
[Group("Movement")]
[Property]
[Range(0f, 10f, 0.1f, false)]
public float WallTolerance { get; set; } = 1f;
/// <summary>
/// Player feels like it's gripping walls too much? Try more Grip Factor Reduction!
/// </summary>
[Group("Movement")]
[Property]
[Range(1f, 10f, 0.1f, true)]
public float GripFactorReduction { get; set; } = 1f;
/// <summary>
/// Stick the MoveHelper to the ground (IsOnGround will default to false if disabled)
/// </summary>
[FeatureEnabled("GroundStick")]
[Property]
public bool GroundStickEnabled { get; set; } = true;
/// <summary>
/// How steep terrain can be for you to stand on without slipping
/// </summary>
[Property]
[Feature("GroundStick")]
[Range(0f, 89f, 1f, true, true)]
public float MaxGroundAngle { get; set; } = 60f;
/// <summary>
/// How far from the ground the MoveHelper is going to stick (Useful for going down stairs!)
/// </summary>
[Property]
[Feature("GroundStick")]
[Range(1f, 32f, 1f, false)]
public float GroundStickDistance { get; set; } = 12f;
/// <summary>
/// Enable steps climbing (+1 Trace call)
/// </summary>
[FeatureEnabled("Steps")]
[Property]
public bool StepsEnabled { get; set; } = true;
/// <summary>
/// How high steps can be for you to climb on
/// </summary>
[Feature("Steps")]
[Property]
[Range(1f, 132f, 1f, false)]
public float StepHeight { get; set; } = 12f;
/// <summary>
/// How deep it checks for steps (Minimum depth)
/// </summary>
[Feature("Steps")]
[Property]
[Range(0.1f, 8f, 0.1f, false)]
public float StepDepth { get; set; } = 2f;
/// <summary>
/// Tolerance from a 90° surface before it's considered a valid step (Ex. Tolerance 1 = Between 89° and 91° can be a step, 0.1 = 89.9° to 90.1°)
/// </summary>
[Feature("Steps")]
[Property]
[Range(0f, 10f, 0.1f, false)]
public float StepTolerance { get; set; } = 1f;
/// <summary>
/// Enable to ability to walk on a surface that's too steep if it's equal or smaller than a step (+1 Trace call when on steep terrain)
/// </summary>
[Feature("Steps")]
[Property]
public bool PseudoStepsEnabled { get; set; } = true;
/// <summary>
/// Instead of colliding with these tags the MoveHelper will be pushed away (Make sure the tags are in IgnoreTags as well!)
/// </summary>
[FeatureEnabled("Push")]
[Property]
public bool PushEnabled { get; set; } = false;
[Sync]
Dictionary<string, float> _pushTagsWeight { get; set; } = new Dictionary<string, float>() { { "player", 1f } };
/// <summary>
/// Which tags will push this MoveHelper away and with how much force (Make sure they are also included in IgnoreTags!) (+1 Trace call)
/// </summary>
[Property]
[Feature("Push")]
public Dictionary<string, float> PushTagsWeight
{
get => _pushTagsWeight;
set
{
_pushTagsWeight = value;
_pushTags = BuildPushTags();
}
}
/// <summary>
/// Apply gravity to this MoveHelper when not on the ground
/// </summary>
[FeatureEnabled("Gravity")]
[Property]
public bool GravityEnabled { get; set; } = true;
private bool _useSceneGravity = true;
/// <summary>
/// Use the scene's gravity or our own
/// </summary>
[Property]
[Feature("Gravity")]
public bool UseSceneGravity
{
get => _useSceneGravity;
set
{
_useSceneGravity = value;
_appliedGravity = BuildGravity();
}
}
private bool _useVectorGravity = false;
/// <summary>
/// Use a Vector3 gravity instead of a single float (Use this if you want to use a custom gravity)
/// </summary>
[Property]
[Feature("Gravity")]
[HideIf("UseSceneGravity", true)]
public bool UseVectorGravity
{
get => _useVectorGravity;
set
{
_useVectorGravity = value;
_appliedGravity = BuildGravity();
}
}
private bool _usingFloatGravity => !UseVectorGravity && !UseSceneGravity;
private bool _usingVectorGravity => UseVectorGravity && !UseSceneGravity;
private float _gravity = -850f;
/// <summary>
/// Units per second squared (Default is -850f)
/// </summary>
[Property]
[Feature("Gravity")]
[Range(-2000, 2000, 1, false)]
[ShowIf("_usingFloatGravity", true)]
public float Gravity
{
get => _gravity;
set
{
_gravity = value;
_appliedGravity = BuildGravity();
}
}
private Vector3 _vectorGravity = new Vector3(0f, 0f, -850f);
/// <summary>
/// Units per second squared (Default is 0f, 0f, -850f)<br/>
/// Changes which way <see cref="GroundStickEnabled"/> sticks to the ground
/// </summary>
[Property]
[Feature("Gravity")]
[ShowIf("_usingVectorGravity", true)]
public Vector3 VectorGravity
{
get => _vectorGravity;
set
{
_vectorGravity = value;
_appliedGravity = BuildGravity();
}
}
private Vector3 _appliedGravity;
public Vector3 AppliedGravity => _appliedGravity;
/// <summary>
/// Check if the MoveHelper is stuck and try to get it to unstuck (+Trace calls if stuck)
/// </summary>
[FeatureEnabled("Unstuck")]
[Property]
public bool UnstuckEnabled { get; set; } = true;
/// <summary>
/// How many trace calls it will attempt to get the MoveHelper unstuck
/// </summary>
[Property]
[Feature("Unstuck")]
[Range(1, 50, 1, false)]
public int MaxUnstuckTries { get; set; } = 20;
/// <summary>
/// The simulated target velocity for our MoveHelper (Units per second, we apply Time.Delta inside)
/// </summary>
[Sync] public Vector3 WishVelocity { get; set; }
/// <summary>
/// The resulting velocity after the simulation is done (Units per second)
/// </summary>
[Sync] public Vector3 Velocity { get; set; }
/// <summary>
/// Is the MoveHelper currently touching the ground
/// </summary>
[Sync] public bool IsOnGround { get; set; }
/// <summary>
/// The current ground normal you're standing on (Always Vector3.Zero if IsOnGround false)
/// </summary>
public Vector3 GroundNormal { get; private set; } = Vector3.Zero;
public Vector3 Up { get; set; } = Vector3.Up;
/// <summary>
/// The current ground angle you're standing on (Always 0f if IsOnGround false)
/// </summary>
public float GroundAngle => Vector3.GetAngle(GroundNormal, Up);
/// <summary>
/// The current surface you're standing on
/// </summary>
public Surface GroundSurface { get; private set; }
/// <summary>
/// The gameobject you're currently standing on
/// </summary>
public GameObject GroundObject { get; set; }
/// <summary>
/// Is the MoveHelper currently pushing against a wall
/// </summary>
public bool IsPushingAgainstWall { get; private set; }
/// <summary>
/// The current wall normal you're pushing against (Always Vector3.Zero if IsPushingAgainstWall false)
/// </summary>
public Vector3 WallNormal { get; private set; } = Vector3.Zero;
/// <summary>
/// The gameobject you're currently pushing on
/// </summary>
public GameObject WallObject { get; set; }
/// <summary>
/// Is the MoveHelper standing on a terrain too steep to stand on (Always false if IsOnGround false)
/// </summary>
[Sync] public bool IsSlipping { get; private set; } // TODO IMPLEMENT
/// <summary>
/// The MoveHelper is stuck and we can't get it out
/// </summary>
[Sync] public bool IsStuck { get; private set; }
/// <summary>
/// To avoid getting stuck due to imprecision we shrink the bounds before checking and compensate for it later
/// </summary>
public float SkinWidth;
public float AppliedWidth => TraceWidth / 2f * WorldScale.x; // The width of the MoveHelper in world units
public float AppliedDepth => TraceWidth / 2f * WorldScale.y; // The depth of the MoveHelper in world units
public float AppliedHeight => TraceHeight / 2f * WorldScale.z; // The height of the MoveHelper in world units
private Vector3 _offset => (RotateWithGameObject ? WorldRotation.Up : Vector3.Up) * AppliedHeight; // The position of the MoveHelper in world units
/// <summary>
/// The bounds of this MoveHelper generated from the TraceWidth and TraceHeight
/// </summary>
public BBox Bounds { get; set; }
private BBox _shrunkenBounds;
private string[] _pushTags;
private Vector3 _lastVelocity;
/// <summary>
/// If another MoveHelper moved at the same time and they're stuck, let this one know that the other already unstuck for us
/// </summary>
public ShrimpleCharacterController UnstuckTarget;
public override int ComponentVersion => 1;
protected override void OnStart()
{
SkinWidth = Math.Min(Math.Max(0.1f, TraceWidth * 0.05f), GroundStickDistance); // SkinWidth is 5% of the total width
Bounds = BuildBounds();
_shrunkenBounds = Bounds.Grow(-SkinWidth);
_pushTags = BuildPushTags();
}
protected override void DrawGizmos()
{
if (Gizmo.IsSelected)
{
Gizmo.GizmoDraw draw = Gizmo.Draw;
draw.Color = Color.Blue;
var bounds = BuildBounds();
if (CylinderTrace)
draw.LineCylinder(Vector3.Zero, WorldRotation.Up * (bounds.Maxs.z - bounds.Mins.z), bounds.Maxs.x, bounds.Maxs.x, 24);
else
draw.LineBBox(bounds.Translate(Vector3.Up * TraceHeight / 2f * GameObject.WorldScale.z));
}
}
private BBox BuildBounds()
{
var x = GameObject.WorldScale.x;
var y = GameObject.WorldScale.y;
var z = GameObject.WorldScale.z;
var width = TraceWidth / 2f * x;
var depth = TraceWidth / 2f * y;
var height = TraceHeight / 2f * z;
return new BBox(new Vector3(-width, -depth, -height), new Vector3(width, depth, height));
}
private Vector3 BuildGravity() => UseSceneGravity ? Scene.PhysicsWorld.Gravity : UseVectorGravity ? VectorGravity : new Vector3(0f, 0f, Gravity);
private string[] BuildPushTags()
{
return PushTagsWeight.Keys.ToArray();
}
/// <summary>
/// Casts the current bounds from to and returns the scene trace result
/// </summary>
/// <param name="bounds"></param>
/// <param name="from"></param>
/// <param name="to"></param>
/// <returns></returns>
public SceneTraceResult BuildTrace(BBox bounds, Vector3 from, Vector3 to)
{
SceneTrace builder = new SceneTrace(); // Empty trace builder
if (CylinderTrace)
builder = Game.SceneTrace.Cylinder(bounds.Maxs.z - bounds.Mins.z, bounds.Maxs.x, from, to);
else
builder = Game.SceneTrace.Box(bounds, from, to);
builder = builder
.IgnoreGameObjectHierarchy(GameObject)
.WithoutTags(IgnoreTags);
if (RotateWithGameObject)
builder = builder.Rotated(GameObject.WorldRotation);
return builder.Run();
}
private SceneTraceResult BuildPushTrace(BBox bounds, Vector3 from, Vector3 to)
{
SceneTrace builder = new SceneTrace(); // Empty trace builder
if (CylinderTrace)
builder = Game.SceneTrace.Cylinder(bounds.Maxs.z - bounds.Mins.z, bounds.Maxs.x, from, to);
else
builder = Game.SceneTrace.Box(bounds, from, to);
builder = builder
.IgnoreGameObjectHierarchy(GameObject)
.WithAnyTags(_pushTags); // Check for only the push tags
if (RotateWithGameObject)
builder = builder.Rotated(GameObject.WorldRotation);
return builder.Run();
}
/// <summary>
/// Detach the MoveHelper from the ground and launch it somewhere (Units per second)
/// </summary>
/// <param name="amount"></param>
public void Punch(in Vector3 amount)
{
IsOnGround = false;
Velocity += amount;
}
/// <summary>
/// Apply the WishVelocity, update the Velocity and the Position of the GameObject by simulating the MoveHelper
/// </summary>
/// <param name="manualUpdate">Just calculate but don't update position</param>
public MoveHelperResult Move(bool manualUpdate = false) => Move(Time.Delta, manualUpdate);
/// <summary>
/// Apply the WishVelocity, update the Velocity and the Position of the GameObject by simulating the MoveHelper
/// </summary>
/// <param name="delta">The time step</param>
/// <param name="manualUpdate">Just calculate but don't update position</param>
public MoveHelperResult Move(float delta, bool manualUpdate = false)
{
var goalVelocity = CalculateGoalVelocity(delta); // Calculate the goal velocity using our Acceleration and Deceleration values
// KNOWN ISSUE: Velocity starts to build up to massive amounts when trying to climb terrain too steep?
// SIMULATE PUSH FORCES //
if (PushEnabled)
{
var pushTrace = BuildPushTrace(Bounds, WorldPosition + _offset, WorldPosition); // Build a trace but using the Push tags instead of the Ignore tags
if (pushTrace.Hit) // We're inside any of the push tags
{
foreach (var tag in pushTrace.GameObject.Tags)
{
if (PushTagsWeight.TryGetValue(tag, out var tagWeight))
{
var otherPosition = pushTrace.GameObject.WorldPosition.WithZ(WorldPosition.z); // Only horizontal pushing
var pushDirection = (otherPosition - WorldPosition).Normal;
var pushVelocity = pushDirection * tagWeight * 50f; // I find 50 u/s to be a good amount to push if the weight is 1.0 (!!!)
goalVelocity -= pushVelocity;
}
}
}
}
var moveHelperResult = CollideAndSlide(goalVelocity, WorldPosition + _offset, delta); // Simulate the MoveHelper
var finalPosition = moveHelperResult.Position;
var finalVelocity = moveHelperResult.Velocity;
// SIMULATE GRAVITY //
if (GravityEnabled && Gravity != 0f)
{
if (!IsOnGround || IsSlipping || !GroundStickEnabled)
{
var gravity = AppliedGravity * delta;
var gravityResult = CollideAndSlide(gravity, moveHelperResult.Position, delta, gravityPass: true); // Apply and simulate the gravity step
finalPosition = gravityResult.Position;
finalVelocity += gravityResult.Velocity;
}
}
_lastVelocity = Velocity * delta;
if (!manualUpdate)
{
Velocity = finalVelocity;
WorldPosition = finalPosition - _offset; // Actually updating the position is "expensive" so we only do it once at the end
}
return new MoveHelperResult(finalPosition, finalVelocity);
}
/// <summary>
/// Sometimes we have to update only the position but not the velocity (Like when climbing steps or getting unstuck) so we can't have Position rely only on Velocity
/// </summary>
public struct MoveHelperResult
{
public Vector3 Position;
public Vector3 Velocity;
public MoveHelperResult(Vector3 position, Vector3 velocity)
{
Position = position;
Velocity = velocity;
}
}
private MoveHelperResult CollideAndSlide(Vector3 velocity, Vector3 position, float delta, int depth = 0, bool gravityPass = false) =>
CollideAndSlide(new MoveHelperResult(position, velocity), delta, depth, gravityPass);
private MoveHelperResult CollideAndSlide(MoveHelperResult current, float delta, int depth = 0, bool gravityPass = false)
{
if (depth >= MaxBounces)
return current;
var velocity = current.Velocity * delta; // I like to set Velocity as units/second but we have to deal with units/tick here
var position = current.Position;
// GROUND AND UNSTUCK CHECK //
if (depth == 0) // Only check for the first step since it's impossible to get stuck on other steps
{
var groundTrace = BuildTrace(_shrunkenBounds, position, position + AppliedGravity.Normal * (GroundStickDistance + SkinWidth * 1.1f)); // Compensate for floating inaccuracy
if (groundTrace.StartedSolid)
{
IsStuck = true;
if (UnstuckEnabled)
{
if (UnstuckTarget == null)
{
IsStuck = !TryUnstuck(position, out var result);
if (!IsStuck)
{
position = result; // Update the new position
if (groundTrace.GameObject != null)
if (groundTrace.GameObject.Components.TryGet<ShrimpleCharacterController>(out var otherHelper))
otherHelper.UnstuckTarget = this; // We already solved this, no need to unstuck the other helper
}
else
{
return new MoveHelperResult(position, Vector3.Zero); // Mission failed, bail out!
}
}
else
{
UnstuckTarget = null; // Alright the other MoveHelper got us unstuck so just do nothing
}
}
}
else
{
var hasLanded = !IsOnGround && Vector3.Dot(Velocity, AppliedGravity) >= 0f && groundTrace.Hit && groundTrace.Distance <= SkinWidth * 2f; // Wasn't on the ground and now is
var isGrounded = IsOnGround && groundTrace.Hit; // Was already on the ground and still is, this helps stick when going down stairs
IsOnGround = hasLanded || isGrounded;
GroundSurface = IsOnGround ? groundTrace.Surface : null;
GroundNormal = IsOnGround ? groundTrace.Normal : -AppliedGravity.Normal;
GroundObject = IsOnGround ? groundTrace.GameObject : null;
IsSlipping = IsOnGround && GroundAngle > MaxGroundAngle;
if (IsSlipping && !gravityPass && Vector3.Dot(velocity, AppliedGravity) < 0f)
velocity = velocity.WithZ(0f); // If we're slipping ignore any extra velocity we had
if (IsOnGround && GroundStickEnabled && !IsSlipping)
{
position = groundTrace.EndPosition + -AppliedGravity.Normal * SkinWidth; // Place on the ground
velocity = Vector3.VectorPlaneProject(velocity, GroundNormal); // Follow the ground you're on without projecting Z
}
IsStuck = false;
}
}
if (velocity.IsNearlyZero(0.01f)) // Not worth continuing, reduces small stutter
{
return new MoveHelperResult(position, Vector3.Zero);
}
var toTravel = velocity.Length + SkinWidth;
var targetPosition = position + velocity.Normal * toTravel;
var travelTrace = BuildTrace(_shrunkenBounds, position, targetPosition);
if (travelTrace.Hit)
{
var travelled = velocity.Normal * Math.Max(travelTrace.Distance - SkinWidth, 0f);
var leftover = velocity - travelled; // How much leftover velocity still needs to be simulated
var angle = Vector3.GetAngle(-AppliedGravity.Normal, travelTrace.Normal);
if (toTravel >= SkinWidth && travelTrace.Distance < SkinWidth)
travelled = Vector3.Zero;
if (angle <= MaxGroundAngle) // Terrain we can walk on
{
if (gravityPass || !IsOnGround)
leftover = Vector3.VectorPlaneProject(leftover, travelTrace.Normal); // Don't project the vertical velocity after landing else it boosts your horizontal velocity
else
leftover = leftover.ProjectAndScale(travelTrace.Normal); // Project the velocity along the terrain
IsPushingAgainstWall = false;
WallObject = null;
}
else
{
var climbedStair = false;
if (angle >= 90f - WallTolerance && angle <= 90f + WallTolerance) // Check for walls
IsPushingAgainstWall = true; // We're pushing against a wall
if (true) //StepsEnabled
{
var isStep = angle >= 90f - StepTolerance && angle <= 90f + StepTolerance;
// Log.Info($"Step? {isStep} — Angle: {angle:F2}, StepTolerance: {StepTolerance}");
// Log.Info($"Step detected: angle={angle}, isStep={isStep}, PseudoStepsEnabled={PseudoStepsEnabled}");
if (isStep || PseudoStepsEnabled)
{
if (IsOnGround)
{
var stepHorizontal = Vector3.VectorPlaneProject(velocity, AppliedGravity).Normal * StepDepth;
var stepVertical = -AppliedGravity.Normal * (StepHeight + SkinWidth);
var stepStart = travelTrace.EndPosition + stepHorizontal + stepVertical;
var stepEnd = travelTrace.EndPosition + stepHorizontal;
// Log.Info($"StepTrace start: {stepStart}, end: {stepEnd}");
var stepTrace = BuildTrace(_shrunkenBounds, stepStart, stepEnd);
// Gizmo.Draw.Arrow(stepStart, stepEnd);
var stepAngle = Vector3.GetAngle(stepTrace.Normal, -AppliedGravity.Normal);
// Log.Info($"StepTrace hit: {stepTrace.Hit}, angle: {stepAngle:F2}, startedSolid: {stepTrace.StartedSolid}");
if (!stepTrace.StartedSolid && stepTrace.Hit && stepAngle <= MaxGroundAngle)
{
if (isStep || (!IsSlipping && PseudoStepsEnabled))
{
var stepHeight = Vector3.Dot(stepTrace.EndPosition - travelTrace.EndPosition, Up);
var absStepHeight = Math.Abs(stepHeight);
if(absStepHeight >= stepHeight)
{
var stepTravelled = Up * absStepHeight;
position += stepTravelled;
climbedStair = true;
IsPushingAgainstWall = false;
WallObject = null;
}
// Log.Info($"Climbed stair! Height: {absStepHeight:F4}, New Pos: {position}");
}
}
}
}
}
if (IsPushingAgainstWall)
{
// Scale our leftover velocity based on the angle of approach relative to the wall
// (Perpendicular = 0%, Parallel = 100%)
var scale = ScaleAgainstWalls ? 1f - Vector3.Dot(-travelTrace.Normal.Normal / GripFactorReduction, velocity.Normal) : 1f;
var wallLeftover = ScaleAgainstWalls ? Vector3.VectorPlaneProject(leftover, travelTrace.Normal.Normal) : leftover.ProjectAndScale(travelTrace.Normal.Normal);
leftover = (wallLeftover * scale).WithZ(wallLeftover.z);
WallObject = travelTrace.GameObject;
WallNormal = travelTrace.Normal;
}
else
{
if (!climbedStair)
{
var scale = IsSlipping ? 1f : 1f - Vector3.Dot(-travelTrace.Normal / GripFactorReduction, velocity.Normal);
leftover = ScaleAgainstWalls ? Vector3.VectorPlaneProject(leftover, travelTrace.Normal) * scale : leftover.ProjectAndScale(travelTrace.Normal);
}
}
}
if (travelled.Length <= 0.01f && leftover.Length <= 0.01f)
return new MoveHelperResult(position + travelled, travelled / delta);
var newResult = CollideAndSlide(new MoveHelperResult(position + travelled, leftover / delta), delta, depth + 1, gravityPass); // Simulate another bounce for the leftover velocity from the latest position
var currentResult = new MoveHelperResult(newResult.Position, travelled / delta + newResult.Velocity); // Use the new bounce's position and combine the velocities
return currentResult;
}
if (depth == 0 && !gravityPass)
{
IsPushingAgainstWall = false;
WallObject = null;
}
return new MoveHelperResult(position + velocity, velocity / delta); // We didn't hit anything? Ok just keep going then :-)
}
private float CalculateGoalSpeed(Vector3 wishVelocity, Vector3 velocity, Vector3 surfaceNormal, bool isAccelerating, float delta)
{
Vector3 wishDir = Vector3.VectorPlaneProject(wishVelocity, surfaceNormal).Normal;
Vector3 currentDir = Vector3.VectorPlaneProject(velocity, surfaceNormal).Normal;
bool isSameDirection = velocity.IsNearlyZero(1f) || Vector3.Dot(wishDir, currentDir) >= 0f;
float acceleration = IsOnGround ? GroundAcceleration : AirAcceleration;
float deceleration = IsOnGround ? GroundDeceleration : AirDeceleration;
float goalSpeed = isAccelerating
? acceleration
: (!isSameDirection ? Math.Max(acceleration, deceleration) : deceleration);
if (!IgnoreGroundSurface && GroundSurface != null)
goalSpeed *= GroundSurface.Friction;
return goalSpeed * delta;
}
private Vector3 CalculateGoalVelocity(float delta)
{
Vector3 surfaceNormal = Up;
var wishVelocityProjected = Vector3.VectorPlaneProject(WishVelocity, surfaceNormal);
var velocityProjected = Vector3.VectorPlaneProject(Velocity, surfaceNormal);
bool isAccelerating = wishVelocityProjected.Length >= velocityProjected.Length;
float goalSpeed = CalculateGoalSpeed(wishVelocityProjected, Velocity, surfaceNormal, isAccelerating, delta);
Vector3 goalVelocity = Velocity.MoveTowards(wishVelocityProjected, goalSpeed);
return goalVelocity;
}
public bool TryUnstuck(Vector3 position, out Vector3 result)
{
if (_lastVelocity == Vector3.Zero)
_lastVelocity = -AppliedGravity.Normal;
var velocityLength = _lastVelocity.Length + SkinWidth;
var startPos = position - _lastVelocity.Normal * velocityLength; // Try undoing the last velocity 1st
var endPos = position;
for (int i = 0; i < MaxUnstuckTries + 1; i++)
{
if (i == 1)
startPos = position + -AppliedGravity.Normal * 2f; // Try going up 2nd
if (i > 1)
startPos = position + Vector3.Random.Normal * ((float)i / 2f); // Start randomly checking 3rd
if (startPos - endPos == Vector3.Zero) // No difference!
continue;
var unstuckTrace = BuildTrace(_shrunkenBounds, startPos, endPos);
if (!unstuckTrace.StartedSolid)
{
result = unstuckTrace.EndPosition - _lastVelocity.Normal * SkinWidth / 4f;
_lastVelocity = Vector3.Zero;
return true;
}
}
result = position;
return false;
}
/// <summary>
/// Debug don't use
/// </summary>
/// <param name="position"></param>
/// <param name="title"></param>
/// <returns></returns>
private bool TestPosition(Vector3 position, string title)
{
var testTrace = BuildTrace(_shrunkenBounds, position, position);
if (testTrace.StartedSolid)
{
Log.Info($"[{RealTime.Now}]{title} {GameObject.Name} started solid at {position} against {testTrace.GameObject}");
return true;
}
return false;
}
protected override void OnFixedUpdate()
{
base.OnFixedUpdate();
if (!ManuallyUpdate && Active)
Move();
}
[JsonUpgrader(typeof(ShrimpleCharacterController), 1)]
private static void FloatGravityUpgrader(JsonObject json)
{
json.Remove("Gravity", out var newNode);
json["_gravity"] = newNode;
}
}

View File

@@ -0,0 +1,40 @@
namespace ShrimpleCharacterController;
public static class Vector3Extensions
{
/// <summary>
/// Move a vector3 towards a goal by a fixed distance
/// </summary>
/// <param name="value"></param>
/// <param name="target"></param>
/// <param name="travelSpeed"></param>
/// <returns></returns>
public static Vector3 MoveTowards( this Vector3 value, Vector3 target, float travelSpeed )
{
var difference = target - value;
var distance = difference.Length;
var normal = difference.Normal;
if ( distance <= travelSpeed || distance == 0f )
{
return target;
}
return value + normal * travelSpeed;
}
/// <summary>
/// Project a vector along a plane (normal) and scale it back to its original length
/// </summary>
/// <param name="value"></param>
/// <param name="normal"></param>
/// <returns></returns>
public static Vector3 ProjectAndScale( this Vector3 value, Vector3 normal )
{
var length = value.Length;
value = Vector3.VectorPlaneProject( value, normal ).Normal;
value *= length;
return value;
}
}

View File

@@ -0,0 +1,16 @@
{
"Title": "Shrimple Character Controller",
"Type": "library",
"Org": "fish",
"Ident": "scc",
"Schema": 1,
"IncludeSourceFiles": false,
"Resources": null,
"PackageReferences": [],
"EditorReferences": null,
"Mounts": null,
"IsStandaloneOnly": false,
"Metadata": {
"CsProjName": ""
}
}