using System.Collections.Generic; using UnityEngine; using ArcadeVP; public class TerrainDetector : MonoBehaviour { [System.Serializable] public class SurfaceSetting { public string name = "Surface"; [Tooltip("Optional: Assign the Terrain Layer asset directly (from your Terrain).")] public TerrainLayer terrainLayer; [Tooltip("If Terrain Layer asset is not assigned, this index is used (0 = 1st painted texture, 1 = 2nd, etc.).")] public int textureIndex = -1; [Header("Vehicle Multipliers")] [Tooltip("Multiplier applied to vehicle max speed (e.g. 1.0 = asphalt, 0.7 = dirt, 0.45 = mud).")] [Range(0.1f, 2f)] public float speedMultiplier = 1f; [Tooltip("Multiplier applied to vehicle acceleration.")] [Range(0.1f, 2f)] public float accelerationMultiplier = 1f; [Tooltip("Rigidbody linear damping / drag (higher value = more resistance / harder to move).")] public float linearDamping = 0.05f; [Header("Physics Material")] [Tooltip("Optional Physics Material to apply to vehicle friction / tires on this surface.")] public PhysicsMaterial physicsMaterial; [Tooltip("Multiplier applied to the vehicle's dynamic friction.")] [Range(0.1f, 2f)] public float frictionMultiplier = 1f; } [Header("Raycast Settings")] [Tooltip("Offset from the vehicle/player origin to start the downward ray.")] public Vector3 rayOriginOffset = new Vector3(0f, 1f, 0f); [Tooltip("Maximum raycast distance downwards to find the terrain.")] public float rayDistance = 4f; [Tooltip("Layers to check for ground / terrain (include Terrain and Road layers).")] public LayerMask groundLayers = ~0; [Header("Surface Presets")] [Tooltip("Settings for different painted textures on your terrain.")] public List surfaceSettings = new List(); [Tooltip("Default surface setting used when driving on non-terrain surfaces (e.g. road meshes) or when airborne.")] public SurfaceSetting defaultSurface = new SurfaceSetting { name = "Road / Default", textureIndex = -1, speedMultiplier = 1.0f, accelerationMultiplier = 1.0f, linearDamping = 0.05f, frictionMultiplier = 1.0f }; [Header("Transition Smoothing")] [Tooltip("How quickly surface properties blend when switching terrain types.")] public float transitionSpeed = 5f; [Header("Target References (Auto-detected if blank)")] public ArcadeVehicleController vehicleController; public Rigidbody targetRigidbody; [Header("Debug Info (Read Only)")] [SerializeField] private string currentSurfaceName = "None"; [SerializeField] private int currentDominantTextureIndex = -1; [SerializeField] private float currentTextureWeight = 0f; [SerializeField] private float currentSpeedMultiplier = 1f; // Cached base vehicle values private float baseMaxSpeed = 50f; private float baseAcceleration = 10f; private float baseLinearDamping = 0.05f; private float baseDynamicFriction = 1f; // Current blended values private float targetMaxSpeed; private float targetAcceleration; private float targetLinearDamping; private float targetDynamicFriction; private Terrain lastDetectedTerrain; void Start() { // Auto-find vehicle controller if not assigned if (vehicleController == null) { vehicleController = GetComponent(); if (vehicleController == null) { vehicleController = GetComponentInParent(); } } // Auto-find Rigidbody if not assigned if (targetRigidbody == null) { if (vehicleController != null && vehicleController.rb != null) { targetRigidbody = vehicleController.rb; } else { targetRigidbody = GetComponent(); if (targetRigidbody == null) { targetRigidbody = GetComponentInParent(); } } } // Cache base values if (vehicleController != null) { baseMaxSpeed = vehicleController.maxSpeed; baseAcceleration = vehicleController.acceleration; if (vehicleController.frictionMaterial != null) { baseDynamicFriction = vehicleController.frictionMaterial.dynamicFriction; } } if (targetRigidbody != null) { baseLinearDamping = targetRigidbody.linearDamping; } // Initialize targets to default targetMaxSpeed = baseMaxSpeed; targetAcceleration = baseAcceleration; targetLinearDamping = baseLinearDamping; targetDynamicFriction = baseDynamicFriction; } void FixedUpdate() { DetectSurface(); ApplySurfacePhysics(); } private void DetectSurface() { Vector3 rayStart = transform.position + rayOriginOffset; Ray ray = new Ray(rayStart, Vector3.down); if (Physics.Raycast(ray, out RaycastHit hit, rayDistance, groundLayers)) { Terrain terrain = hit.collider.GetComponent(); if (terrain == null) { terrain = hit.collider.GetComponentInParent(); } if (terrain == null) { terrain = Terrain.activeTerrain; } if (terrain != null && terrain.terrainData != null) { lastDetectedTerrain = terrain; int dominantIndex = GetDominantTextureIndex(hit.point, terrain, out float[] weights); if (dominantIndex >= 0) { currentDominantTextureIndex = dominantIndex; currentTextureWeight = (weights != null && dominantIndex < weights.Length) ? weights[dominantIndex] : 1f; // Match surface setting SurfaceSetting matchedSetting = FindMatchingSetting(terrain, dominantIndex); ApplySettingTargets(matchedSetting); return; } } // Hit a ground object that is not a terrain (e.g. road mesh, bridge, ramp) ApplySettingTargets(defaultSurface); } else { // Airborne currentSurfaceName = "Airborne"; ApplySettingTargets(defaultSurface); } } /// /// Samples the Terrain Alphamap (Splatmap) at the given world position. /// Returns the dominant texture index and an array of all texture weights at that coordinate. /// public static int GetDominantTextureIndex(Vector3 worldPos, Terrain terrain, out float[] textureWeights) { textureWeights = null; if (terrain == null || terrain.terrainData == null) return -1; TerrainData terrainData = terrain.terrainData; Vector3 terrainPos = terrain.transform.position; // 1. Convert world position to terrain-relative coordinates float relX = worldPos.x - terrainPos.x; float relZ = worldPos.z - terrainPos.z; // 2. Convert to normalized terrain coordinates (0.0 to 1.0) float normX = relX / terrainData.size.x; float normZ = relZ / terrainData.size.z; // Bounds check if (normX < 0f || normX > 1f || normZ < 0f || normZ > 1f) { return -1; } // 3. Map normalized coordinates to splatmap resolution int mapX = Mathf.Clamp((int)(normX * terrainData.alphamapResolution), 0, terrainData.alphamapResolution - 1); int mapZ = Mathf.Clamp((int)(normZ * terrainData.alphamapResolution), 0, terrainData.alphamapResolution - 1); // 4. Sample TerrainData.GetAlphamaps(x, z, 1, 1) to get 3D float array of texture weights at that single point float[,,] splatmapData = terrainData.GetAlphamaps(mapX, mapZ, 1, 1); int numLayers = splatmapData.GetLength(2); textureWeights = new float[numLayers]; int dominantIndex = 0; float maxWeight = 0f; for (int i = 0; i < numLayers; i++) { float weight = splatmapData[0, 0, i]; textureWeights[i] = weight; if (weight > maxWeight) { maxWeight = weight; dominantIndex = i; } } return dominantIndex; } private SurfaceSetting FindMatchingSetting(Terrain terrain, int textureIndex) { TerrainLayer currentLayer = null; if (terrain.terrainData != null && terrain.terrainData.terrainLayers != null && textureIndex < terrain.terrainData.terrainLayers.Length) { currentLayer = terrain.terrainData.terrainLayers[textureIndex]; } // 1. Check matching by TerrainLayer asset reference if (currentLayer != null) { foreach (var setting in surfaceSettings) { if (setting.terrainLayer != null && setting.terrainLayer == currentLayer) { return setting; } } } // 2. Check matching by texture index foreach (var setting in surfaceSettings) { if (setting.textureIndex == textureIndex) { return setting; } } // 3. Fallback: construct display name and return default surface string fallbackName = currentLayer != null ? currentLayer.name : $"Texture_{textureIndex}"; currentSurfaceName = $"{fallbackName} (Unconfigured)"; return defaultSurface; } private void ApplySettingTargets(SurfaceSetting setting) { currentSurfaceName = setting.name; currentSpeedMultiplier = setting.speedMultiplier; targetMaxSpeed = baseMaxSpeed * setting.speedMultiplier; targetAcceleration = baseAcceleration * setting.accelerationMultiplier; targetLinearDamping = setting.linearDamping; targetDynamicFriction = baseDynamicFriction * setting.frictionMultiplier; // Apply physics material if specified if (setting.physicsMaterial != null && vehicleController != null && vehicleController.frictionMaterial != null) { vehicleController.frictionMaterial = setting.physicsMaterial; } } private void ApplySurfacePhysics() { float lerpFactor = Time.fixedDeltaTime * transitionSpeed; // Smoothly blend vehicle controller properties if (vehicleController != null) { vehicleController.maxSpeed = Mathf.Lerp(vehicleController.maxSpeed, targetMaxSpeed, lerpFactor); vehicleController.acceleration = Mathf.Lerp(vehicleController.acceleration, targetAcceleration, lerpFactor); if (vehicleController.frictionMaterial != null) { vehicleController.frictionMaterial.dynamicFriction = Mathf.Lerp(vehicleController.frictionMaterial.dynamicFriction, targetDynamicFriction, lerpFactor); } } // Smoothly blend Rigidbody linear damping (drag) if (targetRigidbody != null) { targetRigidbody.linearDamping = Mathf.Lerp(targetRigidbody.linearDamping, targetLinearDamping, lerpFactor); } } private void OnDrawGizmosSelected() { Vector3 rayStart = transform.position + rayOriginOffset; Gizmos.color = Color.cyan; Gizmos.DrawLine(rayStart, rayStart + Vector3.down * rayDistance); Gizmos.DrawWireSphere(rayStart + Vector3.down * rayDistance, 0.15f); } }