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(root, ConfigureBody); EnsureComponent(root, collider => ConfigureBodyCollider(root, collider)); ArcadeBuggyController controller = EnsureComponent(root, null); BuggyWheelSetup wheelSetup = EnsureComponent(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.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 { Selection.activeObject = null; Object.DestroyImmediate(cameraObject); } } private static void SelectBuggyPrefabAsset() { Selection.activeObject = AssetDatabase.LoadAssetAtPath(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(); 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(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(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(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 && existing.GetComponentInChildren(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(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; } private static WheelCollider FindWheelCollider(Transform root, string name) { Transform wheel = FindDescendant(root, name); return wheel != null ? wheel.GetComponent() : null; } } }