Files
BABA_YAGA/Assets/Third Parties/Opsive/UltimateCharacterController/Scripts/Character/Abilities/Drive.cs
Scove 3e39117acc Consolidate third-party plugins into Assets/Plugins
Move and consolidate many third-party plugin files and metadata from various locations (notably Assets/Third Parties/Plugins 1 and scattered Opsive/Photon folders) into a unified Assets/Plugins directory. Includes DOTween, PrimeTween, Native/BackroomsNoise, Sirenix/Odin Inspector, and Opsive UltimateCharacterController/shared libs, plus updates to several .meta files and removal of obsolete installer/legacy files. This standardizes plugin layout and cleans up duplicate/obsolete assets.
2026-06-16 18:41:44 +07:00

446 lines
21 KiB
C#

/// ---------------------------------------------
/// Ultimate Character Controller
/// Copyright (c) Opsive. All Rights Reserved.
/// https://www.opsive.com
/// ---------------------------------------------
namespace Opsive.UltimateCharacterController.Character.Abilities
{
using Opsive.Shared.Events;
using Opsive.Shared.Game;
#if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER
using Opsive.UltimateCharacterController.Networking;
#endif
using Opsive.UltimateCharacterController.Game;
using Opsive.UltimateCharacterController.Objects.CharacterAssist;
using Opsive.UltimateCharacterController.Utility;
using UnityEngine;
/// <summary>
/// Ability that uses the IDriveSource interface to drive a vehicle.
/// </summary>
[DefaultInputName("Action")]
[DefaultState("Drive")]
[DefaultStartType(AbilityStartType.ButtonDown)]
[DefaultStopType(AbilityStopType.ButtonToggle)]
[DefaultAllowPositionalInput(false)]
[DefaultAllowRotationalInput(false)]
[DefaultUseRootMotionPosition(AbilityBoolOverride.True)]
[DefaultUseRootMotionRotation(AbilityBoolOverride.True)]
[DefaultUseGravity(AbilityBoolOverride.False)]
[DefaultDetectHorizontalCollisions(AbilityBoolOverride.False)]
[DefaultDetectVerticalCollisions(AbilityBoolOverride.False)]
[DefaultAbilityIndex(14)]
[DefaultEquippedSlots(0)]
public class Drive : DetectObjectAbilityBase
{
[Tooltip("Should the character teleport for the enter and exit animations?")]
[SerializeField] protected bool m_TeleportEnterExit;
[Tooltip("Can the Drive ability aim?")]
[SerializeField] protected bool m_CanAim;
[Tooltip("The speed at which the character moves towards the seat location.")]
[SerializeField] protected float m_MoveSpeed = 0.2f;
[Tooltip("The speed at which the character rotates towards the seat location.")]
[SerializeField] protected float m_RotationSpeed = 2f;
public bool TeleportEnterExit { get => m_TeleportEnterExit; set => m_TeleportEnterExit = value; }
public bool CanAim { get => m_CanAim; set => m_CanAim = value; }
public float MoveSpeed { get => m_MoveSpeed; set => m_MoveSpeed = value; }
public float RotationSpeed { get => m_RotationSpeed; set => m_RotationSpeed = value; }
/// <summary>
/// Specifies the current status of the character.
/// </summary>
private enum DriveState
{
Enter, // The character is entering the vehicle.
Drive, // The character is driving the vehicle.
Exit, // The character is exiting the vehicle.
ExitComplete // The character has exited the vehicle.
}
private IDriveSource m_DriveSource;
private Transform m_OriginalParent;
private Collider[] m_VehicleColliders;
private DriveState m_DriveState;
private Collider[] m_OverlapColliders;
#if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER
private INetworkInfo m_NetworkInfo;
#endif
private KinematicObjectManager.UpdateLocation m_StartUpdateLocation;
private float m_Epsilon = 0.99999f;
public override int AbilityIntData { get { return m_DriveSource.AnimatorID + (int)m_DriveState; } }
public override float AbilityFloatData { get { return m_CharacterLocomotion.RawInputVector.x; } }
/// <summary>
/// Initializes the default values.
/// </summary>
public override void Awake()
{
base.Awake();
m_OverlapColliders = new Collider[1];
#if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER
m_NetworkInfo = m_GameObject.GetCachedComponent<INetworkInfo>();
#endif
EventHandler.RegisterEvent(m_GameObject, "OnAnimatorEnteredVehicle", OnEnteredVehicle);
EventHandler.RegisterEvent(m_GameObject, "OnAnimatorExitedVehicle", OnExitedVehicle);
}
/// <summary>
/// Validates the object to ensure it is valid for the current ability.
/// </summary>
/// <param name="obj">The object being validated.</param>
/// <param name="raycastHit">The raycast hit of the detected object. Will be null for trigger detections.</param>
/// <returns>True if the object is valid. The object may not be valid if it doesn't have an ability-specific component attached.</returns>
protected override bool ValidateObject(GameObject obj, RaycastHit? raycastHit)
{
if (!base.ValidateObject(obj, raycastHit)) {
return false;
}
m_DriveSource = obj.GetCachedParentComponent<IDriveSource>();
if (m_DriveSource == null) {
return false;
}
return true;
}
/// <summary>
/// Called when the ablity is tried to be started. If false is returned then the ability will not be started.
/// </summary>
/// <returns>True if the ability can be started.</returns>
public override bool CanStartAbility()
{
if (!base.CanStartAbility()) {
return false;
}
return GetValidStartLocation(true) != null;
}
/// <summary>
/// Returns a valid start location.
/// </summary>
/// <param name="groundCheck">Should the ground be checked at the start location?</param>
/// <returns>A valid start location (can be null).</returns>
private MoveTowardsLocation GetValidStartLocation(bool groundCheck)
{
// At least one ability start location must be on the ground and not obstructed by any object.
var startLocations = m_DriveSource.GameObject.GetComponentsInChildren<MoveTowardsLocation>();
for (int i = 0; i < startLocations.Length; ++i) {
// The object must be on the ground.
if (groundCheck && !Physics.Raycast(startLocations[i].transform.TransformPoint(0, 0.1f, 0), -startLocations[i].transform.up, 0.2f,
m_CharacterLayerManager.IgnoreInvisibleCharacterWaterLayers, QueryTriggerInteraction.Ignore)) {
continue;
}
// If the start location has a collider then it should be clear of any other objects.
var collider = startLocations[i].gameObject.GetCachedComponent<Collider>();
if (collider == null || !ColliderOverlap(collider)) {
return startLocations[i];
}
}
return null;
}
/// <summary>
/// Is the collider overlapping with any other objects?
/// </summary>
/// <param name="dismountCollider">The collider to determine if it is overlapping with another object.</param>
/// <returns>True if the collider is overlapping.</returns>
private bool ColliderOverlap(Collider collider)
{
if (collider == null) {
return true;
}
int hitCount;
if (collider is CapsuleCollider) {
Vector3 startEndCap, endEndCap;
var capsuleCollider = collider as CapsuleCollider;
MathUtility.CapsuleColliderEndCaps(capsuleCollider, collider.transform.TransformPoint(capsuleCollider.center), collider.transform.rotation, out startEndCap, out endEndCap);
hitCount = Physics.OverlapCapsuleNonAlloc(startEndCap, endEndCap, capsuleCollider.radius * MathUtility.ColliderRadiusMultiplier(capsuleCollider), m_OverlapColliders,
m_CharacterLayerManager.IgnoreInvisibleCharacterWaterLayers, QueryTriggerInteraction.Ignore);
} else if (collider is BoxCollider) {
var boxCollider = collider as BoxCollider;
hitCount = Physics.OverlapBoxNonAlloc(collider.transform.TransformPoint(boxCollider.center), Vector3.Scale(boxCollider.size, boxCollider.transform.lossyScale) / 2,
m_OverlapColliders, collider.transform.rotation, m_CharacterLayerManager.IgnoreInvisibleCharacterWaterLayers, QueryTriggerInteraction.Ignore);
} else { // SphereCollider.
var sphereCollider = collider as SphereCollider;
hitCount = Physics.OverlapSphereNonAlloc(collider.transform.TransformPoint(sphereCollider.center), sphereCollider.radius * MathUtility.ColliderRadiusMultiplier(sphereCollider),
m_OverlapColliders, m_CharacterLayerManager.IgnoreInvisibleCharacterWaterLayers, QueryTriggerInteraction.Ignore);
}
// Any overlap occurs anytime there is more one collider intersecting the colliders.
return hitCount > 0;
}
/// <summary>
/// Returns the possible MoveTowardsLocations that the character can move towards.
/// </summary>
/// <returns>The possible MoveTowardsLocations that the character can move towards.</returns>
public override MoveTowardsLocation[] GetMoveTowardsLocations()
{
if (m_TeleportEnterExit) {
return null;
}
return m_DriveSource.GameObject.GetComponentsInChildren<MoveTowardsLocation>();
}
/// <summary>
/// The ability has started.
/// </summary>
protected override void AbilityStarted()
{
base.AbilityStarted();
m_OriginalParent = m_Transform.parent;
m_VehicleColliders = m_DriveSource.GameObject.GetComponentsInChildren<Collider>();
for (int i = 0; i < m_VehicleColliders.Length; ++i) {
for (int j = 0; j < m_CharacterLocomotion.ColliderCount; ++j) {
Physics.IgnoreCollision(m_VehicleColliders[i], m_CharacterLocomotion.Colliders[j], true);
}
}
m_CharacterLocomotion.AddIgnoredColliders(m_VehicleColliders);
m_CharacterLocomotion.AlignToGravity = true;
m_StartUpdateLocation = m_CharacterLocomotion.UpdateLocation;
// Used FixedUpdate so the root motion location is accurate when getting into the vehicle.
m_CharacterLocomotion.UpdateLocation = KinematicObjectManager.UpdateLocation.FixedUpdate;
m_CharacterLocomotion.SetPlatform(m_DriveSource.Transform);
m_Transform.parent = m_DriveSource.Transform;
#if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER
// The IDriveSource is responsible for notifying the remote players for the changes.
if (m_NetworkInfo != null && !m_NetworkInfo.IsLocalPlayer()) {
return;
}
#endif
m_DriveState = DriveState.Enter;
m_DriveSource.EnterVehicle(m_GameObject);
// Teleport the character if there are no enter/exit animations.
if (m_TeleportEnterExit) {
OnEnteredVehicle();
m_CharacterLocomotion.InputVector = Vector2.zero;
m_CharacterLocomotion.SetPositionAndRotation(m_DriveSource.DriverLocation.position, m_DriveSource.DriverLocation.rotation, true, false);
}
}
/// <summary>
/// Callback when the character has entered the vehicle.
/// </summary>
private void OnEnteredVehicle()
{
#if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER
// The IDriveSource is responsible for notifying the remote players for the changes.
if (m_NetworkInfo != null && !m_NetworkInfo.IsLocalPlayer()) {
return;
}
#endif
m_DriveSource.EnteredVehicle(m_GameObject);
m_DriveState = DriveState.Drive;
m_CharacterLocomotion.ForceRootMotionRotation = false;
m_CharacterLocomotion.ForceRootMotionPosition = false;
m_CharacterLocomotion.AllowRootMotionRotation = false;
m_CharacterLocomotion.AllowRootMotionPosition = false;
m_CharacterLocomotion.UpdateLocation = m_StartUpdateLocation;
m_CharacterLocomotion.UpdateAbilityAnimatorParameters();
}
/// <summary>
/// Called when another ability is attempting to start and the current ability is active.
/// Returns true or false depending on if the new ability should be blocked from starting.
/// </summary>
/// <param name="startingAbility">The ability that is starting.</param>
/// <returns>True if the ability should be blocked.</returns>
public override bool ShouldBlockAbilityStart(Ability startingAbility)
{
return m_AllowEquippedSlotsMask == 0 && startingAbility is Items.ItemAbility || (!m_CanAim && startingAbility is Items.Aim) || startingAbility is HeightChange;
}
/// <summary>
/// Called when the current ability is attempting to start and another ability is active.
/// Returns true or false depending on if the active ability should be stopped.
/// </summary>
/// <param name="activeAbility">The ability that is currently active.</param>
/// <returns>True if the ability should be stopped.</returns>
public override bool ShouldStopActiveAbility(Ability activeAbility)
{
return m_AllowEquippedSlotsMask == 0 && activeAbility is Items.ItemAbility || (!m_CanAim && activeAbility is Items.Aim);
}
/// <summary>
/// Updates the ability.
/// </summary>
public override void Update()
{
// Try to stop the ability after the character has exited. The ability won't be able to be stopped if the character isn't level with the gravity direction.
if (m_DriveState == DriveState.ExitComplete && !m_TeleportEnterExit) {
StopAbility();
}
}
/// <summary>
/// Update the ability's Animator parameters.
/// </summary>
public override void UpdateAnimator()
{
// The horizontal input value can be used to animate the steering wheel.
SetAbilityFloatDataParameter(m_CharacterLocomotion.RawInputVector.x, Time.deltaTime);
}
/// <summary>
/// Update the controller's rotation values.
/// </summary>
public override void UpdateRotation()
{
var deltaRotation = Quaternion.identity;
var rotation = m_Transform.rotation;
if (m_DriveState != DriveState.Drive) {
if (m_TeleportEnterExit) {
return;
}
var upNormal = m_DriveState == DriveState.Enter ? m_DriveSource.Transform.up : -m_CharacterLocomotion.GravityDirection;
// When the character is entering the vehicle they should rotate to face the same up direction as the car. This allows the character to enter while on slopes.
// Similarly, when the character exits they should rotate to the gravity direction.
var proj = (rotation * Vector3.forward) - Vector3.Dot(rotation * Vector3.forward, upNormal) * upNormal;
if (proj.sqrMagnitude > 0.0001f) {
var speed = m_RotationSpeed * m_CharacterLocomotion.TimeScale * Time.timeScale * Time.deltaTime * (m_DriveState == DriveState.ExitComplete ? 100 : 1);
var targetRotation = Quaternion.Slerp(rotation, Quaternion.LookRotation(proj, upNormal), speed);
deltaRotation = deltaRotation * (Quaternion.Inverse(rotation) * targetRotation);
}
} else if (m_DriveSource.DriverLocation != null) {
// The character should fully rotate towards the target rotation after they have entered.
deltaRotation = MathUtility.InverseTransformQuaternion(m_Transform.rotation, m_DriveSource.DriverLocation.rotation);
}
m_CharacterLocomotion.DeltaRotation = deltaRotation.eulerAngles;
}
/// <summary>
/// Update the controller's position values.
/// </summary>
public override void UpdatePosition()
{
if (m_DriveState != DriveState.Drive || m_TeleportEnterExit || m_DriveSource.DriverLocation == null) {
return;
}
m_CharacterLocomotion.MotorThrottle = Vector3.zero;
var deltaPosition = Vector3.MoveTowards(m_Transform.position, m_DriveSource.DriverLocation.position, m_MoveSpeed) - m_Transform.position;
m_CharacterLocomotion.AbilityMotor = deltaPosition / (m_CharacterLocomotion.TimeScaleSquared * Time.timeScale * TimeUtility.FramerateDeltaTime);
}
/// <summary>
/// Callback when the ability tries to be stopped. Start the dismount.
/// </summary>
public override void WillTryStopAbility()
{
#if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER
// The IDriveSource is responsible for notifying the remote players for the changes.
if (m_NetworkInfo != null && !m_NetworkInfo.IsLocalPlayer()) {
return;
}
#endif
if (m_DriveState != DriveState.Drive) {
return;
}
// The ability can't stop if there are no valid exit locations.
MoveTowardsLocation startLocation;
if ((startLocation = GetValidStartLocation(false)) == null) {
return;
}
m_DriveSource.ExitVehicle(m_GameObject);
m_DriveState = DriveState.Exit;
m_CharacterLocomotion.AbilityMotor = Vector3.zero;
m_CharacterLocomotion.ForceRootMotionRotation = true;
m_CharacterLocomotion.ForceRootMotionPosition = true;
m_CharacterLocomotion.AllowRootMotionRotation = true;
m_CharacterLocomotion.AllowRootMotionPosition = true;
m_CharacterLocomotion.UpdateAbilityAnimatorParameters();
// Teleport the character if there are no enter/exit animations.
if (m_TeleportEnterExit) {
OnExitedVehicle();
var forward = Vector3.ProjectOnPlane(startLocation.transform.forward, -m_CharacterLocomotion.GravityDirection);
m_CharacterLocomotion.SetPositionAndRotation(startLocation.transform.position, Quaternion.LookRotation(forward, -m_CharacterLocomotion.GravityDirection), true, false);
}
}
/// <summary>
/// Callback when the character has exited the vehicle.
/// </summary>
private void OnExitedVehicle()
{
#if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER
// The IDriveSource is responsible for notifying the remote players for the changes.
if (m_NetworkInfo != null && !m_NetworkInfo.IsLocalPlayer()) {
return;
}
#endif
m_DriveSource.ExitedVehicle(m_GameObject);
m_DriveState = DriveState.ExitComplete;
m_CharacterLocomotion.UpdateAbilityAnimatorParameters();
m_Transform.parent = m_OriginalParent;
m_CharacterLocomotion.SetPlatform(null);
m_CharacterLocomotion.AlignToGravity = false;
m_CharacterLocomotion.ForceRootMotionRotation = false;
}
/// <summary>
/// Can the ability be stopped?
/// </summary>
/// <returns>True if the ability can be stopped.</returns>
public override bool CanStopAbility()
{
// The character has to be exited in order to stop.
return m_DriveState == DriveState.ExitComplete &&
(m_TeleportEnterExit || Vector3.Dot(m_Transform.rotation * Vector3.up, -m_CharacterLocomotion.GravityDirection) >= m_Epsilon);
}
/// <summary>
/// The ability has stopped running.
/// </summary>
/// <param name="force">Was the ability force stopped?</param>
protected override void AbilityStopped(bool force)
{
base.AbilityStopped(force);
// If the drive state isn't exit complete then the ability was force stopped.
if (m_DriveState != DriveState.ExitComplete) {
m_DriveSource.ExitVehicle(m_GameObject);
m_CharacterLocomotion.AbilityMotor = Vector3.zero;
m_CharacterLocomotion.UpdateLocation = m_StartUpdateLocation;
OnExitedVehicle();
}
m_CharacterLocomotion.RemoveIgnoredColliders(m_VehicleColliders);
for (int i = 0; i < m_VehicleColliders.Length; ++i) {
for (int j = 0; j < m_CharacterLocomotion.ColliderCount; ++j) {
Physics.IgnoreCollision(m_VehicleColliders[i], m_CharacterLocomotion.Colliders[j], false);
}
}
}
/// <summary>
/// The character has been destroyed.
/// </summary>
public override void OnDestroy()
{
base.OnDestroy();
EventHandler.UnregisterEvent(m_GameObject, "OnAnimatorEnteredVehicle", OnEnteredVehicle);
EventHandler.UnregisterEvent(m_GameObject, "OnAnimatorExitedVehicle", OnExitedVehicle);
}
}
}