Danro-Jump/Assets/Scripts/Runtime/Gameplay/Spawning/PlatformContentSpawner.cs
Danil Iarosh 77186697f8 Prepare v0.1 gameplay and hardware foundation
Подробные изменения:

- Расширен основной gameplay loop: джетпак, новые платформы, оборудование, враги, прогрессия сложности и ограничения взаимодействий игрока.

- Добавлены Bootstrap-сервисы запуска приложения, защита, фокус окна, управление курсором и настройки сервисов через DI.

- Добавлена COM-интеграция с технической схемой Input System, обработкой кнопок/ADC, подсветкой и консольной диагностикой.

- Добавлен отдельный debug/logging модуль с каналами, обработчиками консоли и файловым выводом.

- Добавлены asmdef, link.xml, EditMode/PlayMode тесты и обновлена дорожная карта проекта.

Проверки:

- dotnet build .\\DanroJump.Runtime.csproj --no-restore

- dotnet build .\\DanroJump.Tests.EditMode.csproj --no-restore

- dotnet build .\\DanroJump.Tests.PlayMode.csproj --no-restore
2026-06-08 14:22:48 +03:00

294 lines
10 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Collections.Generic;
using UnityEngine;
using Zenject;
namespace DanroJump.Gameplay
{
/// <summary>
/// Создает collectibles, equipment и enemies на точках спавна платформы.
/// </summary>
public sealed class PlatformContentSpawner
{
private const string EnemyRootName = "Enemy Pool";
private readonly DiContainer container;
private readonly Dictionary<PlatformController, List<GameObject>> spawnedContentByPlatform = new();
private readonly List<PlatformSpawnedContent> orphanMarkerBuffer = new();
private readonly List<PlatformSpawnPoint> spawnPointCandidates = new();
private Transform enemyRoot;
/// <summary>
/// Получает DI-контейнер для создания prefab с injection.
/// </summary>
public PlatformContentSpawner(DiContainer container)
{
this.container = container;
}
/// <summary>
/// Очищает старый контент платформы и спавнит новый по правилам источника.
/// </summary>
public void SpawnContent(
PlatformController platform,
IPlatformContentSource catalog,
System.Random random)
{
ClearSpawnedContent(platform);
if (platform == null || catalog == null)
{
return;
}
SpawnIntoPoints(platform, catalog, PlatformContentKind.Collectible, platform.CollectibleSpawnPoints, random);
SpawnIntoPoints(platform, catalog, PlatformContentKind.Equipment, platform.EquipmentSpawnPoints, random);
SpawnIntoPoints(platform, catalog, PlatformContentKind.Enemy, platform.EnemySpawnPoints, random);
}
/// <summary>
/// Удаляет весь контент, созданный для указанной платформы.
/// </summary>
public void ClearSpawnedContent(PlatformController platform)
{
if (platform == null)
{
return;
}
if (!ClearTrackedContent(platform))
{
ClearOrphanSpawnedContent(platform);
}
}
/// <summary>
/// Очищает контент всех отслеживаемых платформ.
/// </summary>
public void ClearAll()
{
foreach (var pair in spawnedContentByPlatform)
{
ClearContentList(pair.Value);
}
spawnedContentByPlatform.Clear();
}
/// <summary>
/// Спавнит одну категорию контента в одну выбранную точку платформы.
/// </summary>
private void SpawnIntoPoints(
PlatformController platform,
IPlatformContentSource catalog,
PlatformContentKind kind,
IReadOnlyList<PlatformSpawnPoint> spawnPoints,
System.Random random)
{
if (spawnPoints == null || !catalog.ShouldSpawn(kind, random) || !catalog.TryGetPrefab(kind, random, out var prefab))
{
return;
}
var selectedSpawnPoint = SelectSpawnPoint(spawnPoints, random);
if (selectedSpawnPoint != null)
{
Spawn(prefab, selectedSpawnPoint.Anchor, platform);
}
}
/// <summary>
/// Создает конкретный prefab в точке спавна и привязывает оборудование к платформе.
/// </summary>
private void Spawn(GameObject prefab, Transform parent, PlatformController platform)
{
var targetParent = parent != null ? parent : platform.transform;
var isEnemy = EnemyPrefabRules.TryGetEnemy(prefab, out _);
var instanceParent = isEnemy ? GetEnemyRoot(platform) : targetParent;
var instance = container.InstantiatePrefab(prefab, targetParent.position, targetParent.rotation, instanceParent);
if (isEnemy)
{
instance.transform.SetPositionAndRotation(targetParent.position, targetParent.rotation);
}
// Маркер нужен, чтобы при аварийной очистке найти даже неотслеженный дочерний контент.
if (!instance.TryGetComponent(out PlatformSpawnedContent marker))
{
marker = instance.AddComponent<PlatformSpawnedContent>();
}
marker.ResetForPlatformSpawn();
if (instance.TryGetComponent(out IPlatformEquipment equipment))
{
equipment.Install(platform);
}
if (!spawnedContentByPlatform.TryGetValue(platform, out var spawnedContent))
{
spawnedContent = new List<GameObject>();
spawnedContentByPlatform.Add(platform, spawnedContent);
}
spawnedContent.Add(instance);
}
/// <summary>
/// Возвращает отдельный контейнер для врагов, чтобы платформы не управляли их визуалами и коллайдерами как своими дочерними объектами.
/// </summary>
private Transform GetEnemyRoot(PlatformController platform)
{
if (enemyRoot != null)
{
return enemyRoot;
}
var rootObject = GameObject.Find(EnemyRootName);
if (rootObject == null)
{
rootObject = new GameObject(EnemyRootName);
}
enemyRoot = rootObject.transform;
if (platform != null && platform.transform.parent != null && enemyRoot.parent == null)
{
enemyRoot.SetParent(platform.transform.parent, false);
}
return enemyRoot;
}
/// <summary>
/// Удаляет контент, который был записан в словарь отслеживания.
/// </summary>
private bool ClearTrackedContent(PlatformController platform)
{
if (!spawnedContentByPlatform.TryGetValue(platform, out var spawnedContent))
{
return false;
}
ClearContentList(spawnedContent);
spawnedContent.Clear();
spawnedContentByPlatform.Remove(platform);
return true;
}
/// <summary>
/// Уничтожает объекты из уже выделенного списка без освобождения самого списка.
/// </summary>
private static void ClearContentList(List<GameObject> spawnedContent)
{
for (var index = spawnedContent.Count - 1; index >= 0; index--)
{
var content = spawnedContent[index];
if (content == null)
{
spawnedContent.RemoveAt(index);
continue;
}
if (content.TryGetComponent(out PlatformSpawnedContent marker) && marker.IsDetachedFromPlatform)
{
spawnedContent.RemoveAt(index);
continue;
}
Object.Destroy(content);
spawnedContent.RemoveAt(index);
}
}
/// <summary>
/// Fallback-очистка дочерних объектов, если словарь не содержит платформу.
/// </summary>
private void ClearOrphanSpawnedContent(PlatformController platform)
{
platform.GetComponentsInChildren(true, orphanMarkerBuffer);
for (var index = orphanMarkerBuffer.Count - 1; index >= 0; index--)
{
var marker = orphanMarkerBuffer[index];
if (marker != null)
{
if (marker.IsDetachedFromPlatform)
{
continue;
}
Object.Destroy(marker.gameObject);
}
}
orphanMarkerBuffer.Clear();
}
/// <summary>
/// Проверяет, нужно ли пропустить optional-точку спавна по ее весу.
/// </summary>
private static bool ShouldSkipOptionalPoint(PlatformSpawnPoint spawnPoint, System.Random random)
{
if (!spawnPoint.Optional)
{
return false;
}
if (spawnPoint.Weight <= 0f)
{
return true;
}
if (spawnPoint.Weight >= 1f)
{
return false;
}
var roll = random != null ? random.NextDouble() : 0d;
return roll > spawnPoint.Weight;
}
/// <summary>
/// Выбирает одну подходящую точку по весам, чтобы плотность контента не зависела от числа маркеров на prefab.
/// </summary>
private PlatformSpawnPoint SelectSpawnPoint(
IReadOnlyList<PlatformSpawnPoint> spawnPoints,
System.Random random)
{
spawnPointCandidates.Clear();
var totalWeight = 0f;
for (var index = 0; index < spawnPoints.Count; index++)
{
var spawnPoint = spawnPoints[index];
if (spawnPoint == null || ShouldSkipOptionalPoint(spawnPoint, random))
{
continue;
}
spawnPointCandidates.Add(spawnPoint);
totalWeight += Mathf.Max(0f, spawnPoint.Weight);
}
if (totalWeight <= 0f)
{
spawnPointCandidates.Clear();
return null;
}
var roll = (float)((random != null ? random.NextDouble() : 0d) * totalWeight);
for (var index = 0; index < spawnPointCandidates.Count; index++)
{
var spawnPoint = spawnPointCandidates[index];
roll -= Mathf.Max(0f, spawnPoint.Weight);
if (roll <= 0f)
{
spawnPointCandidates.Clear();
return spawnPoint;
}
}
spawnPointCandidates.Clear();
return null;
}
}
}