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

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

Так выглядит 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).

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

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


Тут следует сделать небольшое отступление:
Классы которые наследуются от 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();

}

}

}

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

Темы

Политика

Теги

Популярные авторы

Сообщества

18+

Теги

Популярные авторы

Сообщества

Игры

Теги

Популярные авторы

Сообщества

Юмор

Теги

Популярные авторы

Сообщества

Отношения

Теги

Популярные авторы

Сообщества

Здоровье

Теги

Популярные авторы

Сообщества

Путешествия

Теги

Популярные авторы

Сообщества

Спорт

Теги

Популярные авторы

Сообщества

Хобби

Теги

Популярные авторы

Сообщества

Сервис

Теги

Популярные авторы

Сообщества

Природа

Теги

Популярные авторы

Сообщества

Бизнес

Теги

Популярные авторы

Сообщества

Транспорт

Теги

Популярные авторы

Сообщества

Общение

Теги

Популярные авторы

Сообщества

Юриспруденция

Теги

Популярные авторы

Сообщества

Наука

Теги

Популярные авторы

Сообщества

IT

Теги

Популярные авторы

Сообщества

Животные

Теги

Популярные авторы

Сообщества

Кино и сериалы

Теги

Популярные авторы

Сообщества

Экономика

Теги

Популярные авторы

Сообщества

Кулинария

Теги

Популярные авторы

Сообщества

История

Теги

Популярные авторы

Сообщества