Pull to refresh

Comments 27

UFO just landed and posted this here
Это же tutorial, как построить что-то похожее своими руками. Как комбинировать паттерны. Я не претендую на инновационность

Пожалуй перефразирую, чтобы не было дальнейших казусов. =) Спасибо за отзыв.

На счет устоявшейся практики — это преувеличено. Многие пишут код прямо во View и не парятся.
UFO just landed and posted this here
Нет, такой подход не пробовал, но идея интересная :) Не знаю насколько это будет удобно.
Я использовал UniRx для построения запросов к модели. Было довольно удобно.
UFO just landed and posted this here
А можешь поделиться линком где можно поподробнее почитать?
Я тоже не против почитать, есть линки?
Сам пишу приблизительно таким подходом. Меня смущает куча public полей в GameState, которые просто меняются командами, но, теоретически, их могут менять другие программисты прямо из вьюшек.

    [System.Serializable]
    public class GameState
    {
        public int coins;
    }
Я пришел в итоге к выводу, что если такие люди есть, и они не понимают объяснений, что так делать не надо, то с ними лучше не работать. По моему опыту большинство людей достаточно умны, чтобы так не делать. Ну а новичкам всегда код-ревью делать.

Так же чудаки всегда умудрятся любую защиту сломать =)

Если же это действительно критично, и если уж совсем по кананам, то это фиксится просто:

1. Делаем IReadonlyGameState
    public interface IReadonlyGameState
    {
        int coins { get; }
    }
    [System.Serializable]
    public class GameState : IReadonlyGameState
    {
        public int coins { get; set; }
    }

2. В stateUpdated кидается IReadonlyGameState.

У этого подхода есть большой минус.
Если GameState содержит вложенные объекты, то их тоже нужно делать с двумя интерфейсами мутабельным и иммутабельным. Это выливается в кучу проблем.

Если бы C# поддерживал что-нибудь типа const в С++, то ситуация была бы намного проще =)
Полагаю, если вы говорите о реактивном программировании, то стоит делать стейт иммутабельным, а метод Execute команды должен возвращать новый стейт с измененными параметрами. Но данный подход создает определенные проблемы с затратами ресурсов системы.
Реактивное программирование не обязательно должно быть полностью по ФП.

Иммутабельность стейта в принципе важная и удобная штука. Но на моей практике — больше геморроя, чем пользы.

Так то в ивенты можно отдавать отдельные иммутабельные подмножества стейта.
В любом случае — это довольно субъективный взгляд и каждый решает как ему удобнее.

Что касается того, что каждый раз нужно создавать новый стейт — лишние аллокации в играх ни к чему хорошему обычно не приводят :), а если стейт жирный, то можно на хорошие такие грабли наступить.
UFO just landed and posted this here
Ну я за эти годы повидал несколько крупных проектов. И там стейт был далеко не маленьким. Например, были прецеденты, когда все получаемые в игре вещи/апгрейды складывались в стейт. Таким образом, если юзер хайлевел, и ему некуда их утилизировать, то они накапливались.

Хоть они и стакались, количество уникальных вещей со временем все равно росло сильно. В итоге стейт очень разрастался.

Это проблема и архитектуры и дизайна, но от этого никто не застрахован.
то стоит делать стейт иммутабельным

А как на Шарпах удобно сделать стейт иммутабельным? Ну вот мутабельно я меняю его, допустим, так:
state.ships[1].room[5].level++;

Как сделать то же самое иммутабельно? Чтобы вернулся новый стейт с измененным только кораблем 1 и комнатой 5?
Адекватным подходом — никак. Либо искать реализации каких-либо древовидных структур, которые скрывают возможность изменения стейта за методами, возвращающими новую ссылку на стейт, а под капотом по-умному генерируют дифф над предыдущим состоянием.
Эх. У меня просто как раз задача, которая идеально ложится на иммутабельный стейт. Но пока приходится просто весь копировать и менять кусочки. Благо, стейт маленький.

Не уверен в адекватности подхода, но по идее выглядеть это будет так:


class State {
    public ImmutableArray<Ship> Ships { get; }
    public State (ImmutableArray<Ship> ships) {
        this.Ships = ships;
    }
}
class Ship {
    public ImmutableArray<Room> Rooms { get; }
    public Ship (ImmutableArray<Room> rooms) {
        this.Rooms = rooms;
    }
}
class Room {
    public int Level { get; }
    public Room(int level) {
        this.Level = level;
    }
}
// ...
Ship[] ships = state.Ships.ToArray();
Room[] rooms = ships[shipIndex].Rooms.ToArray();
rooms[roomIndex] = new Room(rooms[roomIndex].Level + 1);
ships[shipIndex] = new Ship(ImmutableArray.ToImmutableArray(rooms));
return new State(ImmutableArray.ToImmutableArray(ships));

Но как было сказано выше, тут многовато аллокаций.

Ship[] ships = state.Ships.ToArray();
Room[] rooms = ships[shipIndex].Rooms.ToArray();
rooms[roomIndex] = new Room(rooms[roomIndex].Level + 1);
ships[shipIndex] = new Ship(ImmutableArray.ToImmutableArray(rooms));
return new State(ImmutableArray.ToImmutableArray(ships));

Ну вы же понимаете, что это просто кошмар? Мне надо было 2 минуты чтобы понять, не ошиблись вы где-то. Как это поддерживать вообще? А если в комнатах появяется еще три поля? А если массив комнат перенесется в филд `interior` внутри корабля? Ваш код ни отрефакторить, ни новую фичу добавить. И он требует писать в
state.ships[1].interior.room[5].level++;


И да, вы написали в 5 раз больше строчек и в 8,5 раз больше символов, чем я. Игры и так крайне сложные механизмы, а если их еще сделать в 5 раз сложнее — как их разрабатывать вообще?
UFO just landed and posted this here
Это как-то слишком экстремально для меня. Все-же надеялся на способ понативнее.
UFO just landed and posted this here

Я не говорю, что так делать нужно. Скорее это была просто деманстрация. И я согласен с вами, что это выглядит уж очень громоздко.
Как альтернативное решение могу предложить, с помощью пространств имён сделать состояние иммутабельным снаружи, а внутри неймспейса состояния и команд оставить возможность изменения объектов.

Не, мне нужно как раз внутри иммутабельное состояние. У меня просто некоторое количество состояний должно сохранятся и я достигаю этого вызовом Clone(), но иммутабельность была бы менее затратной по производительности да и красоте.
Как же аллокации при создании команд (команда — класс, а не структура)? Ведь в реальном проекте может создаваться несколько команд за кадр.

Можно ли команды реализовать в виде struct, избегая boxing(при приведении к интерфейсу) и не теряя полиморфизма при исполнении?
Когда я проводил исследования, аллокации от команд были незначительными, по сравнению с аллокациями при поддержке иммутабельности.

Команды можно сделать struct, но тогда придется пожертвовать, например, строгой типизацией. Ведь чтобы хранить контейнер полиморфных команд, их все равно придется боксить. Поэтому команду придется привести к одному классу, а тип внутри команды обозначить enum. Параметры же придется организовать в виде какого-нибудь Dictionary<string, object>.

В таком случае команда будет выглядеть примерно так:

public struct Command
{
    public enum Type {
        AddCoins
    }

    public Type type;
    public Dictionary<string, object> parameters;
}

...

void HandleCommand(Command cmd)
{
    switch (cmd.type)
    {
        case AddCoins:
            ...
            break;
    }
}


Если же хранить команды в контейнере не надо, то можно писать отдельные типы команды, а сам тип определять по cmd.GetTypeCode() (обычный GetType боксит), по нему же выбирать какой хэндлер вызвать.
Да, этот вариант скорее всего не даст особого выигрыша, из-за аллокаций на Dictionary и параметры команды все равно придется боксить к object. И использовать сложнее.

Но вы натолкнули на идею проверить, во сколько обойдется замена struct на class в одном из проектов с достаточным количеством сообщений, за что отдельное спасибо!

Статистика показала, что в следующем проекте я спокойно могу использовать команды-классы: за 4 мин передано 13103 сообщений, на которые всего аллоцировано 221 Кб.
Sign up to leave a comment.