first commit
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2e3e939402d7fb1cc9bab1bf53ffc289
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1293c8e1da110e426a9118204db9badf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 170f19287f4f8d6a3a78c9caf9cd84d0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c01bde08ec6450b5f977a2b6c5629f9d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c64961fd4f61652f9806e6ec90f6aa58
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 42e347ee8a477dca39724011d10d007c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7c35ac882440faf9b9493afaac932ee6
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e71600b43f1c24c2c9908aa7a64a939c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user