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_Player.prefab"; private const string CameraPrefabPath = "Assets/_Project/Prefabs/Cameras/Buggy_ThirdPersonCamera.prefab"; private const string TireSmokePrefabPath = "Assets/PROMETEO - Car Controller/Effects/Particle Systems/Prefabs/TireSmoke_PS.prefab"; private const string TireSkidPrefabPath = "Assets/PROMETEO - Car Controller/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() { GameObject root = PrefabUtility.LoadPrefabContents(BuggyPrefabPath); try { EnsureComponent(root, ConfigureBody); EnsureComponent(root, ConfigureBodyCollider); ArcadeBuggyController controller = EnsureComponent(root, null); EnsureComponent(root, setup => setup.AlignAllWheels()); SetupWheel(root.transform, "F_L", "WheelCollider_FL"); SetupWheel(root.transform, "F_R", "WheelCollider_FR"); SetupWheel(root.transform, "B_L", "WheelCollider_RL"); SetupWheel(root.transform, "B_R", "WheelCollider_RR"); SetupEffects(root.transform, controller); SetupSounds(root.transform, controller); BuggyWheelSetup wheelSetup = root.GetComponent(); wheelSetup.AlignAllWheels(); PrefabUtility.SaveAsPrefabAsset(root, BuggyPrefabPath); } finally { PrefabUtility.UnloadPrefabContents(root); } RebuildCameraPrefab(); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } private static void RebuildCameraPrefab() { GameObject cameraObject = new GameObject("Buggy_ThirdPersonCamera"); try { Camera camera = cameraObject.AddComponent(); camera.fieldOfView = 62f; camera.nearClipPlane = 0.05f; camera.farClipPlane = 600f; cameraObject.AddComponent(); cameraObject.AddComponent().SnapToTarget(); cameraObject.transform.position = new Vector3(0f, 4.2f, -7.5f); cameraObject.transform.rotation = Quaternion.Euler(18f, 0f, 0f); PrefabUtility.SaveAsPrefabAsset(cameraObject, CameraPrefabPath); } finally { Object.DestroyImmediate(cameraObject); } } 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(); if (wheelCollider == null) wheelCollider = colliderObject.AddComponent(); 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(BoxCollider collider) { collider.center = new Vector3(0f, 0.55f, 0f); collider.size = 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 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(effectsRoot.transform, rearLeftWheel, "LeftTireSmoke_PS", TireSmokePrefabPath, new Vector3(-0.2f, -0.25f, 0f)); ParticleSystem rearRightSmoke = SetupEffectPrefab(effectsRoot.transform, rearRightWheel, "RightTireSmoke_PS", TireSmokePrefabPath, new Vector3(0.2f, -0.25f, 0f)); TrailRenderer rearLeftSkid = SetupEffectPrefab(effectsRoot.transform, rearLeftWheel, "LeftTireSkid", TireSkidPrefabPath, Vector3.zero); TrailRenderer rearRightSkid = SetupEffectPrefab(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(); if (source == null) source = sourceObject.AddComponent(); source.clip = AssetDatabase.LoadAssetAtPath(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(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) { effectObject = existing.gameObject; } else { GameObject prefab = AssetDatabase.LoadAssetAtPath(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(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(); 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(GameObject gameObject, System.Action configure) where T : Component { T component = gameObject.GetComponent(); if (component == null) component = gameObject.AddComponent(); configure?.Invoke(component); return component; } private static Transform FindDescendant(Transform root, string name) { Transform[] children = root.GetComponentsInChildren(true); for (int i = 0; i < children.Length; i++) { if (children[i].name == name) return children[i]; } return null; } } }