Improving the Vehicle Camera


Hey folks,

So production is still trucking along fine - in fact we're gearing up for a playtest soon! If you're interested to play the game & give some feedback, please fill in this google form.

Anyway, this devlog is going to get into some details for the MSSIRT's driving camera; as I've built up a small laundry list of behaviours that all come together to make it a smoother-feeling experience than the jam version.

Before getting into all this, here's what Unity code/math I'll be getting into:

And for reference, here's what my base camera class looks like before we get into these extras:

public class PlayerDrivingCameraRotator : MonoBehaviour 
{
     [Header("Input")]
     public InputActionReference lookAction;
   
     private Vector2 _actionValueRaw;
     private Vector2 _actionValueFollower;
     private Quaternion _rootLocalRotation;
     
     [Header("Settings")]
     [Range(0,1)]
     public float lookMoveTowardsLerp = 0.5f;
     public float maxAngle = 45;
     
     private Quaternion _desiredRotation;
     
     private void Awake()
     {
         _rootLocalRotation = transform.localRotation;
     }
     
     private void Update()
     {
         if(!PlayerStateModel.Instance.canPlayerLookAround)
             return;
     
         // get the raw input value from InputSystem
         _actionValueRaw = 
             lookAction.action.ReadValue<vector2>();
   
         // lerp to the raw input value
         _actionValueFollower = Vector2.Lerp(
             _actionValueFollower, 
             _actionValueRaw,
             lookMoveTowardsLerp );
      
         // use that to get the player's desired aim
         _desiredRotation = 
             Quaternion.AngleAxis( 
                   _actionValueFollower.x * maxAngle, 
                   Vector3.up )
             * Quaternion.AngleAxis( 
                   _actionValueFollower.y * maxAngle, 
                   invertY ? Vector3.right : Vector3.left );
    
    
          transform.localRotation = _desiredRotation;
     }
}

Aligning to the Ground

A big source of muergh when driving around the open desert is the rock/rolling of the camera as you go over dunes.

It's not the worst thing in the world, but I think to being in a bus or something going over hills - you shift your body & head to try and keep yourself upright. So I could do the same for the player camera:

Much better! How this is done is with a straightforward lerp, & a consistent reference for what "flat" is. For the MSSIRT, I've setup the hierarchy in a way where it looks something like this:

Player Truck (rigidbody sits here, along with the core controller components)
-> Cockpit
      -> Camera Root
            -> <various cabin cameras>
-> <other Truck gubbins>

What this means is is that the LocalRotation of the Cockpit Cameras is always going to be based off the Truck rotation (as it should, it's not a wrong thing to do that imo). Now because the truck is free to move around the world, I can't just use Quaternion.identity as my "world flat" - I need the flattened facing of the truck. I also need that rotation to be in the local space of the Camera Root - that way I can just lerp from the Camera's localRotation to/from the Flattened localRotation.

public class FlattenRotation : MonoBehaviour
{
     public Transform reference;
     private void Update()
     {
         transform.eulerAngles = 
             Vector3.up
             * reference.eulerAngles.y;
     }
}

Easy! Just have this script point to the Player Truck transform, and only copy it's world Y euler. With that we now have our flat to lerp to:

[Header("Flat rotation reference")]
public Transform worldIdentityReference;
[Range(0,1)]
public float matchWorldRotationRatio = 0.4f;
  
private Quaternion _baseRotationLerper;
...
Update()
{
    ...
    // 2 layers of lerping:
    // 1 - Lerp from the current value to the new ratio value
    // (this is to keep this smooth whilst driving around)
    // 2 - then use Lerp as a way of easily getting a mid-point
    //     between the Cabin Rotation & our World Flat rotation
    //     (using the LocalRotation as it's more performant, 
    //     as it doesn't need to work up the hierarchy)
    
    _baseRotationLerper = Quaternion.Lerp( 
        _baseRotationLerper,
        Quaternion.Lerp(
            _rootLocalRotation,
            worldIdentityReference.localRotation, 
            matchWorldRotationRatio ),
        0.8f );
    
    transform.localRotation = _desiredRotation 
                              * _baseRotationLerper;
}

Some further steps from this that I found are:

  • If the truck is airborne, don't do this flattening behaviour, otherwise you get a tumble-dryer kind of effect which sucks.
  • It'll take some dialling in of the matchWorldRotationRatio to get this to feel right, based on how much your vehicle rotates.

Looking into a Turn

Ok, next up is a teeny bit easier to grasp - and more obvious in hindsight! When you're turning the truck, also angle the camera in that direction a wee bit.

There we go! I find that this makes it  feel that little bit more natural, rather than being locked in place in the driver's seat.

Once again, this uses a Lerp, but also uses a "follower" to smoothly move to the desired steer angle. This is because if you just use the current steering angle, the camera will *snap* to it, rather than smoothly blend into the turn, which is what I'm looking for (not that that's a bad thing for all games - if you have a fever-pitched driving game, the snapping might be exactly what you want, in this case I need something subtle & smooth)

...
[Header("Turning")]
public float whenTurningAngleMultiplier = 0.03f;
[Range(0,1)]
public float turningFollowLerp = 0.5f;
  
private Quaternion _turningRotation;
private float _turningFollower;
...
Update()
{
    // lerp to & create the Turning rotation
    // in my case, currentTurnValue is a value from -45 to 45,
    // which reduces as you speed up,
    // (hence the need for a multiplier to reduce it)
    _turningFollower = Mathf.Lerp(
        _turningFollower,
        PlayerRoot.VehicleEngine.currentTurnValue 
            * whenTurningAngleMultiplier,
        turningFollowLerp );
    
    // apply around the Y axis to look left/right
    _turningRotation = Quaternion.AngleAxis(
        _turningFollower, 
        Vector3.up );
     
    // note the ordering of this: 
    //   we use the input,
    //   to rotate the steering,
    //   to then rotate onto the base rotation
    transform.localRotation = _desiredRotation 
                              * _turningRotation 
                              * _baseRotationLerper;
}

Leaning forwards when braking

Cool, so we now have a stabilising camera, it looks into the turn you want to do, now what? 

Oh yeah, when you brake a 300,000kg vehicle, it'd probably make you heeaaaaaaaavvvveee forwards a bit. So let's implement that.

In this case, I needed to have a few components working together to get this done in a not-extremely-hacky way:

  1. A BrakingValue calculator component. Which takes the current Rigidbody velocity's magnitude, & based on the current braking 0-1 input value, produce a resulting "braking hardness" 0-1 value.
  2. An offset component, which takes that hardness, then shifts it's transform by a given amount.
  3. And finally our good ol' camera script, which will now add on another rotation axis.

In terms of achieving 1, I've resorted to the most OP of all Unity classes: the AnimationCurve:


Where the x axis is the speed of the truck, the Y axis is used for what the maximum brakin hardness can be - the end goal being that:

  • when you're going slowly, or very fast, it's not that much of an effect,
  • but around 20-30, the effect should be at it's fullest.

My thinking is (at least with my jank vehicle physics), around 20 is when you have a sweet spot of control & speed, so the braking is most noticeable? Dunno. But having this be driven by a curve let's it be immediately editable, rather than doing equations (*shriek*).

Anyway - we got our braking value, now to apply it, and the code for that is again, pretty straightforward:

...
[Header("Braking Lean")]
public float whenBrakingMaxAngle = 15;
private Quaternion _brakingRotation;
...
Update()
{
    // apply around the X axis to lean forwards/backwards
    _brakingRotation =
       Quaternion.AngleAxis( 
           VehicleBrakingValue.BrakingHardness
               * whenBrakingMaxAngle,
           Vector3.right );
    
    transform.localRotation = 
        _desiredRotation 
        * _turningRotation * _brakingRotation
        * _baseRotationLerper;
}

End Result

With that, we have some pretty natural-feeling camera motion:

Nice 👌

I'll need to dial in the values some more of course, the braking is a little heavy, and feels like it could take longer to "undo"; but it always feels good to get in these more subtle effects to get a sense of space into a game.

(btw, if you liked what you see, add the full game to your collection & give me a follow for updates!)

See you all at the next devlog! (...whenever the heck that'll be)

Get A Day of Maintenance (Jam Version)

Leave a comment

Log in with itch.io to leave a comment.