Race_fields/Assets/_Project/Scripts/Editor/BuggyPrefabBuilder.cs
2026-08-12 12:11:13 +03:00

346 lines
16 KiB
C#

using RaceFields.CameraSystem;
using RaceFields.Damage.VehicleDamage;
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);
EnsureComponent<VehicleDamageController>(root, null);
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);
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 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;
}
}
}