330 lines
12 KiB
C#
330 lines
12 KiB
C#
|
|
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<SurfaceSetting> surfaceSettings = new List<SurfaceSetting>();
|
||
|
|
|
||
|
|
[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<ArcadeVehicleController>();
|
||
|
|
if (vehicleController == null)
|
||
|
|
{
|
||
|
|
vehicleController = GetComponentInParent<ArcadeVehicleController>();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Auto-find Rigidbody if not assigned
|
||
|
|
if (targetRigidbody == null)
|
||
|
|
{
|
||
|
|
if (vehicleController != null && vehicleController.rb != null)
|
||
|
|
{
|
||
|
|
targetRigidbody = vehicleController.rb;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
targetRigidbody = GetComponent<Rigidbody>();
|
||
|
|
if (targetRigidbody == null)
|
||
|
|
{
|
||
|
|
targetRigidbody = GetComponentInParent<Rigidbody>();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 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<Terrain>();
|
||
|
|
if (terrain == null)
|
||
|
|
{
|
||
|
|
terrain = hit.collider.GetComponentInParent<Terrain>();
|
||
|
|
}
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// <summary>
|
||
|
|
/// 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.
|
||
|
|
/// </summary>
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
}
|