├── IKFootSolver.cs ├── IKFootSolver.cs.meta ├── LICENSE.txt ├── LegController.cs ├── LegController.cs.meta ├── README.md ├── Rotators.cs └── Rotators.cs.meta /IKFootSolver.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using UnityEditor; 5 | 6 | public class IKFootSolver : MonoBehaviour 7 | { 8 | public LayerMask terrainLayer; 9 | 10 | float stepHeight = 0.2f; 11 | float stepSpeed = 3; 12 | 13 | private Vector3 target; 14 | 15 | private float lerp; 16 | private Vector3 currentPosition; 17 | Vector3 oldPos; 18 | 19 | private float bodySpeed; 20 | public bool grounded; 21 | 22 | void Start(){ 23 | bodySpeed = 0; 24 | grounded = true; 25 | oldPos = transform.position; 26 | } 27 | 28 | void Update() 29 | { 30 | transform.position = currentPosition; 31 | 32 | if(lerp < 1){ 33 | Vector3 footPos = Vector3.Slerp(oldPos,target,lerp); 34 | footPos.y = Mathf.Sin(lerp * Mathf.PI) * stepHeight; 35 | currentPosition = footPos; 36 | lerp += Time.deltaTime * (stepSpeed + (1 + bodySpeed)); 37 | grounded = false; 38 | }else{ 39 | currentPosition = target; 40 | grounded = true; 41 | } 42 | } 43 | 44 | public void UpdatePosition(Vector3 targetPosition){ 45 | target = GetNewPosition(targetPosition); 46 | oldPos = transform.position; 47 | } 48 | 49 | Vector3 GetNewPosition(Vector3 target){ 50 | lerp = 0; 51 | Ray ray = new Ray(target + Vector3.up * 5f,Vector3.down); 52 | if (Physics.Raycast(ray, out RaycastHit info,10,terrainLayer.value)) 53 | { 54 | return info.point; 55 | } 56 | return target; 57 | } 58 | 59 | public bool IsGrounded(){ 60 | return grounded; 61 | } 62 | 63 | public void SetStepHeight(float sh){ 64 | stepHeight = sh; 65 | } 66 | public void SetBaseSpeed(float sp){ 67 | stepSpeed = sp; 68 | } 69 | public void SetSpeed(float sp){ 70 | bodySpeed = sp; 71 | } 72 | 73 | void OnDrawGizmos(){ 74 | Gizmos.color = Color.black; 75 | Gizmos.DrawSphere(target,0.15f); 76 | 77 | if(grounded){ 78 | Handles.Label(transform.position,"Grounded"); 79 | } 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /IKFootSolver.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 254f95a93123b4247a038f6a3de60757 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining 2 | a copy of this software and associated documentation files (the 3 | "Software"), to deal in the Software without restriction, including 4 | without limitation the rights to use, copy, modify, merge, publish, 5 | distribute, sublicense, and/or sell copies of the Software, and to 6 | permit persons to whom the Software is furnished to do so, subject to 7 | the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be 10 | included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 13 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 14 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 15 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 16 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 17 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 18 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /LegController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using System; 5 | 6 | public class LegController : MonoBehaviour 7 | { 8 | 9 | // Foot references 10 | public GameObject leftFoot; 11 | IKFootSolver leftFootIK; 12 | Vector3 leftTarget; 13 | public GameObject rightFoot; 14 | IKFootSolver rightFootIK; 15 | Vector3 rightTarget; 16 | 17 | // Box size at rest 18 | public float stanceWidth; 19 | public float stanceLength; 20 | 21 | // base walk speed 22 | public float stepSpeed; 23 | 24 | public float stepHeight; 25 | 26 | // Determines how the box is scaled with speed 27 | public float strideScale; 28 | public float strafeScale; 29 | 30 | // Determines how far out the Center of Mass is projected 31 | public float strafeSize; 32 | public float strideSize; 33 | 34 | public LayerMask terrainLayer; 35 | 36 | // for checking we are inside the box 37 | public float tolerance = 0.05f; 38 | 39 | // angle determining what is considered walking "diagonal" 40 | public float sideWalkThreshold = 25f; 41 | 42 | Vector3 _vel = Vector3.zero; 43 | Vector3 _oldVel = Vector3.zero; 44 | Vector3 _oldPos; 45 | 46 | // Corners of our box 47 | Vector3 _topLeft; 48 | Vector3 _bottomLeft; 49 | Vector3 _topRight; 50 | Vector3 _bottomRight; 51 | 52 | // Velocity-adjusted center-of-mass 53 | Vector3 _com; 54 | 55 | // 56 | bool leftsTurn = true; 57 | bool rightsTurn = true; 58 | 59 | Vector3 _oldCom; 60 | 61 | Quaternion _oldRot; 62 | 63 | // Start is called before the first frame update 64 | void Start() 65 | { 66 | _oldPos = transform.position; 67 | _com = GetCenterOfMass(); 68 | _oldCom = _com; 69 | UpdateCorners(); 70 | 71 | _oldRot = transform.rotation; 72 | 73 | leftTarget = LeftFootMapping(); 74 | rightTarget = RightFootMapping(); 75 | 76 | // Initialize IK variables 77 | leftFootIK = leftFoot.GetComponent(); 78 | rightFootIK = rightFoot.GetComponent(); 79 | leftFootIK.SetBaseSpeed(stepSpeed); 80 | rightFootIK.SetBaseSpeed(stepSpeed); 81 | leftFootIK.SetStepHeight(stepHeight); 82 | rightFootIK.SetStepHeight(stepHeight); 83 | leftFootIK.UpdatePosition(leftTarget); 84 | rightFootIK.UpdatePosition(rightTarget); 85 | } 86 | 87 | // Update is called once per frame 88 | void Update() 89 | { 90 | // get previous velocity 91 | _oldVel = _vel; 92 | 93 | // get Velocity 94 | _vel = transform.position - _oldPos; 95 | _vel /= Time.deltaTime; 96 | _oldPos = transform.position; 97 | 98 | // set foot animation speed 99 | leftFootIK.SetSpeed(_vel.magnitude); 100 | rightFootIK.SetSpeed(_vel.magnitude); 101 | 102 | // get center of mass 103 | _com = GetCenterOfMass(); 104 | 105 | // update the box when we start moving 106 | if(_oldVel.magnitude == 0 && _vel.magnitude != 0){ 107 | UpdateCorners(); 108 | } 109 | 110 | // if we move far enough, update corners 111 | if(Mathf.Abs(_oldCom.x - _com.x) > stanceWidth || Mathf.Abs(_oldCom.z - _com.z) > stanceLength){ 112 | UpdateCorners(); 113 | } 114 | 115 | // if we are rotating 116 | if(_oldRot != transform.rotation){ 117 | UpdateCorners(); 118 | } 119 | 120 | _oldRot = transform.rotation; 121 | 122 | if(_vel.magnitude == 0){ 123 | //if we stop moving, regain our footing 124 | if(_oldVel.magnitude > 0){ 125 | if(LeftFootFurther()){ 126 | leftTarget = LeftFootMapping(); 127 | leftFootIK.UpdatePosition(leftTarget); 128 | leftsTurn = false; 129 | rightsTurn = true; 130 | }else{ 131 | rightTarget = RightFootMapping(); 132 | rightFootIK.UpdatePosition(rightTarget); 133 | leftsTurn = true; 134 | rightsTurn = false; 135 | } 136 | } 137 | else 138 | { 139 | // Move the feet while stationary or rotating 140 | if(leftsTurn){ 141 | if((leftFoot.transform.position - LeftFootMapping()).magnitude > stanceWidth && rightFootIK.IsGrounded()) 142 | { 143 | // make sure that we are alternating steps 144 | leftTarget = LeftFootMapping(); 145 | leftFootIK.UpdatePosition(leftTarget); 146 | leftsTurn = false; 147 | rightsTurn = true; 148 | } 149 | }else{ 150 | if((rightFoot.transform.position - RightFootMapping()).magnitude > stanceWidth && leftFootIK.IsGrounded()) 151 | { 152 | rightTarget = RightFootMapping(); 153 | rightFootIK.UpdatePosition(rightTarget); 154 | leftsTurn = true; 155 | rightsTurn = false; 156 | } 157 | } 158 | } 159 | 160 | } 161 | 162 | 163 | 164 | // Move the feet while walking 165 | if(leftsTurn){ 166 | if(!FootInsideBox(leftFoot) && rightFootIK.IsGrounded()) 167 | { 168 | // make sure that we are alternating steps 169 | leftTarget = LeftFootMapping(); 170 | leftFootIK.UpdatePosition(leftTarget); 171 | leftsTurn = false; 172 | rightsTurn = true; 173 | } 174 | }else{ 175 | if(!FootInsideBox(rightFoot) && leftFootIK.IsGrounded()) 176 | { 177 | rightTarget = RightFootMapping(); 178 | rightFootIK.UpdatePosition(rightTarget); 179 | leftsTurn = true; 180 | rightsTurn = false; 181 | } 182 | } 183 | } 184 | 185 | // Update the corners of the box 186 | void UpdateCorners(){ 187 | 188 | // get an un-rotated velocity vector 189 | Quaternion unRotate = Quaternion.Euler(0,-transform.rotation.eulerAngles.y,0); 190 | Vector3 _rotVel = Rotators.Rotated(_vel,unRotate,transform.up); 191 | 192 | // get width and length of the box 193 | float _x = (stanceWidth + (Mathf.Abs(_rotVel.x) * strafeScale / 2)); 194 | float _z = (stanceLength + (Mathf.Abs(_rotVel.z) * strideScale / 2)); 195 | 196 | // get positions of corners 197 | _topLeft = -Vector3.right * _x + Vector3.forward * _z; 198 | _topRight = Vector3.right * _x + Vector3.forward * _z; 199 | _bottomLeft = -Vector3.right * _x - Vector3.forward * _z; 200 | _bottomRight = Vector3.right * _x - Vector3.forward * _z; 201 | 202 | // rotate corners 203 | _topLeft = Rotators.Rotated(_topLeft,transform.rotation,transform.up); 204 | _topRight = Rotators.Rotated(_topRight,transform.rotation,transform.up); 205 | _bottomLeft = Rotators.Rotated(_bottomLeft,transform.rotation,transform.up); 206 | _bottomRight = Rotators.Rotated(_bottomRight,transform.rotation,transform.up); 207 | 208 | // move by velocity 209 | _topLeft += _com; 210 | _topRight += _com; 211 | _bottomLeft += _com; 212 | _bottomRight += _com; 213 | 214 | _oldCom = _com; 215 | } 216 | 217 | // Get future Center of mass based on instantaneous velocity 218 | Vector3 GetCenterOfMass(){ 219 | Ray ray = new Ray(transform.position + new Vector3(_vel.x * strafeSize,0,_vel.z * strideSize), Vector3.down); 220 | if (Physics.Raycast(ray, out RaycastHit info,10,terrainLayer.value)) 221 | { 222 | return info.point; 223 | } 224 | return transform.position + _vel; 225 | } 226 | 227 | // The furthest foot is the one getting moved 228 | bool LeftFootFurther(){ 229 | float _leftDist = (_com - leftFoot.transform.position).magnitude; 230 | float _rightDist = (_com - rightFoot.transform.position).magnitude; 231 | if(_leftDist > _rightDist){ 232 | return true; 233 | } 234 | return false; 235 | } 236 | 237 | // Checks if a foot is inside of the box 238 | bool FootInsideBox(GameObject foot){ 239 | 240 | // get an unrotation Quaternion 241 | Quaternion unRotate = Quaternion.Euler(0,-transform.rotation.eulerAngles.y,0); 242 | 243 | // center corners at origin 244 | Vector3 _tl = _topLeft - _com; 245 | Vector3 _tr = _topRight - _com; 246 | Vector3 _bl = _bottomLeft - _com; 247 | 248 | // un-rotate corners 249 | _tl = Rotators.Rotated(_tl,unRotate,transform.up); 250 | _tr = Rotators.Rotated(_tr,unRotate,transform.up); 251 | _bl = Rotators.Rotated(_bl,unRotate,transform.up); 252 | 253 | // center and un-rotate foot 254 | Vector3 _rotFoot = foot.transform.position - _com; 255 | _rotFoot = Rotators.Rotated(_rotFoot,unRotate,transform.up); 256 | 257 | // compare to bounding box as normal 258 | float x = _rotFoot.x; 259 | float z = _rotFoot.z; 260 | 261 | if(x >= _tl.x - tolerance && x <= _tr.x + tolerance && z <= _tl.z + tolerance && z >= _bl.z - tolerance){ 262 | return true; 263 | } 264 | return false; 265 | } 266 | 267 | // TODO: FIND A WAY TO NOT HARDCODE THESE 268 | // Tells us what the foot's destination should be 269 | Vector3 LeftFootMapping(){ 270 | Vector3 footPos = leftFoot.transform.position; 271 | 272 | // get an unrotation Quaternion 273 | Quaternion unRotate = Quaternion.Euler(0,-transform.rotation.eulerAngles.y,0); 274 | Vector3 _rotVel = Rotators.Rotated(_vel,unRotate,transform.up); 275 | 276 | float thresh = _rotVel.magnitude * Mathf.Sin(Mathf.Deg2Rad * sideWalkThreshold); 277 | 278 | if(_rotVel.z >= 0){ 279 | if(_rotVel.x > thresh){ 280 | return _topRight; 281 | } 282 | return _topLeft; 283 | } 284 | if(_rotVel.x > thresh){ 285 | return _bottomRight; 286 | } 287 | return _bottomLeft; 288 | } 289 | 290 | Vector3 RightFootMapping(){ 291 | Vector3 footPos = rightFoot.transform.position; 292 | 293 | // get an unrotation Quaternion 294 | Quaternion unRotate = Quaternion.Euler(0,-transform.rotation.eulerAngles.y,0); 295 | Vector3 _rotVel = Rotators.Rotated(_vel,unRotate,transform.up); 296 | 297 | float thresh = _vel.magnitude * Mathf.Sin(Mathf.Deg2Rad * sideWalkThreshold); 298 | 299 | if(_rotVel.z >= 0){ 300 | if(_rotVel.x < -thresh){ 301 | return _topLeft; 302 | } 303 | return _topRight; 304 | } 305 | if(_rotVel.x < -thresh){ 306 | return _bottomLeft; 307 | } 308 | return _bottomRight; 309 | } 310 | 311 | void OnDrawGizmos(){ 312 | 313 | // Center of mass 314 | Gizmos.color = new Color(1, 0, 0, 1f); 315 | Gizmos.DrawLine(transform.position + Vector3.up, transform.position - Vector3.up); 316 | 317 | // Draw corners 318 | Gizmos.color = Color.blue; 319 | Gizmos.DrawLine(_topLeft,transform.position); 320 | Gizmos.DrawLine(_topRight,transform.position); 321 | Gizmos.DrawLine(_bottomRight,transform.position); 322 | Gizmos.DrawLine(_bottomLeft,transform.position); 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /LegController.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b779b92fb6bdc54498f0d0f99252a55d 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UnityProceduralBiped 2 | 3 | This was just my personal repo for while I worked on this project, 4 | but I made it public so people could look at it if they wanted. I don't recommend 5 | copying this code, I just added and removed things as I went and am not planning on 6 | cleaning it up. 7 | 8 | The initial goal was to have something I could quickly attach to a bipedal 9 | model and get a walking animation that covered all directions, rotations, and 10 | height differences. I don't like working with animations so I figured this would be 11 | a more fun solution. 12 | 13 | The methodology: 14 | https://www.youtube.com/watch?v=tXmRIz1g7s0 15 | 16 | How to use: 17 | 1. Hook up the IK rig for the legs according to this guy or the official 18 | Unity video: 19 | https://www.youtube.com/watch?v=AChwSWU4AaU 20 | 21 | 2. Add IK solver scripts to target position of feet 22 | 23 | 3. Attach LegController script to GameObject. 24 | 25 | 4. Set the variables. There are a lot of them, but most of them just control 26 | the width and length of the box that determines foot placement. 27 | -------------------------------------------------------------------------------- /Rotators.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | 3 | public static class Rotators 4 | { 5 | public static Vector3 Rotated(this Vector3 vector, Quaternion rotation, Vector3 pivot = default(Vector3)) { 6 | return rotation * (vector - pivot) + pivot; 7 | } 8 | 9 | public static Vector3 Rotated(this Vector3 vector, Vector3 rotation, Vector3 pivot = default(Vector3)) { 10 | return Rotated(vector, Quaternion.Euler(rotation), pivot); 11 | } 12 | 13 | public static Vector3 Rotated(this Vector3 vector, float x, float y, float z, Vector3 pivot = default(Vector3)) { 14 | return Rotated(vector, Quaternion.Euler(x, y, z), pivot); 15 | } 16 | } -------------------------------------------------------------------------------- /Rotators.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f71783c575bb1f04c9dbdb8e97000092 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | --------------------------------------------------------------------------------