Danro-Jump/Assets/Scripts/Runtime/Bootstrap/BootstrapEntryPoint.cs
Danil Iarosh 1f6884c5ac Build gameplay systems and service settings
Подробные изменения:
- Добавлены и доработаны модальные окна главного меню, сервисные настройки, шаблоны UI Toolkit и единый scene-flow сервис для переходов между сценами.
- Расширена gameplay-архитектура: сессия забега, HUD, спавн контента платформ, collectible-предметы, оборудование, хрупкие и движущиеся платформы.
- Добавлена база противников: типы спавна, патрулирование, процедурный спавнер, правила размещения через ScriptableObject-сложности и убийство врагов прыжком сверху.
- Исправлены смерть и respawn игрока, финальный GameOver, отскоки от врагов, пружин и boost-предметов, а также проход сквозь врагов при усиленном подъеме.
- Проведен рефакторинг взаимодействия систем: общий landing context, единые правила player-contact, NonLazy gameplay session, fail-fast installer и централизованный выбор точек спавна.
- Обновлены сцены, prefab-ассеты, difficulty stages, ресурсы главного меню, настройки проекта, roadmap и аппаратная COM-интеграция.
- Добавлены ассеты летучей мыши, врагов, collectibles, TextMesh Pro, ProjectContext и gameplay UI.

Проверки:
- dotnet build .\\Assembly-CSharp.csproj --no-restore
- Unity validation ключевых gameplay/UI/scene-flow скриптов
- Unity Console после force refresh без ошибок
2026-05-27 16:34:51 +03:00

201 lines
7.4 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;
using System.IO;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace DanroJump.Bootstrap
{
/// <summary>
/// Первая точка входа приложения: фиксирует портретное соотношение сторон и загружает главное меню.
/// </summary>
[DefaultExecutionOrder(-10000)]
public sealed class BootstrapEntryPoint : MonoBehaviour
{
private const float AspectTolerance = 0.0025f;
private static BootstrapEntryPoint instance;
[SerializeField] private string mainMenuSceneName = "MainMenu";
[SerializeField] private bool forceBuildAspectRatio = true;
[SerializeField] [Min(1)] private int targetAspectWidth = 9;
[SerializeField] [Min(1)] private int targetAspectHeight = 16;
[SerializeField] [Min(1)] private int preferredPortraitHeight = 1920;
[SerializeField] private bool clampToDisplaySize = true;
[SerializeField] private FullScreenMode buildFullScreenMode = FullScreenMode.Windowed;
[SerializeField] [Min(1)] private int startupEnforcementFrames = 8;
[SerializeField] [Min(0.1f)] private float aspectCheckInterval = 0.5f;
private float nextAspectCheckTime;
/// <summary>
/// Создает singleton bootstrap-объект и применяет разрешение билда до загрузки меню.
/// </summary>
private void Awake()
{
if (instance != null && instance != this)
{
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(gameObject);
// В редакторе не вмешиваемся в Game View, а в билде принудительно держим 9:16.
if (!Application.isEditor && forceBuildAspectRatio)
{
ApplyBuildAspectResolution();
StartCoroutine(EnforceBuildAspectOnStartup());
}
}
/// <summary>
/// Периодически возвращает разрешение к целевому aspect ratio, если окно было изменено.
/// </summary>
private void Update()
{
if (Application.isEditor || !forceBuildAspectRatio || Time.unscaledTime < nextAspectCheckTime)
{
return;
}
nextAspectCheckTime = Time.unscaledTime + aspectCheckInterval;
if (!HasTargetAspectRatio(Screen.width, Screen.height))
{
ApplyBuildAspectResolution();
}
}
/// <summary>
/// Загружает сцену главного меню после bootstrap-настройки.
/// </summary>
private IEnumerator Start()
{
if (string.IsNullOrWhiteSpace(mainMenuSceneName))
{
Debug.LogError("[BootstrapEntryPoint] Main menu scene name is empty.", this);
yield break;
}
if (!SceneExistsInBuildSettings(mainMenuSceneName))
{
Debug.LogError($"[BootstrapEntryPoint] Scene '{mainMenuSceneName}' is not registered in Build Settings.", this);
yield break;
}
AsyncOperation operation = SceneManager.LoadSceneAsync(mainMenuSceneName, LoadSceneMode.Single);
if (operation == null)
{
Debug.LogError($"[BootstrapEntryPoint] Failed to start loading scene '{mainMenuSceneName}'.", this);
yield break;
}
while (!operation.isDone)
yield return null;
}
/// <summary>
/// Очищает singleton-ссылку при выходе из Play Mode или уничтожении bootstrap-объекта.
/// </summary>
private void OnDestroy()
{
if (instance == this)
{
instance = null;
}
}
/// <summary>
/// Несколько кадров подряд повторяет установку разрешения, пока Unity завершает старт окна.
/// </summary>
private IEnumerator EnforceBuildAspectOnStartup()
{
for (int i = 0; i < startupEnforcementFrames; i++)
{
if (!HasTargetAspectRatio(Screen.width, Screen.height))
{
ApplyBuildAspectResolution();
}
yield return null;
}
}
/// <summary>
/// Применяет рассчитанное разрешение к окну игры.
/// </summary>
private void ApplyBuildAspectResolution()
{
Vector2Int resolution = CalculateTargetResolution();
Screen.SetResolution(resolution.x, resolution.y, buildFullScreenMode);
}
/// <summary>
/// Рассчитывает целевое портретное разрешение с ограничением по размеру основного дисплея.
/// </summary>
private Vector2Int CalculateTargetResolution()
{
int height = Mathf.Max(1, preferredPortraitHeight);
int width = Mathf.Max(1, Mathf.RoundToInt(height * GetTargetAspect()));
if (clampToDisplaySize && Display.main != null)
{
int displayWidth = Display.main.systemWidth;
int displayHeight = Display.main.systemHeight;
if (displayHeight > 0 && height > displayHeight)
{
height = displayHeight;
width = Mathf.Max(1, Mathf.RoundToInt(height * GetTargetAspect()));
}
if (displayWidth > 0 && width > displayWidth)
{
width = displayWidth;
height = Mathf.Max(1, Mathf.RoundToInt(width / GetTargetAspect()));
}
}
return new Vector2Int(width, height);
}
/// <summary>
/// Проверяет, совпадает ли текущее разрешение с целевым соотношением сторон.
/// </summary>
private bool HasTargetAspectRatio(int width, int height)
{
if (width <= 0 || height <= 0)
{
return false;
}
return Mathf.Abs(width / (float)height - GetTargetAspect()) <= AspectTolerance;
}
/// <summary>
/// Возвращает целевой aspect ratio из настроек ширины и высоты.
/// </summary>
private float GetTargetAspect()
{
return targetAspectWidth / (float)targetAspectHeight;
}
/// <summary>
/// Проверяет, добавлена ли сцена в Build Settings.
/// </summary>
private static bool SceneExistsInBuildSettings(string sceneName)
{
for (int i = 0; i < SceneManager.sceneCountInBuildSettings; i++)
{
string scenePath = SceneUtility.GetScenePathByBuildIndex(i);
string buildSceneName = Path.GetFileNameWithoutExtension(scenePath);
if (buildSceneName == sceneName)
return true;
}
return false;
}
}
}