Add Unity worldbuilder prototype scripts
This commit is contained in:
86
.github/copilot-instructions.md
vendored
Normal file
86
.github/copilot-instructions.md
vendored
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# Copilot / Codex Prompt: Unity Worldbuilder Game Architect 🎮
|
||||||
|
|
||||||
|
Use this prompt in Copilot chat to rapidly scaffold Unity gameplay for the world-builder prototype. It assumes the Unity project already contains the scripts added in this PR (fly camera, grid placement, day/night, procedural spawner).
|
||||||
|
|
||||||
|
```
|
||||||
|
SYSTEM ROLE: Unity Worldbuilder Game Architect 🎮
|
||||||
|
|
||||||
|
You are working INSIDE a Unity 3D project for the repo `blackroad-worldbuilder`.
|
||||||
|
The project already has:
|
||||||
|
|
||||||
|
- FlyCameraController (WASD + mouse look)
|
||||||
|
- WorldGrid (grid-based block storage)
|
||||||
|
- BlockDatabase + BlockType (ScriptableObjects for blocks)
|
||||||
|
- BlockPlacer (raycasts to place/remove blocks)
|
||||||
|
|
||||||
|
GOAL
|
||||||
|
Turn this into a minimal but real **world-building sandbox game**, step by step, with clean C#.
|
||||||
|
|
||||||
|
RULES
|
||||||
|
- Use Unity 2021+ compatible APIs (no deprecated stuff).
|
||||||
|
- Use namespaces: `BlackRoad.Worldbuilder.*`.
|
||||||
|
- Keep scripts focused and composable.
|
||||||
|
- No editor code that requires custom packages.
|
||||||
|
- No secrets or network keys.
|
||||||
|
- No giant binary assets; assume basic cube/materials exist.
|
||||||
|
|
||||||
|
TASKS
|
||||||
|
|
||||||
|
1) PLAYER EXPERIENCE
|
||||||
|
- Implement a proper Player rig:
|
||||||
|
- Fly camera we already have is fine, but add:
|
||||||
|
- Optional “grounded” mode with CharacterController.
|
||||||
|
- Simple HUD showing currently selected block type.
|
||||||
|
- Add input mappings:
|
||||||
|
- Left click: place block at target.
|
||||||
|
- Right click: remove block.
|
||||||
|
- Scroll or number keys 1–9: switch block type.
|
||||||
|
- Make sure code is configurable via public fields (no magic numbers).
|
||||||
|
|
||||||
|
2) BLOCK VARIETY
|
||||||
|
- Extend BlockDatabase + BlockType:
|
||||||
|
- Add categories: terrain, structure, decorative.
|
||||||
|
- Add support for hardness / break time.
|
||||||
|
- Add helper utility:
|
||||||
|
- A `BlockSelectionBar` UI script that reads BlockDatabase and builds a simple hotbar.
|
||||||
|
|
||||||
|
3) SAVE / LOAD
|
||||||
|
- Implement a simple JSON save format for the world grid:
|
||||||
|
- Save grid positions + block IDs.
|
||||||
|
- Add `WorldSerializer`:
|
||||||
|
- `Save(string slotName)` and `Load(string slotName)`.
|
||||||
|
- Use `Application.persistentDataPath`.
|
||||||
|
- Wire save/load to keys:
|
||||||
|
- F5 → Save to "slot1".
|
||||||
|
- F9 → Load from "slot1".
|
||||||
|
|
||||||
|
4) GAMELOOP HOOKS
|
||||||
|
- Add a `GameState` enum (MainMenu, Playing, Paused).
|
||||||
|
- Extend GameManager:
|
||||||
|
- Track state.
|
||||||
|
- Manage pause (stop camera & building, show pause menu).
|
||||||
|
- Implement a super simple main menu scene:
|
||||||
|
- “New World”, “Load World”, “Quit”.
|
||||||
|
|
||||||
|
5) CODE QUALITY
|
||||||
|
- For each new script:
|
||||||
|
- Include XML summary comments at class + key methods.
|
||||||
|
- Use `SerializeField` with private fields where possible.
|
||||||
|
- Provide example usage in comments at top of each script.
|
||||||
|
|
||||||
|
OUTPUT
|
||||||
|
- Generate C# scripts for:
|
||||||
|
- `Assets/Scripts/Core/GameState.cs`
|
||||||
|
- `Assets/Scripts/Core/WorldSerializer.cs`
|
||||||
|
- `Assets/Scripts/UI/BlockSelectionBar.cs`
|
||||||
|
- Any additional small scripts you need (e.g., simple MainMenu UI).
|
||||||
|
- Make them self-contained and ready to paste into the Unity project.
|
||||||
|
- Do NOT generate Unity meta files.
|
||||||
|
|
||||||
|
If something is ambiguous, assume:
|
||||||
|
- PC / keyboard + mouse.
|
||||||
|
- Single-player.
|
||||||
|
- No networking yet.
|
||||||
|
|
||||||
|
Respond with only code blocks and very short comments around them so I can open PRs with minimal editing.
|
||||||
|
```
|
||||||
55
Assets/Scripts/Building/BlockDatabase.cs
Normal file
55
Assets/Scripts/Building/BlockDatabase.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BlackRoad.Worldbuilder.Building
|
||||||
|
{
|
||||||
|
[CreateAssetMenu(
|
||||||
|
fileName = "BlockDatabase",
|
||||||
|
menuName = "BlackRoad/Worldbuilder/BlockDatabase",
|
||||||
|
order = 1
|
||||||
|
)]
|
||||||
|
public class BlockDatabase : ScriptableObject
|
||||||
|
{
|
||||||
|
public BlockType[] blocks;
|
||||||
|
|
||||||
|
private Dictionary<string, BlockType> _byId;
|
||||||
|
|
||||||
|
private void OnEnable()
|
||||||
|
{
|
||||||
|
BuildIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildIndex()
|
||||||
|
{
|
||||||
|
_byId = new Dictionary<string, BlockType>();
|
||||||
|
|
||||||
|
if (blocks == null) return;
|
||||||
|
|
||||||
|
foreach (var block in blocks)
|
||||||
|
{
|
||||||
|
if (block == null || string.IsNullOrEmpty(block.blockId)) continue;
|
||||||
|
|
||||||
|
if (!_byId.ContainsKey(block.blockId))
|
||||||
|
{
|
||||||
|
_byId.Add(block.blockId, block);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockType Get(string id)
|
||||||
|
{
|
||||||
|
if (_byId == null || _byId.Count == 0)
|
||||||
|
BuildIndex();
|
||||||
|
|
||||||
|
return _byId != null && _byId.TryGetValue(id, out var block) ? block : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BlockType GetDefault()
|
||||||
|
{
|
||||||
|
if (blocks != null && blocks.Length > 0)
|
||||||
|
return blocks[0];
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
63
Assets/Scripts/Building/BlockPlacer.cs
Normal file
63
Assets/Scripts/Building/BlockPlacer.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BlackRoad.Worldbuilder.Building
|
||||||
|
{
|
||||||
|
[RequireComponent(typeof(Camera))]
|
||||||
|
public class BlockPlacer : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("Refs")]
|
||||||
|
public WorldGrid worldGrid;
|
||||||
|
public BlockDatabase blockDatabase;
|
||||||
|
|
||||||
|
[Header("Placement")]
|
||||||
|
public float maxDistance = 50f;
|
||||||
|
public LayerMask placementMask = ~0;
|
||||||
|
|
||||||
|
private Camera _camera;
|
||||||
|
private BlockType _currentBlock;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
_camera = GetComponent<Camera>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
if (blockDatabase != null)
|
||||||
|
{
|
||||||
|
_currentBlock = blockDatabase.GetDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (worldGrid == null)
|
||||||
|
{
|
||||||
|
// Try to find one in the scene
|
||||||
|
worldGrid = FindObjectOfType<WorldGrid>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (_camera == null || worldGrid == null || _currentBlock == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Ray ray = _camera.ScreenPointToRay(UnityEngine.Input.mousePosition);
|
||||||
|
|
||||||
|
if (Physics.Raycast(ray, out RaycastHit hit, maxDistance, placementMask))
|
||||||
|
{
|
||||||
|
// Place with left click
|
||||||
|
if (UnityEngine.Input.GetMouseButtonDown(0))
|
||||||
|
{
|
||||||
|
Vector3Int gridPos = worldGrid.WorldToGrid(hit.point + hit.normal * 0.5f);
|
||||||
|
worldGrid.PlaceBlock(gridPos, _currentBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove with right click
|
||||||
|
if (UnityEngine.Input.GetMouseButtonDown(1))
|
||||||
|
{
|
||||||
|
Vector3Int gridPos = worldGrid.WorldToGrid(hit.point - hit.normal * 0.5f);
|
||||||
|
worldGrid.RemoveBlock(gridPos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
Assets/Scripts/Building/BlockType.cs
Normal file
17
Assets/Scripts/Building/BlockType.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BlackRoad.Worldbuilder.Building
|
||||||
|
{
|
||||||
|
[CreateAssetMenu(
|
||||||
|
fileName = "BlockType",
|
||||||
|
menuName = "BlackRoad/Worldbuilder/BlockType",
|
||||||
|
order = 0
|
||||||
|
)]
|
||||||
|
public class BlockType : ScriptableObject
|
||||||
|
{
|
||||||
|
public string blockId = "block.dirt";
|
||||||
|
public string displayName = "Dirt";
|
||||||
|
public GameObject prefab;
|
||||||
|
public Color gizmoColor = Color.gray;
|
||||||
|
}
|
||||||
|
}
|
||||||
67
Assets/Scripts/Building/WorldGrid.cs
Normal file
67
Assets/Scripts/Building/WorldGrid.cs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BlackRoad.Worldbuilder.Building
|
||||||
|
{
|
||||||
|
public class WorldGrid : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("Grid Settings")]
|
||||||
|
public float cellSize = 1f;
|
||||||
|
|
||||||
|
private readonly Dictionary<Vector3Int, GameObject> _placedBlocks =
|
||||||
|
new Dictionary<Vector3Int, GameObject>();
|
||||||
|
|
||||||
|
public Vector3Int WorldToGrid(Vector3 worldPos)
|
||||||
|
{
|
||||||
|
return new Vector3Int(
|
||||||
|
Mathf.RoundToInt(worldPos.x / cellSize),
|
||||||
|
Mathf.RoundToInt(worldPos.y / cellSize),
|
||||||
|
Mathf.RoundToInt(worldPos.z / cellSize)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector3 GridToWorld(Vector3Int gridPos)
|
||||||
|
{
|
||||||
|
return new Vector3(
|
||||||
|
gridPos.x * cellSize,
|
||||||
|
gridPos.y * cellSize,
|
||||||
|
gridPos.z * cellSize
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetBlock(Vector3Int gridPos, out GameObject block)
|
||||||
|
{
|
||||||
|
return _placedBlocks.TryGetValue(gridPos, out block);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GameObject PlaceBlock(Vector3Int gridPos, BlockType blockType)
|
||||||
|
{
|
||||||
|
if (blockType == null || blockType.prefab == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (_placedBlocks.ContainsKey(gridPos))
|
||||||
|
return _placedBlocks[gridPos];
|
||||||
|
|
||||||
|
Vector3 worldPos = GridToWorld(gridPos);
|
||||||
|
var instance = Instantiate(blockType.prefab, worldPos, Quaternion.identity, transform);
|
||||||
|
_placedBlocks.Add(gridPos, instance);
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RemoveBlock(Vector3Int gridPos)
|
||||||
|
{
|
||||||
|
if (!_placedBlocks.TryGetValue(gridPos, out var instance))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_placedBlocks.Remove(gridPos);
|
||||||
|
|
||||||
|
if (instance != null)
|
||||||
|
{
|
||||||
|
Destroy(instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
Assets/Scripts/Core/GameManager.cs
Normal file
40
Assets/Scripts/Core/GameManager.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BlackRoad.Worldbuilder.Core
|
||||||
|
{
|
||||||
|
public class GameManager : MonoBehaviour
|
||||||
|
{
|
||||||
|
public static GameManager Instance { get; private set; }
|
||||||
|
|
||||||
|
[Header("Building")]
|
||||||
|
public Building.BlockDatabase blockDatabase;
|
||||||
|
public Building.BlockPlacer blockPlacer;
|
||||||
|
|
||||||
|
[Header("Input")]
|
||||||
|
public Input.FlyCameraController flyCamera;
|
||||||
|
|
||||||
|
[Header("Settings")]
|
||||||
|
public bool showGridGizmos = true;
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (Instance != null && Instance != this)
|
||||||
|
{
|
||||||
|
Destroy(gameObject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Instance = this;
|
||||||
|
DontDestroyOnLoad(gameObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
// Simple escape to quit / future menus
|
||||||
|
if (Input.GetKeyDown(KeyCode.Escape))
|
||||||
|
{
|
||||||
|
Application.Quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
92
Assets/Scripts/Environment/DayNightCycle.cs
Normal file
92
Assets/Scripts/Environment/DayNightCycle.cs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BlackRoad.Worldbuilder.Environment
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Simple day/night cycle: rotates the directional light and
|
||||||
|
/// adjusts intensity + fog color over a 24h-style loop.
|
||||||
|
/// </summary>
|
||||||
|
public class DayNightCycle : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("References")]
|
||||||
|
[Tooltip("Main directional light used as the sun.")]
|
||||||
|
public Light sunLight;
|
||||||
|
|
||||||
|
[Header("Time")]
|
||||||
|
[Tooltip("Length of a full day/night cycle in real seconds.")]
|
||||||
|
public float dayLengthSeconds = 600f; // 10 minutes
|
||||||
|
|
||||||
|
[Range(0f, 1f)]
|
||||||
|
[Tooltip("Current normalized time of day (0 = sunrise, 0.5 = sunset).")]
|
||||||
|
public float timeOfDay = 0.25f;
|
||||||
|
|
||||||
|
[Header("Lighting")]
|
||||||
|
public Gradient sunColorOverDay;
|
||||||
|
public AnimationCurve sunIntensityOverDay = AnimationCurve.Linear(0, 0, 1, 1);
|
||||||
|
|
||||||
|
[Header("Fog")]
|
||||||
|
public bool controlFog = true;
|
||||||
|
public Gradient fogColorOverDay;
|
||||||
|
public AnimationCurve fogDensityOverDay = AnimationCurve.Linear(0, 0.002f, 1, 0.01f);
|
||||||
|
|
||||||
|
private float _timeSpeed;
|
||||||
|
|
||||||
|
private void Reset()
|
||||||
|
{
|
||||||
|
// Reasonable defaults if you drop it into scene
|
||||||
|
dayLengthSeconds = 600f;
|
||||||
|
timeOfDay = 0.25f;
|
||||||
|
sunIntensityOverDay = AnimationCurve.EaseInOut(0, 0, 0.25f, 1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
if (sunLight == null)
|
||||||
|
{
|
||||||
|
sunLight = RenderSettings.sun;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dayLengthSeconds <= 0f)
|
||||||
|
dayLengthSeconds = 600f;
|
||||||
|
|
||||||
|
_timeSpeed = 1f / dayLengthSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
// Advance normalized time [0–1]
|
||||||
|
timeOfDay += _timeSpeed * Time.deltaTime;
|
||||||
|
if (timeOfDay > 1f)
|
||||||
|
timeOfDay -= 1f;
|
||||||
|
|
||||||
|
UpdateSun();
|
||||||
|
UpdateFog();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSun()
|
||||||
|
{
|
||||||
|
if (sunLight == null) return;
|
||||||
|
|
||||||
|
// Rotate sun: 0..1 -> 0..360 degrees
|
||||||
|
float sunAngle = timeOfDay * 360f - 90f; // -90 makes 0 = sunrise on horizon
|
||||||
|
sunLight.transform.rotation = Quaternion.Euler(sunAngle, 170f, 0f);
|
||||||
|
|
||||||
|
if (sunColorOverDay != null)
|
||||||
|
sunLight.color = sunColorOverDay.Evaluate(timeOfDay);
|
||||||
|
|
||||||
|
if (sunIntensityOverDay != null)
|
||||||
|
sunLight.intensity = sunIntensityOverDay.Evaluate(timeOfDay);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateFog()
|
||||||
|
{
|
||||||
|
if (!controlFog) return;
|
||||||
|
|
||||||
|
if (fogColorOverDay != null)
|
||||||
|
RenderSettings.fogColor = fogColorOverDay.Evaluate(timeOfDay);
|
||||||
|
|
||||||
|
if (fogDensityOverDay != null)
|
||||||
|
RenderSettings.fogDensity = fogDensityOverDay.Evaluate(timeOfDay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
129
Assets/Scripts/Environment/ProceduralSpawner.cs
Normal file
129
Assets/Scripts/Environment/ProceduralSpawner.cs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BlackRoad.Worldbuilder.Environment
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Spawns environment prefabs (trees, rocks, houses) on a Terrain
|
||||||
|
/// using simple rules for height, slope, and density.
|
||||||
|
/// Run once in editor or at runtime.
|
||||||
|
/// </summary>
|
||||||
|
public class ProceduralSpawner : MonoBehaviour
|
||||||
|
{
|
||||||
|
[System.Serializable]
|
||||||
|
public class SpawnRule
|
||||||
|
{
|
||||||
|
public string id = "trees";
|
||||||
|
public GameObject prefab;
|
||||||
|
[Tooltip("How many instances to try to place.")]
|
||||||
|
public int attempts = 500;
|
||||||
|
[Tooltip("Minimum normalized height [0..1] on terrain.")]
|
||||||
|
[Range(0f, 1f)] public float minHeight = 0f;
|
||||||
|
[Range(0f, 1f)] public float maxHeight = 1f;
|
||||||
|
[Tooltip("Maximum terrain slope allowed in degrees.")]
|
||||||
|
[Range(0f, 90f)] public float maxSlope = 30f;
|
||||||
|
[Tooltip("Min distance from other instances of this rule.")]
|
||||||
|
public float minSpacing = 3f;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Header("Terrain")]
|
||||||
|
public Terrain targetTerrain;
|
||||||
|
|
||||||
|
[Header("Spawning Rules")]
|
||||||
|
public List<SpawnRule> rules = new List<SpawnRule>();
|
||||||
|
|
||||||
|
[Header("Randomness")]
|
||||||
|
public int randomSeed = 12345;
|
||||||
|
|
||||||
|
private TerrainData _terrainData;
|
||||||
|
private Vector3 _terrainPos;
|
||||||
|
|
||||||
|
private void OnValidate()
|
||||||
|
{
|
||||||
|
if (targetTerrain == null)
|
||||||
|
targetTerrain = GetComponent<Terrain>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[ContextMenu("Spawn All")]
|
||||||
|
public void SpawnAll()
|
||||||
|
{
|
||||||
|
if (targetTerrain == null)
|
||||||
|
{
|
||||||
|
Debug.LogError("[ProceduralSpawner] No Terrain assigned.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_terrainData = targetTerrain.terrainData;
|
||||||
|
_terrainPos = targetTerrain.transform.position;
|
||||||
|
Random.InitState(randomSeed);
|
||||||
|
|
||||||
|
foreach (var rule in rules)
|
||||||
|
{
|
||||||
|
SpawnForRule(rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SpawnForRule(SpawnRule rule)
|
||||||
|
{
|
||||||
|
if (rule.prefab == null)
|
||||||
|
{
|
||||||
|
Debug.LogWarning($"[ProceduralSpawner] Rule {rule.id} has no prefab.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var placedPositions = new List<Vector3>();
|
||||||
|
|
||||||
|
for (int i = 0; i < rule.attempts; i++)
|
||||||
|
{
|
||||||
|
// Random point in terrain space (0..1)
|
||||||
|
float rx = Random.value;
|
||||||
|
float rz = Random.value;
|
||||||
|
|
||||||
|
float height = _terrainData.GetInterpolatedHeight(rx, rz);
|
||||||
|
float normHeight = Mathf.InverseLerp(
|
||||||
|
_terrainData.bounds.min.y,
|
||||||
|
_terrainData.bounds.max.y,
|
||||||
|
height + _terrainPos.y
|
||||||
|
);
|
||||||
|
|
||||||
|
if (normHeight < rule.minHeight || normHeight > rule.maxHeight)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// World position
|
||||||
|
float worldX = _terrainPos.x + rx * _terrainData.size.x;
|
||||||
|
float worldZ = _terrainPos.z + rz * _terrainData.size.z;
|
||||||
|
Vector3 worldPos = new Vector3(worldX, height + _terrainPos.y, worldZ);
|
||||||
|
|
||||||
|
// Check slope (avoid steep areas)
|
||||||
|
Vector3 normal = _terrainData.GetInterpolatedNormal(rx, rz);
|
||||||
|
float slope = Vector3.Angle(normal, Vector3.up);
|
||||||
|
if (slope > rule.maxSlope)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Respect min spacing
|
||||||
|
bool tooClose = false;
|
||||||
|
foreach (var p in placedPositions)
|
||||||
|
{
|
||||||
|
if (Vector3.SqrMagnitude(p - worldPos) < rule.minSpacing * rule.minSpacing)
|
||||||
|
{
|
||||||
|
tooClose = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tooClose) continue;
|
||||||
|
|
||||||
|
// Random rotation around Y
|
||||||
|
Quaternion rot = Quaternion.Euler(0f, Random.Range(0f, 360f), 0f);
|
||||||
|
|
||||||
|
// Slight random scale
|
||||||
|
float scaleFactor = Random.Range(0.85f, 1.15f);
|
||||||
|
var instance = Instantiate(rule.prefab, worldPos, rot, transform);
|
||||||
|
instance.transform.localScale *= scaleFactor;
|
||||||
|
|
||||||
|
placedPositions.Add(worldPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug.Log($"[ProceduralSpawner] Rule {rule.id}: placed {placedPositions.Count} instances.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
94
Assets/Scripts/Input/FlyCameraController.cs
Normal file
94
Assets/Scripts/Input/FlyCameraController.cs
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace BlackRoad.Worldbuilder.Input
|
||||||
|
{
|
||||||
|
[RequireComponent(typeof(Camera))]
|
||||||
|
public class FlyCameraController : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("Movement")]
|
||||||
|
public float moveSpeed = 10f;
|
||||||
|
public float fastMultiplier = 2f;
|
||||||
|
public float slowMultiplier = 0.5f;
|
||||||
|
|
||||||
|
[Header("Look")]
|
||||||
|
public float lookSensitivity = 3f;
|
||||||
|
public float minPitch = -80f;
|
||||||
|
public float maxPitch = 80f;
|
||||||
|
|
||||||
|
private float _yaw;
|
||||||
|
private float _pitch;
|
||||||
|
|
||||||
|
private void Start()
|
||||||
|
{
|
||||||
|
var euler = transform.eulerAngles;
|
||||||
|
_yaw = euler.y;
|
||||||
|
_pitch = euler.x;
|
||||||
|
|
||||||
|
Cursor.lockState = CursorLockMode.Locked;
|
||||||
|
Cursor.visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
HandleLook();
|
||||||
|
HandleMove();
|
||||||
|
|
||||||
|
// Toggle cursor lock for debugging
|
||||||
|
if (UnityEngine.Input.GetKeyDown(KeyCode.Tab))
|
||||||
|
{
|
||||||
|
if (Cursor.lockState == CursorLockMode.Locked)
|
||||||
|
{
|
||||||
|
Cursor.lockState = CursorLockMode.None;
|
||||||
|
Cursor.visible = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Cursor.lockState = CursorLockMode.Locked;
|
||||||
|
Cursor.visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleLook()
|
||||||
|
{
|
||||||
|
if (Cursor.lockState != CursorLockMode.Locked) return;
|
||||||
|
|
||||||
|
float mouseX = UnityEngine.Input.GetAxis("Mouse X") * lookSensitivity;
|
||||||
|
float mouseY = UnityEngine.Input.GetAxis("Mouse Y") * lookSensitivity;
|
||||||
|
|
||||||
|
_yaw += mouseX;
|
||||||
|
_pitch -= mouseY;
|
||||||
|
_pitch = Mathf.Clamp(_pitch, minPitch, maxPitch);
|
||||||
|
|
||||||
|
transform.rotation = Quaternion.Euler(_pitch, _yaw, 0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleMove()
|
||||||
|
{
|
||||||
|
float speed = moveSpeed;
|
||||||
|
|
||||||
|
if (UnityEngine.Input.GetKey(KeyCode.LeftShift))
|
||||||
|
speed *= fastMultiplier;
|
||||||
|
else if (UnityEngine.Input.GetKey(KeyCode.LeftControl))
|
||||||
|
speed *= slowMultiplier;
|
||||||
|
|
||||||
|
Vector3 input =
|
||||||
|
new Vector3(
|
||||||
|
UnityEngine.Input.GetAxisRaw("Horizontal"),
|
||||||
|
0f,
|
||||||
|
UnityEngine.Input.GetAxisRaw("Vertical")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Vertical up/down
|
||||||
|
if (UnityEngine.Input.GetKey(KeyCode.E))
|
||||||
|
input.y += 1f;
|
||||||
|
if (UnityEngine.Input.GetKey(KeyCode.Q))
|
||||||
|
input.y -= 1f;
|
||||||
|
|
||||||
|
Vector3 worldMove =
|
||||||
|
transform.TransformDirection(input.normalized) * (speed * Time.deltaTime);
|
||||||
|
|
||||||
|
transform.position += worldMove;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user