Danil Iarosh cefb2fd345 Build initial vertical platformer foundation
Подробные изменения:
- Добавлены сцены Bootstrap, MainMenu и Game с текущей вертикальной 9:16 структурой проекта.
- Добавлены runtime-скрипты gameplay-архитектуры: DI через Extenject, спавнер платформ, прогрессия этапов, one-way платформы, игрок, камера и оборудование JumpPad.
- Добавлены ScriptableObject-этапы сложности, weighted-настройки платформ, оборудования, противников и collectibles.
- Добавлены UI Toolkit ресурсы главного меню, preview-материалы, шрифты, спрайты, анимации и игровые prefab-ассеты.
- Перенесена и оформлена новая COM-система для работы с аппаратными портами и StreamingAssets.
- Обновлены проектные гайды, AGENTS.md, правила Git/Git LFS, .gitignore, Unity Packages и ProjectSettings.
- Удалён локальный skill start-required-mcp-servers из репозитория.
2026-05-11 07:41:14 +03:00

359 lines
13 KiB
C#

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
using Zenject;
namespace DanroJump.Gameplay
{
public sealed class PlatformSpawner : MonoBehaviour
{
[Min(1)]
[SerializeField] private int initialPlatformCount = 34;
[Min(0f)]
[SerializeField] private float horizontalPadding = 0.85f;
[FormerlySerializedAs("recycleDistanceBelowCamera")]
[SerializeField]
[Min(0f)]
[Tooltip("How far below the camera a platform must fall before it is recycled. Increase this to make platforms disappear lower.")]
private float recycleDistanceBelowCameraBottom = 12f;
[SerializeField] private bool useHardwareRandomSeed = true;
[SerializeField] private int randomSeed = 1207;
private readonly List<PlatformController> platforms = new();
private readonly Dictionary<PlatformController, GameObject> prefabByPlatform = new();
private DiContainer container;
private Camera gameplayCamera;
private GameplayDifficultyProgression difficultyProgression;
private PlatformContentSpawner contentSpawner;
private JumpPlayerController playerController;
private System.Random random;
private int currentRandomSeed;
private float highestPlatformY;
private int nextSpawnIndex;
public int CurrentRandomSeed => currentRandomSeed;
public float CurrentGenerationY => highestPlatformY;
public string CurrentGenerationStage => GetStageName(highestPlatformY);
public float CurrentGenerationStageProgress => ProgressionForDiagnostics != null ? ProgressionForDiagnostics.GetStageProgress(highestPlatformY) : 0f;
public float CurrentPlatformDistribution => ProgressionForDiagnostics != null ? ProgressionForDiagnostics.GetHorizontalDistribution(highestPlatformY) : 0f;
private GameplayDifficultyProgression ProgressionForDiagnostics =>
difficultyProgression != null ? difficultyProgression : FindFirstObjectByType<GameplayDifficultyProgression>();
[Inject]
public void Construct(
DiContainer injectedContainer,
JumpPlayerController playerController,
Camera injectedGameplayCamera,
PlatformContentSpawner injectedContentSpawner,
GameplayDifficultyProgression injectedDifficultyProgression)
{
container = injectedContainer;
this.playerController = playerController;
gameplayCamera = injectedGameplayCamera;
contentSpawner = injectedContentSpawner;
difficultyProgression = injectedDifficultyProgression;
}
private void Awake()
{
currentRandomSeed = useHardwareRandomSeed ? CreateHardwareRandomSeed() : randomSeed;
random = new System.Random(currentRandomSeed);
Debug.Log($"{nameof(PlatformSpawner)} random seed: {currentRandomSeed} ({(useHardwareRandomSeed ? "hardware" : "manual")}).", this);
}
private void Start()
{
if (!ValidateRuntimeState())
{
return;
}
GenerateInitialPlatforms();
}
private void Update()
{
RecyclePlatformsBelowCamera();
}
private void OnDestroy()
{
if (contentSpawner == null)
{
return;
}
foreach (var platform in platforms)
{
contentSpawner.ClearSpawnedContent(platform);
}
prefabByPlatform.Clear();
}
private void GenerateInitialPlatforms()
{
highestPlatformY = difficultyProgression.InitialSpawnY;
nextSpawnIndex = 0;
SpawnPlatform(new Vector2(0f, highestPlatformY), true);
for (var index = 1; index < initialPlatformCount; index++)
{
highestPlatformY += RandomVerticalSpacing();
SpawnPlatform(new Vector2(RandomHorizontalPosition(highestPlatformY), highestPlatformY), false);
}
}
private void RecyclePlatformsBelowCamera()
{
if (gameplayCamera == null || platforms.Count == 0)
{
return;
}
var bottomLimit = gameplayCamera.transform.position.y - recycleDistanceBelowCameraBottom;
for (var index = 0; index < platforms.Count; index++)
{
var platform = platforms[index];
if (platform == null || platform.transform.position.y >= bottomLimit)
{
continue;
}
highestPlatformY += RandomVerticalSpacing();
platforms[index] = RecyclePlatform(platform, new Vector2(RandomHorizontalPosition(highestPlatformY), highestPlatformY));
}
}
private void SpawnPlatform(Vector2 position, bool isStartPlatform)
{
var selectedPlatformPrefab = GetPlatformPrefab(position.y);
var platform = CreatePlatform(selectedPlatformPrefab, position, isStartPlatform);
if (platform == null)
{
return;
}
platforms.Add(platform);
InitializePlatform(platform, CreateContext(platform, isStartPlatform));
}
private PlatformController RecyclePlatform(PlatformController platform, Vector2 position)
{
var selectedPlatformPrefab = GetPlatformPrefab(position.y);
if (ShouldReplacePlatform(platform, selectedPlatformPrefab))
{
return ReplacePlatform(platform, selectedPlatformPrefab, position);
}
contentSpawner.ClearSpawnedContent(platform);
platform.gameObject.SetActive(true);
platform.transform.position = new Vector3(position.x, position.y, platform.transform.position.z);
RecyclePlatform(platform, CreateContext(platform, false));
return platform;
}
private PlatformController CreatePlatform(GameObject prefab, Vector2 position, bool isStartPlatform)
{
if (prefab == null)
{
Debug.LogError($"{nameof(PlatformSpawner)} cannot spawn a platform because no platform prefab is configured.", this);
return null;
}
var platformObject = container.InstantiatePrefab(prefab, new Vector3(position.x, position.y, 0f), Quaternion.identity, transform);
platformObject.name = isStartPlatform ? "Start Platform" : "Jump Platform";
platformObject.SetActive(true);
if (!platformObject.TryGetComponent(out PlatformController platform))
{
Debug.LogError($"{nameof(PlatformSpawner)} spawned platform without {nameof(PlatformController)}.", platformObject);
Destroy(platformObject);
return null;
}
prefabByPlatform[platform] = prefab;
return platform;
}
private PlatformController ReplacePlatform(PlatformController platform, GameObject selectedPlatformPrefab, Vector2 position)
{
contentSpawner.ClearSpawnedContent(platform);
prefabByPlatform.Remove(platform);
platform.gameObject.SetActive(false);
Destroy(platform.gameObject);
var replacement = CreatePlatform(selectedPlatformPrefab, position, false);
if (replacement == null)
{
return null;
}
InitializePlatform(replacement, CreateContext(replacement, false));
return replacement;
}
private bool ShouldReplacePlatform(PlatformController platform, GameObject selectedPlatformPrefab)
{
if (platform == null || selectedPlatformPrefab == null)
{
return false;
}
return prefabByPlatform.TryGetValue(platform, out var currentPrefab) && currentPrefab != selectedPlatformPrefab;
}
private void InitializePlatform(PlatformController platform, PlatformSpawnContext context)
{
platform.Initialize(context);
contentSpawner.SpawnContent(platform, GetContentSource(context.WorldY), random);
}
private void RecyclePlatform(PlatformController platform, PlatformSpawnContext context)
{
platform.Recycle(context);
contentSpawner.SpawnContent(platform, GetContentSource(context.WorldY), random);
}
private PlatformSpawnContext CreateContext(PlatformController platform, bool isStartPlatform)
{
return new PlatformSpawnContext(container, playerController, gameplayCamera, nextSpawnIndex++, isStartPlatform, platform.transform.position.y);
}
private bool ValidateRuntimeState()
{
var isValid = true;
if (container == null)
{
Debug.LogError($"{nameof(PlatformSpawner)} was not injected. Check SceneContext and {nameof(GameplayInstaller)} setup.", this);
isValid = false;
}
if (playerController == null)
{
Debug.LogError($"{nameof(PlatformSpawner)} has no player reference.", this);
isValid = false;
}
if (gameplayCamera == null)
{
Debug.LogError($"{nameof(PlatformSpawner)} has no gameplay camera reference.", this);
isValid = false;
}
if (contentSpawner == null)
{
Debug.LogError($"{nameof(PlatformSpawner)} has no platform content spawner.", this);
isValid = false;
}
if (difficultyProgression == null)
{
Debug.LogError($"{nameof(PlatformSpawner)} has no difficulty progression reference.", this);
isValid = false;
}
else if (!difficultyProgression.HasStages)
{
Debug.LogError($"{nameof(PlatformSpawner)} has no difficulty stages configured.", this);
isValid = false;
}
return isValid;
}
private float RandomHorizontalPosition(float worldY)
{
var halfWidth = gameplayCamera != null ? gameplayCamera.orthographicSize * gameplayCamera.aspect : 3.75f;
var usableHalfWidth = Mathf.Max(0f, halfWidth - horizontalPadding);
if (usableHalfWidth <= 0f)
{
return 0f;
}
var distribution = difficultyProgression != null
? difficultyProgression.GetHorizontalDistribution(worldY)
: 0f;
var signed = RandomRange(-1f, 1f);
var sign = signed < 0f ? -1f : 1f;
var magnitude = Mathf.Abs(signed);
var normalizedMagnitude = distribution < 0f
? magnitude * (distribution + 1f)
: Mathf.Lerp(magnitude, 1f, distribution);
return sign * normalizedMagnitude * usableHalfWidth;
}
private float RandomRange(float min, float max)
{
return Mathf.Lerp(min, max, (float)random.NextDouble());
}
private static int CreateHardwareRandomSeed()
{
var unixTime = System.DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
var seedSource = string.Concat(
unixTime,
"|",
SystemInfo.processorType,
"|",
SystemInfo.processorCount,
"|",
SystemInfo.graphicsDeviceName);
return StableHash(seedSource);
}
private static int StableHash(string value)
{
unchecked
{
const int offsetBasis = -2128831035;
const int prime = 16777619;
var hash = offsetBasis;
for (var index = 0; index < value.Length; index++)
{
hash ^= value[index];
hash *= prime;
}
return hash == int.MinValue ? int.MaxValue : Mathf.Abs(hash);
}
}
private float RandomVerticalSpacing()
{
if (difficultyProgression != null)
{
return difficultyProgression.GetVerticalSpacing(highestPlatformY);
}
return 0f;
}
private GameObject GetPlatformPrefab(float worldY)
{
return difficultyProgression != null
? difficultyProgression.GetPlatformPrefab(worldY, random)
: null;
}
private IPlatformContentSource GetContentSource(float worldY)
{
return difficultyProgression != null
? difficultyProgression.CreateContentSource(worldY)
: null;
}
private string GetStageName(float worldY)
{
return ProgressionForDiagnostics != null ? ProgressionForDiagnostics.GetStageName(worldY) : string.Empty;
}
}
}