first commit

This commit is contained in:
2026-06-04 17:37:58 +03:00
commit 67b3baf017
91 changed files with 11209 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
public class EnemyTarget : MonoBehaviour
{
public float maxHealth = 100f;
public float currentHealth;
public Text healthText;
public Renderer targetRenderer;
private Color originalColor = Color.white;
private Collider targetCollider;
private Coroutine flashRoutine;
private void Awake()
{
if (targetRenderer == null)
targetRenderer = GetComponentInChildren<Renderer>();
targetCollider = GetComponent<Collider>();
if (targetRenderer != null)
originalColor = targetRenderer.material.color;
ResetTarget();
}
private void LateUpdate()
{
if (healthText != null && Camera.main != null)
healthText.transform.rotation = Quaternion.LookRotation(healthText.transform.position - Camera.main.transform.position);
}
public void TakeDamage(float amount)
{
if (currentHealth <= 0f)
return;
// Targets can be reused in tests through ResetTarget without rebuilding the scene.
currentHealth = Mathf.Max(0f, currentHealth - Mathf.Max(0f, amount));
UpdateHealthText();
if (currentHealth <= 0f)
{
Die();
return;
}
if (flashRoutine != null)
StopCoroutine(flashRoutine);
flashRoutine = StartCoroutine(FlashRoutine());
}
public void ResetTarget()
{
currentHealth = maxHealth;
if (targetCollider != null)
targetCollider.enabled = true;
if (targetRenderer != null)
targetRenderer.material.color = originalColor;
UpdateHealthText();
}
private void Die()
{
if (targetCollider != null)
targetCollider.enabled = false;
if (targetRenderer != null)
targetRenderer.material.color = Color.black;
UpdateHealthText();
}
private IEnumerator FlashRoutine()
{
if (targetRenderer != null)
targetRenderer.material.color = Color.yellow;
yield return new WaitForSeconds(0.08f);
if (targetRenderer != null && currentHealth > 0f)
targetRenderer.material.color = originalColor;
flashRoutine = null;
}
private void UpdateHealthText()
{
if (healthText != null)
healthText.text = "HP: " + Mathf.CeilToInt(currentHealth) + " / " + Mathf.CeilToInt(maxHealth);
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 91bdd1bdbdab9312fb7492c3e0712c7f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+74
View File
@@ -0,0 +1,74 @@
using UnityEngine;
[RequireComponent(typeof(CharacterController))]
public class SimpleFPSController : MonoBehaviour
{
public Camera playerCamera;
public float moveSpeed = 6f;
public float mouseSensitivity = 2f;
public float gravity = -20f;
public float jumpHeight = 1.2f;
public float recoilReturnSpeed = 18f;
private CharacterController characterController;
private float verticalVelocity;
private float pitch;
private float recoilPitchOffset;
private void Awake()
{
characterController = GetComponent<CharacterController>();
if (playerCamera == null)
playerCamera = GetComponentInChildren<Camera>();
}
private void Start()
{
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
}
private void Update()
{
Look();
Move();
}
public void AddRecoil(float amount)
{
recoilPitchOffset -= Mathf.Clamp(amount, 0f, 8f);
}
private void Look()
{
float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity;
float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity;
transform.Rotate(Vector3.up * mouseX);
pitch = Mathf.Clamp(pitch - mouseY, -85f, 85f);
recoilPitchOffset = Mathf.MoveTowards(recoilPitchOffset, 0f, recoilReturnSpeed * Time.deltaTime);
if (playerCamera != null)
playerCamera.transform.localRotation = Quaternion.Euler(pitch + recoilPitchOffset, 0f, 0f);
}
private void Move()
{
Vector3 input = new Vector3(Input.GetAxisRaw("Horizontal"), 0f, Input.GetAxisRaw("Vertical"));
input = Vector3.ClampMagnitude(input, 1f);
Vector3 move = transform.TransformDirection(input) * moveSpeed;
if (characterController.isGrounded && verticalVelocity < 0f)
verticalVelocity = -2f;
if (characterController.isGrounded && Input.GetKeyDown(KeyCode.Space))
verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity);
verticalVelocity += gravity * Time.deltaTime;
move.y = verticalVelocity;
characterController.Move(move * Time.deltaTime);
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cffd232c30a413e2580e7585626facfb
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+307
View File
@@ -0,0 +1,307 @@
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
public class Weapon : MonoBehaviour
{
public WeaponData data;
public Text ammoText;
public Text weaponNameText;
public Text reloadText;
public Transform firePoint;
public Camera playerCamera;
public WeaponHUD hud;
private int currentAmmo;
private int currentReserve;
private float nextFireTime;
private bool isReloading;
private Vector3 originalLocalPosition;
private Quaternion originalCameraRotation;
private bool initialized;
private AudioSource audioSource;
private Coroutine reloadRoutine;
private Coroutine recoilRoutine;
private SimpleFPSController fpsController;
private void Awake()
{
audioSource = GetComponent<AudioSource>();
originalLocalPosition = transform.localPosition;
}
private void OnEnable()
{
if (Application.isPlaying)
OnSelected();
}
private void Update()
{
if (data == null)
return;
bool wantsToShoot = data.automatic ? Input.GetButton("Fire1") : Input.GetButtonDown("Fire1");
if (wantsToShoot)
TryShoot();
if (Input.GetKeyDown(KeyCode.R))
Reload();
UpdateUI();
}
public void Initialize()
{
if (data == null || initialized)
return;
currentAmmo = Mathf.Max(0, data.magazineSize);
currentReserve = Mathf.Max(0, data.totalReserveAmmo);
originalLocalPosition = transform.localPosition;
if (playerCamera != null)
{
originalCameraRotation = playerCamera.transform.localRotation;
fpsController = playerCamera.GetComponentInParent<SimpleFPSController>();
}
initialized = true;
UpdateUI();
}
public void TryShoot()
{
if (data == null)
return;
// Shooting behavior is data-driven: automatic weapons use held Fire1, semi-auto weapons use one click.
if (isReloading)
return;
if (currentAmmo <= 0)
{
ShowMessage("No ammo. Press R to reload.");
return;
}
if (Time.time < nextFireTime)
return;
if (!CanShoot())
return;
Shoot();
}
public void Shoot()
{
if (data == null || playerCamera == null)
return;
currentAmmo--;
nextFireTime = Time.time + Mathf.Max(0.01f, data.fireRate);
PlaySound(data.shootSound);
SpawnMuzzleFlash();
int pelletCount = Mathf.Max(1, data.pelletsPerShot);
for (int i = 0; i < pelletCount; i++)
FireRay();
ApplyRecoil();
UpdateUI();
}
public void Reload()
{
if (data == null || isReloading)
return;
if (currentAmmo >= data.magazineSize)
{
ShowMessage("Magazine is full.");
return;
}
if (currentReserve <= 0)
{
ShowMessage("No reserve ammo.");
return;
}
reloadRoutine = StartCoroutine(ReloadRoutine());
}
public void UpdateUI()
{
if (data != null && weaponNameText != null)
weaponNameText.text = "Weapon: " + data.weaponName;
if (ammoText != null)
ammoText.text = "Ammo: " + currentAmmo + " / " + currentReserve;
if (reloadText != null)
reloadText.text = isReloading ? "Reloading..." : "Ready";
if (hud != null)
{
hud.SetWeapon(data);
hud.SetAmmo(currentAmmo, currentReserve);
hud.SetReloading(isReloading);
}
}
public void OnSelected()
{
Initialize();
UpdateUI();
}
public void OnDeselected()
{
if (recoilRoutine != null)
{
StopCoroutine(recoilRoutine);
recoilRoutine = null;
}
transform.localPosition = originalLocalPosition;
}
public bool CanShoot()
{
return data != null && !isReloading && currentAmmo > 0 && Time.time >= nextFireTime;
}
public int GetCurrentAmmo()
{
return currentAmmo;
}
public int GetCurrentReserve()
{
return currentReserve;
}
public bool IsReloading()
{
return isReloading;
}
private IEnumerator ReloadRoutine()
{
isReloading = true;
UpdateUI();
PlaySound(data.reloadSound);
yield return new WaitForSeconds(Mathf.Max(0f, data.reloadTime));
int neededAmmo = Mathf.Max(0, data.magazineSize - currentAmmo);
int ammoToLoad = Mathf.Min(neededAmmo, currentReserve);
currentAmmo += ammoToLoad;
currentReserve -= ammoToLoad;
isReloading = false;
reloadRoutine = null;
UpdateUI();
}
private void FireRay()
{
Vector3 origin = playerCamera.transform.position;
// Spread is applied around the camera forward vector, so all weapons use the same aiming source.
Vector2 randomOffset = Random.insideUnitCircle * Mathf.Max(0f, data.spread);
Vector3 direction = playerCamera.transform.forward +
playerCamera.transform.right * randomOffset.x +
playerCamera.transform.up * randomOffset.y;
direction.Normalize();
Debug.DrawRay(origin, direction * data.range, Color.yellow, 1f);
if (Physics.Raycast(origin, direction, out RaycastHit hit, Mathf.Max(0.1f, data.range)))
{
EnemyTarget target = hit.collider.GetComponentInParent<EnemyTarget>();
if (target != null)
target.TakeDamage(data.damage);
SpawnHitEffect(hit);
}
}
private void ApplyRecoil()
{
// The FPS controller owns camera look; the weapon only feeds a temporary visual recoil offset.
if (fpsController != null)
fpsController.AddRecoil(data.recoilForce);
else if (playerCamera != null)
playerCamera.transform.localRotation = originalCameraRotation * Quaternion.Euler(-data.recoilForce, 0f, 0f);
if (recoilRoutine != null)
StopCoroutine(recoilRoutine);
recoilRoutine = StartCoroutine(RecoilRoutine());
}
private IEnumerator RecoilRoutine()
{
Vector3 recoilPosition = originalLocalPosition - Vector3.forward * Mathf.Clamp(data.recoilForce * 0.035f, 0.02f, 0.18f);
float t = 0f;
while (t < 1f)
{
t += Time.deltaTime * 20f;
transform.localPosition = Vector3.Lerp(transform.localPosition, recoilPosition, t);
yield return null;
}
t = 0f;
while (t < 1f)
{
t += Time.deltaTime * 10f;
transform.localPosition = Vector3.Lerp(transform.localPosition, originalLocalPosition, t);
if (fpsController == null && playerCamera != null)
playerCamera.transform.localRotation = Quaternion.Slerp(playerCamera.transform.localRotation, originalCameraRotation, t);
yield return null;
}
transform.localPosition = originalLocalPosition;
recoilRoutine = null;
}
private void SpawnMuzzleFlash()
{
if (data.muzzleFlashPrefab == null || firePoint == null)
return;
GameObject flash = Instantiate(data.muzzleFlashPrefab, firePoint.position, firePoint.rotation, firePoint);
Destroy(flash, 0.08f);
}
private void SpawnHitEffect(RaycastHit hit)
{
if (data.hitEffectPrefab == null)
return;
Quaternion rotation = Quaternion.LookRotation(hit.normal);
GameObject effect = Instantiate(data.hitEffectPrefab, hit.point + hit.normal * 0.01f, rotation);
Destroy(effect, 0.8f);
}
private void PlaySound(AudioClip clip)
{
if (clip != null && audioSource != null)
audioSource.PlayOneShot(clip);
}
private void ShowMessage(string message)
{
if (hud != null)
hud.ShowMessage(message);
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c13135be72d369f4899a428acb107b04
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+23
View File
@@ -0,0 +1,23 @@
using UnityEngine;
[CreateAssetMenu(fileName = "WeaponData", menuName = "WeaponSystem/WeaponData")]
public class WeaponData : ScriptableObject
{
public string weaponName;
public float damage;
public float fireRate;
public int magazineSize;
public int totalReserveAmmo;
public float spread;
public float recoilForce;
public float reloadTime;
public int pelletsPerShot;
public float range;
public bool automatic;
public GameObject muzzleFlashPrefab;
public GameObject hitEffectPrefab;
public AudioClip shootSound;
public AudioClip reloadSound;
public Color weaponColor;
[TextArea(2, 5)] public string description;
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5413f03360d4e8dc6835d6046d00f28b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+92
View File
@@ -0,0 +1,92 @@
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
public class WeaponHUD : MonoBehaviour
{
public Text labTitleText;
public Text weaponNameText;
public Text ammoText;
public Text reloadText;
public Text hintsText;
public Text statsText;
public Text messageText;
public Text dataDrivenText;
private Coroutine messageRoutine;
private void Awake()
{
SetText(labTitleText, "Лабораторная работа №2 — Data-Driven система оружия");
SetText(hintsText, "ЛКМ — стрелять\nR — перезарядка\n1/2/3 или колесо мыши — смена оружия\nWASD + мышь — движение");
SetText(dataDrivenText, "Параметры оружия хранятся в ScriptableObject-ассетах в папке Assets/_Data. Для добавления нового оружия достаточно создать новый WeaponData и назначить его объекту Weapon.");
SetReloading(false);
}
public void SetWeapon(WeaponData data)
{
if (data == null)
{
SetText(weaponNameText, "Weapon: None");
UpdateWeaponStats(null);
return;
}
SetText(weaponNameText, "Weapon: " + data.weaponName);
UpdateWeaponStats(data);
}
public void SetAmmo(int currentAmmo, int reserve)
{
SetText(ammoText, "Ammo: " + currentAmmo + " / " + reserve);
}
public void SetReloading(bool isReloading)
{
SetText(reloadText, isReloading ? "Reloading..." : "Ready");
}
public void ShowMessage(string message)
{
if (messageText == null)
return;
if (messageRoutine != null)
StopCoroutine(messageRoutine);
messageRoutine = StartCoroutine(MessageRoutine(message));
}
public void UpdateWeaponStats(WeaponData data)
{
if (data == null)
{
SetText(statsText, "Damage: -\nFire Rate: -\nSpread: -\nRecoil: -\nMagazine: -\nPellets: -");
return;
}
string stats =
"Damage: " + data.damage.ToString("0.##") +
"\nFire Rate: " + data.fireRate.ToString("0.##") + " sec" +
"\nSpread: " + data.spread.ToString("0.###") +
"\nRecoil: " + data.recoilForce.ToString("0.##") +
"\nMagazine: " + data.magazineSize +
"\nPellets: " + Mathf.Max(1, data.pelletsPerShot);
SetText(statsText, stats);
}
private IEnumerator MessageRoutine(string message)
{
SetText(messageText, message);
yield return new WaitForSeconds(2f);
SetText(messageText, string.Empty);
messageRoutine = null;
}
private static void SetText(Text target, string value)
{
if (target != null)
target.text = value;
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f8b80e2f8ae09bd4ab5926760700b87a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+115
View File
@@ -0,0 +1,115 @@
using UnityEngine;
using UnityEngine.UI;
public class WeaponSwitcher : MonoBehaviour
{
public Weapon[] weapons;
public int currentIndex;
public Text weaponNameText;
public WeaponHUD hud;
private void Start()
{
if (weapons == null || weapons.Length == 0)
return;
for (int i = 0; i < weapons.Length; i++)
{
if (weapons[i] == null)
continue;
weapons[i].Initialize();
weapons[i].gameObject.SetActive(false);
}
currentIndex = Mathf.Clamp(currentIndex, 0, weapons.Length - 1);
SwitchTo(currentIndex);
}
private void Update()
{
if (weapons == null || weapons.Length == 0)
return;
if (Input.GetKeyDown(KeyCode.Alpha1))
SwitchTo(0);
else if (Input.GetKeyDown(KeyCode.Alpha2))
SwitchTo(1);
else if (Input.GetKeyDown(KeyCode.Alpha3))
SwitchTo(2);
float scroll = Input.GetAxis("Mouse ScrollWheel");
if (scroll > 0.01f)
NextWeapon();
else if (scroll < -0.01f)
PreviousWeapon();
}
public void SwitchTo(int index)
{
if (weapons == null || weapons.Length == 0 || index < 0 || index >= weapons.Length)
return;
// Reloading locks weapon switching to keep ammo state predictable.
Weapon currentWeapon = GetCurrentWeapon();
if (currentWeapon != null && currentWeapon.IsReloading())
{
if (hud != null)
hud.ShowMessage("Cannot switch while reloading.");
return;
}
for (int i = 0; i < weapons.Length; i++)
{
if (weapons[i] == null)
continue;
bool active = i == index;
if (!active && weapons[i].gameObject.activeSelf)
weapons[i].OnDeselected();
weapons[i].gameObject.SetActive(active);
}
currentIndex = index;
Weapon selected = GetCurrentWeapon();
if (selected != null)
{
selected.OnSelected();
if (weaponNameText != null && selected.data != null)
weaponNameText.text = "Weapon: " + selected.data.weaponName;
if (hud != null)
hud.SetWeapon(selected.data);
}
}
public void NextWeapon()
{
if (weapons == null || weapons.Length == 0)
return;
SwitchTo((currentIndex + 1) % weapons.Length);
}
public void PreviousWeapon()
{
if (weapons == null || weapons.Length == 0)
return;
int nextIndex = currentIndex - 1;
if (nextIndex < 0)
nextIndex = weapons.Length - 1;
SwitchTo(nextIndex);
}
public Weapon GetCurrentWeapon()
{
if (weapons == null || weapons.Length == 0 || currentIndex < 0 || currentIndex >= weapons.Length)
return null;
return weapons[currentIndex];
}
}
+11
View File
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dfb51c15323d884dd814637ab1d51485
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: