Initial commit
This commit is contained in:
36
Assets/Scripts/Character.cs
Normal file
36
Assets/Scripts/Character.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.AI;
|
||||
|
||||
public abstract class Character : MonoBehaviour
|
||||
{
|
||||
[Header("Stats")]
|
||||
public int CurHp = 10;
|
||||
public int MaxHp = 10;
|
||||
|
||||
[Header("Components")]
|
||||
// Optional movement controller reference, assign in inspector if used
|
||||
public NavMeshMovementController MovementController;
|
||||
|
||||
protected Character target;
|
||||
public event UnityAction onTakeDamage;
|
||||
|
||||
public void TakeDamage(int damageToTake)
|
||||
{
|
||||
CurHp -= damageToTake;
|
||||
onTakeDamage?.Invoke();
|
||||
if (CurHp <= 0) Die();
|
||||
}
|
||||
|
||||
public virtual void Die()
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
public void SetTarget(Character t)
|
||||
{
|
||||
target = t;
|
||||
}
|
||||
|
||||
public Character GetTarget() => target;
|
||||
}
|
||||
2
Assets/Scripts/Character.cs.meta
Normal file
2
Assets/Scripts/Character.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 88a8c6497f18c2440a367d9c2daa3146
|
||||
97
Assets/Scripts/ClickToMove.cs
Normal file
97
Assets/Scripts/ClickToMove.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
public class ClickToMoveInputSystem : MonoBehaviour
|
||||
{
|
||||
[Header("References")]
|
||||
public Camera cam;
|
||||
public LayerMask movementLayers; // ground
|
||||
public LayerMask selectionLayers; // enemies, NPCs, interactables
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (cam == null) cam = Camera.main;
|
||||
}
|
||||
|
||||
// Hook this to PointerPos action (Value Vector2)
|
||||
Vector2 pointerScreenPos;
|
||||
public void OnPointerPos(InputAction.CallbackContext ctx)
|
||||
{
|
||||
pointerScreenPos = ctx.ReadValue<Vector2>();
|
||||
}
|
||||
|
||||
// Hook this to LeftClick action (Button)
|
||||
public void OnLeftClick(InputAction.CallbackContext ctx)
|
||||
{
|
||||
if (!ctx.performed) return;
|
||||
if (cam == null) return;
|
||||
|
||||
Ray ray = cam.ScreenPointToRay(pointerScreenPos);
|
||||
if (Physics.Raycast(ray, out RaycastHit hit, 500f, selectionLayers))
|
||||
{
|
||||
// Try to find a Character on the hit object or its parents
|
||||
var character = hit.collider.GetComponentInParent<Character>();
|
||||
if (character != null)
|
||||
{
|
||||
if (Player.Current != null) Player.Current.SetTarget(character);
|
||||
var selectable = hit.collider.GetComponentInParent<ISelectable>();
|
||||
selectable?.OnSelected();
|
||||
return;
|
||||
}
|
||||
|
||||
// Generic interactable
|
||||
var interactable = hit.collider.GetComponentInParent<Interactable>();
|
||||
interactable?.OnSelect();
|
||||
return;
|
||||
}
|
||||
|
||||
// Nothing selected, clear target
|
||||
if (Player.Current != null) Player.Current.SetTarget(null);
|
||||
}
|
||||
|
||||
// Hook this to RightClick action (Button)
|
||||
public void OnRightClick(InputAction.CallbackContext ctx)
|
||||
{
|
||||
if (!ctx.performed) return;
|
||||
if (cam == null) return;
|
||||
if (Player.Current == null) return;
|
||||
|
||||
Ray ray = cam.ScreenPointToRay(pointerScreenPos);
|
||||
if (Physics.Raycast(ray, out RaycastHit hit, 500f, movementLayers))
|
||||
{
|
||||
// Move player's movement controller or NavMeshAgent
|
||||
var movement = Player.Current.MovementController;
|
||||
if (movement != null)
|
||||
{
|
||||
movement.MoveTo(hit.point);
|
||||
}
|
||||
else
|
||||
{
|
||||
// fallback: try to find NavMeshAgent on player root
|
||||
var agent = Player.Current.GetComponent<UnityEngine.AI.NavMeshAgent>();
|
||||
if (agent != null) agent.SetDestination(hit.point);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Optional fallback if PointerPos not wired
|
||||
void Update()
|
||||
{
|
||||
if (cam == null) return;
|
||||
if (Mouse.current == null) return;
|
||||
|
||||
pointerScreenPos = Mouse.current.position.ReadValue();
|
||||
|
||||
if (Mouse.current.leftButton.wasPressedThisFrame)
|
||||
{
|
||||
// simulate InputAction callback context by calling handler directly
|
||||
OnLeftClick(new InputAction.CallbackContext());
|
||||
}
|
||||
|
||||
if (Mouse.current.rightButton.wasPressedThisFrame)
|
||||
{
|
||||
OnRightClick(new InputAction.CallbackContext());
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/ClickToMove.cs.meta
Normal file
2
Assets/Scripts/ClickToMove.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a26e88079fb52fe44b7e04af98cac672
|
||||
9
Assets/Scripts/IInteractable.cs
Normal file
9
Assets/Scripts/IInteractable.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class Interactable : MonoBehaviour
|
||||
{
|
||||
public virtual void OnSelect()
|
||||
{
|
||||
// implement interaction logic here
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/IInteractable.cs.meta
Normal file
2
Assets/Scripts/IInteractable.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ce12caa097a3e5b4cb22f8f35a138639
|
||||
8
Assets/Scripts/Interfaces.meta
Normal file
8
Assets/Scripts/Interfaces.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8610e8ff47636c24ab8dde7251de0ea4
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
4
Assets/Scripts/Interfaces/ISelectable.cs
Normal file
4
Assets/Scripts/Interfaces/ISelectable.cs
Normal file
@@ -0,0 +1,4 @@
|
||||
public interface ISelectable
|
||||
{
|
||||
void OnSelected();
|
||||
}
|
||||
2
Assets/Scripts/Interfaces/ISelectable.cs.meta
Normal file
2
Assets/Scripts/Interfaces/ISelectable.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bbae0bd474cd44546bebb76860d5c417
|
||||
47
Assets/Scripts/NameplateController.cs
Normal file
47
Assets/Scripts/NameplateController.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class NameplateController : MonoBehaviour
|
||||
{
|
||||
[Header("UI refs")]
|
||||
public TextMeshProUGUI nameText;
|
||||
public Image backgroundImage; // optional background to tint
|
||||
public Outline uiOutline; // optional UI outline component (for name text)
|
||||
|
||||
Camera mainCam;
|
||||
Transform target;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
mainCam = Camera.main;
|
||||
}
|
||||
|
||||
public void Initialize(Selectable s, Color color, Vector3 offset)
|
||||
{
|
||||
target = s.transform;
|
||||
if (nameText != null) nameText.text = s.displayName;
|
||||
ApplyColor(color);
|
||||
FollowTarget(target, offset);
|
||||
}
|
||||
|
||||
public void FollowTarget(Transform t, Vector3 offset)
|
||||
{
|
||||
if (t == null) return;
|
||||
Vector3 worldPos = t.position + offset;
|
||||
transform.position = worldPos;
|
||||
|
||||
// Face camera
|
||||
if (mainCam != null)
|
||||
{
|
||||
transform.rotation = Quaternion.LookRotation(transform.position - mainCam.transform.position);
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyColor(Color c)
|
||||
{
|
||||
if (nameText != null) nameText.color = c;
|
||||
if (backgroundImage != null) backgroundImage.color = new Color(c.r, c.g, c.b, backgroundImage.color.a);
|
||||
if (uiOutline != null) uiOutline.effectColor = c;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/NameplateController.cs.meta
Normal file
2
Assets/Scripts/NameplateController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b3a4939aaa4eb774f98d66e15a6b4240
|
||||
32
Assets/Scripts/NavMeshMovementController.cs
Normal file
32
Assets/Scripts/NavMeshMovementController.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
|
||||
[RequireComponent(typeof(NavMeshAgent))]
|
||||
public class NavMeshMovementController : MonoBehaviour
|
||||
{
|
||||
NavMeshAgent agent;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
agent = GetComponent<NavMeshAgent>();
|
||||
}
|
||||
|
||||
public void MoveTo(Vector3 worldPosition)
|
||||
{
|
||||
if (agent == null) return;
|
||||
agent.isStopped = false;
|
||||
agent.SetDestination(worldPosition);
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (agent == null) return;
|
||||
agent.isStopped = true;
|
||||
}
|
||||
|
||||
public bool IsAtDestination(float tolerance = 0.1f)
|
||||
{
|
||||
if (agent == null) return true;
|
||||
return !agent.pathPending && agent.remainingDistance <= agent.stoppingDistance + tolerance;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/NavMeshMovementController.cs.meta
Normal file
2
Assets/Scripts/NavMeshMovementController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 448c1cb12aa58e0408460dc7f94df117
|
||||
23
Assets/Scripts/Player.cs
Normal file
23
Assets/Scripts/Player.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class Player : Character
|
||||
{
|
||||
public static Player Current;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
Current = this;
|
||||
}
|
||||
|
||||
void OnDestroy()
|
||||
{
|
||||
if (Current == this) Current = null;
|
||||
}
|
||||
|
||||
// Example hook for when a target is set
|
||||
public override void Die()
|
||||
{
|
||||
base.Die();
|
||||
// add player-specific death logic here
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Player.cs.meta
Normal file
2
Assets/Scripts/Player.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 63c7f1b83c004a1419e85f2827182ac1
|
||||
28
Assets/Scripts/Selectable.cs
Normal file
28
Assets/Scripts/Selectable.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using UnityEngine;
|
||||
|
||||
public enum Attitude { Friendly, Neutral, Enemy }
|
||||
|
||||
[RequireComponent(typeof(Collider))]
|
||||
public class Selectable : MonoBehaviour
|
||||
{
|
||||
[Header("Identification")]
|
||||
public string displayName = "Name";
|
||||
public Attitude attitude = Attitude.Neutral;
|
||||
|
||||
[Header("Optional visuals")]
|
||||
[Tooltip("Optional child GameObject used as a 3D outline/highlight. Enable/disable to show outline.")]
|
||||
public GameObject outlineObject;
|
||||
|
||||
// Called by selection system
|
||||
public void OnSelected()
|
||||
{
|
||||
if (outlineObject != null) outlineObject.SetActive(true);
|
||||
SelectionManager.Instance.Select(this);
|
||||
}
|
||||
|
||||
// Called by selection system
|
||||
public void OnDeselected()
|
||||
{
|
||||
if (outlineObject != null) outlineObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Selectable.cs.meta
Normal file
2
Assets/Scripts/Selectable.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e0195dcf5dc18ae4ebb6b7676aa33531
|
||||
81
Assets/Scripts/SelectionManager.cs
Normal file
81
Assets/Scripts/SelectionManager.cs
Normal file
@@ -0,0 +1,81 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
|
||||
public class SelectionManager : MonoBehaviour
|
||||
{
|
||||
public static SelectionManager Instance { get; private set; }
|
||||
|
||||
[Header("Nameplate")]
|
||||
public Canvas worldSpaceCanvas; // a World Space Canvas in scene
|
||||
public GameObject nameplatePrefab; // prefab containing TextMeshProUGUI
|
||||
public Vector3 nameplateOffset = new Vector3(0, 2.2f, 0);
|
||||
|
||||
[Header("Colors")]
|
||||
public Color friendlyColor = Color.green;
|
||||
public Color neutralColor = Color.cyan;
|
||||
public Color enemyColor = Color.red;
|
||||
|
||||
Selectable current;
|
||||
GameObject activeNameplate;
|
||||
NameplateController nameplateController;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this) Destroy(gameObject);
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
public void Select(Selectable s)
|
||||
{
|
||||
if (current == s) return;
|
||||
|
||||
ClearSelection();
|
||||
|
||||
current = s;
|
||||
|
||||
if (nameplatePrefab != null && worldSpaceCanvas != null)
|
||||
{
|
||||
activeNameplate = Instantiate(nameplatePrefab, worldSpaceCanvas.transform, false);
|
||||
nameplateController = activeNameplate.GetComponent<NameplateController>();
|
||||
if (nameplateController != null) nameplateController.Initialize(s, GetColorForAttitude(s.attitude), nameplateOffset);
|
||||
}
|
||||
}
|
||||
|
||||
public void ClearSelection()
|
||||
{
|
||||
if (current != null) current.OnDeselected();
|
||||
current = null;
|
||||
|
||||
if (activeNameplate != null) Destroy(activeNameplate);
|
||||
activeNameplate = null;
|
||||
nameplateController = null;
|
||||
}
|
||||
|
||||
Color GetColorForAttitude(Attitude a)
|
||||
{
|
||||
switch (a)
|
||||
{
|
||||
case Attitude.Friendly: return friendlyColor;
|
||||
case Attitude.Neutral: return neutralColor;
|
||||
case Attitude.Enemy: return enemyColor;
|
||||
default: return neutralColor;
|
||||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// If nameplate exists, update its world position to follow the selected target
|
||||
if (nameplateController != null && current != null)
|
||||
{
|
||||
nameplateController.FollowTarget(current.transform, nameplateOffset);
|
||||
}
|
||||
|
||||
// Optional: clear selection if target destroyed
|
||||
if (current == null && activeNameplate != null)
|
||||
{
|
||||
Destroy(activeNameplate);
|
||||
activeNameplate = null;
|
||||
nameplateController = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/SelectionManager.cs.meta
Normal file
2
Assets/Scripts/SelectionManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 833844438e957ca44921bb85bb9e97b4
|
||||
40
Assets/Scripts/SimplePlayerController.cs
Normal file
40
Assets/Scripts/SimplePlayerController.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.AI;
|
||||
|
||||
[RequireComponent(typeof(NavMeshAgent))]
|
||||
public class SimplePlayerController : MonoBehaviour
|
||||
{
|
||||
public float rotationSpeed = 10f;
|
||||
public float stopDistanceTolerance = 0.1f;
|
||||
NavMeshAgent agent;
|
||||
Vector3 lastVelocity;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
agent = GetComponent<NavMeshAgent>();
|
||||
agent.updateRotation = false; // manual rotation
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
Vector3 vel = agent.velocity;
|
||||
if (vel.sqrMagnitude > 0.01f)
|
||||
{
|
||||
Quaternion targetRot = Quaternion.LookRotation(vel.normalized);
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, targetRot, rotationSpeed * Time.deltaTime);
|
||||
}
|
||||
|
||||
if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance + stopDistanceTolerance)
|
||||
{
|
||||
agent.isStopped = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
agent.isStopped = false;
|
||||
}
|
||||
|
||||
lastVelocity = vel;
|
||||
}
|
||||
|
||||
public Vector3 GetVelocity() => lastVelocity;
|
||||
}
|
||||
2
Assets/Scripts/SimplePlayerController.cs.meta
Normal file
2
Assets/Scripts/SimplePlayerController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9b1cb8a471cfe7d47a408690f585a039
|
||||
73
Assets/Scripts/TopDownCameraController.cs
Normal file
73
Assets/Scripts/TopDownCameraController.cs
Normal file
@@ -0,0 +1,73 @@
|
||||
using UnityEngine;
|
||||
|
||||
[AddComponentMenu("Camera/Top Down Follow Camera")]
|
||||
public class TopDownCameraController : MonoBehaviour
|
||||
{
|
||||
[Header("Target")]
|
||||
public Transform target;
|
||||
|
||||
[Header("Positioning")]
|
||||
public float distance = 8f; // horizontal distance from target
|
||||
public float height = 6f; // vertical offset above target
|
||||
[Range(0f, 89f)]
|
||||
public float pitch = 80f; // X rotation in degrees (80 by default)
|
||||
|
||||
[Header("Smoothing")]
|
||||
public bool smoothPosition = true;
|
||||
public float positionSmoothTime = 0.12f;
|
||||
public bool smoothRotation = true;
|
||||
public float rotationSmoothTime = 0.08f;
|
||||
|
||||
Vector3 positionVelocity;
|
||||
Quaternion rotationVelocity;
|
||||
|
||||
void Reset()
|
||||
{
|
||||
pitch = 80f;
|
||||
distance = 8f;
|
||||
height = 6f;
|
||||
}
|
||||
|
||||
void LateUpdate()
|
||||
{
|
||||
if (target == null) return;
|
||||
|
||||
// Calculate desired camera position in local space relative to target
|
||||
// Start with a rotation that looks down by 'pitch' degrees
|
||||
Quaternion pitchRot = Quaternion.Euler(pitch, 0f, 0f);
|
||||
|
||||
// Desired offset: move back along the camera's forward by 'distance', then up by 'height'
|
||||
Vector3 localOffset = pitchRot * new Vector3(0f, height, -distance);
|
||||
|
||||
Vector3 desiredPosition = target.position + localOffset;
|
||||
|
||||
// Smooth position
|
||||
if (smoothPosition)
|
||||
{
|
||||
transform.position = Vector3.SmoothDamp(transform.position, desiredPosition, ref positionVelocity, positionSmoothTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
transform.position = desiredPosition;
|
||||
}
|
||||
|
||||
// Desired rotation: look at target but keep the pitch
|
||||
Vector3 lookDirection = (target.position - transform.position).normalized;
|
||||
Quaternion desiredRotation = Quaternion.LookRotation(lookDirection, Vector3.up);
|
||||
|
||||
// Optionally lock X rotation to the pitch value to avoid subtle tilts
|
||||
Vector3 euler = desiredRotation.eulerAngles;
|
||||
euler.x = pitch;
|
||||
desiredRotation = Quaternion.Euler(euler);
|
||||
|
||||
// Smooth rotation
|
||||
if (smoothRotation)
|
||||
{
|
||||
transform.rotation = Quaternion.Slerp(transform.rotation, desiredRotation, 1f - Mathf.Exp(-rotationSmoothTime * 60f * Time.deltaTime));
|
||||
}
|
||||
else
|
||||
{
|
||||
transform.rotation = desiredRotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/TopDownCameraController.cs.meta
Normal file
2
Assets/Scripts/TopDownCameraController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 13cefd56ccade4042bdb196df1a17efe
|
||||
Reference in New Issue
Block a user