You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Driftology/Assets/Scripts/VehicleController.cs

369 lines
14 KiB
C#

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

using UnityEngine;
using CnControls;
using Fusion;
public enum SurfaceDetection
{
RayCast,
SphereCast
}
public class VehicleController : NetworkBehaviour
{
[SerializeField] float currentSpeed = 0f;
[SerializeField] float steeringAI = 0f;
[SerializeField] bool isAIControlled = false;
[SerializeField] SurfaceDetection groundDetection = SurfaceDetection.RayCast;
[SerializeField] LayerMask driveableSurface = -1;
[SerializeField] float maxSpeed = 100f;
[SerializeField] float baseAcceleration = 10f;
[SerializeField] float steeringSensitivity = 10f;
[SerializeField] float gravityForce = 9.8f;
[SerializeField] float downforce = 5f;
[SerializeField] float brakeThreshold = 30f;
[SerializeField] float targetStoppingDistance = 5f;
[SerializeField] bool allowAirControl = false;
[SerializeField] AnimationCurve _frictionCurve = new AnimationCurve();
[SerializeField] AnimationCurve _turnCurve = new AnimationCurve();
[SerializeField, Range(0, 10)] float bodyTiltAmount = 0.5f;
[SerializeField] Transform carBodyVisual = null;
[SerializeField, Range(0.1f, 1f)] float skidMarkWidth = 0.25f;
[SerializeField, Range(0.1f, 10f)] float skidMarkDuration = 5f;
[SerializeField] Transform wheelFrontLeft = null;
[SerializeField] Transform wheelFrontRight = null;
[SerializeField] Transform wheelRearLeft = null;
[SerializeField] Transform wheelRearRight = null;
[SerializeField] Transform frontLeftAxle = null;
[SerializeField] Transform frontRightAxle = null;
[SerializeField] GameObject speedVisualEffects = null;
[SerializeField] Rigidbody sphereRigidbody = null;
[SerializeField] PhysicMaterial wheelFrictionMaterial = null;
[SerializeField] Transform cameraFollowTarget = null;
private bool isRunning = false;
private bool isOnGround = false;
private bool hasTurboBoost = true;
public bool nosActive = false;
private float accelerationInput = 0f;
private float brakeInput = 0f;
private float accelerationForce = 0f;
private float steeringSign = 0f;
private float inputHorizontal = 0f;
private float inputVertical = 0f;
private float wheelRadius = 0f;
private float desiredTurnAngle = 0f;
private float distanceToTarget = 0f;
private float angleDifference = 0f;
private float maxGroundDistance = 0f;
private float counterSteerValue = 0f;
private float steeringMultiplier = 0f;
private Vector3 velocityLocal = Vector3.zero;
private Vector3 forwardDirection = Vector3.zero;
private Vector3 directionToTarget = Vector3.zero;
private Vector3 movementDirection = Vector3.zero;
private RaycastHit surfaceHit;
private Rigidbody vehicleRigidbody = null;
private VehicleTracker targetTracker = null;
public bool AIControlled => isAIControlled;
public bool Running => isRunning;
public bool Grounded => isOnGround;
public float SkidWidth => skidMarkWidth;
public float SkidDuration => skidMarkDuration;
public float CurrentSpeed => velocityLocal.z;
public float MaximumSpeed => maxSpeed;
public bool InputsActive => inputHorizontal != 0 || inputVertical != 0 || nosActive;
public NOSController nosController;
internal float SteeringSensitivity
{
get => steeringSensitivity;
set => steeringSensitivity = value;
}
public Vector3 LocalVelocity => velocityLocal;
public Transform FollowTarget => cameraFollowTarget;
[Range(0.3f, 1f)] public float steeringFactor = 1f;
public override void Spawned()
{
base.Spawned();
InitializeVehicle();
bool isLocalPlayer = Object.HasInputAuthority; // true on the runner that owns this object
bool isHostRunner = Runner.IsServer; // true on the hosts runner
Debug.Log("Spawned");
// if (local && hostRunner) OR (remote && clientRunner) ⇒ this is the hosts car
if (isLocalPlayer == isHostRunner)
gameObject.name = "PlayerHost";
else
gameObject.name = "PlayerClient";
}
// 1) Fusion will call this every tick to collect local input
public override void FixedUpdateNetwork()
{
if (!isRunning || !RaceCountdownManager.Instance || !RaceCountdownManager.Instance.RaceStarted)
return;
if (/*Object.HasInputAuthority && */GetInput(out CarNetworkInput data))
{
inputHorizontal = data.Horizontal;
inputVertical = data.Vertical;
if (data.NOS) ActivateNOS();
}
// ✅ Only simulate physics on authoritative runner
//if (!Object.HasStateAuthority) return;
ApplyControls();
CheckGround();
HandleMovement();
UpdateVisuals(); // OK on all clients
}
// 2) Fusion runs your simulation here instead of Update/FixedUpdate
//public override void FixedUpdateNetwork()
//{
// if (!isRunning || !RaceCountdownManager.Instance || !RaceCountdownManager.Instance.RaceStarted)
// return;
// if (!Object.HasInputAuthority)
// {
// Debug.Log("🚫 No Input Authority on this player");
// return;
// }
// if (Object.HasInputAuthority && GetInput(out CarNetworkInput data))
// {
// inputHorizontal = data.Horizontal;
// inputVertical = data.Vertical;
// Debug.Log("inputVertical: " + inputVertical);
// Debug.Log("inputHorizontal : " + inputHorizontal);
// if (data.NOS)
// ActivateNOS();
// ApplyControls();
// CheckGround();
// HandleMovement();
// }
// UpdateVisuals();
//}
private void Awake()
{
vehicleRigidbody = GetComponent<Rigidbody>();
targetTracker = GetComponent<VehicleTracker>();
wheelRadius = sphereRigidbody.GetComponent<SphereCollider>().radius;
nosController = GetComponent<NOSController>();
maxGroundDistance = wheelRadius + 0.5f;
InitializeVehicle();
}
public void InitializeVehicle()
{
isRunning = true;
if (!isAIControlled)
accelerationForce = baseAcceleration;
}
public void StopVehicle()
{
isRunning = false;
velocityLocal = Vector3.zero;
currentSpeed = 0f;
steeringAI = 0f;
}
//private void Update()
//{
// if (!isRunning) return;
// UpdateVisuals();
// ProcessInputs();
// ApplyControls();
//}
private void ProcessInputs()
{
inputVertical = CnInputManager.GetAxis("Vertical");
inputHorizontal = CnInputManager.GetAxis("Horizontal");
}
private void UpdateVisuals()
{
counterSteerValue = (Mathf.Abs(velocityLocal.x) > 20f) ? -1f : 1f;
if (wheelFrontLeft) wheelFrontLeft.localRotation = Quaternion.Slerp(wheelFrontLeft.localRotation, Quaternion.Euler(0, 45f * counterSteerValue * steeringAI, 0), 0.1f);
if (wheelFrontRight) wheelFrontRight.localRotation = Quaternion.Slerp(wheelFrontRight.localRotation, Quaternion.Euler(0, 45f * counterSteerValue * steeringAI, 0), 0.1f);
frontLeftAxle?.Rotate(Vector3.right * (CurrentSpeed * 0.75f));
frontRightAxle?.Rotate(Vector3.right * (CurrentSpeed * 0.75f));
wheelRearLeft?.Rotate(Vector3.right * (CurrentSpeed * 0.75f));
wheelRearRight?.Rotate(Vector3.right * (CurrentSpeed * 0.75f));
if (carBodyVisual)
{
if (velocityLocal.z > 1)
carBodyVisual.localRotation = Quaternion.Slerp(carBodyVisual.localRotation,
Quaternion.Euler(Mathf.Lerp(0, -5, velocityLocal.z / maxSpeed), 0, Mathf.Clamp(desiredTurnAngle * steeringAI, -bodyTiltAmount, bodyTiltAmount)), 0.05f);
else
carBodyVisual.localRotation = Quaternion.Slerp(carBodyVisual.localRotation, Quaternion.identity, 0.05f);
}
if (speedVisualEffects)
speedVisualEffects.SetActive(velocityLocal.z > 10);
}
private void ApplyControls()
{
Vector3 targetPoint = targetTracker.TargetPos;
targetPoint.y = transform.position.y;
directionToTarget = (targetPoint - transform.position).normalized;
forwardDirection = transform.forward.normalized;
desiredTurnAngle = Mathf.Abs(Vector3.Angle(forwardDirection, Vector3.ProjectOnPlane(directionToTarget, transform.up)));
distanceToTarget = Vector3.Distance(transform.position, targetTracker.TargetPos);
movementDirection = (targetTracker.TargetPos - transform.position).normalized;
float alignment = Vector3.Dot(transform.forward, movementDirection);
angleDifference = Vector3.Angle(transform.forward, movementDirection);
brakeInput = 0f;
if (distanceToTarget > targetStoppingDistance)
{
accelerationInput = alignment > 0f ? inputVertical : (distanceToTarget > 5f ? inputVertical : -1f);
steeringAI = (Vector3.SignedAngle(transform.forward, movementDirection, Vector3.up) > 0f ? 1f : -1f)
* _turnCurve.Evaluate(desiredTurnAngle / 160f);
}
else
{
steeringAI = 0f;
}
if (!isAIControlled)
steeringAI += inputHorizontal;
currentSpeed = Mathf.Round(CurrentSpeed);
}
//private void FixedUpdate()
//{
// if (!isRunning) return;
// CheckGround();
// HandleMovement();
//}
private void CheckGround()
{
isOnGround = true;
//if (groundDetection == SurfaceDetection.RayCast)
//{
// isOnGround = Physics.Raycast(sphereRigidbody.position, Vector3.down, out surfaceHit, maxGroundDistance, driveableSurface)
// || Physics.Raycast(sphereRigidbody.position + Vector3.forward * 3f, Vector3.down, out surfaceHit, maxGroundDistance, driveableSurface)
// || Physics.Raycast(sphereRigidbody.position - Vector3.forward * 3f, Vector3.down, out surfaceHit, maxGroundDistance, driveableSurface);
//}
//else if (groundDetection == SurfaceDetection.SphereCast)
//{
// isOnGround = Physics.SphereCast(sphereRigidbody.position + wheelRadius * Vector3.up, wheelRadius + 0.25f, -transform.up, out surfaceHit, maxGroundDistance, driveableSurface);
//}
//else isOnGround = false;
}
private void HandleMovement()
{
if (!Object.HasInputAuthority)
{
Debug.LogWarning("Client without InputAuthority is simulating movement — this should not happen!");
}
else
{
if (Object.HasStateAuthority || Object.HasInputAuthority)
{
velocityLocal = vehicleRigidbody.transform.InverseTransformDirection(vehicleRigidbody.velocity);
}
// velocityLocal = vehicleRigidbody.transform.InverseTransformDirection(vehicleRigidbody.velocity);
if (float.IsNaN(velocityLocal.z) || float.IsInfinity(velocityLocal.z))
{
Debug.LogWarning($"⚠ Detected invalid velocity: {velocityLocal}. Resetting to zero.");
velocityLocal = Vector3.zero;
}
if (angleDifference > brakeThreshold && velocityLocal.z > 15f)
{
wheelFrictionMaterial.dynamicFriction = 0.01f;
}
else
{
wheelFrictionMaterial.dynamicFriction = _frictionCurve.Evaluate(Mathf.Abs(velocityLocal.x / 100));
}
steeringMultiplier = _turnCurve.Evaluate(velocityLocal.magnitude / maxSpeed);
if (Grounded)
{
steeringSign = Mathf.Sign(velocityLocal.z);
if (Mathf.Abs(accelerationInput) > 0.1f)
vehicleRigidbody.AddTorque(Vector3.up * (steeringAI * steeringSign * steeringSensitivity * 100f * steeringMultiplier * steeringFactor));
if (Mathf.Abs(accelerationInput) > 0.1f)
sphereRigidbody.velocity = Vector3.Lerp(sphereRigidbody.velocity, vehicleRigidbody.transform.forward * (accelerationInput * maxSpeed), accelerationForce / 10f * Time.fixedDeltaTime);
sphereRigidbody.AddForce(-transform.up * (downforce * sphereRigidbody.mass));
vehicleRigidbody.MoveRotation(
Quaternion.Slerp(
vehicleRigidbody.rotation,
Quaternion.FromToRotation(vehicleRigidbody.transform.up, surfaceHit.normal) * vehicleRigidbody.transform.rotation,
0.12f
)
);
}
else
{
if (allowAirControl)
vehicleRigidbody.AddTorque(Vector3.up * (steeringAI * steeringSensitivity * 100f * steeringMultiplier * steeringFactor));
sphereRigidbody.velocity = Vector3.Lerp(
sphereRigidbody.velocity,
(vehicleRigidbody.transform.forward * (accelerationInput * maxSpeed)) + Vector3.down * (gravityForce * 9.8f),
(accelerationForce / 25f) * Time.deltaTime
);
}
}
}
internal void DisableVehicle()
{
enabled = false;
sphereRigidbody.gameObject.SetActive(false);
sphereRigidbody.velocity = Vector3.zero;
sphereRigidbody.Sleep();
vehicleRigidbody.velocity = Vector3.zero;
vehicleRigidbody.Sleep();
}
public void ActivateNOS()
{
ActivateNOS(true);
//nosController._isBoosting = true;
}
public void ActivateNOS(bool active, float bonusAcceleration = 10f)
{
nosActive = active;
accelerationForce = active ? accelerationForce + bonusAcceleration : baseAcceleration;
}
internal void EnableVehicle()
{
enabled = true;
sphereRigidbody.gameObject.SetActive(true);
}
}