Merge branch origin/codex/add-player-controller-with-fly-mode into main

This commit is contained in:
Alexa Amundson
2025-11-28 23:18:13 -06:00
3 changed files with 319 additions and 0 deletions

View File

@@ -0,0 +1,116 @@
using System.Collections.Generic;
using UnityEngine;
using BlackRoad.Worldbuilder.Environment;
using BlackRoad.Worldbuilder.Player;
namespace BlackRoad.Worldbuilder.Building
{
/// <summary>
/// Lets the player place and remove prefabs on terrain using raycasts.
/// Left click: place selected prefab (snapped to grid).
/// Right click: remove hit buildable object.
/// Number keys 1..N: select prefab.
/// </summary>
public class BuildTool : MonoBehaviour
{
[Header("Refs")]
[SerializeField] private Camera playerCamera;
[SerializeField] private PlayerController playerController;
[SerializeField] private float maxPlaceDistance = 80f;
[SerializeField] private LayerMask placementMask = ~0;
[Header("Building")]
[SerializeField] private float gridSize = 2f;
[SerializeField] private List<GameObject> buildPrefabs = new List<GameObject>();
[Header("Debug")]
[SerializeField] private Color previewColor = new Color(0f, 1f, 0f, 0.4f);
public int CurrentIndex { get; private set; } = 0;
public GameObject CurrentPrefab =>
buildPrefabs != null && buildPrefabs.Count > 0 && CurrentIndex >= 0 && CurrentIndex < buildPrefabs.Count
? buildPrefabs[CurrentIndex]
: null;
private Vector3 _lastPreviewPos;
private bool _hasPreview;
private void Awake()
{
if (playerCamera == null)
playerCamera = Camera.main;
if (playerController == null)
playerController = FindObjectOfType<PlayerController>();
}
private void Update()
{
HandleSelection();
HandlePlacement();
}
private void HandleSelection()
{
// 1..9 selects prefabs
for (int i = 0; i < buildPrefabs.Count && i < 9; i++)
{
if (Input.GetKeyDown(KeyCode.Alpha1 + i))
{
CurrentIndex = i;
}
}
}
private void HandlePlacement()
{
if (playerCamera == null || CurrentPrefab == null)
return;
Ray ray = playerCamera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0f));
if (Physics.Raycast(ray, out RaycastHit hit, maxPlaceDistance, placementMask))
{
Vector3 pos = hit.point;
// Snap to grid horizontally, keep terrain height
pos.x = Mathf.Round(pos.x / gridSize) * gridSize;
pos.z = Mathf.Round(pos.z / gridSize) * gridSize;
_lastPreviewPos = pos;
_hasPreview = true;
// Place
if (Input.GetMouseButtonDown(0))
{
Instantiate(CurrentPrefab, pos, Quaternion.identity);
}
// Remove
if (Input.GetMouseButtonDown(1))
{
if (hit.collider != null)
{
// Simple rule: destroy hit object if it has tag "Buildable" or no Rigidbody.
var go = hit.collider.gameObject;
if (go.CompareTag("Buildable") || go.GetComponent<Rigidbody>() == null)
{
Destroy(go);
}
}
}
}
else
{
_hasPreview = false;
}
}
private void OnDrawGizmos()
{
if (!_hasPreview || CurrentPrefab == null) return;
Gizmos.color = previewColor;
Vector3 size = Vector3.one * gridSize;
Gizmos.DrawCube(_lastPreviewPos, size);
}
}
}

View File

@@ -0,0 +1,135 @@
using UnityEngine;
namespace BlackRoad.Worldbuilder.Player
{
/// <summary>
/// First-person controller with walk/run/jump and a fly-mode toggle.
/// Attach to a GameObject with CharacterController and a child Camera.
/// </summary>
[RequireComponent(typeof(CharacterController))]
public class PlayerController : MonoBehaviour
{
[Header("View")]
[SerializeField] private Camera playerCamera;
[SerializeField] private float mouseSensitivity = 2f;
[SerializeField] private float minPitch = -80f;
[SerializeField] private float maxPitch = 80f;
[Header("Movement")]
[SerializeField] private float walkSpeed = 5f;
[SerializeField] private float runMultiplier = 1.8f;
[SerializeField] private float jumpForce = 5f;
[SerializeField] private float gravity = 9.81f;
[Header("Fly Mode")]
[SerializeField] private bool startInFlyMode = false;
[SerializeField] private float flySpeedMultiplier = 2f;
[SerializeField] private KeyCode flyToggleKey = KeyCode.F;
public bool IsFlying { get; private set; }
private CharacterController _controller;
private float _yaw;
private float _pitch;
private Vector3 _velocity;
private void Awake()
{
_controller = GetComponent<CharacterController>();
if (playerCamera == null)
playerCamera = GetComponentInChildren<Camera>();
Vector3 euler = transform.localEulerAngles;
_yaw = euler.y;
_pitch = euler.x;
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
IsFlying = startInFlyMode;
}
private void Update()
{
HandleLook();
HandleModeToggle();
HandleMove();
}
private void HandleLook()
{
if (Cursor.lockState != CursorLockMode.Locked || playerCamera == null)
return;
float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity;
float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity;
_yaw += mouseX;
_pitch -= mouseY;
_pitch = Mathf.Clamp(_pitch, minPitch, maxPitch);
transform.rotation = Quaternion.Euler(0f, _yaw, 0f);
playerCamera.transform.localRotation = Quaternion.Euler(_pitch, 0f, 0f);
}
private void HandleModeToggle()
{
if (Input.GetKeyDown(flyToggleKey))
{
IsFlying = !IsFlying;
// Reset vertical velocity when switching modes
_velocity.y = 0f;
}
}
private void HandleMove()
{
Vector3 input = new Vector3(
Input.GetAxisRaw("Horizontal"),
0f,
Input.GetAxisRaw("Vertical")
).normalized;
float speed = walkSpeed;
if (Input.GetKey(KeyCode.LeftShift))
speed *= runMultiplier;
if (IsFlying)
speed *= flySpeedMultiplier;
Vector3 move = transform.TransformDirection(input) * speed;
if (IsFlying)
{
// Vertical fly
if (Input.GetKey(KeyCode.Space))
move.y += speed;
if (Input.GetKey(KeyCode.LeftControl))
move.y -= speed;
// No gravity
_controller.Move(move * Time.deltaTime);
}
else
{
// Grounded mode with gravity / jump
if (_controller.isGrounded)
{
_velocity.y = -1f; // small downward force
if (Input.GetKeyDown(KeyCode.Space))
{
_velocity.y = jumpForce;
}
}
else
{
_velocity.y -= gravity * Time.deltaTime;
}
Vector3 finalMove = new Vector3(move.x, _velocity.y, move.z);
_controller.Move(finalMove * Time.deltaTime);
}
}
}
}

View File

@@ -0,0 +1,68 @@
using UnityEngine;
using BlackRoad.Worldbuilder.Environment;
using BlackRoad.Worldbuilder.Building;
using BlackRoad.Worldbuilder.Player;
namespace BlackRoad.Worldbuilder.UI
{
/// <summary>
/// Very simple on-screen HUD for development:
/// shows time of day, current build prefab, and fly/walk mode.
/// </summary>
public class DebugHUD : MonoBehaviour
{
[SerializeField] private DayNightCycle dayNight;
[SerializeField] private BuildTool buildTool;
[SerializeField] private PlayerController playerController;
[Header("Style")]
[SerializeField] private int fontSize = 14;
[SerializeField] private Color textColor = Color.white;
[SerializeField] private Vector2 margin = new Vector2(10f, 10f);
private GUIStyle _style;
private void Awake()
{
if (dayNight == null)
dayNight = FindObjectOfType<DayNightCycle>();
if (buildTool == null)
buildTool = FindObjectOfType<BuildTool>();
if (playerController == null)
playerController = FindObjectOfType<PlayerController>();
}
private void OnGUI()
{
if (_style == null)
{
_style = new GUIStyle(GUI.skin.label)
{
fontSize = fontSize,
normal = { textColor = textColor }
};
}
float x = margin.x;
float y = margin.y;
string mode = playerController != null && playerController.IsFlying ? "FLY" : "WALK";
string blockName = buildTool != null && buildTool.CurrentPrefab != null
? buildTool.CurrentPrefab.name
: "(none)";
string timeStr = dayNight != null
? $"{(dayNight.timeOfDay * 24f):0.0}h"
: "n/a";
GUI.Label(new Rect(x, y, 400f, 24f),
$"Time: {timeStr} Mode: {mode} Block: {blockName}",
_style);
y += 22f;
GUI.Label(new Rect(x, y, 400f, 24f),
"LMB: place RMB: remove 19: select prefab F: toggle fly",
_style);
}
}
}