Подробные изменения: - Добавлены и доработаны модальные окна главного меню, сервисные настройки, шаблоны 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 без ошибок
201 lines
7.4 KiB
C#
201 lines
7.4 KiB
C#
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;
|
||
}
|
||
}
|
||
}
|