Friday, February 28, 2014

Tightly Controlled Physics Forces in Unity




This post pertains to an ongoing struggle of mine.

Unity has a character controller. This is very useful if you want to precisely control a character through movement deltas.  Unity also has a fully-fledged physics system, which is very useful if you want to control a character with physically-simulated forces and have it interact realistically with the world.

Unfortunately, this is an either-or situation. As useful as it would be, you can't apply a direct movement delta to a rigid body, or apply a knockback force to a character controller. This comes up more often than you might think. Any game with knock-back from explosions ("rocket jumping") uses a mix of physics simulation and direct-driven character movement, in general.

I'm working on a prototype (currently in the sort of limbo where it may never see any sort of public release) of a physics-heavy FPS where every weapon has different movement tricks attached to it. (Similar in concept to the traditional rocket jump, but obviously very different for each weapon.) I needed to be able to do this.

Unfortunately, I still kind of can't. I built three (well, two and a half) controllers, and while two of them work to some extend, neither of them work as well as I need them to, which is why I'm temporary shelving the prototype.

My first controller tried to precisely control a rigidbody using physics forces.

 if (rigidbody.velocity.HorizontalComponent().sqrMagnitude < speed * speed)  
           {  
                rigidbody.AddForce(movement - rigidbody.velocity.HorizontalComponent(), ForceMode.VelocityChange);  
           }  
           else  
           {  
                Debug.Log(influenceMove * Time.fixedDeltaTime);  
                rigidbody.velocity.Influence(influenceMove * Time.fixedDeltaTime);  
           }  
           if (Input.GetKeyDown(KeyCode.Space))  
                rigidbody.AddForce(Vector3.up * jumpPower, ForceMode.VelocityChange);  

This immediately ran into some problems. I still didn't have precise control, and although that could be managed, the controller was jumpy on some slopes, and the constant application of force let the player stick to near-vertical walls.  This is more or less the solution I had used in the past, it's acceptable for much simpler applications, but it simply didn't meet my needs here.

My second (attempted) controller tried to basically re-implement a primitive version of Unity's character controller using CapsuleCast and SweepTest to check collisions.

 RaycastHit[] hits;  
           hits = rigidbody.SweepTestAll(movement);  
           float hitDistance = Mathf.Infinity;  
           foreach (RaycastHit hit in hits)  
           {  
                Debug.Log(hit.collider.gameObject.name + " at " + hit.distance);  
                if (hit.distance < hitDistance)  
                     hitDistance = hit.distance;  
           }  
           //Debug.Log(hitDistance);  
           Debug.Log("Moving " + movement.magnitude * Time.deltaTime);  
           if (hits.Length == 0 || hitDistance > movement.magnitude * Time.deltaTime)  
           {  
                Debug.Log("FullMove");  
                transform.Translate(movement * Time.deltaTime);  
           }  
           else  
           {  
                Debug.Log("TruncatedMove");  
                transform.Translate(movement.normalized * hitDistance);  
           }  

 Unfortunately, these Unity functions don't work quite as advertised. Rather than checking if my capsule will collide with anything along the ray that represents the sweep path, SweepTest and CapsuleCast just cast a single ray from the center of the capsule to the destination point, and if the ray is interrupted, it adjusts the hit point so that the capsule will touch the object that was hit, rather than being stuck in it. This isn't a useless function, but it doesn't do what I need it to here, and as far as I could tell after several hours in the Unity documentation, there is no function that does what I need, which left this approach dead in the water.  It's certainly possible to do, but it isn't easy enough to do in two weeks.

My third controller was the closest to being successful. Instead of trying to build a character controller around rigidbody physics, I tried to build primitive rigidbody physics around a character controller.
 public class PhysicsAccumulator : MonoBehaviour {  
      CharacterController controller;  
      Dictionary<string, PhysicsForce> forces = new Dictionary<string,PhysicsForce>();  
      public PhysicsForce Force(string name)  
      {  
           if (ForceExists(name))  
                return forces[name];  
           else  
                return null;  
      }  
      public bool ForceExists(string name)  
      {  
           if(forces.ContainsKey(name))  
                return true;  
           else  
                return false;  
      }  
      public bool AddForce(string name, PhysicsForce force)  
      {  
           if (ForceExists(name))  
                return false;  
           forces[name] = force;  
           return true;  
      }  
      public bool RemoveForce(string name)  
      {  
           if (!ForceExists(name))  
                return false;  
           forces.Remove(name);  
           return true;  
      }  
      // Use this for initialization  
      void Start () {  
           controller = GetComponent<CharacterController>();  
           forces.Add("Controller", new ControllerForce(controller, 4));  
           forces.Add("BunnyHop", new JumpForce(controller, 7f, 5, 10));  
           forces["BunnyHop"].FlatDamping = 3;  
           forces.Add("Gravity", new VectorLockedForce());  
           forces["Gravity"].Acceleration = new Vector3(0, -15f, 0);  
           ((VectorLockedForce)forces["Gravity"]).LockDirection = Vector3.down;  
           forces.Add("Rocket", new PhysicsForce());  
           forces["Rocket"].LinearDamping = 1;  
           forces["Rocket"].Aerial = true;  
      }  
      // Update is called once per frame  
      void LateUpdate () {  
           foreach (KeyValuePair<string, PhysicsForce> f in forces)  
                f.Value.Update(Time.deltaTime);  
           Vector3 netForce = Vector3.zero;  
           foreach (KeyValuePair<string, PhysicsForce> f in forces)  
                netForce += f.Value.Displacement;  
           controller.Move(netForce);  
      }  
      void OnControllerColliderHit(ControllerColliderHit hit)  
      {  
           foreach (KeyValuePair<string, PhysicsForce> f in forces)  
           {  
                f.Value.HandleHit(hit);  
           }  
      }  
 }  

I built a simple physics force registry and set it up with an array of forces representing both my "control" forces for movement and jumping and various environmental forces for things like gravity and rocket knockback. This worked perfectly until I tried to implement spherical planet gravity for the player. Character controllers in Unity have to be axis-aligned, which makes using them on a sphere impossible. This wasn't technically something I felt I needed going in, but the point of this experiment was to find a way to avoid being constrained by the limitations of these two systems in the future, and this is at least one limitation I have yet to overcome.

I think my next attempt, whenever that might be, will probably involve attempting to solve a problem I encountered before any of the controllers mentioned here. It is possible to drive a rigidbody's velocity directly, but it effectively overrides any meaningful physics motion. (You can't, for example, drive a vehicle off of a jump believably if you reset it's velocity every physics frame.) There might be a better approach to this method, but that's a task for the future.