first commit

This commit is contained in:
2026-05-26 01:16:57 +03:00
commit ecee27aa85
79 changed files with 12350 additions and 0 deletions
+36
View File
@@ -0,0 +1,36 @@
using UnityEngine;
[DisallowMultipleComponent]
public class CoverChecker : MonoBehaviour
{
[Tooltip("Если объект находится между врагом и игроком, он должен блокировать линию видимости.")]
public bool blocksVision = true;
[Tooltip("Небольшая метка для будущих расширений механики укрытий.")]
public float concealmentStrength = 1f;
public static bool IsVisionBlocked(Vector3 origin, Vector3 target, LayerMask obstacleMask)
{
Vector3 direction = target - origin;
float distance = direction.magnitude;
if (distance <= Mathf.Epsilon)
{
return false;
}
return Physics.Raycast(origin, direction.normalized, distance, obstacleMask, QueryTriggerInteraction.Ignore);
}
private void OnDrawGizmosSelected()
{
Gizmos.color = blocksVision ? new Color(0.2f, 0.7f, 1f, 0.5f) : new Color(1f, 0.4f, 0.2f, 0.3f);
Collider objectCollider = GetComponent<Collider>();
if (objectCollider != null)
{
Gizmos.matrix = transform.localToWorldMatrix;
Gizmos.DrawWireCube(objectCollider.bounds.center - transform.position, objectCollider.bounds.size);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2e3e939402d7fb1cc9bab1bf53ffc289
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+296
View File
@@ -0,0 +1,296 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
[RequireComponent(typeof(CapsuleCollider))]
public class EnemyStateMachine : MonoBehaviour
{
public enum State
{
Patrol,
Suspicion,
Alert
}
[Header("References")]
public Transform[] patrolPoints;
public NavMeshAgent agent;
public EnemyVision vision;
[Header("Movement")]
public float patrolSpeed = 2.2f;
public float suspicionSpeed = 2.8f;
public float alertSpeed = 3.8f;
public float manualRotationSpeed = 8f;
public float waypointTolerance = 0.7f;
[Header("Awareness")]
public float hearingRange = 12f;
public float suspicionWaitDuration = 2.5f;
public float loseSightDelay = 1.3f;
public float captureDistance = 1.6f;
public int captureDamage = 10;
public float captureCooldown = 1f;
public State CurrentState { get; private set; } = State.Patrol;
public static IReadOnlyList<EnemyStateMachine> ActiveEnemies => activeEnemies;
private static readonly List<EnemyStateMachine> activeEnemies = new List<EnemyStateMachine>();
private Transform player;
private PlayerHealth playerHealth;
private int patrolIndex;
private Vector3 investigationTarget;
private Vector3 lastKnownPlayerPosition;
private float suspicionTimer;
private float lastSeenTime = -999f;
private float lastCaptureTime = -999f;
private float lastHandledNoiseTimestamp = -999f;
private void OnEnable()
{
if (!activeEnemies.Contains(this))
{
activeEnemies.Add(this);
}
}
private void OnDisable()
{
activeEnemies.Remove(this);
}
private void Awake()
{
agent = agent != null ? agent : GetComponent<NavMeshAgent>();
vision = vision != null ? vision : GetComponent<EnemyVision>();
}
private void Start()
{
AcquirePlayer();
ApplySpeedForState(CurrentState);
}
private void Update()
{
AcquirePlayer();
ListenForNoise();
switch (CurrentState)
{
case State.Patrol:
UpdatePatrol();
break;
case State.Suspicion:
UpdateSuspicion();
break;
case State.Alert:
UpdateAlert();
break;
}
if (CurrentState == State.Alert && Time.time - lastSeenTime > loseSightDelay)
{
investigationTarget = lastKnownPlayerPosition;
EnterState(State.Suspicion);
}
}
public void OnPlayerDetected(Vector3 playerPos)
{
lastKnownPlayerPosition = playerPos;
lastSeenTime = Time.time;
if (CurrentState != State.Alert)
{
EnterState(State.Alert);
}
}
public void OnPlayerLost()
{
if (CurrentState == State.Alert)
{
investigationTarget = lastKnownPlayerPosition;
}
}
public void OnNoiseHeard(Vector3 noisePosition)
{
if (CurrentState == State.Alert)
{
return;
}
investigationTarget = noisePosition;
suspicionTimer = 0f;
EnterState(State.Suspicion);
}
private void UpdatePatrol()
{
if (patrolPoints == null || patrolPoints.Length == 0)
{
return;
}
Transform targetPoint = patrolPoints[patrolIndex];
MoveTowards(targetPoint.position, patrolSpeed);
if (HasReached(targetPoint.position))
{
patrolIndex = (patrolIndex + 1) % patrolPoints.Length;
}
}
private void UpdateSuspicion()
{
MoveTowards(investigationTarget, suspicionSpeed);
if (HasReached(investigationTarget))
{
suspicionTimer += Time.deltaTime;
if (suspicionTimer >= suspicionWaitDuration)
{
suspicionTimer = 0f;
EnterState(State.Patrol);
}
}
else
{
suspicionTimer = 0f;
}
}
private void UpdateAlert()
{
if (player == null)
{
return;
}
lastKnownPlayerPosition = player.position;
MoveTowards(lastKnownPlayerPosition, alertSpeed);
if (Vector3.Distance(transform.position, player.position) <= captureDistance && Time.time >= lastCaptureTime + captureCooldown)
{
lastCaptureTime = Time.time;
if (playerHealth != null)
{
playerHealth.TakeDamage(captureDamage);
}
Debug.Log("Игрок обнаружен!");
}
}
private void ListenForNoise()
{
NoiseManager.NoiseEvent noise = NoiseManager.GetClosestNoise(transform.position, hearingRange);
if (noise == null || noise.timestamp <= lastHandledNoiseTimestamp)
{
return;
}
lastHandledNoiseTimestamp = noise.timestamp;
OnNoiseHeard(noise.position);
}
private void EnterState(State newState)
{
CurrentState = newState;
ApplySpeedForState(newState);
switch (newState)
{
case State.Patrol:
suspicionTimer = 0f;
break;
case State.Suspicion:
suspicionTimer = 0f;
break;
case State.Alert:
lastSeenTime = Time.time;
break;
}
}
private void ApplySpeedForState(State state)
{
if (agent == null)
{
return;
}
switch (state)
{
case State.Patrol:
agent.speed = patrolSpeed;
break;
case State.Suspicion:
agent.speed = suspicionSpeed;
break;
case State.Alert:
agent.speed = alertSpeed;
break;
}
}
private void MoveTowards(Vector3 destination, float manualSpeed)
{
if (agent != null && agent.isActiveAndEnabled && agent.isOnNavMesh)
{
agent.SetDestination(destination);
return;
}
// Fallback нужен на случай, если NavMesh ещё не запечён или агент не смог встать на него.
Vector3 flatDestination = new Vector3(destination.x, transform.position.y, destination.z);
Vector3 direction = flatDestination - transform.position;
direction.y = 0f;
if (direction.sqrMagnitude <= 0.001f)
{
return;
}
Vector3 step = direction.normalized * manualSpeed * Time.deltaTime;
transform.position += step.sqrMagnitude > direction.sqrMagnitude ? direction : step;
Quaternion targetRotation = Quaternion.LookRotation(direction.normalized, Vector3.up);
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, Time.deltaTime * manualRotationSpeed);
}
private bool HasReached(Vector3 destination)
{
if (agent != null && agent.isActiveAndEnabled && agent.isOnNavMesh)
{
if (agent.pathPending)
{
return false;
}
return agent.remainingDistance <= Mathf.Max(agent.stoppingDistance, waypointTolerance);
}
Vector3 flatDestination = new Vector3(destination.x, transform.position.y, destination.z);
return Vector3.Distance(transform.position, flatDestination) <= waypointTolerance;
}
private void AcquirePlayer()
{
if (player != null)
{
return;
}
GameObject playerObject = GameObject.FindGameObjectWithTag("Player");
if (playerObject == null)
{
return;
}
player = playerObject.transform;
playerHealth = playerObject.GetComponent<PlayerHealth>();
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1293c8e1da110e426a9118204db9badf
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+119
View File
@@ -0,0 +1,119 @@
using UnityEngine;
public class EnemyVision : MonoBehaviour
{
[Header("References")]
public EnemyStateMachine stateMachine;
public Transform player;
[Header("Vision")]
public float viewRadius = 14f;
[Range(1f, 180f)]
public float viewAngle = 65f;
public LayerMask obstacleMask;
public LayerMask targetMask;
public float eyeHeight = 1.4f;
public float standingDetectionDelay = 0.12f;
public float crouchDetectionDelay = 0.35f;
private bool hadLineOfSight;
private float visibleTimer;
private void Awake()
{
stateMachine = stateMachine != null ? stateMachine : GetComponent<EnemyStateMachine>();
}
private void Update()
{
AcquirePlayer();
if (player == null || stateMachine == null)
{
return;
}
bool canSeePlayer = CanSeePlayer(out Vector3 eyePosition, out Vector3 targetPosition, out Color debugColor);
Debug.DrawRay(eyePosition, targetPosition - eyePosition, debugColor);
if (canSeePlayer)
{
PlayerStealth stealth = player.GetComponent<PlayerStealth>();
float requiredTime = stealth != null && stealth.IsCrouching ? crouchDetectionDelay : standingDetectionDelay;
visibleTimer += Time.deltaTime;
if (visibleTimer >= requiredTime)
{
hadLineOfSight = true;
stateMachine.OnPlayerDetected(player.position);
}
}
else
{
visibleTimer = 0f;
if (hadLineOfSight)
{
hadLineOfSight = false;
stateMachine.OnPlayerLost();
}
}
}
public float GetViewRadiusForCurrentTarget()
{
if (player == null)
{
return viewRadius;
}
PlayerStealth stealth = player.GetComponent<PlayerStealth>();
return viewRadius * (stealth != null ? stealth.VisibilityMultiplier : 1f);
}
private bool CanSeePlayer(out Vector3 eyePosition, out Vector3 targetPosition, out Color debugColor)
{
eyePosition = transform.position + Vector3.up * eyeHeight;
targetPosition = player.position + Vector3.up * 0.9f;
debugColor = Color.gray;
float effectiveRadius = GetViewRadiusForCurrentTarget();
Vector3 directionToPlayer = targetPosition - eyePosition;
float distanceToPlayer = directionToPlayer.magnitude;
if (distanceToPlayer > effectiveRadius)
{
return false;
}
float angleToPlayer = Vector3.Angle(transform.forward, directionToPlayer);
if (angleToPlayer > viewAngle * 0.5f)
{
return false;
}
int combinedMask = obstacleMask | targetMask;
if (Physics.Raycast(eyePosition, directionToPlayer.normalized, out RaycastHit hit, distanceToPlayer, combinedMask, QueryTriggerInteraction.Ignore))
{
// Если первым попался не игрок, значит между врагом и игроком есть укрытие или стена.
bool isPlayerHit = hit.transform == player || hit.transform.IsChildOf(player);
debugColor = isPlayerHit ? Color.green : Color.red;
return isPlayerHit;
}
debugColor = Color.red;
return false;
}
private void AcquirePlayer()
{
if (player != null)
{
return;
}
GameObject playerObject = GameObject.FindGameObjectWithTag("Player");
if (playerObject != null)
{
player = playerObject.transform;
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 170f19287f4f8d6a3a78c9caf9cd84d0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+214
View File
@@ -0,0 +1,214 @@
using UnityEngine;
using UnityEngine.Rendering;
public class EnemyVisionVisualizer : MonoBehaviour
{
public EnemyVision vision;
public EnemyStateMachine stateMachine;
public Material visionMaterialTemplate;
public int segmentCount = 28;
public float groundOffset = 0.05f;
private Transform meshRoot;
private MeshFilter meshFilter;
private MeshRenderer meshRenderer;
private Mesh visionMesh;
private Material runtimeMaterial;
private void Awake()
{
vision = vision != null ? vision : GetComponent<EnemyVision>();
stateMachine = stateMachine != null ? stateMachine : GetComponent<EnemyStateMachine>();
EnsureMeshObjects();
RebuildMesh();
UpdateVisualState();
}
private void LateUpdate()
{
EnsureMeshObjects();
UpdateVisualState();
}
private void OnValidate()
{
segmentCount = Mathf.Max(6, segmentCount);
if (!Application.isPlaying)
{
EnsureMeshObjects();
RebuildMesh();
UpdateVisualState();
}
}
private void EnsureMeshObjects()
{
int visualizationLayer = LayerMask.NameToLayer("Vision");
if (visualizationLayer < 0)
{
visualizationLayer = LayerMask.NameToLayer("Ignore Raycast");
}
if (meshRoot == null)
{
Transform existing = transform.Find("VisionCone");
if (existing != null)
{
meshRoot = existing;
}
else
{
GameObject root = new GameObject("VisionCone");
root.transform.SetParent(transform, false);
root.transform.localPosition = new Vector3(0f, groundOffset, 0f);
root.transform.localRotation = Quaternion.identity;
root.layer = visualizationLayer;
meshRoot = root.transform;
}
}
meshRoot.localPosition = new Vector3(0f, groundOffset, 0f);
meshRoot.gameObject.layer = visualizationLayer;
if (meshFilter == null)
{
meshFilter = meshRoot.GetComponent<MeshFilter>();
if (meshFilter == null)
{
meshFilter = meshRoot.gameObject.AddComponent<MeshFilter>();
}
}
if (meshRenderer == null)
{
meshRenderer = meshRoot.GetComponent<MeshRenderer>();
if (meshRenderer == null)
{
meshRenderer = meshRoot.gameObject.AddComponent<MeshRenderer>();
meshRenderer.shadowCastingMode = ShadowCastingMode.Off;
meshRenderer.receiveShadows = false;
}
}
if (visionMesh == null)
{
visionMesh = new Mesh { name = "EnemyVisionCone" };
meshFilter.sharedMesh = visionMesh;
}
if (runtimeMaterial == null)
{
runtimeMaterial = visionMaterialTemplate != null ? new Material(visionMaterialTemplate) : BuildTransparentMaterial();
runtimeMaterial.name = "EnemyVisionRuntimeMaterial";
meshRenderer.sharedMaterial = runtimeMaterial;
}
}
private void RebuildMesh()
{
if (vision == null || visionMesh == null)
{
return;
}
float radius = vision.viewRadius;
float halfAngle = vision.viewAngle * 0.5f;
Vector3[] vertices = new Vector3[segmentCount + 2];
int[] triangles = new int[segmentCount * 3];
Vector2[] uv = new Vector2[vertices.Length];
vertices[0] = Vector3.zero;
uv[0] = new Vector2(0.5f, 0f);
for (int i = 0; i <= segmentCount; i++)
{
float progress = i / (float)segmentCount;
float angle = -halfAngle + progress * vision.viewAngle;
float radians = Mathf.Deg2Rad * angle;
Vector3 direction = new Vector3(Mathf.Sin(radians), 0f, Mathf.Cos(radians));
vertices[i + 1] = direction * radius;
uv[i + 1] = new Vector2(progress, 1f);
if (i == segmentCount)
{
continue;
}
int triangleIndex = i * 3;
triangles[triangleIndex] = 0;
triangles[triangleIndex + 1] = i + 2;
triangles[triangleIndex + 2] = i + 1;
}
visionMesh.Clear();
visionMesh.vertices = vertices;
visionMesh.triangles = triangles;
visionMesh.uv = uv;
visionMesh.RecalculateNormals();
visionMesh.RecalculateBounds();
}
private void UpdateVisualState()
{
if (runtimeMaterial == null || stateMachine == null)
{
return;
}
Color stateColor = new Color(0.2f, 0.95f, 0.35f, 0.22f);
switch (stateMachine.CurrentState)
{
case EnemyStateMachine.State.Suspicion:
stateColor = new Color(1f, 0.85f, 0.2f, 0.24f);
break;
case EnemyStateMachine.State.Alert:
stateColor = new Color(1f, 0.25f, 0.2f, 0.28f);
break;
}
runtimeMaterial.color = stateColor;
if (runtimeMaterial.HasProperty("_Color"))
{
runtimeMaterial.SetColor("_Color", stateColor);
}
}
private Material BuildTransparentMaterial()
{
Shader shader = Shader.Find("Standard");
Material material = new Material(shader);
material.SetFloat("_Mode", 3f);
material.SetInt("_SrcBlend", (int)BlendMode.SrcAlpha);
material.SetInt("_DstBlend", (int)BlendMode.OneMinusSrcAlpha);
material.SetInt("_ZWrite", 0);
material.DisableKeyword("_ALPHATEST_ON");
material.EnableKeyword("_ALPHABLEND_ON");
material.DisableKeyword("_ALPHAPREMULTIPLY_ON");
material.renderQueue = (int)RenderQueue.Transparent;
material.color = new Color(0.2f, 0.95f, 0.35f, 0.22f);
return material;
}
private void OnDrawGizmosSelected()
{
if (vision == null)
{
vision = GetComponent<EnemyVision>();
}
if (vision == null)
{
return;
}
Gizmos.color = Color.cyan;
Vector3 origin = transform.position + Vector3.up * 0.1f;
Gizmos.DrawWireSphere(origin, vision.viewRadius);
Quaternion leftRotation = Quaternion.Euler(0f, -vision.viewAngle * 0.5f, 0f);
Quaternion rightRotation = Quaternion.Euler(0f, vision.viewAngle * 0.5f, 0f);
Gizmos.DrawLine(origin, origin + leftRotation * transform.forward * vision.viewRadius);
Gizmos.DrawLine(origin, origin + rightRotation * transform.forward * vision.viewRadius);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f37b7500e8489416b94b2a9245935426
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+30
View File
@@ -0,0 +1,30 @@
using UnityEngine;
[RequireComponent(typeof(Collider))]
public class LevelExit : MonoBehaviour
{
private bool hasTriggered;
private void Awake()
{
Collider exitCollider = GetComponent<Collider>();
exitCollider.isTrigger = true;
}
private void OnTriggerEnter(Collider other)
{
if (hasTriggered || !other.CompareTag("Player"))
{
return;
}
PlayerHealth playerHealth = other.GetComponent<PlayerHealth>();
if (playerHealth == null || !playerHealth.IsAlive)
{
return;
}
hasTriggered = true;
playerHealth.CompleteLevel();
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c01bde08ec6450b5f977a2b6c5629f9d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+105
View File
@@ -0,0 +1,105 @@
using System.Collections.Generic;
using UnityEngine;
public class NoiseManager : MonoBehaviour
{
[System.Serializable]
public class NoiseEvent
{
public Vector3 position;
public float intensity;
public float timestamp;
}
public float noiseLifetime = 1.25f;
public static NoiseManager Instance { get; private set; }
public static IReadOnlyList<NoiseEvent> ActiveNoises => activeNoises;
private static readonly List<NoiseEvent> activeNoises = new List<NoiseEvent>();
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
}
private void Update()
{
PruneExpiredNoise();
}
public static void EmitNoise(Vector3 position, float intensity)
{
EnsureInstance();
PruneExpiredNoise();
// Шум живёт короткое время, чтобы враги реагировали на свежие события, а не на старые следы.
activeNoises.Add(new NoiseEvent
{
position = position,
intensity = intensity,
timestamp = Time.time
});
}
public static NoiseEvent GetClosestNoise(Vector3 listenerPosition, float hearingRange)
{
PruneExpiredNoise();
NoiseEvent closest = null;
float closestDistance = float.MaxValue;
for (int i = 0; i < activeNoises.Count; i++)
{
NoiseEvent noise = activeNoises[i];
float effectiveRange = Mathf.Max(0.1f, hearingRange * noise.intensity);
float distance = Vector3.Distance(listenerPosition, noise.position);
if (distance > effectiveRange || distance >= closestDistance)
{
continue;
}
closest = noise;
closestDistance = distance;
}
return closest;
}
public static void ClearNoise()
{
activeNoises.Clear();
}
private static void EnsureInstance()
{
if (Instance != null || !Application.isPlaying)
{
return;
}
GameObject managerObject = new GameObject("NoiseManager");
Instance = managerObject.AddComponent<NoiseManager>();
}
private static void PruneExpiredNoise()
{
float lifetime = Instance != null ? Instance.noiseLifetime : 1.25f;
float minTimestamp = Time.time - lifetime;
for (int i = activeNoises.Count - 1; i >= 0; i--)
{
if (activeNoises[i].timestamp < minTimestamp)
{
activeNoises.RemoveAt(i);
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c64961fd4f61652f9806e6ec90f6aa58
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+84
View File
@@ -0,0 +1,84 @@
using System;
using UnityEngine;
public class PlayerHealth : MonoBehaviour
{
public int health = 100;
public bool IsAlive => health > 0;
public bool HasCompletedLevel => levelCompleted;
public event Action Died;
public event Action LevelCompleted;
private bool hasTriggeredDeath;
private bool levelCompleted;
public void TakeDamage(int amount)
{
if (!IsAlive)
{
return;
}
health = Mathf.Max(0, health - Mathf.Abs(amount));
if (!IsAlive && !hasTriggeredDeath)
{
HandleDeath();
}
}
private void Start()
{
Time.timeScale = 1f;
}
public void CompleteLevel()
{
if (!IsAlive || levelCompleted)
{
return;
}
levelCompleted = true;
DisableGameplayControl();
Debug.Log("Игрок достиг зоны выхода.");
LevelCompleted?.Invoke();
}
private void HandleDeath()
{
hasTriggeredDeath = true;
DisableGameplayControl();
Debug.Log("Игрок обнаружен и выведен из строя.");
Died?.Invoke();
}
private void DisableGameplayControl()
{
enabled = false;
SimpleFPSController fpsController = GetComponent<SimpleFPSController>();
if (fpsController != null)
{
fpsController.enabled = false;
}
PlayerStealth stealth = GetComponent<PlayerStealth>();
if (stealth != null)
{
stealth.enabled = false;
}
CharacterController characterController = GetComponent<CharacterController>();
if (characterController != null)
{
characterController.enabled = false;
}
Cursor.lockState = CursorLockMode.None;
Cursor.visible = true;
Time.timeScale = 0f;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 42e347ee8a477dca39724011d10d007c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+157
View File
@@ -0,0 +1,157 @@
using UnityEngine;
[DefaultExecutionOrder(-50)]
[RequireComponent(typeof(CharacterController))]
[RequireComponent(typeof(SimpleFPSController))]
public class PlayerStealth : MonoBehaviour
{
public enum NoiseState
{
Silent,
Low,
Medium,
High
}
[Header("References")]
public CharacterController characterController;
public SimpleFPSController fpsController;
public Camera playerCamera;
[Header("Crouch")]
public float standingHeight = 2f;
public float crouchingHeight = 1.2f;
public float crouchSpeedMultiplier = 0.55f;
public float standingCameraLocalY = 0.8f;
public float crouchCameraLocalY = 0.35f;
public float crouchTransitionSpeed = 8f;
public float crouchVisibilityMultiplier = 0.72f;
[Header("Noise")]
public float noiseEmitInterval = 0.35f;
public float crouchNoiseIntensity = 0.45f;
public float walkNoiseIntensity = 0.8f;
public float runNoiseIntensity = 1.25f;
public float testNoiseIntensity = 1.6f;
public bool IsCrouching { get; private set; }
public NoiseState CurrentNoiseState { get; private set; } = NoiseState.Silent;
public float CurrentNoiseIntensity { get; private set; }
public float VisibilityMultiplier => IsCrouching ? crouchVisibilityMultiplier : 1f;
public string CurrentNoiseLabel => CurrentNoiseState.ToString().ToLowerInvariant();
private float currentControllerHeight;
private float targetCameraLocalY;
private float lastNoiseEmitTime = -999f;
private void Awake()
{
characterController = characterController != null ? characterController : GetComponent<CharacterController>();
fpsController = fpsController != null ? fpsController : GetComponent<SimpleFPSController>();
playerCamera = playerCamera != null ? playerCamera : GetComponentInChildren<Camera>();
currentControllerHeight = standingHeight;
targetCameraLocalY = standingCameraLocalY;
}
private void Start()
{
ApplyCharacterHeightImmediate(standingHeight);
SetCameraLocalY(standingCameraLocalY);
}
private void Update()
{
UpdateCrouchState();
UpdateNoiseState();
HandleManualNoise();
}
private void LateUpdate()
{
SmoothCrouchPresentation();
}
private void UpdateCrouchState()
{
IsCrouching = Input.GetKey(KeyCode.LeftControl);
fpsController.AllowRunning = !IsCrouching;
fpsController.SpeedScale = IsCrouching ? crouchSpeedMultiplier : 1f;
targetCameraLocalY = IsCrouching ? crouchCameraLocalY : standingCameraLocalY;
}
private void UpdateNoiseState()
{
if (!fpsController.HasMovementInput)
{
SetNoiseState(NoiseState.Silent, 0f);
return;
}
if (IsCrouching)
{
SetNoiseState(NoiseState.Low, crouchNoiseIntensity);
}
else if (fpsController.IsRunning)
{
SetNoiseState(NoiseState.High, runNoiseIntensity);
}
else
{
SetNoiseState(NoiseState.Medium, walkNoiseIntensity);
}
if (CurrentNoiseState != NoiseState.Silent && Time.time >= lastNoiseEmitTime + noiseEmitInterval)
{
lastNoiseEmitTime = Time.time;
// Периодические импульсы шума проще настраивать и удобнее для AI, чем шум каждый кадр.
NoiseManager.EmitNoise(transform.position, CurrentNoiseIntensity);
}
}
private void HandleManualNoise()
{
if (Input.GetKeyDown(KeyCode.R))
{
NoiseManager.EmitNoise(transform.position, testNoiseIntensity);
}
}
private void SmoothCrouchPresentation()
{
float desiredHeight = IsCrouching ? crouchingHeight : standingHeight;
currentControllerHeight = Mathf.Lerp(currentControllerHeight, desiredHeight, Time.deltaTime * crouchTransitionSpeed);
ApplyCharacterHeightImmediate(currentControllerHeight);
if (playerCamera != null)
{
Vector3 localPosition = playerCamera.transform.localPosition;
localPosition.y = Mathf.Lerp(localPosition.y, targetCameraLocalY, Time.deltaTime * crouchTransitionSpeed);
playerCamera.transform.localPosition = localPosition;
}
}
private void ApplyCharacterHeightImmediate(float height)
{
characterController.height = height;
characterController.center = new Vector3(0f, height * 0.5f, 0f);
}
private void SetCameraLocalY(float localY)
{
if (playerCamera == null)
{
return;
}
Vector3 localPosition = playerCamera.transform.localPosition;
localPosition.y = localY;
playerCamera.transform.localPosition = localPosition;
}
private void SetNoiseState(NoiseState state, float intensity)
{
CurrentNoiseState = state;
CurrentNoiseIntensity = intensity;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7c35ac882440faf9b9493afaac932ee6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+93
View File
@@ -0,0 +1,93 @@
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class SimpleFPSController : MonoBehaviour
{
[Header("Movement")]
public float walkSpeed = 4f;
public float runSpeed = 7f;
public float mouseSensitivity = 2f;
public float gravity = -20f;
[Header("References")]
public CharacterController characterController;
public Camera playerCamera;
public Vector3 PlanarVelocity { get; private set; }
public bool HasMovementInput { get; private set; }
public bool IsRunning { get; private set; }
public float CurrentMoveSpeed { get; private set; }
public float SpeedScale { get; set; } = 1f;
public bool AllowRunning { get; set; } = true;
private float verticalVelocity;
private float cameraPitch;
private void Awake()
{
characterController = characterController != null ? characterController : GetComponent<CharacterController>();
playerCamera = playerCamera != null ? playerCamera : GetComponentInChildren<Camera>();
}
private void Start()
{
LockCursor(true);
}
private void OnApplicationFocus(bool hasFocus)
{
LockCursor(hasFocus);
}
private void Update()
{
HandleMouseLook();
HandleMovement();
}
private void HandleMouseLook()
{
float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity;
float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity;
transform.Rotate(0f, mouseX, 0f);
cameraPitch -= mouseY;
cameraPitch = Mathf.Clamp(cameraPitch, -80f, 80f);
if (playerCamera != null)
{
playerCamera.transform.localRotation = Quaternion.Euler(cameraPitch, 0f, 0f);
}
}
private void HandleMovement()
{
Vector2 input = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
input = Vector2.ClampMagnitude(input, 1f);
HasMovementInput = input.sqrMagnitude > 0.001f;
IsRunning = AllowRunning && HasMovementInput && Input.GetKey(KeyCode.LeftShift);
CurrentMoveSpeed = (IsRunning ? runSpeed : walkSpeed) * Mathf.Max(0.01f, SpeedScale);
Vector3 moveDirection = (transform.right * input.x + transform.forward * input.y).normalized;
PlanarVelocity = moveDirection * (HasMovementInput ? CurrentMoveSpeed : 0f);
if (characterController.isGrounded && verticalVelocity < 0f)
{
verticalVelocity = -2f;
}
verticalVelocity += gravity * Time.deltaTime;
Vector3 motion = PlanarVelocity;
motion.y = verticalVelocity;
characterController.Move(motion * Time.deltaTime);
}
private static void LockCursor(bool shouldLock)
{
Cursor.lockState = shouldLock ? CursorLockMode.Locked : CursorLockMode.None;
Cursor.visible = !shouldLock;
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: abb0c64b16486035691c6b0e3e895ef9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+239
View File
@@ -0,0 +1,239 @@
using System.Text;
using UnityEngine;
using UnityEngine.UI;
public class StealthIndicator : MonoBehaviour
{
[Header("UI")]
public Image stateBar;
public Text statusText;
public Text controlsText;
public Text debugText;
public Text healthText;
public Text gameOverText;
[Header("References")]
public PlayerStealth playerStealth;
public PlayerHealth playerHealth;
private readonly StringBuilder debugBuilder = new StringBuilder(256);
private bool subscribedToPlayerHealth;
private void Start()
{
PopulateControlsText();
FindPlayerReferences();
SubscribeToPlayerHealth();
if (gameOverText != null)
{
gameOverText.gameObject.SetActive(false);
}
}
private void Update()
{
FindPlayerReferences();
SubscribeToPlayerHealth();
UpdateThreatState();
UpdateDebugText();
UpdateHealthText();
}
private void PopulateControlsText()
{
if (controlsText == null)
{
return;
}
controlsText.text =
"WASD - движение\n" +
"Mouse - обзор\n" +
"Shift - бег / больше шума\n" +
"Ctrl - присесть / меньше шума\n" +
"R - тестовый шум";
}
private void UpdateThreatState()
{
int suspicionCount = 0;
int alertCount = 0;
foreach (EnemyStateMachine enemy in EnemyStateMachine.ActiveEnemies)
{
if (enemy == null)
{
continue;
}
if (enemy.CurrentState == EnemyStateMachine.State.Alert)
{
alertCount++;
}
else if (enemy.CurrentState == EnemyStateMachine.State.Suspicion)
{
suspicionCount++;
}
}
Color stateColor = new Color(0.18f, 0.85f, 0.34f, 0.95f);
string stateLabel = "Скрытность: безопасно";
if (playerHealth != null && playerHealth.HasCompletedLevel)
{
stateColor = new Color(0.3f, 1f, 0.42f, 0.95f);
stateLabel = "Скрытность: выход достигнут";
}
else if (playerHealth != null && !playerHealth.IsAlive)
{
stateColor = new Color(0.95f, 0.15f, 0.15f, 0.95f);
stateLabel = "Скрытность: провал";
}
else if (alertCount > 0)
{
stateColor = new Color(0.95f, 0.25f, 0.2f, 0.95f);
stateLabel = "Скрытность: обнаружен";
}
else if (suspicionCount > 0)
{
stateColor = new Color(1f, 0.8f, 0.15f, 0.95f);
stateLabel = "Скрытность: подозрение";
}
if (stateBar != null)
{
stateBar.color = stateColor;
}
if (statusText != null)
{
statusText.text = stateLabel;
statusText.color = stateColor;
}
}
private void UpdateDebugText()
{
if (debugText == null)
{
return;
}
int suspicionCount = 0;
int alertCount = 0;
foreach (EnemyStateMachine enemy in EnemyStateMachine.ActiveEnemies)
{
if (enemy == null)
{
continue;
}
if (enemy.CurrentState == EnemyStateMachine.State.Suspicion)
{
suspicionCount++;
}
if (enemy.CurrentState == EnemyStateMachine.State.Alert)
{
alertCount++;
}
}
debugBuilder.Length = 0;
if (playerStealth != null)
{
debugBuilder.Append("Поза: ");
debugBuilder.Append(playerStealth.IsCrouching ? "присел" : "стоит");
debugBuilder.Append('\n');
debugBuilder.Append("Шум: ");
debugBuilder.Append(playerStealth.CurrentNoiseLabel);
debugBuilder.Append('\n');
}
else
{
debugBuilder.Append("Поза: нет данных\n");
debugBuilder.Append("Шум: нет данных\n");
}
debugBuilder.Append("Враги в Suspicion: ");
debugBuilder.Append(suspicionCount);
debugBuilder.Append('\n');
debugBuilder.Append("Враги в Alert: ");
debugBuilder.Append(alertCount);
debugText.text = debugBuilder.ToString();
}
private void UpdateHealthText()
{
if (healthText == null || playerHealth == null)
{
return;
}
healthText.text = "Здоровье: " + playerHealth.health;
healthText.color = playerHealth.IsAlive ? Color.white : Color.red;
}
private void FindPlayerReferences()
{
if (playerStealth != null && playerHealth != null)
{
return;
}
GameObject playerObject = GameObject.FindGameObjectWithTag("Player");
if (playerObject == null)
{
return;
}
if (playerStealth == null)
{
playerStealth = playerObject.GetComponent<PlayerStealth>();
}
if (playerHealth == null)
{
playerHealth = playerObject.GetComponent<PlayerHealth>();
}
}
private void SubscribeToPlayerHealth()
{
if (playerHealth == null)
{
return;
}
if (subscribedToPlayerHealth)
{
return;
}
playerHealth.Died += HandlePlayerDeath;
playerHealth.LevelCompleted += HandleLevelCompleted;
subscribedToPlayerHealth = true;
}
private void HandlePlayerDeath()
{
if (gameOverText != null)
{
gameOverText.gameObject.SetActive(true);
gameOverText.text = "Игрок устранен";
gameOverText.color = new Color(1f, 0.2f, 0.2f, 1f);
}
}
private void HandleLevelCompleted()
{
if (gameOverText != null)
{
gameOverText.gameObject.SetActive(true);
gameOverText.text = "Миссия выполнена";
gameOverText.color = new Color(0.35f, 1f, 0.45f, 1f);
}
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e71600b43f1c24c2c9908aa7a64a939c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: