R&S Devlog 4
За месяц вставил локализацию описания комнат первой локации, сделал структуру этих комнат и впилил пара фичей. Первая - туториал, вторая - инвентарь, простые предметы и реварды из них. Ну и по мелочи - залипушное окошко инвентаря, квестлога и окна наград.
Туториал
Вот так вот выглядит описание первых 3 туторов:
[
{
"Id": 1,
"Triggers": [
{
"$type": "StartGameTutorialTrigger"
}
],
"Stages": [
{
"$type": "StartConversationTutorialStage",
"Conversation": "conversation-self-diag"
}
]
},
{
"Id": 2,
"Triggers": [
{
"$type": "TutorialCompleteTrigger",
"TutorialId": 1
}
],
"Stages": [
{
"$type": "ClickUITutorialStage",
"UIButtonName": "Room_SecondRoom"
}
]
},
{
"Id": 3,
"Triggers": [
{
"$type": "TutorialCompleteTrigger",
"TutorialId": 2
}
],
"Stages": [
{
"$type": "ClickUITutorialStage",
"UIButtonName": "RoomObject_1"
},
{
"$type": "ClickUITutorialStage",
"UIButtonName": "OpenChestButton"
}
]
}
]
А вот так классы этих моделек:
[MemoryPackable]
public partial class Tutorial
{
public int Id { get; private set;}
public ReadOnlyCollection<TutorialTrigger> Triggers { get; private set;} = null!;
public ReadOnlyCollection<TutorialStage> Stages { get; private set;} = null!;
}
[MemoryPackable]
[MemoryPackUnion(0, typeof(StartGameTutorialTrigger))]
[MemoryPackUnion(1, typeof(TutorialCompleteTrigger))]
public abstract partial class TutorialTrigger
{
}
[MemoryPackable]
public partial class StartGameTutorialTrigger : TutorialTrigger
{
}
[MemoryPackable]
public partial class TutorialCompleteTrigger : TutorialTrigger
{
public int TutorialId { get; private set;}
}
[MemoryPackable]
[MemoryPackUnion(0, typeof(StartConversationTutorialStage))]
[MemoryPackUnion(1, typeof(ClickUITutorialStage))]
public abstract partial class TutorialStage
{
}
[MemoryPackable]
public partial class StartConversationTutorialStage : TutorialStage
{
public string Conversation { get; private set;}
}
[MemoryPackable]
public partial class ClickUITutorialStage : TutorialStage
{
public string UIButtonName { get; private set;}
}
Туторы - это как квесты, только стартуют автоматически по триггерам, и считается завершённым, только если завершена последняя стадия, а иначе - при новом старте игры начинается занова.
Реализация туториал контроллера следующая: при старте для всех незавершённых туторов создаются врапперы тригеров, которые следят за игрой. Если тригер срабатывает, то он оповещает контроллер, который уже в конце фрейма смотрит все сработавшие тригеры и запускает враппер стадии тутора, который также делает всё сам. В итоге логика тригеров в тригерах, логика стадий в стадии, а туториал контроллер лишь управляет запуском, при чем даже не сам, а с помощью tutorialRunner.
Есть ещё TutorialButtonHighlighter, который с помощью библиотечки UIOutline с гитхаба выводит красивую подсветку для кнопки, если нужно.
По-моему это отличная система, из фич ей не хватает только читерский пропуск туториалов, да может ещё откат нескольких стадий тутора.
В прошлых играх я всегда делал тригеры туториалов через монобехи, которые были прямо внутри префабов, и новая система гораздо лучше в том плане, что вся её настройка происходит отдельно, т.е. ГД может кастомизировать её полностью через json.
Лут
Чтобы выдавать айтемы и всё остальное нужны классы-врапперы - это реварды (IReward). Чтобы выдавать реварды - нужны классы-контнейнеры - это лут (ILootTableElement).
ILootTableElement сделан рекурсивно, т.е. часть наследников - декораторы для других наследников. Итоговый лут - просто список ревардов -
[MemoryPackable]
public partial class RewardLootTableElement : ILootTableElement
{
public ReadOnlyCollection<IReward> Rewards { get; private set; } = null!;
}
Пример декоратора:
[MemoryPackable]
public partial class LootTableElement : ILootTableElement
{
public float? Weight { get; private set; } // если не указан, то выдаётся всегда
public ILootTableElement Element { get; private set; } = null!;
public ReadOnlyCollection<Restriction>? Restrictions { get; private set; } }
Restriction - очень важная концепция. Пока её реализацию я не писал, но в целом она используется для А/Б тестов и любых настроек, зависящих от контекста. С помощью этой системы с мобов можно выдавать квестовый лут, только когда квест активен, или выдавать уникальный предмет только тогда, когда он ещё не был получен, или просто выдавать разный лут в зависимости от уровня игрока. Её можно будет применять много где, не только в ревардах.
Из возможных ревардов пока только простые айтемы - например, квестовые. Т.е. пока нет статов и всяких свойств, только иконка, название и описание. Система инвентаря тоже крайне простая.
private readonly Dictionary<Guid, IItemWrapper> _items = new();
Количество однотипных айтемов в свойства Count у IItemWrapper.
Следующим шагом на месяц будет как реализация эквип айтемов.