Simple FPC integration for Edy's Vehicle Physics

forzabo

Member
I've successfully created a simple integration for Edy's Vehicle Physics for Opsive UFPS/ FPC.

I wrestled around a good bit with Redhawk's EVP integration but ran into several issues and couldn't get it to work. For my needs I really don't need the character to do anything other than be able to drive the vehicle (e.g. the character doesn't need to look around or shoot while driving) so really all I wanted was a seamless transistion from the FPC to EVP first-person driving view.

That said, I got a lot of ideas and borrowed much from Redhawk's scripts. I recommend people watch his video and check out his scripts for ideas on how to get these things to work.

video here -- version 1
https://www.opsive.com/forum/index.php?threads/edys-vehicle-physics.720/
version 2 here

Ok, so my approach is, as stated, quite simplistic and involves some redundancies, but it seems to work really well. Basically I start with a standard Opsive FPC scene implementation, with the character rig, cameras, game manager, UI canvas etc all out of the Opsive box so to speak. For the car setup I copy everything from the EVP demo scene, including the camera with its Vehicle Camera Controller. I changed the mode of the VehicleCameraController to AttachTo in order to get the first person, driver's eye view. The vehicle gets a dummy driver (more on that below) and the whole idea is that when the scene starts, the EVP stuff is dormant until the ability is activated, at which time the Opsive stuff is disabled, the character is hidden and the EVP stuff, including camera, takes over. When the ability stops, the process is reversed.

At the top level of each vehicle gameobject hierarchy (i.e. the gameobject with all of the EVP components), I added this script. It's mostly responsible for sniffing the Ability End player input and animating the driver's feet. There are also placeholders for detecting the vehicle's speed and whether it is going forward or reverse and whether it's accelerating. (these hooks may already be avaible somewhere in the EVP scripts, so this might be really hacky but it works for what I needed) EDIT-- EVP's vehicle controller exposes a speed variable so that can simply be polled instead of recalculating velocity:

Code:
using System.Collections.Generic;
using UnityEngine;
using EVP;

[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(VehicleStandardInput))]
[RequireComponent(typeof(VehicleController))]
[RequireComponent(typeof(VehicleAudio))]


public class EVP_Vehicle : MonoBehaviour
{
    public enum State
    {
        stopped,
        forward,
        reverse,
        brake
    }

    public State state = State.stopped;

    [Tooltip ("Rigged Driver character")]
    public GameObject m_Driver;
    [Tooltip ("Game Object with EVP Camera Controller")]
    public GameObject m_VehicleCameraGO;

   // Edy's Vehicle Physics
    VehicleStandardInput m_VehicleStandardInput;
    VehicleController m_VehicleController;
    VehicleAudio m_VehicleAudio;
    Rigidbody m_Rigidbody;


    // copies of IK targets to use with foot animation
    Transform m_LeftFootTarget;
    Transform m_RightFootTarget;

    /// <summary>
    /// Initialization.
    /// </summary>
    void Start()
    {
        //Disable camera, listener, and cam controller used by EVP
        m_VehicleCameraGO.SetActive(false);
        //Hide dummy driver
        m_Driver.SetActive(false);
        //Get EVP Component references
        m_VehicleStandardInput = this.GetComponent<VehicleStandardInput>();
        m_VehicleController = this.GetComponent<VehicleController>();
        m_VehicleAudio = this.GetComponent<VehicleAudio>();
        m_Rigidbody = gameObject.GetComponent<Rigidbody>();

        //obtain references to foot IK targets
        m_LeftFootTarget = m_Driver.GetComponent<EVP_Driver_Animation>().m_LeftFootTarget;
        m_RightFootTarget = m_Driver.GetComponent<EVP_Driver_Animation>().m_RightFootTarget;

        //Disable EVP input and audio
        m_VehicleStandardInput.enabled = false;
        m_VehicleAudio.enabled = false;
 
        // Make sure the Vehicle doesn't move around
        m_VehicleController.throttleInput = 0.0f;
        m_VehicleController.brakeInput = 1.0f;

        //ignore updates while vehicle is not in use.
        enabled = false;
    }

    private bool action = false; //bool flag to avoid multiple action events
    private float oldVelocity = 0;

    /// <summary>
    /// Poll for end ability input and update character animation based on input and velocity.
    /// Update is not enabled until ability starts.
    /// </summary>
    private void Update()
    {
        // hack poll for action key to stop ability
        if(Input.GetAxis("Action") > 0 & !action)
        {
            action = true;
            EVP_DriveAbility ability = Globals.instance.uCL.GetAbility<EVP_DriveAbility>();
            Globals.instance.uCL.TryStopAbility(ability);
        }
        else if (action && Input.GetAxis("Action") < Mathf.Epsilon)
        {
            action = false;
        }


        // The following uses the forward velocity of the vehicle along with the input values to compute
        //  simple IK target transformations for the drivers feet (e.g. mashing pedals)
        // NOTE (edit): this next line of code is unneccessary -- can use m_VehicleController.speed instead
        float velocity = transform.InverseTransformDirection(m_Rigidbody.velocity).z;
        if (Mathf.Abs(velocity) < Mathf.Epsilon)
        {
            //car is stopped
            state = State.stopped;
        }
        else if(velocity > 0)
        {
            state = State.forward;
            if (velocity >= oldVelocity)
            {

                //car is accelerating fwd -- do something here, maybe

            }
            else
            {
                //car is decelerating/braking
            }
        }
        else
        {
            state = State.reverse;
            if (velocity <= oldVelocity)
            {
                //car is accelerating in reverse
         
            }
            else
            {
             //car is decelerating, in reverse
            }
        }
        oldVelocity = velocity;

        //animate feet in response to gas/brake inputs
        float gas_brake_input = Input.GetAxis("Vertical");
        if (Mathf.Abs(gas_brake_input) > Mathf.Epsilon)
        {
            if (state == State.stopped || state == State.forward)
            {
                m_RightFootTarget.localEulerAngles = new Vector3(Mathf.Clamp(45.0f * gas_brake_input - 45.0f, -45, 0), 0, 0);
                m_LeftFootTarget.localEulerAngles = new Vector3(Mathf.Clamp(-(45.0f * gas_brake_input + 45.0f), -45, 0), 0, 0);
            } else
            {
                m_LeftFootTarget.localEulerAngles = new Vector3(Mathf.Clamp(45.0f * gas_brake_input - 45.0f, -45, 0), 0, 0);
                m_RightFootTarget.localEulerAngles = new Vector3(Mathf.Clamp(-(45.0f * gas_brake_input + 45.0f), -45, 0), 0, 0);
            }
        }
    }
}

Per Redhawk's EVP integration, I added a game object to the vehicle hierachy with a box collider trigger component to act as the ability trigger. This also receives an Object Identifier component (from Opsive) with an arbritary ID number assignment to be used by the ability:

Screen Shot 2019-11-17 at 10.28.12 AM.png

Again, like Redhawk I have added empty gameobjects to the the car to act as IK targets for the character rig. The targets for the hand are children of the steering wheel transform.

Screen Shot 2019-11-17 at 10.34.00 AM.png

(to be continued in next post)
 

Attachments

  • Screen Shot 2019-11-17 at 10.24.56 AM.png
    Screen Shot 2019-11-17 at 10.24.56 AM.png
    797.7 KB · Views: 0
Last edited:
Here's where I diverge from Redhawk. To the car I also add a copy of my rigged character model, but without any of the Opsive stuff:

Screen Shot 2019-11-17 at 10.39.02 AM.png

This dummy driver has its own simple animation controller (it simply goes from entry to a driving idle pose), importantly, IK pass is enabled on the base layer:

Screen Shot 2019-11-17 at 10.49.19 AM.png



The driver also needs an EVP_Driver_Animation component that is this script:

Code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(Animator))]

public class EVP_Driver_Animation : MonoBehaviour
{

    Animator animator;

    [Tooltip("Right hand IK target -- should be child of EVP steering wheel transform")]
    [SerializeField] private Transform m_RightHandTarget = null;
    [Tooltip("Left hand IK target -- should be child of EVP steering wheel transform")]
    [SerializeField] private Transform m_LeftHandTarget = null;
    [Tooltip("Right foot IK target -- should be child of EVP car")]
    public Transform m_RightFootTarget = null;
    [Tooltip("Left foot IK target -- should be child of EVP car")]
    public Transform m_LeftFootTarget = null;
    [Tooltip("Right elbow IK hint -- should be child of EVP car (or leave empty)")]
    [SerializeField] private Transform m_RightElbowHint = null;
    [Tooltip("Left elbow IK hint -- should be child of EVP car (or leave empty)")]
    [SerializeField] private Transform m_LeftElbowHint = null;
    [Tooltip("Right knee IK hint -- should be child of EVP car (or leave empty)")]
    [SerializeField] private Transform m_RightKneeHint = null;
    [Tooltip("Left knee IK hint -- should be child of EVP car (or leave empty)")]
    [SerializeField] private Transform m_LeftKneeHint = null;


    void Start()
    {
        animator = GetComponent<Animator>();
    }


    void OnAnimatorIK()
    {
        if (animator)
        {
            //null checks could be removed for performance later...

            if (m_RightFootTarget != null)
            {
                animator.SetIKPositionWeight(AvatarIKGoal.RightFoot, 1);
                animator.SetIKRotationWeight(AvatarIKGoal.RightFoot, 1);
                animator.SetIKPosition(AvatarIKGoal.RightFoot, m_RightFootTarget.position);
                animator.SetIKRotation(AvatarIKGoal.RightFoot, m_RightFootTarget.rotation);
            }

            if (m_LeftFootTarget != null)
            {
                animator.SetIKPositionWeight(AvatarIKGoal.LeftFoot, 1);
                animator.SetIKRotationWeight(AvatarIKGoal.LeftFoot, 1);
                animator.SetIKPosition(AvatarIKGoal.LeftFoot, m_LeftFootTarget.position);
                animator.SetIKRotation(AvatarIKGoal.LeftFoot, m_LeftFootTarget.rotation);
            }


            if (m_RightHandTarget != null)
            {
                animator.SetIKPositionWeight(AvatarIKGoal.RightHand, 1);
                animator.SetIKRotationWeight(AvatarIKGoal.RightHand, 1);
                animator.SetIKPosition(AvatarIKGoal.RightHand, m_RightHandTarget.position);
                animator.SetIKRotation(AvatarIKGoal.RightHand, m_RightHandTarget.rotation);
            }

            if (m_LeftHandTarget != null)
            {
                animator.SetIKPositionWeight(AvatarIKGoal.LeftHand, 1);
                animator.SetIKRotationWeight(AvatarIKGoal.LeftHand, 1);
                animator.SetIKPosition(AvatarIKGoal.LeftHand, m_LeftHandTarget.position);
                animator.SetIKRotation(AvatarIKGoal.LeftHand, m_LeftHandTarget.rotation);
            }
            if (m_LeftElbowHint != null)
            {
                animator.SetIKHintPositionWeight(AvatarIKHint.LeftElbow, 1);
                animator.SetIKHintPosition(AvatarIKHint.LeftElbow, m_LeftElbowHint.position);
            }
            if (m_RightElbowHint != null)
            {
                animator.SetIKHintPositionWeight(AvatarIKHint.RightElbow, 1);
                animator.SetIKHintPosition(AvatarIKHint.RightElbow, m_RightElbowHint.position);
            }
            if (m_LeftKneeHint != null)
            {
                animator.SetIKHintPositionWeight(AvatarIKHint.LeftKnee, 1);
                animator.SetIKHintPosition(AvatarIKHint.LeftKnee, m_LeftKneeHint.position);
            }
            if (m_RightKneeHint != null)
            {
                animator.SetIKHintPositionWeight(AvatarIKHint.RightKnee, 1);
                animator.SetIKHintPosition(AvatarIKHint.RightKnee, m_RightKneeHint.position);
            }

        }
    }

}

(continued in next post)
 
In the scene, I've added a plane well below the main scenery. This is where the Opsive character will hang out while the car is active. Leaving the character active but out of sight may seem a bit hacky and a kludge, but it works and simplifies a lot of things. For example, the character inventory remains active and accessible. Likewise character attributes can still be accessed and modified, can auto update, and so on.

Screen Shot 2019-11-17 at 10.51.54 AM.png

Now for the ability script. Essentially, what happens is control is swapped from Opsive input to EVP; Opsive cameras are disabled and EVP cameras enabled; Opsive character is "exiled" and dummy driver is enabled. The EVP_Vehicle_Controller is enabled which sniffs for the key to exit the ability and also positions the feet based on player input. I borrowed Redhawks' ability exit logic which prevents the driver from exiting if the door is blocked. When the ability is stopped, the process is reversed and control and rendering go back to the Opsive elements. The character is moved from its exile position to the interaction location next to the car.

Code:
using UnityEngine;
using Opsive.UltimateCharacterController.Character.Abilities; //Used for DetectObjectAbilityBase
using EVP; //Used for Edy's Vehicle Physics
using Opsive.UltimateCharacterController.Character.Abilities.Items;
using Opsive.UltimateCharacterController.Events;

/// <summary>
/// Bo Monroe copyright 2019
/// derived from EVP 3rd person integration by
/// Chris "RedHawk" Ferguson copyright 2019 https://redhwk.wordpress.com/opsive-and-edys/
/// </summary>

public class EVP_DriveAbility : DetectObjectAbilityBase
{
    //EVP Component references
    VehicleStandardInput m_VehicleStandardInput;
    VehicleController m_VehicleController;
    VehicleAudio m_VehicleAudio;
    EVP_Vehicle m_EVP_Vehicle;
    EVP_Driver_Animation m_EVP_Driver_Animation;


    //General references
    Transform m_Vehicle;
    GameObject InteractLocation;
    GameObject MainCamera;

    public override void Start()
    {
        base.Start();
    }

    //Prevent Ability from being prematurely stopped
    public override bool CanForceStopAbility()
    {
        return false;
    }

    /// <summary>
    /// The ability has started.
    /// </summary>
    protected override void AbilityStarted()
    {
        Debug.Log("Starting Ability");
        base.AbilityStarted();
      
        //Get EVP components attached to the Opsive Ability Detected Object
        m_Vehicle = m_DetectedObject.GetComponentInParent<VehicleStandardInput>().transform;
        m_EVP_Vehicle = m_Vehicle.GetComponentInChildren<EVP_Vehicle>();
        m_VehicleStandardInput = m_Vehicle.GetComponent<VehicleStandardInput>();
        m_VehicleController = m_Vehicle.GetComponent<VehicleController>();
        m_VehicleAudio = m_Vehicle.GetComponent<VehicleAudio>();

        // hack: MainCamera refers to the Camera GameObject (and its child camera) used by Opsive FPC
        // it needs to be tagged "MainCamera" for this to work.
        MainCamera = GameObject.FindWithTag("MainCamera");
        if (!MainCamera)
        {
            Debug.LogError("Opsive FPC Camera not found. Be sure to add \"MainCamera\" tag to Camera with Opsive controllers");
        }
        // Instead of fighting with them, just disable them while ability is active
        MainCamera.SetActive(false);

        //Enable the EVP camera, and set its target to the detected vehicle.
        m_EVP_Vehicle.m_VehicleCameraGO.SetActive(true);
        m_EVP_Vehicle.m_VehicleCameraGO.GetComponent<VehicleCameraController>().target = m_Vehicle;

        //Exile the Opsive character to the underworld while the ability is active.
        m_CharacterLocomotion.SetPositionAndRotation(new Vector3(0,-90,0), Quaternion.identity,false);
        //Turn off Opsive FPC game input.
        EventHandler.ExecuteEvent(Globals.instance.m_Character, "OnEnableGameplayInput", false);

        //Let the EVP_Vehicle script get Updates
        m_EVP_Vehicle.enabled = true;
        //Unhide the driver dummy
        m_EVP_Vehicle.m_Driver.SetActive(true);

        //Enable EVP game input and audio
        m_VehicleStandardInput.enabled = true;
        m_VehicleAudio.enabled = true;

        //This is the trigger box. Used when ability attempts to stop to make sure driver can get out. (thanks RedHawk)
        InteractLocation = m_DetectedObject;
    }


    /// <summary>
    /// The Ability has ended. Restore the FPS character and disable the EVP camera and input.
    /// </summary>
    /// <param name="force"></param>
    protected override void AbilityStopped(bool force)
    {

        base.AbilityStopped(force);
    
        //Stop the vehicle and put the brakes on
        m_VehicleController.throttleInput = 0.0f;
        m_VehicleController.brakeInput = 1.0f;
        //Disable the EVP Input
        m_VehicleStandardInput.enabled = false;
        //Disable the EVP Audio
        m_VehicleAudio.enabled = false;
        //Stop EVP_Vehicle from getting Updates.
        m_EVP_Vehicle.enabled = false;
    
        //Summon the character back from exile in Hades.
        m_CharacterLocomotion.SetPositionAndRotation(InteractLocation.transform.position, InteractLocation.transform.rotation);
        //Restore Opsive FPC game input.
        EventHandler.ExecuteEvent(Globals.instance.m_Character, "OnEnableGameplayInput", true);
        //Restore Opsive FPC camera rig.
        MainCamera.SetActive(true);
        //Disable EVP camera rig.
        m_EVP_Vehicle.m_VehicleCameraGO.SetActive(false);
        //Hide the dummy driver.
        m_EVP_Vehicle.m_Driver.SetActive(false);
    }

    public override bool CanStopAbility()
    {
        return ExitDriverBlocked(InteractLocation.GetComponent<Collider>());
    }

    /// <summary>
    /// Check whether driver exit is blocked.
    /// Make sure the pivot of the gameobject containing the trigger is located where the driver should exit
    /// (might not be the case if you edited the box collider with its handles)
    /// Also make sure trigger does not overlap vehicle colliders (lest driver can never exit)
    /// </summary>
    /// <returns><c>true</c>, if driver exit not blocked <c>false</c> otherwise.</returns>
    /// <param name="exitCollider">Exit collider.</param>
    private bool ExitDriverBlocked(Collider exitCollider)
    {
        Collider[] overlap = new Collider[1];
        int hitCount = 0;
        if (exitCollider is BoxCollider)
        {
            var boxCollider = exitCollider as BoxCollider;
            hitCount = Physics.OverlapBoxNonAlloc(exitCollider.transform.TransformPoint(boxCollider.center), Vector3.Scale(boxCollider.size, boxCollider.transform.lossyScale) / 2,
                                overlap, exitCollider.transform.rotation, m_CharacterLayerManager.SolidObjectLayers, QueryTriggerInteraction.Ignore);
        }
        if (hitCount > 0)
        {
            Debug.Log("Exit Blocked"); //for debugging
            return false;
        }
        else
        {
            return true;
        }
    }


    public override bool ShouldBlockAbilityStart(Ability startingAbility)
    {
        return startingAbility is ItemAbility;
    }
}


That's essentially it. The Ability set up is exactly as Redhawk's integration:

Screen Shot 2019-11-17 at 11.09.24 AM.png

Hope this is useful to somebody.
 
Last edited:
Top