sdsandbox-rl-scripts/Scripts/Car.cs

380 lines
8.8 KiB
C#
Executable File

using UnityEngine;
using System.Collections;
public class Car : MonoBehaviour, ICar{
public CarSpawner carSpawner;
public WheelCollider[] wheelColliders;
public Transform[] wheelMeshes;
public float maxSpeed = 30f;
public float maxTorque = 50f;
public float maxBreakTorque = 50f;
public AnimationCurve torqueCurve;
public Transform centrOfMass;
public float requestTorque = 0f;
public float requestBrake = 0f;
public float requestSteering = 0f;
public Vector3 acceleration = Vector3.zero;
public Vector3 velocity = Vector3.zero;
public Vector3 prevVel = Vector3.zero;
public Vector3 startPos;
public Quaternion startRot;
private Quaternion rotation = Quaternion.identity;
private Vector3 gyro = Vector3.zero;
public Rigidbody rb;
//for logging
public float lastSteer = 0.0f;
public float lastAccel = 0.0f;
//when the car is doing multiple things, we sometimes want to sort out parts of the training
//use this label to pull partial training samples from a run
public string activity = "keep_lane";
public float maxSteer = 16.0f;
//name of the last object we hit.
public string last_collision = "none";
private float last_collision_time = -999f;
private const float collision_hold_seconds = 0.75f;
// Pre-allocated buffer for per-wheel barrier overlap checks (avoids GC per frame).
private Collider[] _wheelOverlapBuf = new Collider[8];
// Use this for initialization
void Awake ()
{
rb = GetComponent<Rigidbody>();
if(rb && centrOfMass)
{
rb.centerOfMass = centrOfMass.localPosition;
}
// Continuous collision detection prevents the car from tunnelling through
// thin or fast-moving colliders (including barrier walls) between frames.
if (rb)
rb.collisionDetectionMode = CollisionDetectionMode.Continuous;
requestTorque = 0f;
requestSteering = 0f;
SavePosRot();
// had to disable this because PID max steering was affecting the global max_steering
// maxSteer = PlayerPrefs.GetFloat("max_steer", 16.0f);
}
public void SavePosRot()
{
startPos = transform.position;
startRot = transform.rotation;
}
public void RestorePosRot()
{
Set(startPos, startRot);
}
public void RequestThrottle(float val)
{
requestTorque = val;
requestBrake = 0f;
//Debug.Log("request throttle: " + val);
}
public void SetMaxSteering(float val)
{
maxSteer = val;
// had to disable this because PID max steering was affecting the global max_steering
// PlayerPrefs.SetFloat("max_steer", maxSteer);
// PlayerPrefs.Save();
}
public float GetMaxSteering()
{
return maxSteer;
}
public void RequestSteering(float val)
{
requestSteering = Mathf.Clamp(val, -maxSteer, maxSteer);
//Debug.Log("request steering: " + val);
}
public void Set(Vector3 pos, Quaternion rot)
{
rb.position = pos;
rb.rotation = rot;
//just setting it once doesn't seem to work. Try setting it multiple times..
StartCoroutine(KeepSetting(pos, rot, 1));
}
IEnumerator KeepSetting(Vector3 pos, Quaternion rot, int numIter)
{
while(numIter > 0)
{
rb.isKinematic = true;
yield return new WaitForFixedUpdate();
rb.position = pos;
rb.rotation = rot;
transform.position = pos;
transform.rotation = rot;
numIter--;
rb.isKinematic = false;
}
}
public float GetSteering()
{
return requestSteering;
}
public float GetThrottle()
{
return requestTorque;
}
public float GetFootBrake()
{
return requestBrake;
}
public float GetHandBrake()
{
return 0.0f;
}
public Vector3 GetVelocity()
{
return velocity;
}
public Vector3 GetAccel()
{
return acceleration;
}
public Vector3 GetGyro()
{
return gyro;
}
public float GetOrient ()
{
Vector3 dir = transform.forward;
return Mathf.Atan2( dir.z, dir.x);
}
public Transform GetTransform()
{
return this.transform;
}
public bool IsStill()
{
return rb.IsSleeping();
}
public void RequestFootBrake(float val)
{
requestBrake = val;
}
public void RequestHandBrake(float val)
{
//todo
}
// Update is called once per frame
void Update () {
UpdateWheelPositions();
}
public string GetActivity()
{
return activity;
}
public void SetActivity(string act)
{
activity = act;
}
void FixedUpdate()
{
lastSteer = requestSteering;
lastAccel = requestTorque;
prevVel = velocity;
velocity = transform.InverseTransformDirection(rb.velocity);
acceleration = (velocity - prevVel)/Time.deltaTime;
gyro = rb.angularVelocity;
rotation = rb.rotation;
// use the torque curve
float throttle = torqueCurve.Evaluate(velocity.magnitude / maxSpeed) * requestTorque * maxTorque;
float steerAngle = requestSteering;
float brake = requestBrake * maxBreakTorque;
//front two tires.
wheelColliders[2].steerAngle = steerAngle;
wheelColliders[3].steerAngle = steerAngle;
//four wheel drive at the moment
foreach(WheelCollider wc in wheelColliders)
{
wc.motorTorque = throttle;
wc.brakeTorque = brake;
}
// WheelColliders don't fire OnCollisionStay on this MonoBehaviour. We use
// two complementary checks to catch barrier contact from any angle:
//
// 1) Forward raycast — probes ahead of the car even before contact occurs,
// so it fires quickly when driving into a barrier nose-first.
if (requestTorque > 0.05f)
{
Vector3 rayOrigin = transform.position + transform.up * 0.3f;
RaycastHit rhit;
if (Physics.Raycast(rayOrigin, transform.forward, out rhit, 0.8f))
{
if (!IsStartingLine(rhit.collider.gameObject.name))
RegisterCollision(rhit.collider.gameObject.name);
}
}
// 2) Per-wheel OverlapSphere — catches actual contact from any direction
// (side, rear, diagonal) regardless of throttle or car orientation.
// Uses barrier name filter to avoid false positives from the road surface.
foreach (WheelCollider wc in wheelColliders)
{
Vector3 wheelCenter;
Quaternion wheelRot;
wc.GetWorldPose(out wheelCenter, out wheelRot);
int n = Physics.OverlapSphereNonAlloc(wheelCenter, wc.radius + 0.05f, _wheelOverlapBuf);
for (int i = 0; i < n; i++)
{
if (_wheelOverlapBuf[i] == null) continue;
string hitName = _wheelOverlapBuf[i].gameObject.name;
if (hitName.Contains("barrier"))
{
RegisterCollision(hitName);
break;
}
}
}
}
void FlipUpright()
{
Quaternion rot = Quaternion.Euler(180f, 0f, 0f);
this.transform.rotation = transform.rotation * rot;
transform.position = transform.position + Vector3.up * 2;
}
void UpdateWheelPositions()
{
Quaternion rot;
Vector3 pos;
for(int i = 0; i < wheelColliders.Length; i++)
{
WheelCollider wc = wheelColliders[i];
Transform tm = wheelMeshes[i];
wc.GetWorldPose(out pos, out rot);
tm.position = pos;
tm.rotation = rot;
}
}
//get the name of the last object we collided with
public string GetLastCollision()
{
return last_collision;
}
public void ClearLastCollision()
{
if (Time.time - last_collision_time > collision_hold_seconds)
{
last_collision = "none";
}
}
bool ShouldPersistCollision(string collisionName, float speedMagnitude)
{
if (speedMagnitude < 1.5f)
{
return true;
}
string lowered = collisionName.ToLowerInvariant();
return lowered.Contains("barrier") || lowered.Contains("tree") || lowered.Contains("wall");
}
// The starting/finish line is a physical collider used for lap counting.
// It must not be treated as a barrier hit — lap counting is handled separately
// via the collision_with_starting_line TCP message in TcpCarHandler.
bool IsStartingLine(string objectName)
{
string lower = objectName.ToLowerInvariant();
return lower.Contains("starting_line") || lower.Contains("startingline") || lower.Contains("finishline");
}
void RegisterCollision(string collisionName)
{
last_collision = collisionName;
last_collision_time = Time.time;
}
void OnCollisionEnter(Collision col)
{
if (IsStartingLine(col.gameObject.name)) return;
RegisterCollision(col.gameObject.name);
}
void OnCollisionStay(Collision col)
{
if (IsStartingLine(col.gameObject.name)) return;
float speedMagnitude = (rb != null) ? rb.velocity.magnitude : 0.0f;
if (ShouldPersistCollision(col.gameObject.name, speedMagnitude))
{
RegisterCollision(col.gameObject.name);
}
}
// Trigger-based barriers (used by built-in tracks like mini-monaco) fire
// OnTriggerEnter rather than OnCollisionEnter. Handle them here so that
// out-of-bounds detection works on all tracks, not just our custom ones.
void OnTriggerEnter(Collider other)
{
if (IsStartingLine(other.gameObject.name)) return;
RegisterCollision(other.gameObject.name);
}
void OnTriggerStay(Collider other)
{
if (IsStartingLine(other.gameObject.name)) return;
float speedMagnitude = (rb != null) ? rb.velocity.magnitude : 0.0f;
if (ShouldPersistCollision(other.gameObject.name, speedMagnitude))
{
RegisterCollision(other.gameObject.name);
}
}
}