1

UNITY3D. RTS/RPG. Уровень: Бестолочь. Часть 1

Простите, мне пипец как стыдно просить помощи, но данный материал пока пишется с целью получения хоть каких нибудь денег... Блять, испанский стыд... докатился....

Итак... игра.

Посмотрев на то, что народ публикует под вывеской - "Я сделялъ" (тут картинка, где почки показывают мозгу камни) - я немного офигел и подумав решил, что буду делать игру RTS/RPG в космосе.
Почему RTS/RPG? Достойный вызов, изумруд в портфолио, да и не солидно делать, вот эти вот покатушки шариком, бесконечный бегун (runners)...


Поизучав игры, я вначале хотел сделать игру на мобильный телефон (прекрати называть ЭТО телефоном - запомни, это - устройство) на Android, но слегка приуныл от нехватки знаний.


Почему в космосе, спросят самые любопытные? А, потому, что в этом случае надо меньше объектов.

В итоге посмотрев ещё несколько видосов от CodeMonkey (хитрый дядька, но об этом потом) я понял, что:
1. Сэттинг - будущее, 2250 год, космос.
2. Жанр - RTS/RPG
3. Платформа: Windows
4. Распространение: Steam
5. Тип: Singleplayer (пока что)

Задача максимум:
31.05.2025 - создать MVP* и опубликовать игру в раннем доступе по сходной цене.


*Шо такое MVP - Minimal Viable Product, тобишь, по-нашему, это будет - Минимально Жизнеспособный Продукт.

Я пропущу такие моменты (они, кстати, очень важны. Пожалуйста - не пренебрегайте.):

1. Составление Game Design Document
2. Составление Technical Design Document

Итак, немного о предстоящей игре.... Я вдохновлялся следующими играми: EVE Online, Homeworld2, Star Wolves 3: Civil War, X4

Игровой мир: Галактика, звездные системы, соединенные прыжковыми вратами или черными дырами, кстати галактика планируется на 60+ систем, но для начала попробую сделать хотя бы 10
4 Основные фракции - Калгарцы, Аракибы, Воссолны, Сларусы.
Второстепенные фракции - Пираты, Мусорщики, Наемники, Исследователи (ещё думаю).

Игровой мир (диспозиция): Основные фракции развиваются, конкурируют между собой, но цель одна - одна фракция будет владеть Галактикой. Игрок может влиять на это дело, а может и забить на это дело и заниматься своими делами.

Сюжет: В процессе т.к. еще не готова система диалогов и квестов.

Фишки игры: Ага, так я вам и сказал ) Узнаете в процессе.

Итак, начнем. Начнем мы с того, что будем использовать паттерн - Единая Точка Входа. Хотя следует сначала определится (где твоё место и что ты за птица (с)) c архитектурой проекта и его структурой.

1. Открываем Unity Hub
2. Качаем версию LTS 2023.3.61f1

UNITY3D. RTS/RPG. Уровень: Бестолочь. Часть 1 Unity, Российский игрострой, Программирование, Странный юмор, Мат, Длиннопост

Так выглядит Unity 2023.3.61f1

3. Создаём проект на BRP (Built-in Render Pipeline)*
*Да, знаю, что будут делать URP (Universal Render Pipeline) проекты в новых версиях Unity по-умолчанию и дело движется к деприкации BRP.

Итак для себя я решил, что буду использовать следующие полезные (не факт) приобретенные знания:

Игра будет состоять из (пока) 7 сцен:

1. Bootstrapper - отвечает за инициализацию сервисов.
2. MainMenu - главное меню. Позволяет начать новую игру (продолжить - пока не работает), загрузить игру, изменить настройки (применить и сохранить),
3. Gameplay - сцена в которой и будет происходить всё действо.
4. UIScene - сцена загружающаяся поверх Gameplay и отвечающая за отображение HUD и других UI.... Так, надоело... HUD - Heads-Up Display, UI - User Interface.
4. EndGame - Тут будем показывать заставку/видео/кат-сцену Победы/Проигрыша/Секретная_Концовка (пока одна).
5. Utility - по идее должна использоваться для гарантированной выгрузки всего, что было в памяти, дабы предотвратить возможные (я ведь не волшебник, а только учусь) утечки памяти

Где, 1 - Bootstrap сцена, в которой мы будем инициализировать полезные нам сервисы, мэ-э-э-энеджеры и т.д. С этой целью создадим несколько скриптиков (придется попечатать немного):
- Удалить все объекты со сцены.
- Добавить объект, переименовать в Bootstrap (вообще без разницы, как вы его назовете - ни на что не влияет).
- В папочке (см. структуру проекта ниже) создаём скрипт: Bootstrap.cs который будет отвечать за инициализацию сервисов.
- Сервисы мы будем хранить и использовать централизованное (Service Locator шаблон). Более того Service Locator у нас будет не MonoBehaviour, а обычный Plain Old Class Object (POCO).

UNITY3D. RTS/RPG. Уровень: Бестолочь. Часть 1 Unity, Российский игрострой, Программирование, Странный юмор, Мат, Длиннопост

Структура проекта. Всё красиво разложено по папочкам. Всё как мы любим.


Тут следует сделать небольшое отступление:
Классы которые наследуются от MonoBehaviour ВСЕГДА должны быть компонентом (прикреплены) GameObject и присутствовать на сцене.
*При смене сцены, например (сцена1 -> сцена2) все объекты сцены1 будут выгружены из памяти (т.е. канут в Лету)

В таком случае мы будем использовать шаблон Singleton (Одиночка.... одинокий одиночка.... лол)
Поскольку мы его применяем к Service Locator, следовательно сервисы, зарегистрированные в нем будут присутствовать во всех сценах.

Внимание вопрос:
Каким следует сделать Service Locator - MonoBehviour или POCO. Почему?

Я остановился на POCO:

using System;

using System.Collections.Generic;

using UnityEngine;


namespace SpaceMercsLife

{

/// <summary>

/// ServiceLocator - это паттерн проектирования, который предоставляет глобальную точку доступа к сервисам, не связывая клиентский код с конкретными реализациями этих сервисов.

/// В данном случае мы используем ServiceLocator как POCO класс, что бы он был доступен везде.

/// По окончанию использования вызвать Dispose из другого класса - MonoBehaviour

/// </summary>

/// //: MonoBehaviour

public class ServiceLocator : IDisposable

{

// Словарь для хранения зарегистрированных сервисов. Ключ - тип сервиса, значение - экземпляр сервиса.

private readonly Dictionary<Type, object> _services = new Dictionary<Type, object>();

// Статическое поле для хранения единственного экземпляра ServiceLocator (Singleton).

private static ServiceLocator _instance;

// Статическое свойство для доступа к единственному экземпляру ServiceLocator.

// Ленивая инициализация: экземпляр создается только при первом обращении.

public static ServiceLocator Instance => _instance ??= new ServiceLocator();

/// <summary>

/// Регистрирует сервис указанного типа с предоставленным экземпляром.

/// Если сервис такого типа уже зарегистрирован, он будет перезаписан, о чем будет выведено предупреждение.

/// </summary>

/// <typeparam name="T">Тип регистрируемого сервиса (интерфейс или класс).</typeparam>

/// <param name="service">Экземпляр сервиса для регистрации.</param>

// Событие, вызываемое после регистрации сервиса.

public static event Action<Type, object> ServiceRegistered;


// Событие, вызываемое после отмены регистрации сервиса.

public static event Action<Type> ServiceUnregistered;

/// <summary>

/// Регистрирует сервис указанного типа (интерфейса) с предоставленным экземпляром.

/// Если сервис такого типа уже зарегистрирован, он будет перезаписан, о чем будет выведено предупреждение.

/// </summary>

/// <typeparam name="TInterface">Тип интерфейса регистрируемого сервиса.</typeparam>

/// <typeparam name="TImplementation">Тип конкретной реализации сервиса.</typeparam>

/// <param name="service">Экземпляр сервиса для регистрации.</param>

public void Register<TInterface, TImplementation>(TImplementation service)

where TInterface : class

where TImplementation : class, TInterface

{

var interfaceType = typeof(TInterface);

if (_services.ContainsKey(interfaceType))

{

Debug.LogWarning($"Service {interfaceType} already registered. Overriding with {service.GetType().Name}...");

_services[interfaceType] = service;

}

else

{

_services.Add(interfaceType, service);

ServiceRegistered?.Invoke(interfaceType, service);

}

}

/// <summary>

/// Отменяет регистрацию сервиса указанного типа (интерфейса).

/// </summary>

/// <typeparam name="TInterface">Тип интерфейса сервиса, который необходимо отменить регистрацию.</typeparam>

public void Unregister<TInterface>() where TInterface : class

{

var interfaceType = typeof(TInterface);

if (_services.ContainsKey(interfaceType))

{

_services.Remove(interfaceType);

ServiceUnregistered?.Invoke(interfaceType);

Debug.Log($"Service {interfaceType} unregistered.");

}

}

/// <summary>

/// Получает зарегистрированный сервис указанного типа (интерфейса).

/// </summary>

/// <typeparam name="TInterface">Тип интерфейса запрашиваемого сервиса.</typeparam>

/// <returns>Экземпляр зарегистрированного сервиса.</returns>

/// <exception cref="Exception">Выбрасывает исключение, если сервис указанного типа не найден.</exception>

public TInterface Get<TInterface>() where TInterface : class

{

var interfaceType = typeof(TInterface);

if (!_services.TryGetValue(interfaceType, out var service))

{

throw new Exception($"Service {interfaceType} not found");

}


return (TInterface)service;

}


/// <summary>

/// Пытается получить зарегистрированный сервис указанного типа (интерфейса).

/// </summary>

/// <typeparam name="TInterface">Тип интерфейса запрашиваемого сервиса.</typeparam>

/// <param name="service">Выходной параметр, содержащий экземпляр сервиса, если он найден, иначе null.</param>

/// <returns>True, если сервис найден, иначе false.</returns>

public bool TryGet<TInterface>(out TInterface service) where TInterface : class

{

var interfaceType = typeof(TInterface);

if (_services.TryGetValue(interfaceType, out var obj))

{

service = (TInterface)obj;

return true;

}

service = null;

return false;

}


/// <summary>

/// Удаляет все зарегистрированные сервисы и очищает события.

/// </summary>

public void Clear()

{

_services.Clear();

ServiceRegistered = null;

ServiceUnregistered = null;

// Можно добавить Debug.Log("ServiceLocator cleared.") для обратной связи.

}


/// <summary>

/// Реализация интерфейса IDisposable. Освобождает ресурсы, удерживаемые ServiceLocator.

/// В данном случае, очищает список сервисов, отписывается от событий и обнуляет ссылку на экземпляр.

/// </summary>

public void Dispose()

{

if (_instance != null)

{

_instance.Clear();

_instance = null;

}

GC.SuppressFinalize(this);

}

}

}

А вот на "прям вот сейчас, сию минуту" выглядит Bootstrap.cs

using UnityEngine;

using SpaceMercsLife.Service.Interfaces;

using SpaceMercsLife.Service.Implementation.Mono;

using SpaceMercsLife.Service.Implementation.POCO;

using System.Threading.Tasks;

using UnityEngine.SceneManagement;

using SpaceMercsLife.SLocator;


namespace SpaceMercsLife

{

public class Bootstrap : MonoBehaviour

{

private string configFileName = "game_config";

private string defaultLocale = "en";


private async void Start()

{

MonoBehaviourServicesRegistration();

await POCOServicesRegistration();

ReleaseResources();


await LoadGameSettings();


// Воспроизведение фоновой музыки

var audioService = ServiceLocator.Instance.Get<IAudioService>();

await audioService.PlayBackgroundMusicAsync("main_theme", volume: 1.0f, fadeInDuration: 2.0f);


// Загрузка сцены главного меню

var sceneService = ServiceLocator.Instance.Get<ISceneService>();

await sceneService.LoadSceneAsync("MainMenu", LoadSceneMode.Single, showLoadingScreen: true, uiSceneName: null);

}


private async Task LoadGameSettings()

{

// Загрузка графических настроек

var graphicsService = ServiceLocator.Instance.Get<IGraphicsService>();

await graphicsService.LoadSettingsAsync();


// Загрузка настроек ввода

var inputService = ServiceLocator.Instance.Get<IInputService>();

await inputService.LoadBindingsAsync();


// Загрузка настроек VFX

var vfxService = ServiceLocator.Instance.Get<IVFXService>();

await vfxService.LoadSettingsAsync();


// Загрузка настроек UI

var uiService = ServiceLocator.Instance.Get<IUIService>();

await uiService.LoadSettingsAsync();


// Инициализация камеры

var cameraService = ServiceLocator.Instance.Get<ICameraService>();

var mainCamera = cameraService.CreateCamera("MainCamera");

cameraService.SetMainCamera(mainCamera);


// Загрузка настроек камеры и мыши

await cameraService.LoadSettingsAsync();

}


private void MonoBehaviourServicesRegistration()

{

VFXService vfxServicePrefab = Resources.Load<VFXService>("Prefabs/VFXService");

SceneService sceneServicePrefab = Resources.Load<SceneService>("Prefabs/SceneService");


var vfxService = Instantiate(vfxServicePrefab);

DontDestroyOnLoad(vfxService.gameObject);

vfxService.Initialize(ServiceLocator.Instance.Get<IConfigurationService>());

ServiceLocator.Instance.Register<IVFXService, VFXService>(vfxService);


var sceneService = Instantiate(sceneServicePrefab);

DontDestroyOnLoad(sceneService.gameObject);

ServiceLocator.Instance.Register<ISceneService, SceneService>(sceneService);

}


private void ConfigInitialization()

{

var configService = new SimpleConfigurationService();

configService.LoadConfig($"Config/{configFileName}");

ServiceLocator.Instance.Register<IConfigurationService, SimpleConfigurationService>(configService);


var audioService = new AudioService(configService);

ServiceLocator.Instance.Register<IAudioService, AudioService>(audioService);


var graphicsService = new GraphicsService(configService);

ServiceLocator.Instance.Register<IGraphicsService, GraphicsService>(graphicsService);


var inputService = new InputService(configService);

ServiceLocator.Instance.Register<IInputService, InputService>(inputService);


var cameraService = new CameraService(configService);

ServiceLocator.Instance.Register<ICameraService, CameraService>(cameraService);


var uiService = new UIService(configService);

ServiceLocator.Instance.Register<IUIService, UIService>(uiService);


var localizationService = new SimpleLocalizationService();

localizationService.SetLocale(defaultLocale);

ServiceLocator.Instance.Register<ILocalizationService, SimpleLocalizationService>(localizationService);

}


private async Task POCOServicesRegistration()

{

ConfigInitialization();


ServiceLocator.Instance.Register<IResourceService, ResourceService>(new ResourceService());

ServiceLocator.Instance.Register<IPhysicsService, PhysicsService>(new PhysicsService());

ServiceLocator.Instance.Register<IEventBus, EventBus>(new EventBus());

ServiceLocator.Instance.Register<IStateMachineService, SimpleStateMachineService>(new SimpleStateMachineService());


var binaryDataService = new BinaryDataService();

await binaryDataService.InitializeAsync();

ServiceLocator.Instance.Register<IDataService, BinaryDataService>(binaryDataService);

}


private void ReleaseResources()

{

Resources.UnloadUnusedAssets();

}

}

}

А вот так вот выглядят все сервисы

UNITY3D. RTS/RPG. Уровень: Бестолочь. Часть 1 Unity, Российский игрострой, Программирование, Странный юмор, Мат, Длиннопост