Dateien sortiert, Hintergrund angepasst, Platzierungslogik und Basis für Combat angelegt. Gewehr schießt und Gegner nehmen Schaden
This commit is contained in:
1 parent
02b9ddb226
commit
e8c81646c4
59 files changed
+2616
-179
No files matched your search
@@ -0,0 +1,108 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
[RequireComponent(typeof(Rigidbody2D))]
|
||||
[RequireComponent(typeof(Collider2D))]
|
||||
public class Bullet : MonoBehaviour
|
||||
{
|
||||
[Header("Runtime Values")]
|
||||
private Vector2 direction;
|
||||
|
||||
private float speed;
|
||||
private float damage;
|
||||
private float maxDistance;
|
||||
|
||||
private float distanceTravelled;
|
||||
|
||||
private bool initialized;
|
||||
|
||||
// Speichert alle Gegner, die diese Kugel bereits getroffen hat.
|
||||
private HashSet<Enemy> hitEnemies = new HashSet<Enemy>();
|
||||
|
||||
private Rigidbody2D rb;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
rb = GetComponent<Rigidbody2D>();
|
||||
|
||||
rb.gravityScale = 0f;
|
||||
rb.bodyType = RigidbodyType2D.Kinematic;
|
||||
rb.collisionDetectionMode =
|
||||
CollisionDetectionMode2D.Continuous;
|
||||
}
|
||||
|
||||
public void Initialize(
|
||||
Vector2 shootDirection,
|
||||
float bulletSpeed,
|
||||
float bulletDamage,
|
||||
float range
|
||||
)
|
||||
{
|
||||
direction = shootDirection.normalized;
|
||||
|
||||
speed = bulletSpeed;
|
||||
damage = bulletDamage;
|
||||
maxDistance = range;
|
||||
|
||||
// Sprite zeigt standardmäßig mit seiner lokalen Y-Achse nach oben.
|
||||
float angle =
|
||||
Mathf.Atan2(direction.y, direction.x)
|
||||
* Mathf.Rad2Deg - 90f;
|
||||
|
||||
transform.rotation =
|
||||
Quaternion.Euler(0f, 0f, angle);
|
||||
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (!initialized)
|
||||
return;
|
||||
|
||||
float remainingDistance =
|
||||
maxDistance - distanceTravelled;
|
||||
|
||||
if (remainingDistance <= 0f)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
float movementDistance = Mathf.Min(
|
||||
speed * Time.fixedDeltaTime,
|
||||
remainingDistance
|
||||
);
|
||||
|
||||
Vector2 movement =
|
||||
direction * movementDistance;
|
||||
|
||||
rb.MovePosition(rb.position + movement);
|
||||
|
||||
distanceTravelled += movementDistance;
|
||||
|
||||
if (distanceTravelled >= maxDistance)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerEnter2D(Collider2D other)
|
||||
{
|
||||
Enemy enemy =
|
||||
other.GetComponentInParent<Enemy>();
|
||||
|
||||
if (enemy == null)
|
||||
return;
|
||||
|
||||
// Bereits getroffenen Gegner ignorieren.
|
||||
if (!hitEnemies.Add(enemy))
|
||||
return;
|
||||
|
||||
// Keine weiteren Treffer auf tote Gegner.
|
||||
if (enemy.IsDead)
|
||||
return;
|
||||
|
||||
enemy.TakeDamage(damage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4e19c6e90338ca14d8f1aa88ff9b01fc
|
||||
@@ -0,0 +1,92 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class CharacterRotation : MonoBehaviour
|
||||
{
|
||||
[Header("Visual")]
|
||||
[SerializeField]
|
||||
private Transform visual;
|
||||
|
||||
[Header("Rotation")]
|
||||
[SerializeField]
|
||||
private float rotationSpeed = 540f;
|
||||
|
||||
[Tooltip("In welche lokale Richtung zeigt die Grafik standardm??ig?")]
|
||||
[SerializeField]
|
||||
private SpriteForwardDirection spriteForward =
|
||||
SpriteForwardDirection.Up;
|
||||
|
||||
private IMovementDirection movement;
|
||||
|
||||
private enum SpriteForwardDirection
|
||||
{
|
||||
Up,
|
||||
Right
|
||||
}
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
movement =
|
||||
GetComponent<IMovementDirection>();
|
||||
|
||||
if (movement == null)
|
||||
{
|
||||
Debug.LogError(
|
||||
"CharacterRotation ben?tigt eine Komponente, " +
|
||||
"die IMovementDirection implementiert.",
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
if (visual == null)
|
||||
{
|
||||
Debug.LogError(
|
||||
"CharacterRotation: Kein Visual zugewiesen.",
|
||||
this
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (movement == null || visual == null)
|
||||
return;
|
||||
|
||||
Vector2 direction =
|
||||
movement.MovementDirection;
|
||||
|
||||
// Keine Bewegung -> aktuelle Rotation behalten.
|
||||
if (direction.sqrMagnitude < 0.001f)
|
||||
return;
|
||||
|
||||
RotateTowards(direction);
|
||||
}
|
||||
|
||||
private void RotateTowards(Vector2 direction)
|
||||
{
|
||||
float targetAngle =
|
||||
Mathf.Atan2(
|
||||
direction.y,
|
||||
direction.x
|
||||
) * Mathf.Rad2Deg;
|
||||
|
||||
if (spriteForward ==
|
||||
SpriteForwardDirection.Up)
|
||||
{
|
||||
targetAngle -= 90f;
|
||||
}
|
||||
|
||||
Quaternion targetRotation =
|
||||
Quaternion.Euler(
|
||||
0f,
|
||||
0f,
|
||||
targetAngle
|
||||
);
|
||||
|
||||
visual.rotation =
|
||||
Quaternion.RotateTowards(
|
||||
visual.rotation,
|
||||
targetRotation,
|
||||
rotationSpeed * Time.deltaTime
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bea87a19f3a99294c9d065a24307484c
|
||||
@@ -0,0 +1,195 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class Enemy : MonoBehaviour, IMovementDirection
|
||||
{
|
||||
[Header("Movement")]
|
||||
[SerializeField]
|
||||
private float moveSpeed = 0.5f;
|
||||
|
||||
[SerializeField]
|
||||
private float targetReachedDistance = 0.1f;
|
||||
|
||||
private EnemyTargetManager targetManager;
|
||||
private Transform currentTarget;
|
||||
|
||||
// Wird von CharacterRotation verwendet.
|
||||
public Vector2 MovementDirection { get; private set; }
|
||||
|
||||
[Header("Health")]
|
||||
[SerializeField]
|
||||
private float maxHealth = 100f;
|
||||
|
||||
private float currentHealth;
|
||||
|
||||
public float CurrentHealth => currentHealth;
|
||||
|
||||
public bool IsDead { get; private set; }
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
currentHealth = maxHealth;
|
||||
|
||||
targetManager =
|
||||
FindFirstObjectByType<EnemyTargetManager>();
|
||||
|
||||
if (targetManager == null)
|
||||
{
|
||||
Debug.LogError(
|
||||
"Enemy: Kein EnemyTargetManager gefunden!",
|
||||
this
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (targetManager != null)
|
||||
{
|
||||
targetManager.OnTargetsChanged +=
|
||||
SelectClosestTarget;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (targetManager != null)
|
||||
{
|
||||
targetManager.OnTargetsChanged -=
|
||||
SelectClosestTarget;
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (targetManager == null)
|
||||
return;
|
||||
|
||||
SelectClosestTarget();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (targetManager == null)
|
||||
return;
|
||||
|
||||
// Kein aktuelles Ziel vorhanden.
|
||||
if (currentTarget == null)
|
||||
{
|
||||
MovementDirection = Vector2.zero;
|
||||
|
||||
SelectClosestTarget();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Das aktuelle Ziel wurde deaktiviert.
|
||||
if (!currentTarget.gameObject.activeInHierarchy)
|
||||
{
|
||||
SelectClosestTarget();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
MoveTowardsTarget();
|
||||
|
||||
// Ziel erreicht.
|
||||
if (HasReachedTarget())
|
||||
{
|
||||
SelectClosestTarget();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// BEWEGUNGSLOGIK
|
||||
//
|
||||
|
||||
private void SelectClosestTarget()
|
||||
{
|
||||
if (targetManager == null)
|
||||
return;
|
||||
|
||||
currentTarget =
|
||||
targetManager.GetClosestActiveTarget(
|
||||
transform.position
|
||||
);
|
||||
|
||||
if (currentTarget == null)
|
||||
{
|
||||
MovementDirection = Vector2.zero;
|
||||
}
|
||||
}
|
||||
|
||||
private void MoveTowardsTarget()
|
||||
{
|
||||
if (currentTarget == null)
|
||||
return;
|
||||
|
||||
Vector3 direction =
|
||||
currentTarget.position - transform.position;
|
||||
|
||||
MovementDirection = new Vector2(
|
||||
direction.x,
|
||||
direction.y
|
||||
).normalized;
|
||||
|
||||
transform.position = Vector3.MoveTowards(
|
||||
transform.position,
|
||||
currentTarget.position,
|
||||
moveSpeed * Time.deltaTime
|
||||
);
|
||||
}
|
||||
|
||||
private bool HasReachedTarget()
|
||||
{
|
||||
if (currentTarget == null)
|
||||
return false;
|
||||
|
||||
return Vector3.Distance(
|
||||
transform.position,
|
||||
currentTarget.position
|
||||
) <= targetReachedDistance;
|
||||
}
|
||||
|
||||
|
||||
// LEBENSLOGIK
|
||||
//
|
||||
|
||||
public void TakeDamage(float damage)
|
||||
{
|
||||
if (IsDead || damage <= 0f)
|
||||
return;
|
||||
|
||||
currentHealth -= damage;
|
||||
|
||||
Debug.Log(
|
||||
gameObject.name +
|
||||
" erhält " + damage +
|
||||
" Schaden. HP: " +
|
||||
currentHealth + "/" + maxHealth
|
||||
);
|
||||
|
||||
if (currentHealth <= 0f)
|
||||
{
|
||||
Die();
|
||||
}
|
||||
}
|
||||
|
||||
private void Die()
|
||||
{
|
||||
if (IsDead)
|
||||
return;
|
||||
|
||||
IsDead = true;
|
||||
|
||||
Debug.Log(
|
||||
gameObject.name + " wurde besiegt!"
|
||||
);
|
||||
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
|
||||
// ANGRIFFSLOGIK (möglicherweise in anderem Skript)
|
||||
//
|
||||
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 544fe3d5f687b6b4bbe68d79ffb11bd8
|
||||
@@ -0,0 +1,425 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class EnemySpawner : MonoBehaviour
|
||||
{
|
||||
[System.Serializable]
|
||||
public class EnemyTypeData
|
||||
{
|
||||
[Header("Enemy Prefabs")]
|
||||
public List<GameObject> prefabVariants = new List<GameObject>();
|
||||
|
||||
[Header("Spawn Selection")]
|
||||
[Min(0)]
|
||||
public int spawnChance = 10;
|
||||
|
||||
public float danger = 0f;
|
||||
|
||||
[Min(0)]
|
||||
public int enemyDifficulty = 1;
|
||||
}
|
||||
|
||||
[Header("Enemy Types")]
|
||||
public List<EnemyTypeData> enemyTypes = new List<EnemyTypeData>();
|
||||
|
||||
[Header("Spawn Point")]
|
||||
public Transform spawnPoint;
|
||||
|
||||
[Header("Difficulty")]
|
||||
public float raiseRate = 0.1f;
|
||||
public float difficultyRaise = 0f;
|
||||
public float difficulty = 0f;
|
||||
|
||||
[Header("Initial Spawn")]
|
||||
public float startDelay = 5f;
|
||||
|
||||
[Header("Failed Spawn Break")]
|
||||
public float minBreak = 3f;
|
||||
public float maxBreak = 7f;
|
||||
|
||||
[Header("Group Spawn")]
|
||||
public float spawnDelay = 0.5f;
|
||||
public float minGroupBreak = 2f;
|
||||
public float maxGroupBreak = 5f;
|
||||
|
||||
public int minGroup = 2;
|
||||
public int maxGroup = 5;
|
||||
|
||||
private float tempDifficulty;
|
||||
private bool hasSpawnedGroupThisCycle = false;
|
||||
private bool pauseDifficultyGrowth = false;
|
||||
private float time = 0f;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (spawnPoint == null)
|
||||
{
|
||||
Debug.LogError(
|
||||
"EnemySpawner: Kein SpawnPoint zugewiesen!",
|
||||
this
|
||||
);
|
||||
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!HasUsableEnemyType())
|
||||
{
|
||||
Debug.LogError(
|
||||
"EnemySpawner: Keine gültigen Gegnertypen vorhanden!",
|
||||
this
|
||||
);
|
||||
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
StartCoroutine(SpawnLoop());
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Freischaltschwelle wächst immer weiter.
|
||||
time = time + Time.deltaTime;
|
||||
//Debug.Log(time);
|
||||
|
||||
// Schwierigkeit erhöht sich.
|
||||
difficultyRaise += raiseRate * Time.deltaTime;
|
||||
|
||||
// Budget wächst nur, wenn es nicht pausiert ist.
|
||||
if (!pauseDifficultyGrowth)
|
||||
{
|
||||
difficulty += difficultyRaise * Time.deltaTime;
|
||||
}
|
||||
Debug.Log("Time: " + time + ", DifficultyRaise: " + difficultyRaise + " und Difficulty: " + difficulty);
|
||||
}
|
||||
|
||||
private IEnumerator SpawnLoop()
|
||||
{
|
||||
// Einmalige Wartezeit beim Spielstart.
|
||||
yield return new WaitForSeconds(
|
||||
Mathf.Max(0f, startDelay)
|
||||
);
|
||||
|
||||
while (true)
|
||||
{
|
||||
// Neuer Zyklus beginnt.
|
||||
hasSpawnedGroupThisCycle = false;
|
||||
|
||||
while (true)
|
||||
{
|
||||
List<EnemyTypeData> group;
|
||||
float resultingDifficulty;
|
||||
|
||||
bool groupCanSpawn = TryBuildGroup(
|
||||
out group,
|
||||
out resultingDifficulty
|
||||
);
|
||||
|
||||
// Keine gültige Gruppe möglich:
|
||||
// Der aktuelle Zyklus endet.
|
||||
if (!groupCanSpawn)
|
||||
{
|
||||
pauseDifficultyGrowth = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Nur zwischen Gruppen desselben Zyklus warten.
|
||||
if (hasSpawnedGroupThisCycle)
|
||||
{
|
||||
pauseDifficultyGrowth = true;
|
||||
|
||||
yield return new WaitForSeconds(
|
||||
GetRandomDelay(minGroupBreak, maxGroupBreak)
|
||||
);
|
||||
}
|
||||
|
||||
// Budget während des tatsächlichen Gruppenspawns
|
||||
// ebenfalls einfrieren.
|
||||
pauseDifficultyGrowth = true;
|
||||
|
||||
for (int i = 0; i < group.Count; i++)
|
||||
{
|
||||
SpawnEnemy(group[i]);
|
||||
|
||||
if (i < group.Count - 1)
|
||||
{
|
||||
yield return new WaitForSeconds(
|
||||
Mathf.Max(0f, spawnDelay)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
difficulty = resultingDifficulty;
|
||||
|
||||
// Ab jetzt darf eine weitere Gruppe in diesem
|
||||
// Zyklus erst nach einer GroupBreak spawnen.
|
||||
hasSpawnedGroupThisCycle = true;
|
||||
|
||||
// Budget nach dem Gruppenspawn wieder freigeben.
|
||||
pauseDifficultyGrowth = false;
|
||||
}
|
||||
|
||||
// Zyklus beendet: normale Pause, während der das
|
||||
// Budget weiterwachsen darf.
|
||||
pauseDifficultyGrowth = false;
|
||||
|
||||
yield return new WaitForSeconds(
|
||||
GetRandomDelay(minBreak, maxBreak)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryBuildGroup(
|
||||
out List<EnemyTypeData> group,
|
||||
out float resultingDifficulty)
|
||||
{
|
||||
group = new List<EnemyTypeData>();
|
||||
resultingDifficulty = difficulty;
|
||||
|
||||
// Nur freigeschaltete Gegnertypen berücksichtigen.
|
||||
// Die Budgetkosten werden hier noch NICHT gefiltert.
|
||||
List<EnemyTypeData> availableEnemies =
|
||||
GetAvailableEnemies();
|
||||
|
||||
if (availableEnemies.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int targetGroupSize = Random.Range(
|
||||
minGroup,
|
||||
maxGroup + 1
|
||||
);
|
||||
|
||||
tempDifficulty = difficulty;
|
||||
|
||||
for (int i = 0; i < targetGroupSize; i++)
|
||||
{
|
||||
// Auswahl ausschließlich anhand von spawnChance.
|
||||
EnemyTypeData selectedEnemy =
|
||||
ChooseWeightedEnemy(availableEnemies);
|
||||
|
||||
if (selectedEnemy == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Kosten des ausgewählten Gegners abziehen.
|
||||
tempDifficulty -= selectedEnemy.enemyDifficulty;
|
||||
|
||||
if (tempDifficulty < 0f)
|
||||
{
|
||||
// D2: Die bisherigen Gegner reichen bereits
|
||||
// für die Mindestgruppengröße.
|
||||
if (group.Count >= minGroup)
|
||||
{
|
||||
// Die Kosten des abgelehnten Gegners
|
||||
// wieder hinzurechnen.
|
||||
resultingDifficulty =
|
||||
tempDifficulty +
|
||||
selectedEnemy.enemyDifficulty;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// D3: Mindestgruppengröße nicht erreicht.
|
||||
// Nichts spawnen und Budget nicht verändern.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Der Gegner passt ins Budget und wird vorgemerkt.
|
||||
group.Add(selectedEnemy);
|
||||
}
|
||||
|
||||
// D1: Die gewünschte Gruppengröße wurde erreicht.
|
||||
resultingDifficulty = tempDifficulty;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<EnemyTypeData> GetAvailableEnemies()
|
||||
{
|
||||
List<EnemyTypeData> available =
|
||||
new List<EnemyTypeData>();
|
||||
|
||||
foreach (EnemyTypeData enemy in enemyTypes)
|
||||
{
|
||||
if (enemy == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (enemy.danger > time)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (enemy.spawnChance <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (enemy.enemyDifficulty < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (enemy.prefabVariants == null ||
|
||||
enemy.prefabVariants.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
bool hasValidPrefab = false;
|
||||
|
||||
foreach (GameObject prefab in enemy.prefabVariants)
|
||||
{
|
||||
if (prefab != null)
|
||||
{
|
||||
hasValidPrefab = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasValidPrefab)
|
||||
{
|
||||
available.Add(enemy);
|
||||
}
|
||||
}
|
||||
|
||||
return available;
|
||||
}
|
||||
|
||||
private EnemyTypeData ChooseWeightedEnemy(
|
||||
List<EnemyTypeData> availableEnemies)
|
||||
{
|
||||
int totalWeight = 0;
|
||||
|
||||
foreach (EnemyTypeData enemy in availableEnemies)
|
||||
{
|
||||
totalWeight += enemy.spawnChance;
|
||||
}
|
||||
|
||||
if (totalWeight <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
int roll = Random.Range(1, totalWeight + 1);
|
||||
int cumulativeWeight = 0;
|
||||
|
||||
foreach (EnemyTypeData enemy in availableEnemies)
|
||||
{
|
||||
cumulativeWeight += enemy.spawnChance;
|
||||
|
||||
if (roll <= cumulativeWeight)
|
||||
{
|
||||
return enemy;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void SpawnEnemy(EnemyTypeData enemy)
|
||||
{
|
||||
List<GameObject> validPrefabs = new List<GameObject>();
|
||||
|
||||
foreach (GameObject prefab in enemy.prefabVariants)
|
||||
{
|
||||
if (prefab != null)
|
||||
{
|
||||
validPrefabs.Add(prefab);
|
||||
}
|
||||
}
|
||||
|
||||
if (validPrefabs.Count == 0)
|
||||
{
|
||||
Debug.LogError(
|
||||
"EnemySpawner: Keine gültige Prefabvariante vorhanden!",
|
||||
this
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Zufällige Prefabvariante wählen.
|
||||
int prefabIndex = Random.Range(0, validPrefabs.Count);
|
||||
GameObject selectedPrefab = validPrefabs[prefabIndex];
|
||||
|
||||
GameObject spawnedEnemy = Instantiate(
|
||||
selectedPrefab,
|
||||
spawnPoint.position,
|
||||
spawnPoint.rotation
|
||||
);
|
||||
|
||||
// Das Sprite hat eine 50-prozentige Chance,
|
||||
// vertikal gespiegelt zu werden.
|
||||
SpriteRenderer spriteRenderer =
|
||||
spawnedEnemy.GetComponentInChildren<SpriteRenderer>();
|
||||
|
||||
if (spriteRenderer != null)
|
||||
{
|
||||
spriteRenderer.flipX = Random.value < 0.5f;
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasUsableEnemyType()
|
||||
{
|
||||
return GetAvailableEnemies().Count > 0 ||
|
||||
HasAnyConfiguredEnemy();
|
||||
}
|
||||
|
||||
private bool HasAnyConfiguredEnemy()
|
||||
{
|
||||
foreach (EnemyTypeData enemy in enemyTypes)
|
||||
{
|
||||
if (enemy == null || enemy.spawnChance <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (enemy.prefabVariants == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (GameObject prefab in enemy.prefabVariants)
|
||||
{
|
||||
if (prefab != null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private float GetRandomDelay(float min, float max)
|
||||
{
|
||||
min = Mathf.Max(0f, min);
|
||||
max = Mathf.Max(0f, max);
|
||||
|
||||
if (max < min)
|
||||
{
|
||||
float temp = min;
|
||||
min = max;
|
||||
max = temp;
|
||||
}
|
||||
|
||||
return Random.Range(min, max);
|
||||
}
|
||||
|
||||
private void OnValidate()
|
||||
{
|
||||
raiseRate = Mathf.Max(0f, raiseRate);
|
||||
startDelay = Mathf.Max(0f, startDelay);
|
||||
spawnDelay = Mathf.Max(0f, spawnDelay);
|
||||
|
||||
minGroup = Mathf.Max(1, minGroup);
|
||||
maxGroup = Mathf.Max(minGroup, maxGroup);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eed070947de809f4d9fd7c87e1d9b1c3
|
||||
@@ -0,0 +1,193 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class EnemyTargetManager : MonoBehaviour
|
||||
{
|
||||
[Header("Target Points")]
|
||||
[SerializeField]
|
||||
private List<Transform> targetPoints = new List<Transform>();
|
||||
|
||||
[Header("Active Target Chance")]
|
||||
[SerializeField]
|
||||
[Range(0f, 1f)]
|
||||
private float twoTargetsChance = 0.25f;
|
||||
|
||||
[Header("Switch Timing")]
|
||||
[SerializeField]
|
||||
private float minSwitchDelay = 15f;
|
||||
|
||||
[SerializeField]
|
||||
private float maxSwitchDelay = 25f;
|
||||
|
||||
[Header("Target Selection")]
|
||||
[SerializeField]
|
||||
[Min(0f)]
|
||||
private float equalDistanceTolerance = 0.1f; //In Unity-Einheiten!
|
||||
|
||||
// Wird ausgelöst, wenn sich die aktiven Ziele ändern.
|
||||
public event Action OnTargetsChanged;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (targetPoints.Count == 0)
|
||||
{
|
||||
Debug.LogError(
|
||||
"EnemyTargetManager: Keine Target Points zugewiesen!",
|
||||
this
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (targetPoints.Count < 2)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
"EnemyTargetManager: Es sind weniger als 2 Target Points vorhanden. Zwei aktive Targets sind dadurch nicht möglich.",
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
if (minSwitchDelay > maxSwitchDelay)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
"EnemyTargetManager: minSwitchDelay ist größer als maxSwitchDelay. Werte werden getauscht.",
|
||||
this
|
||||
);
|
||||
|
||||
float temp = minSwitchDelay;
|
||||
minSwitchDelay = maxSwitchDelay;
|
||||
maxSwitchDelay = temp;
|
||||
}
|
||||
|
||||
// Zu Beginn zufällig Targets aktivieren.
|
||||
SetRandomActiveTargets();
|
||||
|
||||
// Danach regelmäßig wechseln.
|
||||
StartCoroutine(TargetSwitchLoop());
|
||||
}
|
||||
|
||||
private IEnumerator TargetSwitchLoop()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
float delay = UnityEngine.Random.Range(
|
||||
minSwitchDelay,
|
||||
maxSwitchDelay
|
||||
);
|
||||
|
||||
yield return new WaitForSeconds(delay);
|
||||
|
||||
SetRandomActiveTargets();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetRandomActiveTargets()
|
||||
{
|
||||
// Zuerst alle Targets deaktivieren.
|
||||
foreach (Transform target in targetPoints)
|
||||
{
|
||||
if (target != null)
|
||||
{
|
||||
target.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Entscheiden, ob 1 oder 2 Targets aktiv sein sollen.
|
||||
int activeTargetCount = 1;
|
||||
|
||||
if (targetPoints.Count >= 2 &&
|
||||
UnityEngine.Random.value < twoTargetsChance)
|
||||
{
|
||||
activeTargetCount = 2;
|
||||
}
|
||||
|
||||
// Alle gültigen Targets sammeln.
|
||||
List<Transform> availableTargets =
|
||||
new List<Transform>();
|
||||
|
||||
foreach (Transform target in targetPoints)
|
||||
{
|
||||
if (target != null)
|
||||
{
|
||||
availableTargets.Add(target);
|
||||
}
|
||||
}
|
||||
|
||||
// Zufällige Targets aktivieren.
|
||||
for (int i = 0; i < activeTargetCount; i++)
|
||||
{
|
||||
int randomIndex = UnityEngine.Random.Range(
|
||||
0,
|
||||
availableTargets.Count
|
||||
);
|
||||
|
||||
Transform target =
|
||||
availableTargets[randomIndex];
|
||||
|
||||
target.gameObject.SetActive(true);
|
||||
|
||||
// Verhindert doppelte Auswahl.
|
||||
availableTargets.RemoveAt(randomIndex);
|
||||
}
|
||||
|
||||
// Alle Gegner informieren.
|
||||
OnTargetsChanged?.Invoke();
|
||||
}
|
||||
|
||||
public Transform GetClosestActiveTarget(Vector3 position)
|
||||
{
|
||||
List<Transform> closestTargets =
|
||||
new List<Transform>();
|
||||
|
||||
float closestDistance = float.MaxValue;
|
||||
|
||||
foreach (Transform target in targetPoints)
|
||||
{
|
||||
if (target == null ||
|
||||
!target.gameObject.activeInHierarchy)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Tatsächliche Entfernung in Unity-Einheiten.
|
||||
float distance =
|
||||
Vector3.Distance(
|
||||
position,
|
||||
target.position
|
||||
);
|
||||
|
||||
// Eindeutig näher als die bisher gefundenen Targets.
|
||||
if (distance <
|
||||
closestDistance - equalDistanceTolerance)
|
||||
{
|
||||
closestDistance = distance;
|
||||
|
||||
closestTargets.Clear();
|
||||
closestTargets.Add(target);
|
||||
}
|
||||
// Innerhalb der eingestellten Toleranz:
|
||||
// Dieses Target gilt als gleich nah.
|
||||
else if (Mathf.Abs(
|
||||
distance - closestDistance
|
||||
) <= equalDistanceTolerance)
|
||||
{
|
||||
closestTargets.Add(target);
|
||||
}
|
||||
}
|
||||
|
||||
if (closestTargets.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Bei mehreren gleich nahen Targets zufällig wählen.
|
||||
int randomIndex = UnityEngine.Random.Range(
|
||||
0,
|
||||
closestTargets.Count
|
||||
);
|
||||
|
||||
return closestTargets[randomIndex];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6ac3932d87de0d64180930ddb4e2f576
|
||||
@@ -0,0 +1,6 @@
|
||||
using UnityEngine;
|
||||
|
||||
public interface IMovementDirection
|
||||
{
|
||||
Vector2 MovementDirection { get; }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 649a0945a4204564187826046e548805
|
||||
@@ -0,0 +1,68 @@
|
||||
using System.Collections;
|
||||
using UnityEngine;
|
||||
|
||||
public class OLD_EnemySpawner : MonoBehaviour
|
||||
{
|
||||
[Header("Enemy")]
|
||||
[SerializeField]
|
||||
private GameObject enemyPrefab;
|
||||
|
||||
[Header("Spawn")]
|
||||
[SerializeField]
|
||||
private Transform spawnPoint;
|
||||
|
||||
[SerializeField]
|
||||
private float minSpawnDelay = 0.5f;
|
||||
|
||||
[SerializeField]
|
||||
private float maxSpawnDelay = 2f;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (enemyPrefab == null)
|
||||
{
|
||||
Debug.LogError(
|
||||
"EnemySpawner: Kein Enemy-Prefab zugewiesen!",
|
||||
this
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (spawnPoint == null)
|
||||
{
|
||||
Debug.LogError(
|
||||
"EnemySpawner: Kein SpawnPoint zugewiesen!",
|
||||
this
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
StartCoroutine(SpawnLoop());
|
||||
}
|
||||
|
||||
private IEnumerator SpawnLoop()
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
float delay = UnityEngine.Random.Range(
|
||||
minSpawnDelay,
|
||||
maxSpawnDelay
|
||||
);
|
||||
|
||||
yield return new WaitForSeconds(delay);
|
||||
|
||||
SpawnEnemy();
|
||||
}
|
||||
}
|
||||
|
||||
private void SpawnEnemy()
|
||||
{
|
||||
Instantiate(
|
||||
enemyPrefab,
|
||||
spawnPoint.position,
|
||||
spawnPoint.rotation
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f8b6d72d4e53e5d4cb31ba8ec6903a94
|
||||
@@ -0,0 +1,209 @@
|
||||
using UnityEngine;
|
||||
using DepthsOfGreed.HandbookUI;
|
||||
|
||||
public class Turret : MonoBehaviour
|
||||
{
|
||||
[Header("Tower Data")]
|
||||
[SerializeField]
|
||||
private HandbookTowerEntry towerData;
|
||||
|
||||
[Header("Turret Setup")]
|
||||
[SerializeField]
|
||||
private Transform turret;
|
||||
|
||||
[SerializeField]
|
||||
private Transform tip;
|
||||
|
||||
[SerializeField]
|
||||
private float rotationSpeed = 360f;
|
||||
|
||||
[Header("Projectile Setup")]
|
||||
[SerializeField]
|
||||
private GameObject bulletPrefab;
|
||||
|
||||
[SerializeField]
|
||||
private float bulletSpeed = 8f;
|
||||
|
||||
private CombatDefinition combatStats;
|
||||
|
||||
private float nextAttackTime;
|
||||
|
||||
private Enemy currentTarget;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (towerData == null)
|
||||
{
|
||||
Debug.LogError(
|
||||
"TowerShooting: Kein HandbookTowerEntry zugewiesen!",
|
||||
this
|
||||
);
|
||||
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (turret == null || tip == null || bulletPrefab == null)
|
||||
{
|
||||
Debug.LogError(
|
||||
"TowerShooting: Turret, Tip oder Bullet Prefab fehlt!",
|
||||
this
|
||||
);
|
||||
|
||||
enabled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
combatStats = towerData.CreateCombatSnapshot(0);
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (combatStats == null)
|
||||
return;
|
||||
|
||||
// Ziel innerhalb der Reichweite suchen.
|
||||
currentTarget = FindTarget();
|
||||
|
||||
if (currentTarget != null)
|
||||
{
|
||||
RotateTowardsTarget();
|
||||
TryShoot();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Zur ursprünglichen Ausrichtung zurückkehren.
|
||||
RotateTowardsOrigin();
|
||||
}
|
||||
}
|
||||
|
||||
private Enemy FindTarget()
|
||||
{
|
||||
Enemy[] enemies =
|
||||
FindObjectsByType<Enemy>(
|
||||
FindObjectsSortMode.None
|
||||
);
|
||||
|
||||
Enemy closestEnemy = null;
|
||||
|
||||
float closestDistance = float.MaxValue;
|
||||
|
||||
float rangeSquared =
|
||||
combatStats.range * combatStats.range;
|
||||
|
||||
foreach (Enemy enemy in enemies)
|
||||
{
|
||||
if (enemy == null || enemy.IsDead)
|
||||
continue;
|
||||
|
||||
Vector2 difference =
|
||||
enemy.transform.position - transform.position;
|
||||
|
||||
float distanceSquared = difference.sqrMagnitude;
|
||||
|
||||
if (distanceSquared > rangeSquared)
|
||||
continue;
|
||||
|
||||
if (distanceSquared < closestDistance)
|
||||
{
|
||||
closestDistance = distanceSquared;
|
||||
closestEnemy = enemy;
|
||||
}
|
||||
}
|
||||
|
||||
return closestEnemy;
|
||||
}
|
||||
|
||||
private void RotateTowardsTarget()
|
||||
{
|
||||
Vector3 worldDirection =
|
||||
currentTarget.transform.position - turret.position;
|
||||
|
||||
// Richtung in den lokalen Raum der Turmbasis umwandeln.
|
||||
Vector3 localDirection =
|
||||
turret.parent != null
|
||||
? turret.parent.InverseTransformDirection(worldDirection)
|
||||
: worldDirection;
|
||||
|
||||
float targetAngle =
|
||||
Mathf.Atan2(localDirection.y, localDirection.x)
|
||||
* Mathf.Rad2Deg - 90f;
|
||||
|
||||
Quaternion targetRotation =
|
||||
Quaternion.Euler(0f, 0f, targetAngle);
|
||||
|
||||
turret.localRotation = Quaternion.RotateTowards(
|
||||
turret.localRotation,
|
||||
targetRotation,
|
||||
rotationSpeed * Time.deltaTime
|
||||
);
|
||||
}
|
||||
|
||||
private void RotateTowardsOrigin()
|
||||
{
|
||||
turret.localRotation = Quaternion.RotateTowards(
|
||||
turret.localRotation,
|
||||
Quaternion.identity,
|
||||
rotationSpeed * Time.deltaTime
|
||||
);
|
||||
}
|
||||
|
||||
private void TryShoot()
|
||||
{
|
||||
if (combatStats.attacksPerSecond <= 0f)
|
||||
return;
|
||||
|
||||
if (Time.time < nextAttackTime)
|
||||
return;
|
||||
|
||||
Shoot();
|
||||
|
||||
float attackInterval =
|
||||
1f / combatStats.attacksPerSecond;
|
||||
|
||||
nextAttackTime = Time.time + attackInterval;
|
||||
}
|
||||
|
||||
private void Shoot()
|
||||
{
|
||||
GameObject bulletObject = Instantiate(
|
||||
bulletPrefab,
|
||||
tip.position,
|
||||
tip.rotation
|
||||
);
|
||||
|
||||
Bullet bullet =
|
||||
bulletObject.GetComponent<Bullet>();
|
||||
|
||||
if (bullet == null)
|
||||
{
|
||||
Debug.LogError(
|
||||
"TowerShooting: Bullet Prefab besitzt kein TowerBullet-Skript!",
|
||||
bulletObject
|
||||
);
|
||||
|
||||
Destroy(bulletObject);
|
||||
return;
|
||||
}
|
||||
|
||||
bullet.Initialize(
|
||||
tip.up,
|
||||
bulletSpeed,
|
||||
combatStats.damagePerHit,
|
||||
combatStats.range
|
||||
);
|
||||
}
|
||||
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
if (towerData == null)
|
||||
return;
|
||||
|
||||
Gizmos.color = Color.red;
|
||||
|
||||
Gizmos.DrawWireSphere(
|
||||
transform.position,
|
||||
towerData.combat.range
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ec586d6b5f1d324d89950df76c68b91
|
||||
Reference in new issue
Block a user