mirror of
https://github.com/kuhyx/praca_magisterska.git
synced 2026-07-04 13:23:05 +02:00
feat: make the game okish
This commit is contained in:
parent
0e60cbd402
commit
a9cfdb1245
@ -44,7 +44,7 @@ namespace Magisterka.BulletHell
|
||||
|
||||
_body = gameObject.AddComponent<Rigidbody2D>();
|
||||
_body.gravityScale = 0f;
|
||||
_body.isKinematic = true;
|
||||
_body.bodyType = RigidbodyType2D.Kinematic;
|
||||
_body.interpolation = RigidbodyInterpolation2D.Interpolate;
|
||||
|
||||
_collider = gameObject.AddComponent<CircleCollider2D>();
|
||||
@ -75,6 +75,22 @@ namespace Magisterka.BulletHell
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
if (other.TryGetComponent(out Bullet otherBullet))
|
||||
{
|
||||
if (otherBullet == this || otherBullet._ownerFaction == _ownerFaction)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (otherBullet.gameObject.activeInHierarchy)
|
||||
{
|
||||
otherBullet.Despawn();
|
||||
}
|
||||
|
||||
Despawn();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!other.TryGetComponent(out Health health))
|
||||
{
|
||||
return;
|
||||
|
||||
@ -27,7 +27,6 @@ namespace Magisterka.BulletHell
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
|
||||
public void Initialize(Transform parent)
|
||||
@ -54,6 +53,13 @@ namespace Magisterka.BulletHell
|
||||
var color = hitFaction == Faction.Player ? playerColor : enemyColor;
|
||||
var endSize = Mathf.Lerp(0.6f, 1.8f, Mathf.Clamp01(intensity + 0.2f));
|
||||
PlayPulse(position, color, 0.25f, endSize, 0.18f);
|
||||
|
||||
if (hitFaction == Faction.Player)
|
||||
{
|
||||
float boosted = Mathf.Clamp01(intensity + 0.3f);
|
||||
PlayPulse(position, Color.white, 0.18f, Mathf.Lerp(1.4f, 2.8f, boosted), 0.24f);
|
||||
PlayPulse(position, playerColor, 0.45f, Mathf.Lerp(1.8f, 3.4f, boosted), 0.32f);
|
||||
}
|
||||
}
|
||||
|
||||
public void SpawnExplosion(Vector2 position, Faction faction, float size = 2.5f)
|
||||
@ -66,8 +72,6 @@ namespace Magisterka.BulletHell
|
||||
float duration = 0.22f + i * 0.06f;
|
||||
PlayPulse(position, color, start, end, duration);
|
||||
}
|
||||
|
||||
_cameraShaker?.Shake(0.18f, 0.45f * size);
|
||||
}
|
||||
|
||||
public void SpawnScreenClear(Vector2 position, float radius)
|
||||
@ -85,6 +89,11 @@ namespace Magisterka.BulletHell
|
||||
_cameraShaker?.Shake(0.4f, 1.2f * radius);
|
||||
}
|
||||
|
||||
public void ShakeCamera(float duration, float strength)
|
||||
{
|
||||
_cameraShaker?.Shake(duration, strength);
|
||||
}
|
||||
|
||||
private void Warmup(int count)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
|
||||
88
magisterka_2/Assets/Scripts/EnemyBlueprint.cs
Normal file
88
magisterka_2/Assets/Scripts/EnemyBlueprint.cs
Normal file
@ -0,0 +1,88 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Magisterka.BulletHell
|
||||
{
|
||||
public enum EnemyMovementStyle
|
||||
{
|
||||
ZigZag,
|
||||
Straight,
|
||||
Sine,
|
||||
Orbit
|
||||
}
|
||||
|
||||
public enum EnemyFireStyle
|
||||
{
|
||||
None,
|
||||
Radial,
|
||||
Stream,
|
||||
Burst
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runtime configuration for enemy archetypes.
|
||||
/// </summary>
|
||||
public sealed class EnemyBlueprint
|
||||
{
|
||||
public EnemyBlueprint(
|
||||
string name,
|
||||
Color color,
|
||||
float scale,
|
||||
EnemyMovementStyle movement,
|
||||
EnemyFireStyle fire,
|
||||
float moveSpeed,
|
||||
float horizontalAmplitude,
|
||||
float horizontalFrequency,
|
||||
float fireInterval,
|
||||
int bulletsPerVolley,
|
||||
int aimedShots,
|
||||
float bulletSpeed,
|
||||
float bulletDamage,
|
||||
float contactDamage,
|
||||
float health,
|
||||
int score,
|
||||
float orbitRadius,
|
||||
Sprite sprite,
|
||||
float centeringBias)
|
||||
{
|
||||
Name = name;
|
||||
Color = color;
|
||||
Scale = scale;
|
||||
Movement = movement;
|
||||
Fire = fire;
|
||||
MoveSpeed = moveSpeed;
|
||||
HorizontalAmplitude = horizontalAmplitude;
|
||||
HorizontalFrequency = horizontalFrequency;
|
||||
FireInterval = fireInterval;
|
||||
BulletsPerVolley = bulletsPerVolley;
|
||||
AimedShots = aimedShots;
|
||||
BulletSpeed = bulletSpeed;
|
||||
BulletDamage = bulletDamage;
|
||||
ContactDamage = contactDamage;
|
||||
Health = health;
|
||||
Score = score;
|
||||
OrbitRadius = orbitRadius;
|
||||
Sprite = sprite;
|
||||
CenteringBias = centeringBias;
|
||||
}
|
||||
|
||||
public string Name { get; }
|
||||
public Color Color { get; }
|
||||
public float Scale { get; }
|
||||
public EnemyMovementStyle Movement { get; }
|
||||
public EnemyFireStyle Fire { get; }
|
||||
public float MoveSpeed { get; }
|
||||
public float HorizontalAmplitude { get; }
|
||||
public float HorizontalFrequency { get; }
|
||||
public float FireInterval { get; }
|
||||
public int BulletsPerVolley { get; }
|
||||
public int AimedShots { get; }
|
||||
public float BulletSpeed { get; }
|
||||
public float BulletDamage { get; }
|
||||
public float ContactDamage { get; }
|
||||
public float Health { get; }
|
||||
public int Score { get; }
|
||||
public float OrbitRadius { get; }
|
||||
public Sprite Sprite { get; }
|
||||
public float CenteringBias { get; }
|
||||
}
|
||||
}
|
||||
2
magisterka_2/Assets/Scripts/EnemyBlueprint.cs.meta
Normal file
2
magisterka_2/Assets/Scripts/EnemyBlueprint.cs.meta
Normal file
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c02072540f72f170b8efc887586f480
|
||||
@ -1,4 +1,3 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Magisterka.BulletHell
|
||||
@ -9,112 +8,206 @@ namespace Magisterka.BulletHell
|
||||
[RequireComponent(typeof(Health))]
|
||||
public class EnemyController : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private float moveSpeed = 3f;
|
||||
[SerializeField] private float bulletSpeed = 12f;
|
||||
[SerializeField] private float bulletDamage = 8f;
|
||||
[SerializeField] private float fireInterval = 0.6f;
|
||||
[SerializeField] private int scoreValue = 100;
|
||||
[SerializeField] private float despawnPadding = 2f;
|
||||
|
||||
private EnemySpawner _spawner;
|
||||
private BulletPool _bulletPool;
|
||||
private PlayerController _player;
|
||||
private BulletPool _bulletPool;
|
||||
private PlayerController _player;
|
||||
private EnemyBlueprint _config;
|
||||
private Health _health;
|
||||
private float _phaseSeed;
|
||||
private float _zigzagWidth = 2.8f;
|
||||
private float _difficulty;
|
||||
private Coroutine _fireRoutine;
|
||||
private float _moveTime;
|
||||
private float _phaseSeed;
|
||||
private float _fireTimer;
|
||||
private float _baseX;
|
||||
private Vector2 _worldBounds;
|
||||
|
||||
public void Initialize(EnemySpawner spawner, BulletPool bulletPool, PlayerController player, float difficultyScale, float healthValue)
|
||||
public void Initialize(EnemySpawner spawner, BulletPool bulletPool, PlayerController player, EnemyBlueprint blueprint, float difficultyScale, Vector2 worldBounds)
|
||||
{
|
||||
_spawner = spawner;
|
||||
_bulletPool = bulletPool;
|
||||
_player = player;
|
||||
_config = blueprint;
|
||||
_difficulty = difficultyScale;
|
||||
_health = GetComponent<Health>();
|
||||
_health.Configure(Faction.Enemy, healthValue);
|
||||
_worldBounds = worldBounds;
|
||||
_health.Configure(Faction.Enemy, blueprint.Health + difficultyScale * 12f);
|
||||
_health.Died += HandleDeath;
|
||||
_phaseSeed = Random.Range(0f, 360f);
|
||||
|
||||
moveSpeed += difficultyScale * 0.8f;
|
||||
fireInterval = Mathf.Max(0.22f, fireInterval - difficultyScale * 0.08f);
|
||||
bulletSpeed += difficultyScale * 1.4f;
|
||||
bulletDamage += difficultyScale * 0.6f;
|
||||
scoreValue += Mathf.RoundToInt(40f * difficultyScale);
|
||||
|
||||
if (_fireRoutine != null)
|
||||
var collider = GetComponent<CircleCollider2D>();
|
||||
if (collider != null)
|
||||
{
|
||||
StopCoroutine(_fireRoutine);
|
||||
collider.radius = 0.4f * blueprint.Scale;
|
||||
}
|
||||
|
||||
_fireRoutine = StartCoroutine(FireRoutine());
|
||||
_phaseSeed = Random.Range(0f, Mathf.PI * 2f);
|
||||
_baseX = transform.position.x;
|
||||
_moveTime = 0f;
|
||||
_fireTimer = GetFireDelay(true);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
var pos = transform.position;
|
||||
pos += Vector3.down * (moveSpeed * Time.deltaTime);
|
||||
pos.x += Mathf.Sin(Time.time * (2f + _difficulty) + _phaseSeed) * (_zigzagWidth * Time.deltaTime);
|
||||
transform.position = pos;
|
||||
AdvanceMovement();
|
||||
HandleFiring();
|
||||
|
||||
if (transform.position.y < -12f)
|
||||
float bottomLimit = -_worldBounds.y - despawnPadding;
|
||||
if (transform.position.y < bottomLimit)
|
||||
{
|
||||
_spawner?.DespawnEnemy(this);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator FireRoutine()
|
||||
private void AdvanceMovement()
|
||||
{
|
||||
yield return new WaitForSeconds(Random.Range(0.15f, fireInterval));
|
||||
_moveTime += Time.deltaTime;
|
||||
var pos = transform.position;
|
||||
float speed = _config.MoveSpeed + _difficulty * 0.2f;
|
||||
|
||||
while (true)
|
||||
switch (_config.Movement)
|
||||
{
|
||||
FirePattern();
|
||||
yield return new WaitForSeconds(fireInterval);
|
||||
case EnemyMovementStyle.ZigZag:
|
||||
pos += Vector3.down * (speed * Time.deltaTime);
|
||||
pos.x += Mathf.Sin((_moveTime + _phaseSeed) * (1.4f + _config.HorizontalFrequency)) * (_config.HorizontalAmplitude * Time.deltaTime);
|
||||
break;
|
||||
case EnemyMovementStyle.Straight:
|
||||
pos += Vector3.down * (speed * Time.deltaTime);
|
||||
break;
|
||||
case EnemyMovementStyle.Sine:
|
||||
pos += Vector3.down * (speed * Time.deltaTime);
|
||||
pos.x = _baseX + Mathf.Sin((_moveTime + _phaseSeed) * (1f + _config.HorizontalFrequency)) * _config.HorizontalAmplitude;
|
||||
break;
|
||||
case EnemyMovementStyle.Orbit:
|
||||
float orbitSpeed = _config.HorizontalFrequency + 0.8f + _difficulty * 0.05f;
|
||||
pos += Vector3.down * (speed * 0.6f * Time.deltaTime);
|
||||
pos.x = _baseX + Mathf.Cos((_moveTime + _phaseSeed) * orbitSpeed) * _config.OrbitRadius;
|
||||
pos.y += Mathf.Sin((_moveTime + _phaseSeed) * orbitSpeed) * (_config.OrbitRadius * 0.25f) * Time.deltaTime;
|
||||
break;
|
||||
}
|
||||
|
||||
if (_config.CenteringBias > 0f)
|
||||
{
|
||||
float lerpFactor = Mathf.Clamp01(_config.CenteringBias * Time.deltaTime);
|
||||
pos.x = Mathf.Lerp(pos.x, 0f, lerpFactor);
|
||||
}
|
||||
|
||||
ClampWithinBounds(ref pos);
|
||||
transform.position = pos;
|
||||
}
|
||||
|
||||
private void HandleFiring()
|
||||
{
|
||||
if (_config.Fire == EnemyFireStyle.None)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_fireTimer -= Time.deltaTime;
|
||||
if (_fireTimer > 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FirePattern();
|
||||
_fireTimer = GetFireDelay(false);
|
||||
}
|
||||
|
||||
private void FirePattern()
|
||||
{
|
||||
int radialBullets = Mathf.Clamp(Mathf.RoundToInt(16 + _difficulty * 14f), 12, 48);
|
||||
float angleStep = 360f / radialBullets;
|
||||
float angleOffset = Time.time * 60f + _phaseSeed;
|
||||
|
||||
for (int i = 0; i < radialBullets; i++)
|
||||
switch (_config.Fire)
|
||||
{
|
||||
float angle = (angleStep * i + angleOffset) * Mathf.Deg2Rad;
|
||||
var dir = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle));
|
||||
_bulletPool.Spawn(transform.position, dir, bulletSpeed, bulletDamage);
|
||||
case EnemyFireStyle.Radial:
|
||||
FireRadial();
|
||||
break;
|
||||
case EnemyFireStyle.Stream:
|
||||
FireStream();
|
||||
break;
|
||||
case EnemyFireStyle.Burst:
|
||||
FireBurst();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Additional forward stream for dense barrages.
|
||||
var forward = Vector2.down;
|
||||
for (int i = -2; i <= 2; i++)
|
||||
private void FireRadial()
|
||||
{
|
||||
int bullets = Mathf.Clamp(_config.BulletsPerVolley + Mathf.RoundToInt(_difficulty), 6, 24);
|
||||
float angleStep = 360f / bullets;
|
||||
float offset = (_moveTime + _phaseSeed) * 60f;
|
||||
|
||||
for (int i = 0; i < bullets; i++)
|
||||
{
|
||||
var spreadDir = Quaternion.Euler(0f, 0f, i * 8f) * forward;
|
||||
_bulletPool.Spawn(transform.position, spreadDir, bulletSpeed * 1.2f, bulletDamage * 0.8f);
|
||||
float angle = (angleStep * i + offset) * Mathf.Deg2Rad;
|
||||
var dir = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle));
|
||||
_bulletPool.Spawn(transform.position, dir, _config.BulletSpeed * 0.2f, _config.BulletDamage);
|
||||
}
|
||||
}
|
||||
|
||||
private void FireStream()
|
||||
{
|
||||
var forward = Vector2.down;
|
||||
int count = Mathf.Clamp(_config.BulletsPerVolley, 2, 6);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
float spread = (i - (count - 1) * 0.5f) * 7f;
|
||||
var dir = Quaternion.Euler(0f, 0f, spread) * forward;
|
||||
_bulletPool.Spawn(transform.position, dir, _config.BulletSpeed * 0.2f, _config.BulletDamage);
|
||||
}
|
||||
|
||||
if (_player != null && _player.isActiveAndEnabled)
|
||||
{
|
||||
var toPlayer = ((Vector2)_player.transform.position - (Vector2)transform.position).normalized;
|
||||
int aimedShots = Mathf.Clamp(3 + Mathf.RoundToInt(_difficulty), 3, 8);
|
||||
for (int i = 0; i < aimedShots; i++)
|
||||
_bulletPool.Spawn(transform.position, toPlayer, _config.BulletSpeed * 0.22f, _config.BulletDamage);
|
||||
}
|
||||
}
|
||||
|
||||
private void FireBurst()
|
||||
{
|
||||
int volleys = Mathf.Clamp(_config.BulletsPerVolley, 1, 3);
|
||||
var baseDir = Vector2.down;
|
||||
|
||||
for (int v = 0; v < volleys; v++)
|
||||
{
|
||||
float spread = 12f + v * 4f;
|
||||
for (int i = -1; i <= 1; i++)
|
||||
{
|
||||
float sway = (i - (aimedShots - 1) * 0.5f) * 4f;
|
||||
var dir = Quaternion.Euler(0f, 0f, sway) * toPlayer;
|
||||
_bulletPool.Spawn(transform.position, dir, bulletSpeed * 1.4f, bulletDamage * 0.9f);
|
||||
var dir = Quaternion.Euler(0f, 0f, i * spread) * baseDir;
|
||||
_bulletPool.Spawn(transform.position, dir, _config.BulletSpeed * (0.18f + v * 0.02f), _config.BulletDamage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private float GetFireDelay(bool first)
|
||||
{
|
||||
if (_config.Fire == EnemyFireStyle.None)
|
||||
{
|
||||
return float.MaxValue;
|
||||
}
|
||||
|
||||
float baseDelay = Mathf.Max(0.3f, _config.FireInterval - _difficulty * 0.04f);
|
||||
return first ? Random.Range(0.2f, baseDelay) : baseDelay;
|
||||
}
|
||||
|
||||
private void HandleDeath(Health _)
|
||||
{
|
||||
EffectManager.Instance?.SpawnExplosion(transform.position, Faction.Enemy, 3f + _difficulty);
|
||||
ScoreManager.Instance?.AddScore(scoreValue);
|
||||
EffectManager.Instance?.SpawnExplosion(transform.position, Faction.Enemy, 2.6f + _difficulty * 0.2f);
|
||||
ScoreManager.Instance?.AddScore(_config.Score + Mathf.RoundToInt(_difficulty * 20f));
|
||||
_spawner?.NotifyEnemyKilled(this);
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
if (!other.TryGetComponent(out PlayerController player))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
player.ApplyDamage(Mathf.Max(10f, _config.ContactDamage));
|
||||
EffectManager.Instance?.SpawnHitEffect(player.transform.position, Faction.Player, 0.9f);
|
||||
_health?.Kill();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (_health != null)
|
||||
@ -122,5 +215,17 @@ namespace Magisterka.BulletHell
|
||||
_health.Died -= HandleDeath;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClampWithinBounds(ref Vector3 position)
|
||||
{
|
||||
if (_worldBounds == Vector2.zero)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
position.x = Mathf.Clamp(position.x, -_worldBounds.x + 0.6f, _worldBounds.x - 0.6f);
|
||||
float upperLimit = _worldBounds.y + 4f;
|
||||
position.y = Mathf.Min(position.y, upperLimit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,21 +11,26 @@ namespace Magisterka.BulletHell
|
||||
{
|
||||
public static EnemySpawner Instance { get; private set; }
|
||||
|
||||
[SerializeField] private float totalDuration = 300f;
|
||||
[SerializeField] private float spawnDelayStart = 1.2f;
|
||||
[SerializeField] private float spawnDelayEnd = 0.18f;
|
||||
[SerializeField] private float spawnHeight = 10f;
|
||||
public float TotalDuration => totalDuration;
|
||||
public bool HasActiveEnemies => _liveEnemies.Count > 0;
|
||||
|
||||
[SerializeField] private float totalDuration = 90f;
|
||||
[SerializeField] private float spawnDelayStart = 1.6f;
|
||||
[SerializeField] private float spawnDelayEnd = 0.45f;
|
||||
[SerializeField] private float spawnInset = 0.3f;
|
||||
[SerializeField] private float horizontalPadding = 1.2f;
|
||||
|
||||
private readonly List<EnemyController> _liveEnemies = new List<EnemyController>();
|
||||
|
||||
private BulletPool _enemyBulletPool;
|
||||
private Camera _camera;
|
||||
private Sprite _enemySprite;
|
||||
private float _elapsed;
|
||||
private Vector2 _worldBounds;
|
||||
|
||||
private PlayerController _player;
|
||||
private Vector2 _worldBounds;
|
||||
private EnemyBlueprint[] _blueprints;
|
||||
private Coroutine _spawnRoutine;
|
||||
private float _elapsed;
|
||||
private bool _spawningActive;
|
||||
private int _introIndex;
|
||||
|
||||
public void Initialize(Camera camera, BulletPool enemyPool, PlayerController player, Vector2 worldBounds)
|
||||
{
|
||||
@ -41,9 +46,12 @@ namespace Magisterka.BulletHell
|
||||
_enemyBulletPool = enemyPool;
|
||||
_player = player;
|
||||
_worldBounds = worldBounds;
|
||||
_enemySprite = BuildEnemySprite();
|
||||
_blueprints = BuildBlueprints();
|
||||
|
||||
StartCoroutine(SpawnRoutine());
|
||||
_spawningActive = true;
|
||||
_elapsed = 0f;
|
||||
_introIndex = 0;
|
||||
_spawnRoutine = StartCoroutine(SpawnRoutine());
|
||||
}
|
||||
|
||||
public void NotifyEnemyKilled(EnemyController enemy)
|
||||
@ -64,8 +72,9 @@ namespace Magisterka.BulletHell
|
||||
|
||||
public void ClearScreen()
|
||||
{
|
||||
foreach (var enemy in _liveEnemies.ToArray())
|
||||
for (int i = _liveEnemies.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var enemy = _liveEnemies[i];
|
||||
if (enemy != null && enemy.TryGetComponent(out Health health))
|
||||
{
|
||||
health.Kill();
|
||||
@ -75,17 +84,34 @@ namespace Magisterka.BulletHell
|
||||
_enemyBulletPool?.ClearLiveBullets();
|
||||
}
|
||||
|
||||
public void StopSpawning()
|
||||
{
|
||||
_spawningActive = false;
|
||||
|
||||
if (_spawnRoutine != null)
|
||||
{
|
||||
StopCoroutine(_spawnRoutine);
|
||||
_spawnRoutine = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void StopAndClear()
|
||||
{
|
||||
StopSpawning();
|
||||
ClearScreen();
|
||||
}
|
||||
|
||||
private IEnumerator SpawnRoutine()
|
||||
{
|
||||
yield return new WaitForSeconds(1.5f);
|
||||
|
||||
while (_elapsed < totalDuration)
|
||||
while (_spawningActive && _elapsed < totalDuration)
|
||||
{
|
||||
SpawnWave();
|
||||
|
||||
float progress = Mathf.Clamp01(_elapsed / totalDuration);
|
||||
float delay = Mathf.Lerp(spawnDelayStart, spawnDelayEnd, progress);
|
||||
delay = Mathf.Max(0.08f, delay);
|
||||
delay = Mathf.Max(0.4f, delay);
|
||||
yield return new WaitForSeconds(delay);
|
||||
_elapsed += delay;
|
||||
}
|
||||
@ -93,49 +119,161 @@ namespace Magisterka.BulletHell
|
||||
|
||||
private void SpawnWave()
|
||||
{
|
||||
int targetCount = Mathf.Clamp(Mathf.RoundToInt(3 + _elapsed * 0.12f), 4, 18);
|
||||
float difficulty = Mathf.Clamp01(_elapsed / totalDuration) * 8f;
|
||||
float progression = Mathf.Clamp01(_elapsed / totalDuration);
|
||||
|
||||
if (_introIndex < _blueprints.Length)
|
||||
{
|
||||
var introBlueprint = _blueprints[_introIndex];
|
||||
_introIndex++;
|
||||
float topEdge = _camera.transform.position.y + _camera.orthographicSize;
|
||||
Vector2 spawnPos = new Vector2(0f, topEdge - Mathf.Max(0.02f, spawnInset));
|
||||
SpawnEnemy(spawnPos, introBlueprint, Mathf.Lerp(0.5f, 2.5f, progression));
|
||||
return;
|
||||
}
|
||||
|
||||
int available = Mathf.Clamp(_introIndex, 1, _blueprints.Length);
|
||||
int targetCount = Mathf.Clamp(Mathf.RoundToInt(1 + _elapsed * 0.05f), 2, 10);
|
||||
float difficulty = Mathf.Lerp(1.2f, 6f, progression);
|
||||
|
||||
for (int i = 0; i < targetCount; i++)
|
||||
{
|
||||
Vector2 spawnPos = PickSpawnPosition(i, targetCount);
|
||||
SpawnEnemy(spawnPos, difficulty);
|
||||
var blueprint = _blueprints[Random.Range(0, available)];
|
||||
Vector2 spawnPos = PickSpawnPosition(i, targetCount, blueprint.CenteringBias);
|
||||
SpawnEnemy(spawnPos, blueprint, difficulty);
|
||||
}
|
||||
}
|
||||
|
||||
private Vector2 PickSpawnPosition(int index, int total)
|
||||
private Vector2 PickSpawnPosition(int index, int total, float centeringBias)
|
||||
{
|
||||
float camWidth = _worldBounds.x;
|
||||
float step = (camWidth * 2f - horizontalPadding * 2f) / Mathf.Max(1, total - 1);
|
||||
float x = -camWidth + horizontalPadding + step * index + Random.Range(-0.6f, 0.6f);
|
||||
float y = _camera.orthographicSize + spawnHeight;
|
||||
if (centeringBias > 0f)
|
||||
{
|
||||
float bias = Mathf.Clamp01(centeringBias * 0.15f);
|
||||
x = Mathf.Lerp(x, 0f, bias);
|
||||
}
|
||||
float top = _camera.transform.position.y + _camera.orthographicSize;
|
||||
float y = top - Mathf.Max(0.02f, spawnInset);
|
||||
return new Vector2(x, y);
|
||||
}
|
||||
|
||||
private void SpawnEnemy(Vector2 position, float difficulty)
|
||||
private void SpawnEnemy(Vector2 position, EnemyBlueprint blueprint, float difficulty)
|
||||
{
|
||||
var enemyGO = new GameObject("Enemy");
|
||||
var enemyGO = new GameObject($"Enemy_{blueprint.Name}");
|
||||
enemyGO.transform.SetParent(transform, false);
|
||||
enemyGO.transform.position = position;
|
||||
enemyGO.transform.localScale = Vector3.one * 1.6f;
|
||||
enemyGO.transform.localScale = Vector3.one * blueprint.Scale;
|
||||
|
||||
var renderer = enemyGO.AddComponent<SpriteRenderer>();
|
||||
renderer.sprite = _enemySprite;
|
||||
renderer.sprite = blueprint.Sprite;
|
||||
renderer.sortingOrder = 10;
|
||||
renderer.color = Color.Lerp(new Color(1f, 0.6f, 0.3f, 1f), new Color(1f, 0.1f, 0.2f, 1f), Mathf.Clamp01(difficulty / 8f));
|
||||
renderer.color = blueprint.Color;
|
||||
|
||||
var collider = enemyGO.AddComponent<CircleCollider2D>();
|
||||
collider.isTrigger = true;
|
||||
collider.radius = 0.7f;
|
||||
collider.radius = 0.45f * blueprint.Scale;
|
||||
|
||||
var health = enemyGO.AddComponent<Health>();
|
||||
enemyGO.AddComponent<Health>();
|
||||
|
||||
var controller = enemyGO.AddComponent<EnemyController>();
|
||||
controller.Initialize(this, _enemyBulletPool, _player, difficulty, 30f + difficulty * 20f);
|
||||
controller.Initialize(this, _enemyBulletPool, _player, blueprint, difficulty, _worldBounds);
|
||||
|
||||
_liveEnemies.Add(controller);
|
||||
}
|
||||
|
||||
private EnemyBlueprint[] BuildBlueprints()
|
||||
{
|
||||
var blazeColor = new Color(1f, 0.55f, 0.25f, 1f);
|
||||
var strikerColor = new Color(0.55f, 0.45f, 1f, 1f);
|
||||
var cascadeColor = new Color(0.35f, 1f, 0.65f, 1f);
|
||||
var interceptorColor = new Color(1f, 0.9f, 0.3f, 1f);
|
||||
|
||||
return new[]
|
||||
{
|
||||
new EnemyBlueprint(
|
||||
"Blazer",
|
||||
blazeColor,
|
||||
1.6f,
|
||||
EnemyMovementStyle.ZigZag,
|
||||
EnemyFireStyle.Radial,
|
||||
2.8f,
|
||||
2.8f,
|
||||
1.1f,
|
||||
1.3f,
|
||||
12,
|
||||
3,
|
||||
13f,
|
||||
7f,
|
||||
25f,
|
||||
55f,
|
||||
140,
|
||||
0f,
|
||||
GenerateSprite(EnemyShape.Diamond, blazeColor),
|
||||
1.2f),
|
||||
new EnemyBlueprint(
|
||||
"Striker",
|
||||
strikerColor,
|
||||
1.4f,
|
||||
EnemyMovementStyle.Straight,
|
||||
EnemyFireStyle.Stream,
|
||||
3.3f,
|
||||
0f,
|
||||
0f,
|
||||
1f,
|
||||
4,
|
||||
2,
|
||||
18f,
|
||||
9f,
|
||||
30f,
|
||||
65f,
|
||||
160,
|
||||
0f,
|
||||
GenerateSprite(EnemyShape.Triangle, strikerColor),
|
||||
1.5f),
|
||||
new EnemyBlueprint(
|
||||
"Cascade",
|
||||
cascadeColor,
|
||||
1.7f,
|
||||
EnemyMovementStyle.Sine,
|
||||
EnemyFireStyle.Burst,
|
||||
2.5f,
|
||||
3.2f,
|
||||
1.2f,
|
||||
1.8f,
|
||||
2,
|
||||
0,
|
||||
12f,
|
||||
10f,
|
||||
35f,
|
||||
80f,
|
||||
200,
|
||||
0f,
|
||||
GenerateSprite(EnemyShape.Disc, cascadeColor),
|
||||
1.1f),
|
||||
new EnemyBlueprint(
|
||||
"Interceptor",
|
||||
interceptorColor,
|
||||
1.3f,
|
||||
EnemyMovementStyle.Orbit,
|
||||
EnemyFireStyle.None,
|
||||
5.2f,
|
||||
0f,
|
||||
1.6f,
|
||||
0f,
|
||||
0,
|
||||
0,
|
||||
0f,
|
||||
0f,
|
||||
45f,
|
||||
50f,
|
||||
180,
|
||||
1.9f,
|
||||
GenerateSprite(EnemyShape.Arrow, interceptorColor),
|
||||
1.8f)
|
||||
};
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (Instance == this)
|
||||
@ -144,7 +282,7 @@ namespace Magisterka.BulletHell
|
||||
}
|
||||
}
|
||||
|
||||
private Sprite BuildEnemySprite()
|
||||
private Sprite GenerateSprite(EnemyShape shape, Color color)
|
||||
{
|
||||
const int size = 64;
|
||||
var texture = new Texture2D(size, size, TextureFormat.RGBA32, false)
|
||||
@ -152,18 +290,42 @@ namespace Magisterka.BulletHell
|
||||
filterMode = FilterMode.Point
|
||||
};
|
||||
|
||||
var center = new Vector2(size / 2f, size / 2f);
|
||||
var pixels = new Color[size * size];
|
||||
var body = new Color(1f, 0.35f, 0.35f, 1f);
|
||||
var center = new Vector2(size * 0.5f, size * 0.5f);
|
||||
|
||||
for (int y = 0; y < size; y++)
|
||||
{
|
||||
for (int x = 0; x < size; x++)
|
||||
{
|
||||
int index = x + y * size;
|
||||
float dist = Vector2.Distance(center, new Vector2(x, y));
|
||||
float threshold = size * 0.35f + Mathf.Sin(y * 0.2f) * 2f;
|
||||
pixels[index] = dist <= threshold ? body : new Color(0f, 0f, 0f, 0f);
|
||||
bool fill = false;
|
||||
switch (shape)
|
||||
{
|
||||
case EnemyShape.Disc:
|
||||
fill = Vector2.Distance(center, new Vector2(x, y)) <= size * 0.32f;
|
||||
break;
|
||||
case EnemyShape.Triangle:
|
||||
{
|
||||
float ny = 1f - (y / (float)(size - 1));
|
||||
float halfWidth = ny * size * 0.35f;
|
||||
fill = Mathf.Abs(x - center.x) <= halfWidth && ny > 0.1f;
|
||||
break;
|
||||
}
|
||||
case EnemyShape.Diamond:
|
||||
fill = Mathf.Abs(x - center.x) + Mathf.Abs(y - center.y) <= size * 0.32f;
|
||||
break;
|
||||
case EnemyShape.Arrow:
|
||||
{
|
||||
float ny = y / (float)(size - 1);
|
||||
float half = Mathf.Lerp(size * 0.1f, size * 0.35f, ny);
|
||||
bool head = ny > 0.55f && Mathf.Abs(x - center.x) <= Mathf.Lerp(size * 0.05f, size * 0.4f, Mathf.Clamp01((ny - 0.55f) / 0.45f));
|
||||
bool body = ny <= 0.55f && Mathf.Abs(x - center.x) <= half;
|
||||
fill = head || body;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
pixels[index] = fill ? color : new Color(0f, 0f, 0f, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,5 +334,13 @@ namespace Magisterka.BulletHell
|
||||
|
||||
return Sprite.Create(texture, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f), 64f);
|
||||
}
|
||||
|
||||
private enum EnemyShape
|
||||
{
|
||||
Disc,
|
||||
Triangle,
|
||||
Diamond,
|
||||
Arrow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
103
magisterka_2/Assets/Scripts/GameDirector.cs
Normal file
103
magisterka_2/Assets/Scripts/GameDirector.cs
Normal file
@ -0,0 +1,103 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace Magisterka.BulletHell
|
||||
{
|
||||
/// <summary>
|
||||
/// Oversees overall game flow: timer, victory detection, and difficulty pacing hooks.
|
||||
/// </summary>
|
||||
public class GameDirector : MonoBehaviour
|
||||
{
|
||||
public static GameDirector Instance { get; private set; }
|
||||
|
||||
[SerializeField] private float totalDuration = 90f;
|
||||
|
||||
private EnemySpawner _spawner;
|
||||
private PlayerController _player;
|
||||
private ScoreManager _scoreManager;
|
||||
private Text _timerText;
|
||||
|
||||
private float _timeRemaining;
|
||||
private bool _timerRunning;
|
||||
private bool _timerExpired;
|
||||
private bool _victoryTriggered;
|
||||
|
||||
public float TotalDuration => totalDuration;
|
||||
|
||||
public void Initialize(EnemySpawner spawner, PlayerController player, ScoreManager scoreManager, Text timerText, float duration)
|
||||
{
|
||||
_spawner = spawner;
|
||||
_player = player;
|
||||
_scoreManager = scoreManager;
|
||||
_timerText = timerText;
|
||||
totalDuration = duration > 0f ? duration : totalDuration;
|
||||
_timeRemaining = totalDuration;
|
||||
_timerRunning = true;
|
||||
_timerExpired = false;
|
||||
_victoryTriggered = false;
|
||||
UpdateTimerLabel();
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (_timerRunning && !_timerExpired)
|
||||
{
|
||||
_timeRemaining -= Time.deltaTime;
|
||||
if (_timeRemaining <= 0f)
|
||||
{
|
||||
_timeRemaining = 0f;
|
||||
_timerExpired = true;
|
||||
_timerRunning = false;
|
||||
_spawner?.StopSpawning();
|
||||
}
|
||||
|
||||
UpdateTimerLabel();
|
||||
}
|
||||
|
||||
if (_timerExpired && !_victoryTriggered)
|
||||
{
|
||||
bool enemiesRemain = _spawner != null && _spawner.HasActiveEnemies;
|
||||
if (!enemiesRemain)
|
||||
{
|
||||
TriggerVictory();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void OnPlayerGameOver()
|
||||
{
|
||||
_timerRunning = false;
|
||||
}
|
||||
|
||||
private void TriggerVictory()
|
||||
{
|
||||
_victoryTriggered = true;
|
||||
_scoreManager?.ShowVictory();
|
||||
_player?.HandleVictory();
|
||||
}
|
||||
|
||||
private void UpdateTimerLabel()
|
||||
{
|
||||
if (_timerText == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int seconds = Mathf.CeilToInt(_timeRemaining);
|
||||
int minutes = seconds / 60;
|
||||
seconds %= 60;
|
||||
_timerText.text = $"{minutes:00}:{seconds:00}";
|
||||
}
|
||||
}
|
||||
}
|
||||
2
magisterka_2/Assets/Scripts/GameDirector.cs.meta
Normal file
2
magisterka_2/Assets/Scripts/GameDirector.cs.meta
Normal file
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 40b997f70689be4e1aa9b72fabe123ea
|
||||
@ -11,6 +11,15 @@ namespace Magisterka.BulletHell
|
||||
{
|
||||
[SerializeField] private float cameraSize = 6.5f;
|
||||
|
||||
private struct HudElements
|
||||
{
|
||||
public Text Score;
|
||||
public Image[] Lives;
|
||||
public Text Status;
|
||||
public Text Timer;
|
||||
public Image DamageOverlay;
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
BuildCamera();
|
||||
@ -49,25 +58,34 @@ namespace Magisterka.BulletHell
|
||||
var effectManager = new GameObject("EffectManager").AddComponent<EffectManager>();
|
||||
effectManager.Initialize(root);
|
||||
|
||||
var scoreManager = new GameObject("ScoreManager").AddComponent<ScoreManager>();
|
||||
var scoreText = CreateScoreUI();
|
||||
scoreManager.Initialize(scoreText);
|
||||
var scoreManagerGO = new GameObject("ScoreManager");
|
||||
scoreManagerGO.transform.SetParent(root, false);
|
||||
var scoreManager = scoreManagerGO.AddComponent<ScoreManager>();
|
||||
|
||||
var pools = new GameObject("BulletPools").transform;
|
||||
pools.SetParent(root, false);
|
||||
|
||||
var playerPool = BulletPool.Create(pools, "PlayerBullets", new Color(0.35f, 0.9f, 1f, 1f), Faction.Player, 600);
|
||||
var enemyPool = BulletPool.Create(pools, "EnemyBullets", new Color(1f, 0.4f, 0.2f, 1f), Faction.Enemy, 1200);
|
||||
var playerPool = BulletPool.Create(pools, "PlayerBullets", new Color(0.35f, 0.9f, 1f, 1f), Faction.Player, 400);
|
||||
var enemyPool = BulletPool.Create(pools, "EnemyBullets", new Color(1f, 0.4f, 0.2f, 1f), Faction.Enemy, 900);
|
||||
|
||||
var player = PlayerController.CreatePlayer(root, playerPool, bounds);
|
||||
|
||||
var hud = CreateHud(player.MaxLives);
|
||||
scoreManager.Initialize(hud.Score, hud.Lives, hud.Status, hud.DamageOverlay);
|
||||
scoreManager.SetLives(player.CurrentLives);
|
||||
|
||||
var spawnerGO = new GameObject("EnemySpawner");
|
||||
spawnerGO.transform.SetParent(root, false);
|
||||
var spawner = spawnerGO.AddComponent<EnemySpawner>();
|
||||
spawner.Initialize(cam, enemyPool, player, bounds);
|
||||
|
||||
var directorGO = new GameObject("GameDirector");
|
||||
directorGO.transform.SetParent(root, false);
|
||||
var director = directorGO.AddComponent<GameDirector>();
|
||||
director.Initialize(spawner, player, scoreManager, hud.Timer, 90f);
|
||||
}
|
||||
|
||||
private Text CreateScoreUI()
|
||||
private HudElements CreateHud(int lives)
|
||||
{
|
||||
var canvasGO = new GameObject("Canvas");
|
||||
var canvas = canvasGO.AddComponent<Canvas>();
|
||||
@ -83,7 +101,7 @@ namespace Magisterka.BulletHell
|
||||
{
|
||||
label.font = Font.CreateDynamicFontFromOSFont("Arial", 32);
|
||||
}
|
||||
label.fontSize = 32;
|
||||
label.fontSize = 24;
|
||||
label.alignment = TextAnchor.UpperLeft;
|
||||
label.color = Color.white;
|
||||
label.text = "Score: 0";
|
||||
@ -92,10 +110,116 @@ namespace Magisterka.BulletHell
|
||||
rect.anchorMin = new Vector2(0f, 1f);
|
||||
rect.anchorMax = new Vector2(0f, 1f);
|
||||
rect.pivot = new Vector2(0f, 1f);
|
||||
rect.anchoredPosition = new Vector2(20f, -20f);
|
||||
rect.anchoredPosition = new Vector2(12f, -12f);
|
||||
|
||||
var livesGO = new GameObject("Lives");
|
||||
livesGO.transform.SetParent(canvasGO.transform, false);
|
||||
var livesRect = livesGO.AddComponent<RectTransform>();
|
||||
livesRect.anchorMin = new Vector2(0f, 1f);
|
||||
livesRect.anchorMax = new Vector2(0f, 1f);
|
||||
livesRect.pivot = new Vector2(0f, 1f);
|
||||
livesRect.anchoredPosition = new Vector2(12f, -40f);
|
||||
|
||||
var lifeSprite = BuildLifeSprite();
|
||||
var lifeIcons = new Image[Mathf.Max(1, lives)];
|
||||
|
||||
for (int i = 0; i < lifeIcons.Length; i++)
|
||||
{
|
||||
var iconGO = new GameObject($"Life_{i}");
|
||||
iconGO.transform.SetParent(livesGO.transform, false);
|
||||
var icon = iconGO.AddComponent<Image>();
|
||||
icon.sprite = lifeSprite;
|
||||
icon.color = new Color(0.3f + i * 0.05f, 0.85f, 1f, 0.9f);
|
||||
var iconRect = icon.rectTransform;
|
||||
iconRect.anchorMin = new Vector2(0f, 1f);
|
||||
iconRect.anchorMax = new Vector2(0f, 1f);
|
||||
iconRect.pivot = new Vector2(0f, 1f);
|
||||
iconRect.anchoredPosition = new Vector2(i * 26f, 0f);
|
||||
iconRect.sizeDelta = new Vector2(22f, 22f);
|
||||
lifeIcons[i] = icon;
|
||||
}
|
||||
|
||||
var gameOverGO = new GameObject("GameOverLabel");
|
||||
gameOverGO.transform.SetParent(canvasGO.transform, false);
|
||||
var gameOver = gameOverGO.AddComponent<Text>();
|
||||
gameOver.font = label.font;
|
||||
gameOver.fontSize = 48;
|
||||
gameOver.alignment = TextAnchor.MiddleCenter;
|
||||
gameOver.color = new Color(1f, 0.55f, 0.55f, 1f);
|
||||
gameOver.text = "GAME OVER";
|
||||
gameOver.enabled = false;
|
||||
|
||||
var goRect = gameOver.rectTransform;
|
||||
goRect.anchorMin = new Vector2(0.5f, 0.5f);
|
||||
goRect.anchorMax = new Vector2(0.5f, 0.5f);
|
||||
goRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
goRect.anchoredPosition = Vector2.zero;
|
||||
|
||||
var timerGO = new GameObject("TimerLabel");
|
||||
timerGO.transform.SetParent(canvasGO.transform, false);
|
||||
var timer = timerGO.AddComponent<Text>();
|
||||
timer.font = label.font;
|
||||
timer.fontSize = 24;
|
||||
timer.alignment = TextAnchor.UpperRight;
|
||||
timer.color = Color.white;
|
||||
timer.text = "05:00";
|
||||
|
||||
var timerRect = timer.rectTransform;
|
||||
timerRect.anchorMin = new Vector2(1f, 1f);
|
||||
timerRect.anchorMax = new Vector2(1f, 1f);
|
||||
timerRect.pivot = new Vector2(1f, 1f);
|
||||
timerRect.anchoredPosition = new Vector2(-12f, -12f);
|
||||
|
||||
var overlayGO = new GameObject("DamageOverlay");
|
||||
overlayGO.transform.SetParent(canvasGO.transform, false);
|
||||
var overlay = overlayGO.AddComponent<Image>();
|
||||
overlay.raycastTarget = false;
|
||||
overlay.color = new Color(1f, 0.2f, 0.2f, 0f);
|
||||
var overlayRect = overlay.rectTransform;
|
||||
overlayRect.anchorMin = Vector2.zero;
|
||||
overlayRect.anchorMax = Vector2.one;
|
||||
overlayRect.pivot = new Vector2(0.5f, 0.5f);
|
||||
overlayRect.offsetMin = Vector2.zero;
|
||||
overlayRect.offsetMax = Vector2.zero;
|
||||
|
||||
overlayGO.transform.SetAsLastSibling();
|
||||
|
||||
canvasGO.transform.SetParent(transform, false);
|
||||
return label;
|
||||
return new HudElements
|
||||
{
|
||||
Score = label,
|
||||
Lives = lifeIcons,
|
||||
Status = gameOver,
|
||||
Timer = timer,
|
||||
DamageOverlay = overlay
|
||||
};
|
||||
}
|
||||
|
||||
private static Sprite BuildLifeSprite()
|
||||
{
|
||||
const int size = 32;
|
||||
var texture = new Texture2D(size, size, TextureFormat.RGBA32, false)
|
||||
{
|
||||
filterMode = FilterMode.Bilinear
|
||||
};
|
||||
|
||||
var center = new Vector2(size / 2f, size / 2f);
|
||||
var pixels = new Color[size * size];
|
||||
|
||||
for (int y = 0; y < size; y++)
|
||||
{
|
||||
for (int x = 0; x < size; x++)
|
||||
{
|
||||
int index = x + y * size;
|
||||
float dist = Vector2.Distance(center, new Vector2(x, y));
|
||||
pixels[index] = dist <= size * 0.42f ? Color.white : new Color(0f, 0f, 0f, 0f);
|
||||
}
|
||||
}
|
||||
|
||||
texture.SetPixels(pixels);
|
||||
texture.Apply();
|
||||
|
||||
return Sprite.Create(texture, new Rect(0, 0, size, size), new Vector2(0.5f, 0.5f), 64f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ namespace Magisterka.BulletHell
|
||||
private float _current;
|
||||
|
||||
public event Action<Health> Died;
|
||||
public event Action<float, Vector2> Damaged;
|
||||
|
||||
public Faction Faction => faction;
|
||||
public float MaxHealth => maxHealth;
|
||||
@ -40,6 +41,7 @@ namespace Magisterka.BulletHell
|
||||
|
||||
_current -= damage;
|
||||
EffectManager.Instance?.SpawnHitEffect(hitPoint, faction, Mathf.Clamp01(damage / maxHealth));
|
||||
Damaged?.Invoke(damage, hitPoint);
|
||||
|
||||
if (_current <= 0f)
|
||||
{
|
||||
@ -60,6 +62,7 @@ namespace Magisterka.BulletHell
|
||||
}
|
||||
|
||||
_current = 0f;
|
||||
Damaged?.Invoke(maxHealth, transform.position);
|
||||
Die();
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Magisterka.BulletHell
|
||||
@ -16,30 +17,43 @@ namespace Magisterka.BulletHell
|
||||
[SerializeField] private float focusSpeedMultiplier = 0.6f;
|
||||
|
||||
[Header("Offense")]
|
||||
[SerializeField] private float fireRate = 0.08f;
|
||||
[SerializeField] private float bulletSpeed = 26f;
|
||||
[SerializeField] private float bulletDamage = 16f;
|
||||
[SerializeField] private int volleyWidth = 6;
|
||||
[SerializeField] private float fireRate = 0.12f;
|
||||
[SerializeField] private float bulletSpeed = 22f;
|
||||
[SerializeField] private float bulletDamage = 14f;
|
||||
[SerializeField] private int volleyWidth = 5;
|
||||
|
||||
[Header("Survivability")]
|
||||
[SerializeField] private int maxLives = 5;
|
||||
[SerializeField] private float respawnDelay = 1.4f;
|
||||
[Header("Survivability")]
|
||||
[SerializeField] private int maxLives = 1;
|
||||
[SerializeField] private float respawnDelay = 0.7f;
|
||||
[SerializeField] private float invulnerabilityDuration = 2f;
|
||||
|
||||
[Header("Progression")]
|
||||
[SerializeField] private int baseLevelThreshold = 1500;
|
||||
|
||||
[Header("Controls")]
|
||||
[SerializeField] private KeyCode fireKey = KeyCode.Z;
|
||||
[SerializeField] private KeyCode modifierKey = KeyCode.LeftShift;
|
||||
[SerializeField] private KeyCode bombKey = KeyCode.X;
|
||||
|
||||
private BulletPool _playerBulletPool;
|
||||
private const int MaxLevel = 12;
|
||||
|
||||
private BulletPool _playerBulletPool;
|
||||
private Health _health;
|
||||
private SpriteRenderer _renderer;
|
||||
private Vector2 _bounds = new Vector2(8f, 4.5f);
|
||||
private float _fireCooldown;
|
||||
private bool _bombAvailable = true;
|
||||
private int _remainingLives;
|
||||
private int _lives;
|
||||
private bool _canControl = true;
|
||||
private bool _isAlive = true;
|
||||
private bool _isInvulnerable;
|
||||
private bool _victoryAchieved;
|
||||
private Color _baseColor;
|
||||
private Coroutine _hitFlashRoutine;
|
||||
private Coroutine _levelFlashRoutine;
|
||||
private int _level;
|
||||
private float _baseBulletSpeed;
|
||||
private int _baseVolleyWidth;
|
||||
|
||||
public void Initialize(BulletPool bulletPool, Vector2 movementBounds)
|
||||
{
|
||||
@ -49,12 +63,29 @@ namespace Magisterka.BulletHell
|
||||
_health = GetComponent<Health>();
|
||||
_health.Configure(Faction.Player, 60f);
|
||||
_health.Died += HandleDeath;
|
||||
_health.Damaged += HandleDamageTaken;
|
||||
|
||||
_renderer = GetComponent<SpriteRenderer>();
|
||||
_baseColor = _renderer != null ? _renderer.color : Color.cyan;
|
||||
|
||||
_remainingLives = Mathf.Max(0, maxLives - 1);
|
||||
_baseBulletSpeed = bulletSpeed;
|
||||
_baseVolleyWidth = volleyWidth;
|
||||
_level = 0;
|
||||
|
||||
_lives = Mathf.Max(0, maxLives);
|
||||
ScoreManager.Instance?.SetLives(_lives);
|
||||
transform.position = new Vector3(0f, -_bounds.y + 1.5f, 0f);
|
||||
_isAlive = true;
|
||||
_canControl = true;
|
||||
_bombAvailable = true;
|
||||
_fireCooldown = 0f;
|
||||
_victoryAchieved = false;
|
||||
|
||||
if (ScoreManager.Instance != null)
|
||||
{
|
||||
ScoreManager.Instance.ScoreChanged += HandleScoreChanged;
|
||||
HandleScoreChanged(ScoreManager.Instance.CurrentScore);
|
||||
}
|
||||
}
|
||||
|
||||
public static PlayerController CreatePlayer(Transform parent, BulletPool playerBulletPool, Vector2 movementBounds)
|
||||
@ -68,30 +99,48 @@ namespace Magisterka.BulletHell
|
||||
renderer.color = new Color(0.25f, 0.85f, 1f, 1f);
|
||||
|
||||
var collider = playerGO.AddComponent<CircleCollider2D>();
|
||||
collider.radius = 0.55f;
|
||||
collider.radius = 1f / 64f;
|
||||
collider.isTrigger = true;
|
||||
|
||||
var health = playerGO.AddComponent<Health>();
|
||||
playerGO.AddComponent<Health>();
|
||||
|
||||
var controller = playerGO.AddComponent<PlayerController>();
|
||||
controller.Initialize(playerBulletPool, movementBounds);
|
||||
return controller;
|
||||
}
|
||||
|
||||
public int MaxLives => Mathf.Max(1, maxLives);
|
||||
public int CurrentLives => Mathf.Max(0, _lives);
|
||||
|
||||
private void Update()
|
||||
{
|
||||
HandleMovement();
|
||||
HandleShooting();
|
||||
HandleBomb();
|
||||
if (_victoryAchieved)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_fireCooldown > 0f)
|
||||
{
|
||||
_fireCooldown -= Time.deltaTime;
|
||||
}
|
||||
|
||||
if (!_canControl)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HandleMovement();
|
||||
HandleShooting();
|
||||
HandleBomb();
|
||||
}
|
||||
|
||||
private void HandleMovement()
|
||||
{
|
||||
if (!_canControl)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
float x = 0f;
|
||||
float y = 0f;
|
||||
|
||||
@ -111,6 +160,11 @@ namespace Magisterka.BulletHell
|
||||
|
||||
private void HandleShooting()
|
||||
{
|
||||
if (!_canControl || !_isAlive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool wantsFire = Input.GetKey(fireKey) || Input.GetMouseButton(0);
|
||||
if (!wantsFire || _fireCooldown > 0f)
|
||||
{
|
||||
@ -123,23 +177,34 @@ namespace Magisterka.BulletHell
|
||||
|
||||
private void FireVolley()
|
||||
{
|
||||
var origin = transform.position + Vector3.up * 0.4f;
|
||||
int half = volleyWidth / 2;
|
||||
var origin = transform.position + Vector3.up * 0.35f;
|
||||
float speed = GetLevelAdjustedBulletSpeed();
|
||||
int count = Mathf.Clamp(_baseVolleyWidth + Mathf.Max(0, _level), 1, 17);
|
||||
float mid = (count - 1) * 0.5f;
|
||||
float spreadStep = Mathf.Lerp(5f, 8f, Mathf.Clamp01(_level / 12f));
|
||||
|
||||
for (int i = -half; i <= half; i++)
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
float spread = i * 6f;
|
||||
float offsetIndex = i - mid;
|
||||
float spread = offsetIndex * spreadStep;
|
||||
var direction = Quaternion.Euler(0f, 0f, spread) * Vector2.up;
|
||||
var offset = direction * 0.25f * Mathf.Abs(i);
|
||||
_playerBulletPool.Spawn(origin + (Vector3)offset, direction, bulletSpeed, bulletDamage);
|
||||
var offset = new Vector3(offsetIndex * 0.18f, 0f, 0f);
|
||||
_playerBulletPool.Spawn(origin + offset, direction, speed, bulletDamage);
|
||||
}
|
||||
|
||||
EffectManager.Instance?.SpawnHitEffect(origin + Vector3.up * 0.2f, Faction.Player, 0.6f);
|
||||
foreach (var extra in GetExtraDirectionsForLevel())
|
||||
{
|
||||
var dir = extra.normalized;
|
||||
var spawnPos = transform.position + (Vector3)(dir * 0.4f);
|
||||
_playerBulletPool.Spawn(spawnPos, dir, speed, bulletDamage);
|
||||
}
|
||||
|
||||
EffectManager.Instance?.SpawnHitEffect(origin + Vector3.up * 0.1f, Faction.Player, 0.4f + _level * 0.05f);
|
||||
}
|
||||
|
||||
private void HandleBomb()
|
||||
{
|
||||
if (!_bombAvailable)
|
||||
if (!_bombAvailable || !_canControl || !_isAlive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -156,6 +221,7 @@ namespace Magisterka.BulletHell
|
||||
|
||||
private IEnumerator PerformBomb()
|
||||
{
|
||||
_canControl = false;
|
||||
_isInvulnerable = true;
|
||||
_renderer.color = Color.white;
|
||||
EffectManager.Instance?.SpawnScreenClear(transform.position, 18f);
|
||||
@ -186,10 +252,27 @@ namespace Magisterka.BulletHell
|
||||
|
||||
_isInvulnerable = false;
|
||||
_renderer.color = _baseColor;
|
||||
_canControl = true;
|
||||
}
|
||||
|
||||
private void HandleDeath(Health _)
|
||||
{
|
||||
if (!_isAlive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isAlive = false;
|
||||
_canControl = false;
|
||||
_bombAvailable = false;
|
||||
if (_hitFlashRoutine != null)
|
||||
{
|
||||
StopCoroutine(_hitFlashRoutine);
|
||||
_renderer.color = _baseColor;
|
||||
_hitFlashRoutine = null;
|
||||
}
|
||||
_lives -= 1;
|
||||
ScoreManager.Instance?.SetLives(Mathf.Max(0, _lives));
|
||||
StartCoroutine(RespawnRoutine());
|
||||
}
|
||||
|
||||
@ -199,16 +282,23 @@ namespace Magisterka.BulletHell
|
||||
_renderer.enabled = false;
|
||||
_isInvulnerable = true;
|
||||
|
||||
if (_remainingLives < 0)
|
||||
if (_lives < 0)
|
||||
{
|
||||
yield return new WaitForSeconds(1f);
|
||||
ScoreManager.Instance?.ShowGameOver();
|
||||
EnemySpawner.Instance?.StopAndClear();
|
||||
GameDirector.Instance?.OnPlayerGameOver();
|
||||
gameObject.SetActive(false);
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(respawnDelay);
|
||||
EnemySpawner.Instance?.ClearScreen();
|
||||
transform.position = new Vector3(0f, -_bounds.y + 1.5f, 0f);
|
||||
_renderer.enabled = true;
|
||||
_health.RestoreFull();
|
||||
_fireCooldown = fireRate;
|
||||
_canControl = true;
|
||||
|
||||
float timer = invulnerabilityDuration;
|
||||
while (timer > 0f)
|
||||
@ -229,7 +319,9 @@ namespace Magisterka.BulletHell
|
||||
}
|
||||
|
||||
_isInvulnerable = false;
|
||||
_remainingLives--;
|
||||
_renderer.color = _baseColor;
|
||||
_isAlive = true;
|
||||
_canControl = true;
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
@ -237,6 +329,12 @@ namespace Magisterka.BulletHell
|
||||
if (_health != null)
|
||||
{
|
||||
_health.Died -= HandleDeath;
|
||||
_health.Damaged -= HandleDamageTaken;
|
||||
}
|
||||
|
||||
if (ScoreManager.Instance != null)
|
||||
{
|
||||
ScoreManager.Instance.ScoreChanged -= HandleScoreChanged;
|
||||
}
|
||||
|
||||
if (Instance == this)
|
||||
@ -249,7 +347,7 @@ namespace Magisterka.BulletHell
|
||||
|
||||
public void ApplyDamage(float value)
|
||||
{
|
||||
if (_isInvulnerable)
|
||||
if (_isInvulnerable || !_isAlive || _victoryAchieved)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -257,6 +355,178 @@ namespace Magisterka.BulletHell
|
||||
_health.ApplyDamage(value, transform.position);
|
||||
}
|
||||
|
||||
public void HandleVictory()
|
||||
{
|
||||
_victoryAchieved = true;
|
||||
_canControl = false;
|
||||
_isInvulnerable = true;
|
||||
if (_renderer != null)
|
||||
{
|
||||
_renderer.color = Color.white;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleDamageTaken(float damage, Vector2 hitPoint)
|
||||
{
|
||||
if (_renderer == null || _isInvulnerable || !_isAlive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_hitFlashRoutine != null)
|
||||
{
|
||||
StopCoroutine(_hitFlashRoutine);
|
||||
_renderer.color = _baseColor;
|
||||
}
|
||||
|
||||
_hitFlashRoutine = StartCoroutine(HitFlashRoutine());
|
||||
float strength = Mathf.Clamp01(damage / Mathf.Max(1f, _health.MaxHealth * 0.4f));
|
||||
ScoreManager.Instance?.TriggerDamageFlash(strength);
|
||||
EffectManager.Instance?.ShakeCamera(0.22f, 0.65f);
|
||||
}
|
||||
|
||||
private IEnumerator HitFlashRoutine()
|
||||
{
|
||||
const float duration = 0.35f;
|
||||
float elapsed = 0f;
|
||||
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
float t = Mathf.PingPong(elapsed * 18f, 1f);
|
||||
_renderer.color = Color.Lerp(_baseColor, Color.white, t);
|
||||
yield return null;
|
||||
}
|
||||
|
||||
_renderer.color = _baseColor;
|
||||
_hitFlashRoutine = null;
|
||||
}
|
||||
|
||||
private float GetLevelAdjustedBulletSpeed()
|
||||
{
|
||||
return _baseBulletSpeed;
|
||||
}
|
||||
|
||||
private IEnumerable<Vector2> GetExtraDirectionsForLevel()
|
||||
{
|
||||
if (_level <= 0)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (_level >= 2)
|
||||
{
|
||||
yield return Vector2.left;
|
||||
yield return Vector2.right;
|
||||
}
|
||||
|
||||
if (_level >= 4)
|
||||
{
|
||||
yield return (Vector2.left + Vector2.up).normalized;
|
||||
yield return (Vector2.right + Vector2.up).normalized;
|
||||
}
|
||||
|
||||
if (_level >= 6)
|
||||
{
|
||||
yield return Vector2.down;
|
||||
}
|
||||
|
||||
if (_level >= 8)
|
||||
{
|
||||
yield return (Vector2.left + Vector2.down * 0.7f).normalized;
|
||||
yield return (Vector2.right + Vector2.down * 0.7f).normalized;
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleScoreChanged(int score)
|
||||
{
|
||||
int newLevel = CalculateLevelFromScore(score);
|
||||
|
||||
if (newLevel > _level)
|
||||
{
|
||||
for (int next = _level + 1; next <= newLevel; next++)
|
||||
{
|
||||
_level = next;
|
||||
HandleLevelUpFeedback();
|
||||
}
|
||||
}
|
||||
else if (newLevel < _level)
|
||||
{
|
||||
_level = newLevel;
|
||||
}
|
||||
}
|
||||
|
||||
private int CalculateLevelFromScore(int score)
|
||||
{
|
||||
if (baseLevelThreshold <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int level = 0;
|
||||
int increment = baseLevelThreshold;
|
||||
int threshold = baseLevelThreshold;
|
||||
|
||||
while (level < MaxLevel && score >= threshold)
|
||||
{
|
||||
level++;
|
||||
increment *= 2;
|
||||
threshold += increment;
|
||||
}
|
||||
|
||||
return level;
|
||||
}
|
||||
|
||||
private void HandleLevelUpFeedback()
|
||||
{
|
||||
EffectManager.Instance?.SpawnExplosion(transform.position, Faction.Player, 2f + _level * 0.2f);
|
||||
EffectManager.Instance?.ShakeCamera(0.18f, 0.4f + _level * 0.05f);
|
||||
|
||||
if (_hitFlashRoutine != null)
|
||||
{
|
||||
StopCoroutine(_hitFlashRoutine);
|
||||
_hitFlashRoutine = null;
|
||||
}
|
||||
|
||||
if (_renderer != null)
|
||||
{
|
||||
_renderer.color = _baseColor;
|
||||
}
|
||||
|
||||
if (_levelFlashRoutine != null)
|
||||
{
|
||||
StopCoroutine(_levelFlashRoutine);
|
||||
}
|
||||
|
||||
_levelFlashRoutine = StartCoroutine(LevelUpFlashRoutine());
|
||||
}
|
||||
|
||||
private IEnumerator LevelUpFlashRoutine()
|
||||
{
|
||||
float duration = 0.6f;
|
||||
float elapsed = 0f;
|
||||
var glowColor = Color.Lerp(_baseColor, new Color(1f, 0.95f, 0.35f, 1f), 0.85f);
|
||||
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
float t = Mathf.PingPong(elapsed * 6f, 1f);
|
||||
if (_renderer != null)
|
||||
{
|
||||
_renderer.color = Color.Lerp(glowColor, _baseColor, t);
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (_renderer != null)
|
||||
{
|
||||
_renderer.color = _baseColor;
|
||||
}
|
||||
|
||||
_levelFlashRoutine = null;
|
||||
}
|
||||
|
||||
private static Sprite BuildPlayerSprite()
|
||||
{
|
||||
const int size = 64;
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
@ -11,7 +13,18 @@ namespace Magisterka.BulletHell
|
||||
public static ScoreManager Instance { get; private set; }
|
||||
|
||||
private Text _scoreText;
|
||||
private Image[] _lifeIcons;
|
||||
private Text _statusText;
|
||||
private Image _damageOverlay;
|
||||
private int _score;
|
||||
private int _queuedLives = -1;
|
||||
private string _queuedStatus;
|
||||
private Color _queuedStatusColor = Color.white;
|
||||
private Coroutine _damageFlashRoutine;
|
||||
|
||||
public event Action<int> ScoreChanged;
|
||||
|
||||
public int CurrentScore => _score;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@ -22,25 +35,101 @@ namespace Magisterka.BulletHell
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
DontDestroyOnLoad(gameObject);
|
||||
}
|
||||
|
||||
public void Initialize(Text scoreText)
|
||||
public void Initialize(Text scoreText, Image[] lifeIcons, Text statusText, Image damageOverlay)
|
||||
{
|
||||
_scoreText = scoreText;
|
||||
_lifeIcons = lifeIcons;
|
||||
_statusText = statusText;
|
||||
_damageOverlay = damageOverlay;
|
||||
HideStatus();
|
||||
|
||||
ResetScore();
|
||||
|
||||
if (_queuedLives >= 0)
|
||||
{
|
||||
ApplyLives(_queuedLives);
|
||||
_queuedLives = -1;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(_queuedStatus))
|
||||
{
|
||||
ShowStatus(_queuedStatus, _queuedStatusColor);
|
||||
_queuedStatus = null;
|
||||
}
|
||||
|
||||
if (_damageOverlay != null)
|
||||
{
|
||||
var color = _damageOverlay.color;
|
||||
color.a = 0f;
|
||||
_damageOverlay.color = color;
|
||||
}
|
||||
|
||||
ScoreChanged?.Invoke(_score);
|
||||
}
|
||||
|
||||
public void AddScore(int value)
|
||||
{
|
||||
_score += value;
|
||||
UpdateLabel();
|
||||
ScoreChanged?.Invoke(_score);
|
||||
}
|
||||
|
||||
public void ResetScore()
|
||||
{
|
||||
_score = 0;
|
||||
UpdateLabel();
|
||||
HideStatus();
|
||||
ScoreChanged?.Invoke(_score);
|
||||
}
|
||||
|
||||
public void SetLives(int lives)
|
||||
{
|
||||
if (_lifeIcons == null || _lifeIcons.Length == 0)
|
||||
{
|
||||
_queuedLives = lives;
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyLives(lives);
|
||||
}
|
||||
|
||||
public void ShowGameOver()
|
||||
{
|
||||
ShowStatus("GAME OVER", new Color(1f, 0.5f, 0.5f, 1f));
|
||||
}
|
||||
|
||||
public void ShowVictory()
|
||||
{
|
||||
ShowStatus("YOU WIN", new Color(0.6f, 1f, 0.6f, 1f));
|
||||
}
|
||||
|
||||
public void TriggerDamageFlash(float strength)
|
||||
{
|
||||
if (_damageOverlay == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
strength = Mathf.Clamp01(strength);
|
||||
|
||||
if (_damageFlashRoutine != null)
|
||||
{
|
||||
StopCoroutine(_damageFlashRoutine);
|
||||
}
|
||||
|
||||
_damageFlashRoutine = StartCoroutine(DamageFlashRoutine(strength));
|
||||
}
|
||||
|
||||
public void HideStatus()
|
||||
{
|
||||
if (_statusText == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_statusText.enabled = false;
|
||||
}
|
||||
|
||||
private void UpdateLabel()
|
||||
@ -52,5 +141,58 @@ namespace Magisterka.BulletHell
|
||||
|
||||
_scoreText.text = $"Score: {_score:N0}";
|
||||
}
|
||||
|
||||
private void ApplyLives(int lives)
|
||||
{
|
||||
if (_lifeIcons == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < _lifeIcons.Length; i++)
|
||||
{
|
||||
if (_lifeIcons[i] != null)
|
||||
{
|
||||
_lifeIcons[i].enabled = i < lives;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowStatus(string message, Color color)
|
||||
{
|
||||
if (_statusText == null)
|
||||
{
|
||||
_queuedStatus = message;
|
||||
_queuedStatusColor = color;
|
||||
return;
|
||||
}
|
||||
|
||||
_statusText.text = message;
|
||||
_statusText.color = color;
|
||||
_statusText.enabled = true;
|
||||
}
|
||||
|
||||
private IEnumerator DamageFlashRoutine(float strength)
|
||||
{
|
||||
float peakAlpha = Mathf.Lerp(0.35f, 0.75f, strength);
|
||||
float duration = 0.45f;
|
||||
float elapsed = 0f;
|
||||
|
||||
while (elapsed < duration)
|
||||
{
|
||||
elapsed += Time.deltaTime;
|
||||
float normalized = Mathf.Clamp01(elapsed / duration);
|
||||
float alpha = Mathf.Sin(normalized * Mathf.PI) * peakAlpha;
|
||||
var color = _damageOverlay.color;
|
||||
color.a = alpha;
|
||||
_damageOverlay.color = color;
|
||||
yield return null;
|
||||
}
|
||||
|
||||
var reset = _damageOverlay.color;
|
||||
reset.a = 0f;
|
||||
_damageOverlay.color = reset;
|
||||
_damageFlashRoutine = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user