Подробные изменения: - Расширен основной 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
294 lines
10 KiB
C#
294 lines
10 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|