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
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();
}
}
}
А вот так вот выглядят все сервисы