Gegner Spawnlogik und Schwierigkeitserhöhung mit Normalo-Gegner

This commit is contained in:
2026-10-14 14:59:26 +02:00
parent b18bb2c2bd
commit c33c780066
14 changed files with 1258 additions and 35 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ public class Enemy : MonoBehaviour, IMovementDirection
{
[Header("Movement")]
[SerializeField]
private float moveSpeed = 2f;
private float moveSpeed = 0.5f;
[SerializeField]
private float targetReachedDistance = 0.1f;
+383 -31
View File
@@ -1,34 +1,57 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemySpawner : MonoBehaviour
{
[Header("Enemy")]
[SerializeField]
private GameObject enemyPrefab;
[System.Serializable]
public class EnemyTypeData
{
[Header("Enemy Prefabs")]
public List<GameObject> prefabVariants = new List<GameObject>();
[Header("Spawn")]
[SerializeField]
private Transform spawnPoint;
[Header("Spawn Selection")]
[Min(0)]
public int spawnChance = 10;
[SerializeField]
private float minSpawnDelay = 0.5f;
public float danger = 0f;
[SerializeField]
private float maxSpawnDelay = 2f;
[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 void Start()
{
if (enemyPrefab == null)
{
Debug.LogError(
"EnemySpawner: Kein Enemy-Prefab zugewiesen!",
this
);
return;
}
if (spawnPoint == null)
{
Debug.LogError(
@@ -36,33 +59,362 @@ public class EnemySpawner : MonoBehaviour
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.
difficultyRaise += raiseRate * Time.deltaTime;
// Budget wächst nur, wenn es nicht pausiert ist.
if (!pauseDifficultyGrowth)
{
difficulty += difficultyRaise * Time.deltaTime;
}
Debug.Log("DifficultyRaise: " + difficultyRaise + " und Difficulty: " + difficulty);
}
private IEnumerator SpawnLoop()
{
// Einmalige Wartezeit beim Spielstart.
yield return new WaitForSeconds(
Mathf.Max(0f, startDelay)
);
while (true)
{
float delay = UnityEngine.Random.Range(
minSpawnDelay,
maxSpawnDelay
// 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)
);
yield return new WaitForSeconds(delay);
SpawnEnemy();
}
}
private void SpawnEnemy()
private bool TryBuildGroup(
out List<EnemyTypeData> group,
out float resultingDifficulty)
{
Instantiate(
enemyPrefab,
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 > difficultyRaise)
{
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);
}
}
+68
View File
@@ -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
);
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f8b6d72d4e53e5d4cb31ba8ec6903a94