420 lines
11 KiB
C#
420 lines
11 KiB
C#
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 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.
|
|
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)
|
|
{
|
|
// 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 > 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);
|
|
}
|
|
} |