361 lines
17 KiB
C#
361 lines
17 KiB
C#
using RaceFields.CameraSystem;
|
|
using RaceFields.Vehicles;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
namespace RaceFields.EditorTools
|
|
{
|
|
public static class BuggyPrefabBuilder
|
|
{
|
|
private const string BuggyPrefabPath = "Assets/_Project/Prefabs/Vehicles/Player/Buggy.prefab";
|
|
private const string CameraPrefabPath = "Assets/_Project/Prefabs/Cameras/Buggy_ThirdPersonCamera.prefab";
|
|
private const string TireSmokePrefabPath = "Assets/_Project/Art/VFX/Effects/Particle Systems/Prefabs/TireSmoke_PS.prefab";
|
|
private const string TireSkidPrefabPath = "Assets/_Project/Art/VFX/Effects/Trails/Prefabs/TireSkid.prefab";
|
|
private const string EngineSoundPath = "Assets/PROMETEO - Car Controller/Sounds/CarEngine.wav";
|
|
private const string TireScreechSoundPath = "Assets/PROMETEO - Car Controller/Sounds/TireScreech.wav";
|
|
|
|
[MenuItem("Race Fields/Setup/Rebuild Buggy Prefab")]
|
|
public static void RebuildBuggyPrefab()
|
|
{
|
|
Selection.activeObject = null;
|
|
GameObject root = PrefabUtility.LoadPrefabContents(BuggyPrefabPath);
|
|
try
|
|
{
|
|
EnsureComponent<Rigidbody>(root, ConfigureBody);
|
|
EnsureComponent<BoxCollider>(root, collider => ConfigureBodyCollider(root, collider));
|
|
ArcadeBuggyController controller = EnsureComponent<ArcadeBuggyController>(root, null);
|
|
BuggyWheelSetup wheelSetup = EnsureComponent<BuggyWheelSetup>(root, ConfigureWheelSetup);
|
|
|
|
SetupWheel(root.transform, "Wheel_FL", "WheelCollider_FL");
|
|
SetupWheel(root.transform, "Wheel_FR", "WheelCollider_FR");
|
|
SetupWheel(root.transform, "Wheel_RL", "WheelCollider_RL");
|
|
SetupWheel(root.transform, "Wheel_RR", "WheelCollider_RR");
|
|
SetupControllerReferences(root.transform, controller);
|
|
SetupEffects(root.transform, controller);
|
|
SetupSounds(root.transform, controller);
|
|
|
|
wheelSetup.AlignAllWheels();
|
|
|
|
PrefabUtility.SaveAsPrefabAsset(root, BuggyPrefabPath);
|
|
}
|
|
finally
|
|
{
|
|
Selection.activeObject = null;
|
|
PrefabUtility.UnloadPrefabContents(root);
|
|
}
|
|
|
|
RebuildCameraPrefab();
|
|
AssetDatabase.SaveAssets();
|
|
AssetDatabase.Refresh();
|
|
EditorApplication.delayCall += SelectBuggyPrefabAsset;
|
|
}
|
|
|
|
private static void RebuildCameraPrefab()
|
|
{
|
|
Selection.activeObject = null;
|
|
GameObject cameraObject = new GameObject("Buggy_ThirdPersonCamera");
|
|
try
|
|
{
|
|
Camera camera = cameraObject.AddComponent<Camera>();
|
|
camera.fieldOfView = 62f;
|
|
camera.nearClipPlane = 0.05f;
|
|
camera.farClipPlane = 600f;
|
|
cameraObject.AddComponent<AudioListener>();
|
|
cameraObject.AddComponent<BuggyFollowCamera>().SnapToTarget();
|
|
|
|
cameraObject.transform.position = new Vector3(0f, 4.2f, -7.5f);
|
|
cameraObject.transform.rotation = Quaternion.Euler(18f, 0f, 0f);
|
|
|
|
PrefabUtility.SaveAsPrefabAsset(cameraObject, CameraPrefabPath);
|
|
}
|
|
finally
|
|
{
|
|
Selection.activeObject = null;
|
|
Object.DestroyImmediate(cameraObject);
|
|
}
|
|
}
|
|
|
|
private static void SelectBuggyPrefabAsset()
|
|
{
|
|
Selection.activeObject = AssetDatabase.LoadAssetAtPath<GameObject>(BuggyPrefabPath);
|
|
}
|
|
|
|
private static void SetupWheel(Transform root, string visualName, string colliderName)
|
|
{
|
|
Transform visual = FindDescendant(root, visualName);
|
|
if (visual == null)
|
|
{
|
|
Debug.LogWarning($"Wheel visual '{visualName}' was not found in {BuggyPrefabPath}.");
|
|
return;
|
|
}
|
|
|
|
Transform colliderTransform = FindDescendant(root, colliderName);
|
|
GameObject colliderObject = colliderTransform != null
|
|
? colliderTransform.gameObject
|
|
: new GameObject(colliderName);
|
|
|
|
colliderObject.name = colliderName;
|
|
colliderObject.transform.SetParent(root, true);
|
|
colliderObject.transform.position = visual.position;
|
|
colliderObject.transform.rotation = root.rotation;
|
|
colliderObject.transform.localScale = Vector3.one;
|
|
|
|
WheelCollider wheelCollider = colliderObject.GetComponent<WheelCollider>();
|
|
if (wheelCollider == null)
|
|
wheelCollider = colliderObject.AddComponent<WheelCollider>();
|
|
|
|
ConfigureWheel(wheelCollider, visual);
|
|
}
|
|
|
|
private static void ConfigureBody(Rigidbody body)
|
|
{
|
|
body.mass = 850f;
|
|
body.linearDamping = 0.05f;
|
|
body.angularDamping = 0.8f;
|
|
body.interpolation = RigidbodyInterpolation.Interpolate;
|
|
body.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
|
|
}
|
|
|
|
private static void ConfigureBodyCollider(GameObject root, BoxCollider collider)
|
|
{
|
|
Bounds bounds = CalculateLocalRendererBounds(root);
|
|
collider.center = new Vector3(0f, Mathf.Max(0.55f, bounds.center.y), bounds.center.z);
|
|
collider.size = new Vector3(
|
|
Mathf.Max(1.8f, bounds.size.x * 0.75f),
|
|
Mathf.Max(0.9f, bounds.size.y * 0.75f),
|
|
Mathf.Max(3.1f, bounds.size.z * 0.8f));
|
|
}
|
|
|
|
private static Bounds CalculateLocalRendererBounds(GameObject root)
|
|
{
|
|
Renderer[] renderers = root.GetComponentsInChildren<Renderer>(true);
|
|
bool hasBounds = false;
|
|
Bounds bounds = new Bounds(Vector3.zero, Vector3.one);
|
|
|
|
for (int i = 0; i < renderers.Length; i++)
|
|
{
|
|
if (renderers[i] is ParticleSystemRenderer || renderers[i] is TrailRenderer)
|
|
continue;
|
|
|
|
Bounds rendererBounds = renderers[i].bounds;
|
|
Vector3 localCenter = root.transform.InverseTransformPoint(rendererBounds.center);
|
|
Vector3 localSize = root.transform.InverseTransformVector(rendererBounds.size);
|
|
localSize = new Vector3(Mathf.Abs(localSize.x), Mathf.Abs(localSize.y), Mathf.Abs(localSize.z));
|
|
Bounds localBounds = new Bounds(localCenter, localSize);
|
|
|
|
if (!hasBounds)
|
|
{
|
|
bounds = localBounds;
|
|
hasBounds = true;
|
|
}
|
|
else
|
|
{
|
|
bounds.Encapsulate(localBounds);
|
|
}
|
|
}
|
|
|
|
return hasBounds ? bounds : new Bounds(new Vector3(0f, 0.55f, 0f), new Vector3(1.8f, 0.9f, 3.1f));
|
|
}
|
|
|
|
private static void ConfigureWheel(WheelCollider wheelCollider, Transform visual)
|
|
{
|
|
wheelCollider.mass = 20f;
|
|
wheelCollider.radius = EstimateRadius(visual);
|
|
wheelCollider.suspensionDistance = 0.28f;
|
|
wheelCollider.forceAppPointDistance = 0.05f;
|
|
wheelCollider.wheelDampingRate = 0.35f;
|
|
|
|
JointSpring spring = wheelCollider.suspensionSpring;
|
|
spring.spring = 35000f;
|
|
spring.damper = 4500f;
|
|
spring.targetPosition = 0.5f;
|
|
wheelCollider.suspensionSpring = spring;
|
|
}
|
|
|
|
private static void ConfigureWheelSetup(BuggyWheelSetup setup)
|
|
{
|
|
SerializedObject serializedSetup = new SerializedObject(setup);
|
|
serializedSetup.FindProperty("frontLeftWheelName").stringValue = "Wheel_FL";
|
|
serializedSetup.FindProperty("frontRightWheelName").stringValue = "Wheel_FR";
|
|
serializedSetup.FindProperty("rearLeftWheelName").stringValue = "Wheel_RL";
|
|
serializedSetup.FindProperty("rearRightWheelName").stringValue = "Wheel_RR";
|
|
serializedSetup.FindProperty("frontLeftColliderName").stringValue = "WheelCollider_FL";
|
|
serializedSetup.FindProperty("frontRightColliderName").stringValue = "WheelCollider_FR";
|
|
serializedSetup.FindProperty("rearLeftColliderName").stringValue = "WheelCollider_RL";
|
|
serializedSetup.FindProperty("rearRightColliderName").stringValue = "WheelCollider_RR";
|
|
serializedSetup.ApplyModifiedPropertiesWithoutUndo();
|
|
}
|
|
|
|
private static void SetupControllerReferences(Transform root, ArcadeBuggyController controller)
|
|
{
|
|
SerializedObject serializedController = new SerializedObject(controller);
|
|
serializedController.FindProperty("frontLeftCollider").objectReferenceValue = FindWheelCollider(root, "WheelCollider_FL");
|
|
serializedController.FindProperty("frontRightCollider").objectReferenceValue = FindWheelCollider(root, "WheelCollider_FR");
|
|
serializedController.FindProperty("rearLeftCollider").objectReferenceValue = FindWheelCollider(root, "WheelCollider_RL");
|
|
serializedController.FindProperty("rearRightCollider").objectReferenceValue = FindWheelCollider(root, "WheelCollider_RR");
|
|
serializedController.FindProperty("frontLeftVisual").objectReferenceValue = FindDescendant(root, "Wheel_FL");
|
|
serializedController.FindProperty("frontRightVisual").objectReferenceValue = FindDescendant(root, "Wheel_FR");
|
|
serializedController.FindProperty("rearLeftVisual").objectReferenceValue = FindDescendant(root, "Wheel_RL");
|
|
serializedController.FindProperty("rearRightVisual").objectReferenceValue = FindDescendant(root, "Wheel_RR");
|
|
serializedController.ApplyModifiedPropertiesWithoutUndo();
|
|
}
|
|
|
|
private static void SetupEffects(Transform root, ArcadeBuggyController controller)
|
|
{
|
|
GameObject effectsRoot = GetOrCreateChild(root, "Effects");
|
|
Transform rearLeftWheel = FindDescendant(root, "WheelCollider_RL");
|
|
Transform rearRightWheel = FindDescendant(root, "WheelCollider_RR");
|
|
|
|
ParticleSystem rearLeftSmoke = SetupEffectPrefab<ParticleSystem>(effectsRoot.transform, rearLeftWheel, "LeftTireSmoke_PS", TireSmokePrefabPath, new Vector3(-0.2f, -0.25f, 0f));
|
|
ParticleSystem rearRightSmoke = SetupEffectPrefab<ParticleSystem>(effectsRoot.transform, rearRightWheel, "RightTireSmoke_PS", TireSmokePrefabPath, new Vector3(0.2f, -0.25f, 0f));
|
|
TrailRenderer rearLeftSkid = SetupEffectPrefab<TrailRenderer>(effectsRoot.transform, rearLeftWheel, "LeftTireSkid", TireSkidPrefabPath, Vector3.zero);
|
|
TrailRenderer rearRightSkid = SetupEffectPrefab<TrailRenderer>(effectsRoot.transform, rearRightWheel, "RightTireSkid", TireSkidPrefabPath, Vector3.zero);
|
|
|
|
SerializedObject serializedController = new SerializedObject(controller);
|
|
serializedController.FindProperty("maxSpeedKmh").floatValue = 120f;
|
|
serializedController.FindProperty("maxReverseSpeedKmh").floatValue = 45f;
|
|
serializedController.FindProperty("accelerationMultiplier").floatValue = 6f;
|
|
serializedController.FindProperty("maxSteerAngle").floatValue = 35f;
|
|
serializedController.FindProperty("steeringSpeed").floatValue = 0.5f;
|
|
serializedController.FindProperty("brakeForce").floatValue = 450f;
|
|
serializedController.FindProperty("decelerationMultiplier").floatValue = 2f;
|
|
serializedController.FindProperty("handbrakeDriftMultiplier").floatValue = 5f;
|
|
serializedController.FindProperty("useEffects").boolValue = true;
|
|
serializedController.FindProperty("rearLeftSmoke").objectReferenceValue = rearLeftSmoke;
|
|
serializedController.FindProperty("rearRightSmoke").objectReferenceValue = rearRightSmoke;
|
|
serializedController.FindProperty("rearLeftSkid").objectReferenceValue = rearLeftSkid;
|
|
serializedController.FindProperty("rearRightSkid").objectReferenceValue = rearRightSkid;
|
|
serializedController.ApplyModifiedPropertiesWithoutUndo();
|
|
}
|
|
|
|
private static void SetupSounds(Transform root, ArcadeBuggyController controller)
|
|
{
|
|
GameObject audioRoot = GetOrCreateChild(root, "Audio");
|
|
AudioSource engineSource = SetupAudioSource(audioRoot.transform, "EngineSound", EngineSoundPath, true, 0.65f, 1f, 24f, 90f);
|
|
AudioSource tireScreechSource = SetupAudioSource(audioRoot.transform, "TireScreechSound", TireScreechSoundPath, true, 0.4f, 1f, 12f, 70f);
|
|
|
|
SerializedObject serializedController = new SerializedObject(controller);
|
|
serializedController.FindProperty("useSounds").boolValue = true;
|
|
serializedController.FindProperty("engineSound").objectReferenceValue = engineSource;
|
|
serializedController.FindProperty("tireScreechSound").objectReferenceValue = tireScreechSource;
|
|
serializedController.ApplyModifiedPropertiesWithoutUndo();
|
|
}
|
|
|
|
private static AudioSource SetupAudioSource(Transform parent, string sourceName, string clipPath, bool loop, float volume, float pitch, float minDistance, float maxDistance)
|
|
{
|
|
GameObject sourceObject = GetOrCreateChild(parent, sourceName);
|
|
AudioSource source = sourceObject.GetComponent<AudioSource>();
|
|
if (source == null)
|
|
source = sourceObject.AddComponent<AudioSource>();
|
|
|
|
source.clip = AssetDatabase.LoadAssetAtPath<AudioClip>(clipPath);
|
|
source.playOnAwake = false;
|
|
source.loop = loop;
|
|
source.volume = volume;
|
|
source.pitch = pitch;
|
|
source.spatialBlend = 1f;
|
|
source.rolloffMode = AudioRolloffMode.Linear;
|
|
source.minDistance = minDistance;
|
|
source.maxDistance = maxDistance;
|
|
source.dopplerLevel = 0.3f;
|
|
return source;
|
|
}
|
|
|
|
private static T SetupEffectPrefab<T>(Transform parent, Transform wheel, string effectName, string prefabPath, Vector3 localOffset) where T : Component
|
|
{
|
|
if (wheel == null)
|
|
return null;
|
|
|
|
Transform existing = FindDescendant(parent, effectName);
|
|
GameObject effectObject;
|
|
if (existing != null && existing.GetComponentInChildren<T>(true) != null)
|
|
{
|
|
effectObject = existing.gameObject;
|
|
}
|
|
else
|
|
{
|
|
if (existing != null)
|
|
{
|
|
if (Selection.activeTransform != null && Selection.activeTransform.IsChildOf(existing))
|
|
Selection.activeObject = null;
|
|
|
|
Object.DestroyImmediate(existing.gameObject);
|
|
}
|
|
|
|
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
|
|
effectObject = prefab != null
|
|
? (GameObject)PrefabUtility.InstantiatePrefab(prefab)
|
|
: new GameObject(effectName);
|
|
}
|
|
|
|
effectObject.name = effectName;
|
|
effectObject.transform.SetParent(parent, false);
|
|
effectObject.transform.position = wheel.position + TransformDirection(wheel, localOffset);
|
|
effectObject.transform.rotation = wheel.rotation;
|
|
effectObject.transform.localScale = Vector3.one;
|
|
|
|
T component = effectObject.GetComponentInChildren<T>(true);
|
|
if (component is TrailRenderer trail)
|
|
trail.emitting = false;
|
|
else if (component is ParticleSystem particles)
|
|
particles.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear);
|
|
|
|
return component;
|
|
}
|
|
|
|
private static Vector3 TransformDirection(Transform wheel, Vector3 localOffset)
|
|
{
|
|
return wheel.TransformDirection(localOffset);
|
|
}
|
|
|
|
private static GameObject GetOrCreateChild(Transform root, string childName)
|
|
{
|
|
Transform existing = FindDescendant(root, childName);
|
|
if (existing != null)
|
|
return existing.gameObject;
|
|
|
|
GameObject child = new GameObject(childName);
|
|
child.transform.SetParent(root, false);
|
|
return child;
|
|
}
|
|
|
|
private static float EstimateRadius(Transform visual)
|
|
{
|
|
Renderer renderer = visual.GetComponentInChildren<Renderer>();
|
|
if (renderer == null)
|
|
return 0.34f;
|
|
|
|
Vector3 size = renderer.bounds.size;
|
|
return Mathf.Max(0.12f, Mathf.Max(size.y, size.z) * 0.5f);
|
|
}
|
|
|
|
private static T EnsureComponent<T>(GameObject gameObject, System.Action<T> configure) where T : Component
|
|
{
|
|
T component = gameObject.GetComponent<T>();
|
|
if (component == null)
|
|
component = gameObject.AddComponent<T>();
|
|
|
|
configure?.Invoke(component);
|
|
return component;
|
|
}
|
|
|
|
private static Transform FindDescendant(Transform root, string name)
|
|
{
|
|
Transform[] children = root.GetComponentsInChildren<Transform>(true);
|
|
for (int i = 0; i < children.Length; i++)
|
|
{
|
|
if (children[i].name == name)
|
|
return children[i];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private static WheelCollider FindWheelCollider(Transform root, string name)
|
|
{
|
|
Transform wheel = FindDescendant(root, name);
|
|
return wheel != null ? wheel.GetComponent<WheelCollider>() : null;
|
|
}
|
|
}
|
|
}
|