건설 시스템 기초 생성 및 Kaykit Medival 애셋 추가
This commit is contained in:
76
Assets/Scripts/Building.cs
Normal file
76
Assets/Scripts/Building.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound
|
||||
{
|
||||
public class Building : NetworkBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
public BuildingData buildingData;
|
||||
|
||||
[Header("Runtime Info")]
|
||||
public Vector3Int gridPosition;
|
||||
public int rotation; // 0-3 (0=0°, 1=90°, 2=180°, 3=270°)
|
||||
|
||||
[Header("Debug")]
|
||||
public bool showGridBounds = true;
|
||||
public Color gridBoundsColor = Color.cyan;
|
||||
|
||||
public void Initialize(BuildingData data, Vector3Int gridPos, int rot)
|
||||
{
|
||||
buildingData = data;
|
||||
gridPosition = gridPos;
|
||||
rotation = rot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the grid-based bounds (from BuildingData width/length/height)
|
||||
/// This is used for placement validation, NOT the actual collider bounds
|
||||
/// Bounds are slightly shrunk to allow adjacent buildings to touch
|
||||
/// </summary>
|
||||
public Bounds GetGridBounds()
|
||||
{
|
||||
if (buildingData == null) return new Bounds(transform.position, Vector3.one);
|
||||
|
||||
Vector3 gridSize = buildingData.GetSize(rotation);
|
||||
|
||||
// Shrink slightly to allow buildings to be adjacent without Intersects() returning true
|
||||
Vector3 shrunkSize = gridSize - Vector3.one * 0.01f;
|
||||
return new Bounds(transform.position + Vector3.up * gridSize.y * 0.5f, shrunkSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Legacy method, use GetGridBounds() instead
|
||||
/// </summary>
|
||||
public Bounds GetBounds()
|
||||
{
|
||||
return GetGridBounds();
|
||||
}
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
if (!showGridBounds || buildingData == null) return;
|
||||
|
||||
Bounds bounds = GetGridBounds();
|
||||
Gizmos.color = gridBoundsColor;
|
||||
Gizmos.DrawWireCube(bounds.center, bounds.size);
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (buildingData == null) return;
|
||||
|
||||
Bounds bounds = GetGridBounds();
|
||||
Gizmos.color = Color.yellow;
|
||||
Gizmos.DrawWireCube(bounds.center, bounds.size);
|
||||
|
||||
// Draw grid position
|
||||
if (BuildingManager.Instance != null)
|
||||
{
|
||||
Vector3 worldPos = BuildingManager.Instance.GridToWorld(gridPosition);
|
||||
Gizmos.color = Color.magenta;
|
||||
Gizmos.DrawSphere(worldPos, 0.2f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Building.cs.meta
Normal file
2
Assets/Scripts/Building.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0ceedb9b012d848478813136b65738ae
|
||||
35
Assets/Scripts/BuildingData.cs
Normal file
35
Assets/Scripts/BuildingData.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound
|
||||
{
|
||||
[CreateAssetMenu(fileName = "NewBuilding", menuName = "Northbound/Building Data")]
|
||||
public class BuildingData : ScriptableObject
|
||||
{
|
||||
[Header("Building Info")]
|
||||
public string buildingName;
|
||||
public GameObject prefab;
|
||||
|
||||
[Header("Grid Size")]
|
||||
[Tooltip("Width in grid units")]
|
||||
public int width = 1;
|
||||
[Tooltip("Length in grid units")]
|
||||
public int length = 1;
|
||||
[Tooltip("Height for placement validation")]
|
||||
public float height = 2f;
|
||||
|
||||
[Header("Placement Settings")]
|
||||
[Tooltip("Offset from grid position")]
|
||||
public Vector3 placementOffset = Vector3.zero;
|
||||
[Tooltip("Can rotate this building?")]
|
||||
public bool allowRotation = true;
|
||||
|
||||
public Vector3 GetSize(int rotation)
|
||||
{
|
||||
// Rotation 0,180 = normal, 90,270 = swap width/length
|
||||
bool isRotated = (rotation == 1 || rotation == 3);
|
||||
float w = isRotated ? length : width;
|
||||
float l = isRotated ? width : length;
|
||||
return new Vector3(w, height, l);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/BuildingData.cs.meta
Normal file
2
Assets/Scripts/BuildingData.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 937e64980d44d6b46acb35b8046adf34
|
||||
153
Assets/Scripts/BuildingManager.cs
Normal file
153
Assets/Scripts/BuildingManager.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
using System.Collections.Generic;
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound
|
||||
{
|
||||
public class BuildingManager : NetworkBehaviour
|
||||
{
|
||||
public static BuildingManager Instance { get; private set; }
|
||||
|
||||
[Header("Settings")]
|
||||
public float gridSize = 1f;
|
||||
public LayerMask groundLayer;
|
||||
|
||||
[Header("Building Database")]
|
||||
public List<BuildingData> availableBuildings = new List<BuildingData>();
|
||||
|
||||
private List<Building> placedBuildings = new List<Building>();
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
public Vector3 SnapToGrid(Vector3 worldPosition)
|
||||
{
|
||||
return new Vector3(
|
||||
Mathf.Round(worldPosition.x / gridSize) * gridSize,
|
||||
worldPosition.y,
|
||||
Mathf.Round(worldPosition.z / gridSize) * gridSize
|
||||
);
|
||||
}
|
||||
|
||||
public Vector3Int WorldToGrid(Vector3 worldPosition)
|
||||
{
|
||||
return new Vector3Int(
|
||||
Mathf.RoundToInt(worldPosition.x / gridSize),
|
||||
Mathf.RoundToInt(worldPosition.y / gridSize),
|
||||
Mathf.RoundToInt(worldPosition.z / gridSize)
|
||||
);
|
||||
}
|
||||
|
||||
public Vector3 GridToWorld(Vector3Int gridPosition)
|
||||
{
|
||||
return new Vector3(
|
||||
gridPosition.x * gridSize,
|
||||
gridPosition.y * gridSize,
|
||||
gridPosition.z * gridSize
|
||||
);
|
||||
}
|
||||
|
||||
public bool IsValidPlacement(BuildingData data, Vector3 position, int rotation, out Vector3 groundPosition)
|
||||
{
|
||||
groundPosition = position;
|
||||
|
||||
// Ground check
|
||||
if (!CheckGround(position, out groundPosition))
|
||||
return false;
|
||||
|
||||
// IMPORTANT: Snap to grid BEFORE checking overlap!
|
||||
// Otherwise we check the raw cursor position which might overlap
|
||||
Vector3 snappedPosition = SnapToGrid(groundPosition);
|
||||
groundPosition = snappedPosition; // Update groundPosition to snapped value
|
||||
|
||||
// Overlap check using GRID SIZE from BuildingData (not actual colliders)
|
||||
Vector3 gridSize = data.GetSize(rotation);
|
||||
|
||||
// Shrink bounds slightly to allow buildings to touch without overlapping
|
||||
// This prevents Bounds.Intersects() from returning true for adjacent buildings
|
||||
Vector3 shrunkSize = gridSize - Vector3.one * 0.01f;
|
||||
Bounds checkBounds = new Bounds(snappedPosition + Vector3.up * gridSize.y * 0.5f, shrunkSize);
|
||||
|
||||
foreach (var building in placedBuildings)
|
||||
{
|
||||
if (building == null) continue;
|
||||
|
||||
// Compare grid bounds, not collider bounds
|
||||
Bounds buildingGridBounds = building.GetGridBounds();
|
||||
|
||||
if (checkBounds.Intersects(buildingGridBounds))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the grid bounds for a building at a given position
|
||||
/// Useful for preview visualization
|
||||
/// </summary>
|
||||
public Bounds GetPlacementBounds(BuildingData data, Vector3 position, int rotation)
|
||||
{
|
||||
Vector3 gridSize = data.GetSize(rotation);
|
||||
return new Bounds(position + Vector3.up * gridSize.y * 0.5f, gridSize);
|
||||
}
|
||||
|
||||
private bool CheckGround(Vector3 position, out Vector3 groundPosition)
|
||||
{
|
||||
groundPosition = position;
|
||||
|
||||
// Raycast down to find ground
|
||||
if (Physics.Raycast(position + Vector3.up * 10f, Vector3.down, out RaycastHit hit, 20f, groundLayer))
|
||||
{
|
||||
groundPosition = hit.point;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
|
||||
public void PlaceBuildingServerRpc(int buildingIndex, Vector3 position, int rotation)
|
||||
{
|
||||
if (buildingIndex < 0 || buildingIndex >= availableBuildings.Count)
|
||||
return;
|
||||
|
||||
BuildingData data = availableBuildings[buildingIndex];
|
||||
|
||||
// IsValidPlacement now returns snapped position in groundPosition
|
||||
if (!IsValidPlacement(data, position, rotation, out Vector3 snappedPosition))
|
||||
return;
|
||||
|
||||
Vector3Int gridPosition = WorldToGrid(snappedPosition);
|
||||
|
||||
// Spawn building at snapped position
|
||||
GameObject buildingObj = Instantiate(data.prefab, snappedPosition + data.placementOffset, Quaternion.Euler(0, rotation * 90f, 0));
|
||||
NetworkObject netObj = buildingObj.GetComponent<NetworkObject>();
|
||||
|
||||
if (netObj != null)
|
||||
{
|
||||
netObj.Spawn();
|
||||
|
||||
Building building = buildingObj.GetComponent<Building>();
|
||||
if (building == null)
|
||||
building = buildingObj.AddComponent<Building>();
|
||||
|
||||
building.Initialize(data, gridPosition, rotation);
|
||||
placedBuildings.Add(building);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveBuilding(Building building)
|
||||
{
|
||||
if (placedBuildings.Contains(building))
|
||||
placedBuildings.Remove(building);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/BuildingManager.cs.meta
Normal file
2
Assets/Scripts/BuildingManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 27ee775fc5a0dc7498f3049e65e32513
|
||||
452
Assets/Scripts/BuildingPlacement.cs
Normal file
452
Assets/Scripts/BuildingPlacement.cs
Normal file
@@ -0,0 +1,452 @@
|
||||
using Unity.Netcode;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
namespace Northbound
|
||||
{
|
||||
public class BuildingPlacement : NetworkBehaviour
|
||||
{
|
||||
[Header("Settings")]
|
||||
public LayerMask groundLayer;
|
||||
public float maxPlacementDistance = 100f;
|
||||
|
||||
[Header("Preview Materials")]
|
||||
public Material validMaterial;
|
||||
public Material invalidMaterial;
|
||||
|
||||
[Header("Current Selection")]
|
||||
public int selectedBuildingIndex = 0;
|
||||
|
||||
[Header("Debug Visualization")]
|
||||
public bool showGridBounds = true;
|
||||
|
||||
private bool isBuildModeActive = false;
|
||||
private GameObject previewObject;
|
||||
private int currentRotation = 0; // 0-3
|
||||
private Material[] originalMaterials;
|
||||
private Renderer[] previewRenderers;
|
||||
private PlayerInputActions _inputActions;
|
||||
|
||||
public override void OnNetworkSpawn()
|
||||
{
|
||||
if (!IsOwner) return;
|
||||
|
||||
_inputActions = new PlayerInputActions();
|
||||
_inputActions.Player.ToggleBuildMode.performed += OnToggleBuildMode;
|
||||
_inputActions.Player.Rotate.performed += OnRotate;
|
||||
_inputActions.Player.Build.performed += OnBuild;
|
||||
_inputActions.Enable();
|
||||
|
||||
// Create default materials if not assigned
|
||||
if (validMaterial == null)
|
||||
{
|
||||
validMaterial = CreateGhostMaterial(new Color(0, 1, 0, 0.5f));
|
||||
}
|
||||
|
||||
if (invalidMaterial == null)
|
||||
{
|
||||
invalidMaterial = CreateGhostMaterial(new Color(1, 0, 0, 0.5f));
|
||||
}
|
||||
}
|
||||
|
||||
private Material CreateGhostMaterial(Color color)
|
||||
{
|
||||
// Try to find appropriate shader for current render pipeline
|
||||
Shader shader = Shader.Find("Universal Render Pipeline/Lit");
|
||||
if (shader == null)
|
||||
shader = Shader.Find("Standard");
|
||||
if (shader == null)
|
||||
shader = Shader.Find("Diffuse");
|
||||
|
||||
Material mat = new Material(shader);
|
||||
|
||||
// Set base color
|
||||
if (mat.HasProperty("_BaseColor"))
|
||||
mat.SetColor("_BaseColor", color); // URP
|
||||
else if (mat.HasProperty("_Color"))
|
||||
mat.SetColor("_Color", color); // Standard
|
||||
|
||||
// Enable transparency
|
||||
if (mat.HasProperty("_Surface"))
|
||||
{
|
||||
// URP Transparency
|
||||
mat.SetFloat("_Surface", 1); // Transparent
|
||||
mat.SetFloat("_Blend", 0); // Alpha
|
||||
mat.SetFloat("_AlphaClip", 0);
|
||||
mat.SetFloat("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
|
||||
mat.SetFloat("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
|
||||
mat.SetFloat("_ZWrite", 0);
|
||||
mat.renderQueue = 3000;
|
||||
mat.EnableKeyword("_SURFACE_TYPE_TRANSPARENT");
|
||||
mat.EnableKeyword("_ALPHAPREMULTIPLY_ON");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Standard RP Transparency
|
||||
mat.SetFloat("_Mode", 3); // Transparent mode
|
||||
mat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
|
||||
mat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
|
||||
mat.SetInt("_ZWrite", 0);
|
||||
mat.DisableKeyword("_ALPHATEST_ON");
|
||||
mat.EnableKeyword("_ALPHABLEND_ON");
|
||||
mat.DisableKeyword("_ALPHAPREMULTIPLY_ON");
|
||||
mat.renderQueue = 3000;
|
||||
}
|
||||
|
||||
return mat;
|
||||
}
|
||||
|
||||
public override void OnNetworkDespawn()
|
||||
{
|
||||
if (IsOwner && _inputActions != null)
|
||||
{
|
||||
_inputActions.Player.ToggleBuildMode.performed -= OnToggleBuildMode;
|
||||
_inputActions.Player.Rotate.performed -= OnRotate;
|
||||
_inputActions.Player.Build.performed -= OnBuild;
|
||||
_inputActions.Disable();
|
||||
_inputActions.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (!IsOwner) return;
|
||||
|
||||
if (isBuildModeActive)
|
||||
{
|
||||
UpdatePreviewPosition();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDrawGizmos()
|
||||
{
|
||||
if (!IsOwner || !isBuildModeActive || !showGridBounds) return;
|
||||
if (BuildingManager.Instance == null || previewObject == null || !previewObject.activeSelf) return;
|
||||
|
||||
BuildingData data = BuildingManager.Instance.availableBuildings[selectedBuildingIndex];
|
||||
if (data == null) return;
|
||||
|
||||
// Draw grid bounds being used for collision detection
|
||||
Bounds bounds = BuildingManager.Instance.GetPlacementBounds(data, previewObject.transform.position, currentRotation);
|
||||
|
||||
// Check if valid
|
||||
bool isValid = BuildingManager.Instance.IsValidPlacement(data, previewObject.transform.position, currentRotation, out _);
|
||||
|
||||
Gizmos.color = isValid ? new Color(0, 1, 0, 0.5f) : new Color(1, 0, 0, 0.5f);
|
||||
Gizmos.DrawWireCube(bounds.center, bounds.size);
|
||||
|
||||
// Draw grid cells
|
||||
Gizmos.color = Color.yellow;
|
||||
float gridSize = BuildingManager.Instance.gridSize;
|
||||
Vector3 snappedPos = BuildingManager.Instance.SnapToGrid(previewObject.transform.position);
|
||||
|
||||
// Draw grid origin point
|
||||
Gizmos.DrawSphere(snappedPos, 0.1f);
|
||||
|
||||
// Draw grid outline for each cell
|
||||
int width = Mathf.RoundToInt(bounds.size.x / gridSize);
|
||||
int length = Mathf.RoundToInt(bounds.size.z / gridSize);
|
||||
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
for (int z = 0; z < length; z++)
|
||||
{
|
||||
Vector3 cellPos = snappedPos + new Vector3(
|
||||
(x - width / 2f + 0.5f) * gridSize,
|
||||
0.01f,
|
||||
(z - length / 2f + 0.5f) * gridSize
|
||||
);
|
||||
Gizmos.DrawWireCube(cellPos, new Vector3(gridSize, 0.01f, gridSize));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnToggleBuildMode(InputAction.CallbackContext context)
|
||||
{
|
||||
ToggleBuildMode();
|
||||
}
|
||||
|
||||
private void OnRotate(InputAction.CallbackContext context)
|
||||
{
|
||||
if (!isBuildModeActive) return;
|
||||
|
||||
float rotateValue = context.ReadValue<float>();
|
||||
if (rotateValue > 0)
|
||||
{
|
||||
currentRotation = (currentRotation + 1) % 4;
|
||||
}
|
||||
else if (rotateValue < 0)
|
||||
{
|
||||
currentRotation = (currentRotation - 1 + 4) % 4;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBuild(InputAction.CallbackContext context)
|
||||
{
|
||||
if (!isBuildModeActive) return;
|
||||
TryPlaceBuilding();
|
||||
}
|
||||
|
||||
private void ToggleBuildMode()
|
||||
{
|
||||
isBuildModeActive = !isBuildModeActive;
|
||||
|
||||
if (isBuildModeActive)
|
||||
{
|
||||
EnterBuildMode();
|
||||
}
|
||||
else
|
||||
{
|
||||
ExitBuildMode();
|
||||
}
|
||||
}
|
||||
|
||||
private void EnterBuildMode()
|
||||
{
|
||||
currentRotation = 0;
|
||||
CreatePreview();
|
||||
}
|
||||
|
||||
private void ExitBuildMode()
|
||||
{
|
||||
DestroyPreview();
|
||||
}
|
||||
|
||||
private void CreatePreview()
|
||||
{
|
||||
if (BuildingManager.Instance == null)
|
||||
return;
|
||||
|
||||
if (BuildingManager.Instance.availableBuildings.Count == 0)
|
||||
return;
|
||||
|
||||
selectedBuildingIndex = Mathf.Clamp(selectedBuildingIndex, 0,
|
||||
BuildingManager.Instance.availableBuildings.Count - 1);
|
||||
|
||||
BuildingData data = BuildingManager.Instance.availableBuildings[selectedBuildingIndex];
|
||||
if (data == null || data.prefab == null)
|
||||
return;
|
||||
|
||||
previewObject = Instantiate(data.prefab);
|
||||
previewObject.name = "BuildingPreview";
|
||||
|
||||
// Disable colliders on preview
|
||||
foreach (var collider in previewObject.GetComponentsInChildren<Collider>())
|
||||
{
|
||||
collider.enabled = false;
|
||||
}
|
||||
|
||||
// Remove NetworkObject from preview if exists
|
||||
NetworkObject netObj = previewObject.GetComponent<NetworkObject>();
|
||||
if (netObj != null)
|
||||
{
|
||||
Destroy(netObj);
|
||||
}
|
||||
|
||||
// Store original materials and setup preview renderers
|
||||
previewRenderers = previewObject.GetComponentsInChildren<Renderer>();
|
||||
|
||||
if (previewRenderers.Length == 0)
|
||||
{
|
||||
// Add a debug cube so you can at least see something
|
||||
GameObject debugCube = GameObject.CreatePrimitive(PrimitiveType.Cube);
|
||||
debugCube.transform.SetParent(previewObject.transform);
|
||||
debugCube.transform.localPosition = Vector3.zero;
|
||||
Destroy(debugCube.GetComponent<Collider>());
|
||||
previewRenderers = previewObject.GetComponentsInChildren<Renderer>();
|
||||
}
|
||||
|
||||
originalMaterials = new Material[previewRenderers.Length];
|
||||
|
||||
for (int i = 0; i < previewRenderers.Length; i++)
|
||||
{
|
||||
originalMaterials[i] = previewRenderers[i].material;
|
||||
// Replace all materials with ghost material
|
||||
Material[] mats = new Material[previewRenderers[i].materials.Length];
|
||||
for (int j = 0; j < mats.Length; j++)
|
||||
{
|
||||
mats[j] = validMaterial;
|
||||
}
|
||||
previewRenderers[i].materials = mats;
|
||||
|
||||
// Disable shadow casting
|
||||
previewRenderers[i].shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||
previewRenderers[i].receiveShadows = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void DestroyPreview()
|
||||
{
|
||||
if (previewObject != null)
|
||||
{
|
||||
Destroy(previewObject);
|
||||
previewObject = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePreviewPosition()
|
||||
{
|
||||
if (BuildingManager.Instance == null) return;
|
||||
|
||||
if (previewObject == null)
|
||||
{
|
||||
CreatePreview();
|
||||
if (previewObject == null) return;
|
||||
}
|
||||
|
||||
if (Mouse.current == null)
|
||||
return;
|
||||
|
||||
Vector2 mousePos = Mouse.current.position.ReadValue();
|
||||
Ray ray = Camera.main.ScreenPointToRay(mousePos);
|
||||
|
||||
Vector3 targetPosition = Vector3.zero;
|
||||
bool foundPosition = false;
|
||||
|
||||
// Robust approach: Find where ray intersects ground plane, then raycast down from there
|
||||
// This works even when pointing at empty space between buildings
|
||||
|
||||
// Step 1: Calculate where ray intersects a horizontal plane at Y=0
|
||||
// Using plane intersection math
|
||||
Plane groundPlane = new Plane(Vector3.up, Vector3.zero);
|
||||
float enter;
|
||||
|
||||
if (groundPlane.Raycast(ray, out enter))
|
||||
{
|
||||
// Get the XZ position where ray hits the ground plane
|
||||
Vector3 planeHitPoint = ray.GetPoint(enter);
|
||||
|
||||
// Step 2: Cast down from above this XZ position to find actual ground
|
||||
Vector3 highPoint = new Vector3(planeHitPoint.x, 100f, planeHitPoint.z);
|
||||
|
||||
if (Physics.Raycast(highPoint, Vector3.down, out RaycastHit downHit, 200f, groundLayer))
|
||||
{
|
||||
// Always use the hit point
|
||||
targetPosition = downHit.point;
|
||||
foundPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// No ground found via raycast, use the plane intersection point directly
|
||||
targetPosition = planeHitPoint;
|
||||
foundPosition = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: Try direct raycast to anything
|
||||
if (Physics.Raycast(ray, out RaycastHit anyHit, maxPlacementDistance))
|
||||
{
|
||||
targetPosition = anyHit.point;
|
||||
foundPosition = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundPosition)
|
||||
{
|
||||
BuildingData data = BuildingManager.Instance.availableBuildings[selectedBuildingIndex];
|
||||
|
||||
// IsValidPlacement now returns the snapped position in groundPosition
|
||||
bool isValid = BuildingManager.Instance.IsValidPlacement(data, targetPosition, currentRotation, out Vector3 snappedPosition);
|
||||
|
||||
previewObject.transform.position = snappedPosition + data.placementOffset;
|
||||
previewObject.transform.rotation = Quaternion.Euler(0, currentRotation * 90f, 0);
|
||||
|
||||
if (!previewObject.activeSelf)
|
||||
{
|
||||
previewObject.SetActive(true);
|
||||
}
|
||||
|
||||
// Update material
|
||||
Material previewMat = isValid ? validMaterial : invalidMaterial;
|
||||
foreach (var renderer in previewRenderers)
|
||||
{
|
||||
Material[] mats = new Material[renderer.materials.Length];
|
||||
for (int i = 0; i < mats.Length; i++)
|
||||
{
|
||||
mats[i] = previewMat;
|
||||
}
|
||||
renderer.materials = mats;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (previewObject.activeSelf)
|
||||
{
|
||||
previewObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void TryPlaceBuilding()
|
||||
{
|
||||
if (previewObject == null || !previewObject.activeSelf) return;
|
||||
|
||||
Vector2 mousePos = Mouse.current.position.ReadValue();
|
||||
Ray ray = Camera.main.ScreenPointToRay(mousePos);
|
||||
|
||||
Vector3 targetPosition = Vector3.zero;
|
||||
bool foundPosition = false;
|
||||
|
||||
// Use plane intersection to get XZ position, then raycast down to find ground
|
||||
Plane groundPlane = new Plane(Vector3.up, Vector3.zero);
|
||||
float enter;
|
||||
|
||||
if (groundPlane.Raycast(ray, out enter))
|
||||
{
|
||||
Vector3 planeHitPoint = ray.GetPoint(enter);
|
||||
Vector3 highPoint = new Vector3(planeHitPoint.x, 100f, planeHitPoint.z);
|
||||
|
||||
if (Physics.Raycast(highPoint, Vector3.down, out RaycastHit downHit, 200f, groundLayer))
|
||||
{
|
||||
if (downHit.collider.GetComponentInParent<Building>() == null)
|
||||
{
|
||||
targetPosition = downHit.point;
|
||||
foundPosition = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetPosition = downHit.point;
|
||||
foundPosition = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
targetPosition = planeHitPoint;
|
||||
foundPosition = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: direct raycast
|
||||
if (Physics.Raycast(ray, out RaycastHit anyHit, maxPlacementDistance))
|
||||
{
|
||||
targetPosition = anyHit.point;
|
||||
foundPosition = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundPosition)
|
||||
{
|
||||
BuildingData data = BuildingManager.Instance.availableBuildings[selectedBuildingIndex];
|
||||
|
||||
// IsValidPlacement now returns the snapped position
|
||||
if (BuildingManager.Instance.IsValidPlacement(data, targetPosition, currentRotation, out Vector3 snappedPosition))
|
||||
{
|
||||
BuildingManager.Instance.PlaceBuildingServerRpc(selectedBuildingIndex, snappedPosition, currentRotation);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
DestroyPreview();
|
||||
|
||||
if (_inputActions != null)
|
||||
{
|
||||
_inputActions.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/BuildingPlacement.cs.meta
Normal file
2
Assets/Scripts/BuildingPlacement.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2e305ee9051761b4e8d11fd29fe7b1a1
|
||||
92
Assets/Scripts/GhostMaterialTest.cs
Normal file
92
Assets/Scripts/GhostMaterialTest.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Northbound
|
||||
{
|
||||
/// <summary>
|
||||
/// Simple test script to verify ghost materials work.
|
||||
/// Attach to any GameObject with a Renderer to see the ghost effect.
|
||||
/// </summary>
|
||||
public class GhostMaterialTest : MonoBehaviour
|
||||
{
|
||||
[Header("Test Settings")]
|
||||
public Color ghostColor = new Color(0, 1, 0, 0.5f);
|
||||
public bool applyOnStart = true;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (applyOnStart)
|
||||
{
|
||||
ApplyGhostMaterial();
|
||||
}
|
||||
}
|
||||
|
||||
[ContextMenu("Apply Ghost Material")]
|
||||
public void ApplyGhostMaterial()
|
||||
{
|
||||
Material ghostMat = CreateGhostMaterial(ghostColor);
|
||||
|
||||
Renderer[] renderers = GetComponentsInChildren<Renderer>();
|
||||
foreach (var renderer in renderers)
|
||||
{
|
||||
Material[] mats = new Material[renderer.materials.Length];
|
||||
for (int i = 0; i < mats.Length; i++)
|
||||
{
|
||||
mats[i] = ghostMat;
|
||||
}
|
||||
renderer.materials = mats;
|
||||
renderer.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||
renderer.receiveShadows = false;
|
||||
}
|
||||
|
||||
Debug.Log($"Applied ghost material to {renderers.Length} renderers");
|
||||
}
|
||||
|
||||
private Material CreateGhostMaterial(Color color)
|
||||
{
|
||||
// Try to find appropriate shader for current render pipeline
|
||||
Shader shader = Shader.Find("Universal Render Pipeline/Lit");
|
||||
if (shader == null)
|
||||
shader = Shader.Find("Standard");
|
||||
if (shader == null)
|
||||
shader = Shader.Find("Diffuse");
|
||||
|
||||
Material mat = new Material(shader);
|
||||
|
||||
// Set base color
|
||||
if (mat.HasProperty("_BaseColor"))
|
||||
mat.SetColor("_BaseColor", color); // URP
|
||||
else if (mat.HasProperty("_Color"))
|
||||
mat.SetColor("_Color", color); // Standard
|
||||
|
||||
// Enable transparency
|
||||
if (mat.HasProperty("_Surface"))
|
||||
{
|
||||
// URP Transparency
|
||||
mat.SetFloat("_Surface", 1); // Transparent
|
||||
mat.SetFloat("_Blend", 0); // Alpha
|
||||
mat.SetFloat("_AlphaClip", 0);
|
||||
mat.SetFloat("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
|
||||
mat.SetFloat("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
|
||||
mat.SetFloat("_ZWrite", 0);
|
||||
mat.renderQueue = 3000;
|
||||
mat.EnableKeyword("_SURFACE_TYPE_TRANSPARENT");
|
||||
mat.EnableKeyword("_ALPHAPREMULTIPLY_ON");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Standard RP Transparency
|
||||
mat.SetFloat("_Mode", 3); // Transparent mode
|
||||
mat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
|
||||
mat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
|
||||
mat.SetInt("_ZWrite", 0);
|
||||
mat.DisableKeyword("_ALPHATEST_ON");
|
||||
mat.EnableKeyword("_ALPHABLEND_ON");
|
||||
mat.DisableKeyword("_ALPHAPREMULTIPLY_ON");
|
||||
mat.renderQueue = 3000;
|
||||
}
|
||||
|
||||
Debug.Log($"Created ghost material with shader: {shader.name}");
|
||||
return mat;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/GhostMaterialTest.cs.meta
Normal file
2
Assets/Scripts/GhostMaterialTest.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 99bb7a3f13f744642aca504e3c30a571
|
||||
Reference in New Issue
Block a user