533 lines
19 KiB
C#
533 lines
19 KiB
C#
using System;
|
||
using System.Collections;
|
||
using System.Collections.Concurrent;
|
||
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using BITKit;
|
||
using BITKit.Entities;
|
||
using BITKit.Physics;
|
||
using BITKit.WorldNode;
|
||
using Microsoft.Extensions.DependencyInjection;
|
||
using Microsoft.Extensions.Logging;
|
||
using Net.Project.B.UX;
|
||
using Net.Project.B.World;
|
||
using Net.Project.B.WorldNode;
|
||
using Project.B.CharacterController;
|
||
using Project.B.Player;
|
||
using UnityEditor;
|
||
using UnityEngine;
|
||
using UnityEngine.AI;
|
||
using UnityEngine.InputSystem;
|
||
using UnityEngine.UIElements;
|
||
|
||
namespace Net.Project.B.Vehicle
|
||
{
|
||
public class WorldCarService:IDisposable
|
||
{
|
||
private class RuntimeCarInfo
|
||
{
|
||
public float CurrentSteerAngle = 0f;
|
||
|
||
public float InnerAngle;
|
||
public float OuterAngle;
|
||
|
||
public float WheelBase;
|
||
public float TrackWidth;
|
||
|
||
public Vector3 AngularVelocity;
|
||
}
|
||
|
||
private readonly ILogger<WorldCarService> _logger;
|
||
|
||
private readonly IFixedTicker _ticker;
|
||
|
||
private readonly IUXHud _uxHud;
|
||
|
||
private readonly IEntitiesService _entitiesService;
|
||
|
||
private readonly ConcurrentDictionary<int, UnityVehicleNode> _vehicles = new();
|
||
|
||
private readonly ConcurrentDictionary<int, RuntimeCarInfo> _runtimeCarInfos=new();
|
||
|
||
private readonly IWorldSeatService _seatService;
|
||
|
||
private readonly ICarKeyMap<InputAction> _carKeyMap;
|
||
|
||
private readonly InputActionGroup _inputActionGroup = new();
|
||
|
||
private readonly HashSet<int> _isControlVehicles = new();
|
||
|
||
private readonly HashSet<Rigidbody> _waitLoaded = new();
|
||
|
||
private float _currentVertical;
|
||
private float _currentHorizontal;
|
||
private float _currentHandBrake;
|
||
|
||
public WorldCarService(IEntitiesService entitiesService, IFixedTicker ticker, ICarKeyMap<InputAction> carKeyMap, IWorldSeatService seatService, ILogger<WorldCarService> logger, IUXHud uxHud)
|
||
{
|
||
_entitiesService = entitiesService;
|
||
_ticker = ticker;
|
||
_carKeyMap = carKeyMap;
|
||
_seatService = seatService;
|
||
_logger = logger;
|
||
_uxHud = uxHud;
|
||
|
||
_entitiesService.OnAdd += OnAdd;
|
||
_entitiesService.OnRemove += OnRemove;
|
||
|
||
_ticker.Add(OnTick);
|
||
|
||
_inputActionGroup.RegisterCallback(_carKeyMap.VerticalKey, OnVertical);
|
||
_inputActionGroup.RegisterCallback(_carKeyMap.HorizontalKey, OnHorizontal);
|
||
_inputActionGroup.RegisterCallback(_carKeyMap.HandBrakeKey, OnHandBrake);
|
||
|
||
_inputActionGroup.allowInput.AddElement(this);
|
||
|
||
_seatService.OnSeatOccupied += OnSeatOccupied;
|
||
|
||
}
|
||
|
||
private void OnSeatOccupied(UnitySeatNode arg1, IEntity arg2, IEntity arg3)
|
||
{
|
||
foreach (var (vehicleId, vehicleNode) in _vehicles)
|
||
{
|
||
if (!vehicleNode.seatObject) continue;
|
||
var vehicleSeatId = vehicleNode.seatObject.GetInstanceID();
|
||
if (vehicleSeatId != arg1.Id) continue;
|
||
|
||
if (arg3 is not null)
|
||
{
|
||
if (_isControlVehicles.Add(vehicleId)) ;
|
||
{
|
||
if (vehicleNode.rigidbody.velocity.sqrMagnitude < 1)
|
||
{
|
||
foreach (var wheelCollider in vehicleNode.wheels)
|
||
{
|
||
wheelCollider.motorTorque = 0;
|
||
wheelCollider.brakeTorque = vehicleNode.handbrakeForce;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
if (_isControlVehicles.Remove(vehicleId))
|
||
{
|
||
foreach (var wheelCollider in vehicleNode.wheels)
|
||
{
|
||
wheelCollider.motorTorque = 0;
|
||
wheelCollider.brakeTorque = 0;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private void OnHandBrake(InputAction.CallbackContext obj)
|
||
{
|
||
_currentHandBrake = obj.ReadValue<float>();
|
||
}
|
||
|
||
private void OnHorizontal(InputAction.CallbackContext obj)
|
||
{
|
||
_currentHorizontal = obj.ReadValue<float>();
|
||
}
|
||
|
||
private void OnVertical(InputAction.CallbackContext obj)
|
||
{
|
||
_currentVertical = obj.ReadValue<float>();
|
||
}
|
||
|
||
private void OnTick(float obj)
|
||
{
|
||
foreach (var rigidbody in _waitLoaded)
|
||
{
|
||
if (!rigidbody)
|
||
{
|
||
_waitLoaded.Remove(rigidbody);
|
||
break;
|
||
}
|
||
if (Physics.Raycast(rigidbody.worldCenterOfMass+Vector3.up, Vector3.down, out var hit,64, LayerMask.GetMask("Default")))
|
||
{
|
||
Debug.DrawRay(rigidbody.worldCenterOfMass,Vector3.down,Color.yellow,obj);
|
||
rigidbody.isKinematic = false;
|
||
_waitLoaded.Remove(rigidbody);
|
||
break;
|
||
}
|
||
}
|
||
|
||
foreach (var (id,vehicleNode) in _vehicles)
|
||
{
|
||
var runtimeInfo = _runtimeCarInfos.GetOrCreate(id);
|
||
|
||
if (_isControlVehicles.Contains(id))
|
||
{
|
||
RotateSteeringWheel();
|
||
|
||
Steering();
|
||
|
||
Acceleration();
|
||
|
||
HandBraking();
|
||
|
||
foreach (var wheelCollider in vehicleNode.wheels)
|
||
{
|
||
var text = $"" +
|
||
$"Drive Wheel: {wheelCollider.name}" +
|
||
$"\nTorque: {wheelCollider.motorTorque:F2}" +
|
||
$"\nBrake: {wheelCollider.brakeTorque:F2}" +
|
||
$"\nGround:{wheelCollider.isGrounded}" +
|
||
$"\nRPM:{wheelCollider.rpm}";
|
||
wheelCollider.GetWorldPose(out var pos,out _);
|
||
|
||
DrawXXL.DrawText.Write(text,pos);
|
||
}
|
||
}
|
||
|
||
//Slowdown();
|
||
|
||
/*
|
||
foreach (var wheelCollider in vehicleNode.wheels)
|
||
{
|
||
if (wheelCollider.isGrounded)
|
||
{
|
||
vehicleNode.rigidbody.AddForce(Vector3.down * vehicleNode.rigidbody.velocity.GetLength());
|
||
break;
|
||
}
|
||
}
|
||
*/
|
||
|
||
for (var index = 0; index < vehicleNode.wheelMeshes.Length; index++)
|
||
{
|
||
UpdateWheel(vehicleNode.wheels[index], vehicleNode.wheelMeshes[index]);
|
||
}
|
||
|
||
continue;
|
||
void Steering()
|
||
{
|
||
if(_isControlVehicles.Contains(id) is false)return;
|
||
runtimeInfo.CurrentSteerAngle = vehicleNode.maxSteerAngle * _currentHorizontal;
|
||
|
||
switch (vehicleNode.steeringWheelColliders.Length)
|
||
{
|
||
case 0:
|
||
{
|
||
var newRot = Quaternion.Euler(Vector3.up * ( runtimeInfo.CurrentSteerAngle * Time.fixedDeltaTime));
|
||
|
||
newRot = vehicleNode.rigidbody.rotation * newRot;
|
||
|
||
vehicleNode.rigidbody.MoveRotation(newRot);
|
||
}
|
||
break;
|
||
case 2 when vehicleNode.allowAckermannSteering:
|
||
{
|
||
// 获取当前车轮的转向角度(用于计算)
|
||
var steerAngle = vehicleNode.maxSteerAngle*_currentHorizontal;
|
||
var normalizedSteerAngle =
|
||
Mathf.Clamp(steerAngle, -vehicleNode.maxSteerAngle, vehicleNode.maxSteerAngle);
|
||
|
||
// 计算内外侧车轮的转向角度(阿克曼转向公式)
|
||
runtimeInfo.InnerAngle = Mathf.Atan(Mathf.Tan(normalizedSteerAngle * Mathf.Deg2Rad) *
|
||
(runtimeInfo.WheelBase /
|
||
(runtimeInfo.WheelBase + runtimeInfo.TrackWidth))) *
|
||
Mathf.Rad2Deg;
|
||
|
||
runtimeInfo.OuterAngle = Mathf.Atan(Mathf.Tan(normalizedSteerAngle * Mathf.Deg2Rad) *
|
||
(runtimeInfo.TrackWidth /
|
||
(runtimeInfo.WheelBase + runtimeInfo.TrackWidth))) *
|
||
Mathf.Rad2Deg;
|
||
|
||
// 设置前轮的转向角度
|
||
switch (runtimeInfo.CurrentSteerAngle)
|
||
{
|
||
case > 0:
|
||
vehicleNode.steeringWheelColliders[0].steerAngle = runtimeInfo.OuterAngle;
|
||
vehicleNode.steeringWheelColliders[1].steerAngle = runtimeInfo.InnerAngle;
|
||
break;
|
||
case < 0:
|
||
vehicleNode.steeringWheelColliders[0].steerAngle = runtimeInfo.InnerAngle;
|
||
vehicleNode.steeringWheelColliders[1].steerAngle = runtimeInfo.OuterAngle;
|
||
break;
|
||
case 0:
|
||
vehicleNode.steeringWheelColliders[0].steerAngle = 0;
|
||
vehicleNode.steeringWheelColliders[1].steerAngle = 0;
|
||
break;
|
||
}
|
||
|
||
break;
|
||
}
|
||
default:
|
||
{
|
||
runtimeInfo.InnerAngle = runtimeInfo.OuterAngle = runtimeInfo.CurrentSteerAngle;
|
||
|
||
foreach (var steeringWheelCollider in vehicleNode.steeringWheelColliders)
|
||
{
|
||
steeringWheelCollider.steerAngle = runtimeInfo.CurrentSteerAngle;
|
||
}
|
||
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
void Acceleration()
|
||
{
|
||
var selfVelocityZ = vehicleNode.rigidbody.transform.InverseTransformDirection(vehicleNode.rigidbody.velocity).z;
|
||
|
||
float motorTorque = 0;
|
||
float brakeTorque = 0;
|
||
|
||
// 驱动逻辑
|
||
if (_currentVertical != 0)
|
||
{
|
||
|
||
// 判断是否方向相反,如果相反就刹车
|
||
if ((_currentVertical > 0 && selfVelocityZ < -vehicleNode.stopThreshold) ||
|
||
(_currentVertical < 0 && selfVelocityZ > vehicleNode.stopThreshold))
|
||
{
|
||
brakeTorque = vehicleNode.brakePower;
|
||
}
|
||
else
|
||
{
|
||
// 正常加速
|
||
motorTorque = _currentVertical * vehicleNode.horsePower;
|
||
}
|
||
|
||
}
|
||
|
||
// 应用驱动轮
|
||
foreach (var wheelCollider in vehicleNode.driveWheelColliders)
|
||
{
|
||
wheelCollider.motorTorque = motorTorque;
|
||
wheelCollider.brakeTorque = brakeTorque;
|
||
}
|
||
}
|
||
|
||
|
||
void HandBraking()
|
||
{
|
||
var handBrakeTorque = (_currentHandBrake > 0) ? _currentHandBrake * vehicleNode.handbrakeForce : 0;
|
||
|
||
foreach (var wheelCollider in vehicleNode.handBrakeWheelColliders)
|
||
{
|
||
wheelCollider.brakeTorque = handBrakeTorque;
|
||
}
|
||
}
|
||
|
||
|
||
void RotateSteeringWheel()
|
||
{
|
||
if(!vehicleNode.steeringWheel)return;
|
||
|
||
var currentXAngle =
|
||
vehicleNode.steeringWheel.transform.localEulerAngles.x; // Maximum steer angle in degrees
|
||
|
||
var normalizedSteerAngle =
|
||
Mathf.Clamp(vehicleNode.steeringWheelColliders.First().steerAngle, -vehicleNode.maxSteerAngle,
|
||
vehicleNode.maxSteerAngle);
|
||
var rotation = Mathf.Lerp(vehicleNode.steeringWheelAngle,-vehicleNode.steeringWheelAngle,
|
||
(normalizedSteerAngle + vehicleNode.maxSteerAngle) / (2 * vehicleNode.maxSteerAngle));
|
||
|
||
// Set the local rotation of the steering wheel
|
||
vehicleNode.steeringWheel.localRotation = Quaternion.Lerp(vehicleNode.steeringWheel.localRotation, Quaternion.Euler(currentXAngle, 0, rotation),8*obj);
|
||
}
|
||
|
||
void UpdateWheel(WheelCollider col, Transform mesh)
|
||
{
|
||
col.GetWorldPose(out Vector3 position, out Quaternion rotation);
|
||
mesh.SetPositionAndRotation(position, rotation);
|
||
}
|
||
|
||
void Slowdown()
|
||
{
|
||
if (!vehicleNode.rigidbody || vehicleNode.rigidbody.isKinematic)return;
|
||
{
|
||
if (_currentVertical == 0 && _currentHandBrake == 0)
|
||
{
|
||
vehicleNode .rigidbody.velocity =
|
||
Vector3.Lerp( vehicleNode .rigidbody.velocity, Vector3.zero, Time.deltaTime * 0.5f);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private void OnRemove(IEntity obj)
|
||
{
|
||
_vehicles.TryRemove(obj.Id, out _);
|
||
}
|
||
|
||
private void OnAdd(IEntity obj)
|
||
{
|
||
if (obj.ServiceProvider.GetService<UnityVehicleNode>() is not { } vehicleNode) return;
|
||
|
||
_vehicles.TryAdd(obj.Id, vehicleNode);
|
||
|
||
vehicleNode.rigidbody.interpolation = RigidbodyInterpolation.Interpolate;
|
||
vehicleNode.rigidbody.automaticCenterOfMass = false;
|
||
vehicleNode.rigidbody.centerOfMass = default;
|
||
|
||
obj.ServiceProvider.QueryComponents(out GameObject gameObject, out Transform transform);
|
||
|
||
gameObject.layer= LayerMask.NameToLayer("Vehicle");
|
||
gameObject.isStatic = false;
|
||
|
||
var colliderReady = false;
|
||
|
||
foreach (var boxCollider in gameObject.GetComponentsInChildren<BoxCollider>())
|
||
{
|
||
if (boxCollider.attachedRigidbody == vehicleNode.rigidbody && boxCollider.isTrigger is false && boxCollider.gameObject.layer==gameObject.layer)
|
||
{
|
||
colliderReady = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
try
|
||
{
|
||
if (colliderReady is false)
|
||
{
|
||
var meshRenderers = gameObject
|
||
.GetComponentsInChildren<MeshRenderer>()
|
||
.Where(x=>x.gameObject.name.Contains("Wheel",StringComparison.CurrentCultureIgnoreCase) is false)
|
||
.ToArray()
|
||
;
|
||
|
||
if (meshRenderers.Length == 0)
|
||
{
|
||
Debug.LogWarning($"没有 MeshRenderer 可用于生成 BoxCollider:{gameObject.name}"); throw new OperationCanceledException();
|
||
}
|
||
|
||
Bounds bounds = meshRenderers[0].bounds;
|
||
for (int i = 1; i < meshRenderers.Length; i++)
|
||
{
|
||
bounds.Encapsulate(meshRenderers[i].bounds);
|
||
}
|
||
|
||
var box = gameObject.AddComponent<BoxCollider>();
|
||
|
||
// 将 world bounds 转换为本地坐标系
|
||
var centerLocal = gameObject.transform.InverseTransformPoint(bounds.center);
|
||
var sizeLocal = gameObject.transform.InverseTransformVector(bounds.size);
|
||
|
||
box.center = centerLocal;
|
||
box.size = sizeLocal;
|
||
}
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
|
||
}
|
||
|
||
|
||
|
||
var colliders = obj.ServiceProvider.GetRequiredService<GameObject>().GetComponentsInChildren<Collider>(true);
|
||
foreach (var x in colliders)
|
||
{
|
||
foreach (var y in colliders)
|
||
{
|
||
Physics.IgnoreCollision(x, y, true);
|
||
}
|
||
|
||
if (x.attachedRigidbody == vehicleNode.rigidbody)
|
||
x.gameObject.layer = gameObject.layer;
|
||
}
|
||
|
||
if (!vehicleNode.seatObject)
|
||
{
|
||
var nodes = gameObject.GetComponentsInChildren<UnityNode>().Select(x=>x.WorldNode);
|
||
if (nodes.QueryComponent(out UnitySeatNode seatNode))
|
||
{
|
||
seatNode.Rigidbody = vehicleNode.rigidbody;
|
||
vehicleNode.seatObject = seatNode.SeatObject.gameObject;
|
||
}
|
||
else
|
||
{
|
||
_logger.LogWarning(
|
||
$"VehicleNode {gameObject.name} 没有找到座位节点,请添加 UnitySeatNode 或者 UnitySeatNode 的子节点");
|
||
}
|
||
}
|
||
|
||
if (vehicleNode.seatObject)
|
||
{
|
||
vehicleNode.seatObject.layer = LayerMask.NameToLayer("Default");
|
||
}
|
||
|
||
|
||
if (vehicleNode.steeringWheelColliders.Length is 2)
|
||
{
|
||
var runtimeInfo = _runtimeCarInfos.GetOrCreate(obj.Id);
|
||
|
||
runtimeInfo.WheelBase = Mathf.Abs(
|
||
vehicleNode.rigidbody.transform
|
||
.InverseTransformPoint(vehicleNode.steeringWheelColliders[0].transform.position).z -
|
||
vehicleNode.rigidbody.transform
|
||
.InverseTransformPoint(vehicleNode.handBrakeWheelColliders[0].transform.position).z);
|
||
runtimeInfo.TrackWidth =
|
||
Mathf.Abs(
|
||
vehicleNode.rigidbody.transform
|
||
.InverseTransformPoint(vehicleNode.steeringWheelColliders[0].transform.position).x -
|
||
vehicleNode.rigidbody.transform
|
||
.InverseTransformPoint(vehicleNode.steeringWheelColliders[1].transform.position).x);
|
||
}
|
||
if (vehicleNode.rigidbody)
|
||
{
|
||
vehicleNode.rigidbody.isKinematic = true;
|
||
_waitLoaded.Add(vehicleNode.rigidbody);
|
||
|
||
//var physicsController = vehicleNode.rigidbody.gameObject.AddComponent<UnityCollisionController>();
|
||
|
||
//physicsController.OnUnityCollisionEnter += OnUnityCollisionEnter;
|
||
|
||
void OnUnityCollisionEnter(Collision collision)
|
||
{
|
||
if (_entitiesService.TryGetEntity(collision.transform.gameObject.GetInstanceID(), out var hitEntity))
|
||
{
|
||
Debug.Log(hitEntity.Id);
|
||
}
|
||
}
|
||
|
||
var obstable =vehicleNode.rigidbody.gameObject.GetOrAddComponent<NavMeshObstacle>();
|
||
|
||
var maxVolume = 0f;
|
||
|
||
foreach (var collider in colliders.OfType<BoxCollider>())
|
||
{
|
||
if(ReferenceEquals(collider.attachedRigidbody,vehicleNode.rigidbody) is false)continue;
|
||
var volume = collider.bounds.size.x * collider.bounds.size.y * collider.bounds.size.z;
|
||
if (volume > maxVolume)
|
||
{
|
||
maxVolume = volume;
|
||
|
||
obstable.center = collider.center;
|
||
obstable.size = collider.size;
|
||
}
|
||
}
|
||
|
||
foreach (var wheelCollider in vehicleNode.wheels)
|
||
{
|
||
wheelCollider.brakeTorque = vehicleNode.handbrakeForce;
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
public void Dispose()
|
||
{
|
||
_entitiesService.OnAdd -= OnAdd;
|
||
_ticker.Remove(OnTick);
|
||
|
||
_seatService.OnSeatOccupied -= OnSeatOccupied;
|
||
|
||
_inputActionGroup.allowInput.RemoveElement(this);
|
||
|
||
_inputActionGroup.UnRegisterCallback(_carKeyMap.VerticalKey, OnVertical);
|
||
_inputActionGroup.UnRegisterCallback(_carKeyMap.HorizontalKey, OnHorizontal);
|
||
_inputActionGroup.UnRegisterCallback(_carKeyMap.HandBrakeKey, OnHandBrake);
|
||
}
|
||
}
|
||
|
||
}
|
||
|