RoadParkingLotComponent/RoadParkingLotComponent.cs
using System.Linq;
using Sandbox;
namespace RedSnail.RoadTool;
/// <summary>
/// Generates parking lot lines for parking spaces
/// </summary>
[Icon("local_parking")]
public partial class RoadParkingLotComponent : Component, Component.ExecuteInEditor
{
private bool m_IsDirty;
private const string LinesTag = "parking_lines";
private const string CurbsTag = "parking_curbs";
/// <summary>
/// An optional prefab, if non-empty the parking lot will generate a bunch of child gameobjects positioned at each parking spots center.
/// (e.g. this allows you to use a gameobject prefab with a car spawner system component attached to it)
/// </summary>
[Property, Feature("General", Icon = "public", Tint = EditorTint.White)] private GameObject SpotPrefab { get; set; }
/// <summary>
/// The amount of parking spots you want to generate.
/// </summary>
[Property, Feature("General"), Range(1, 50)] private int SpotCount { get; set { field = value; m_IsDirty = true; } } = 10;
/// <summary>
/// Well that's the parking spot length
/// </summary>
[Property, Feature("General"), Range(10.0f, 1000.0f)] private float SpotLength { get; set { field = value; m_IsDirty = true; } } = 250.0f;
/// <summary>
/// and width...
/// </summary>
[Property, Feature("General"), Range(10.0f, 1000.0f)] private float SpotWidth { get; set { field = value; m_IsDirty = true; } } = 150.0f;
/// <summary>
/// The angle of the parking spots in degrees (0 = perpendicular, 45 = angled, 90 = parallel)
/// </summary>
[Property, Feature("General"), Range(-90.0f, 90.0f), Step(1.0f)] private float SpotAngle { get; set { field = value; m_IsDirty = true; } } = 0.0f;
[Property, Feature("General"), Range(0.5f, 1.0f)] private float SpotAngleThreshold { get; set { field = value; m_IsDirty = true; } } = 0.5f;
protected override void OnEnabled()
{
BuildAllMeshes();
}
protected override void OnDisabled()
{
DestroyMeshChildren();
RemoveParkingSpots();
}
protected override void OnUpdate()
{
if (m_IsDirty)
{
if (!SandboxUtility.IsInPlayMode)
{
DestroyMeshChildren();
BuildAllMeshes();
}
m_IsDirty = false;
}
}
private void DestroyMeshChildren()
{
var toRemove = GameObject.Children
.Where(c => c.Tags.Has(LinesTag) || c.Tags.Has(CurbsTag))
.ToList();
foreach (var child in toRemove)
child.Destroy();
}
private void BuildAllMeshes()
{
if (SandboxUtility.IsInPlayMode)
return;
BuildParkingLines();
BuildCurbs();
RemoveParkingSpots();
CreateParkingSpots();
}
protected override void DrawGizmos()
{
if (!Gizmo.IsSelected)
return;
Gizmo.Draw.LineThickness = 2.0f;
Gizmo.Draw.Color = Color.Green.WithAlpha(0.5f);
float angleRad = SpotAngle.DegreeToRadian();
float sinAngle = float.Sin(angleRad);
float cosAngle = float.Cos(angleRad);
float spacing = CalculateSpacing();
// Draw parking spot outlines
for (int i = 0; i < SpotCount; i++)
{
float xPos = i * spacing;
Vector3 frontLeft = new Vector3(xPos, 0, LinesOffset);
Vector3 frontRight = new Vector3(xPos + SpotWidth * cosAngle, SpotWidth * sinAngle, LinesOffset);
Vector3 backLeft = new Vector3(xPos - SpotLength * sinAngle, SpotLength * cosAngle, LinesOffset);
Vector3 backRight = new Vector3(xPos + SpotWidth * cosAngle - SpotLength * sinAngle, SpotWidth * sinAngle + SpotLength * cosAngle, LinesOffset);
Gizmo.Draw.Line(frontLeft, frontRight);
Gizmo.Draw.Line(frontRight, backRight);
Gizmo.Draw.Line(backRight, backLeft);
Gizmo.Draw.Line(backLeft, frontLeft);
}
}
private void CreateParkingSpots()
{
// If we're in play mode, do not build (Since they're already saved in the scene file)
if (LoadingScreen.IsVisible || Game.IsPlaying)
return;
if (!SpotPrefab.IsValid())
return;
GameObject containerObject = GameObject.Children.FirstOrDefault(x => x.Name == "ParkingSpots");
if (!containerObject.IsValid())
containerObject = new GameObject(GameObject, true, "ParkingSpots");
float angleRad = SpotAngle.DegreeToRadian();
float sinAngle = float.Sin(angleRad);
float cosAngle = float.Cos(angleRad);
float spacing = CalculateSpacing();
for (int i = 0; i < SpotCount; i++)
{
float xPos = i * spacing;
float centerX = xPos + (SpotWidth * 0.5f * cosAngle) - (SpotLength * 0.5f * sinAngle);
float centerY = (SpotWidth * 0.5f * sinAngle) + (SpotLength * 0.5f * cosAngle);
Vector3 position = new Vector3(centerX, centerY, 0);
GameObject gameObject = SpotPrefab.Clone(new Transform(), containerObject);
gameObject.LocalPosition = position;
gameObject.LocalRotation = Rotation.FromYaw(SpotAngle);
}
}
private void RemoveParkingSpots()
{
// If we're in play mode, do not remove (Since they're already saved in the scene file)
if (LoadingScreen.IsVisible || Game.IsPlaying)
return;
GameObject containerObject = GameObject.Children.FirstOrDefault(x => x.Name == "ParkingSpots");
if (!containerObject.IsValid())
return;
foreach (var gameObject in containerObject.Children.Where(x => x.IsValid()))
{
gameObject.Destroy();
}
}
/// <summary>
/// Utility button to directly snap the parking lot to the nearest solid ground
/// </summary>
[Button("Snap to Ground"), Feature("General"), Order(100)]
public void SnapToGround()
{
SceneTraceResult trace = Scene.Trace.Ray(WorldPosition, WorldPosition + Vector3.Down * 10000.0f).Run();
if (trace.Distance < 0.1f) // Ignore really close hits, bcs that mean the parking lot is already properly grounded
return;
if (!trace.Hit)
return;
WorldPosition = trace.HitPosition;
WorldRotation = Rotation.LookAt(trace.Normal, Vector3.Up) * Rotation.FromPitch(90.0f);
}
private float CalculateSpacing()
{
float angleRad = SpotAngle.DegreeToRadian();
float cosAngle = float.Cos(angleRad);
return SpotWidth / float.Max(cosAngle, SpotAngleThreshold);
}
}