Сохранение в игре.

Темы, связанные с проектированием и программированием roguelike-игр

Модераторы: Sanja, Максим Кич

Аватара пользователя
Максим Кич
Администратор
Сообщения: 1560
Зарегистрирован: 03 дек 2006, 20:17
Откуда: Витебск, Беларусь
Контактная информация:

Re: Сохранение в игре.

Сообщение Максим Кич » 08 фев 2017, 08:10

Jesus05 писал(а):
08 фев 2017, 07:57
Максим Кич писал(а):
07 фев 2017, 09:10
Ни разу не сишник, но думаю, что решения как минимум на каком-то уровне примерно схожие.
...
По поводу JSON у меня сложилось впечатление, что "не сишники" считаю, что в Сях JSON реализуеться как-то так:

Код: Выделить всё

json_encode(get_object_vars($class));
или так

Код: Выделить всё

public function toJSON(){
    return json_encode($this);
}
Только в сях все чуточку сложнее...
Да как бы я особых иллюзий я не испытываю. Когда-то сам писал на Java что-то похожее, второй раз не тянет. Но неужели всё до сих пор настолько плохо, что проще тянуть в проект ORM и БД, пусть и урезанно-десктопные, чем сериализовать в JSON/XML?
Dump the screen? [y/n]

Аватара пользователя
Oreyn
Сообщения: 297
Зарегистрирован: 07 авг 2013, 14:59

Re: Сохранение в игре.

Сообщение Oreyn » 08 фев 2017, 08:42

Ручная сериализация не так страшна.
Там сущностей в рогалике - карта, монстры, игрок, итемы, эффекты, спеллы, глобальные всякие параметры, типа уровня проклятия или погода.
С ООП - интерфейс сериалайзабл, перегружаем функции сейв/лоад. Да придеться ручками указать все сохраняемые параметры. Или готовый велосипед взять, который это и делает. Циклические ссылки. Эмм, а может без них? Что там инвентарь указывает на предмет, а тот обратно на инвентарь? Перевести ссылку в айдишник из коллекции. Реализуемо.
С процедурным программированием сложнее чуть чуть. Но по сути также реализуется. Сейв функция на каждый тип сущности, которой передавать контекст сущности. Ну и понятно типовую запись данных в жсон тоже обернуть в функции, если уж совсем все с нуля.

Аватара пользователя
Jesus05
Сообщения: 1792
Зарегистрирован: 02 дек 2009, 07:50
Откуда: Норильск, сейчас Санкт-петербург.
Контактная информация:

Re: Сохранение в игре.

Сообщение Jesus05 » 08 фев 2017, 09:09

Oreyn писал(а):
08 фев 2017, 08:42
Там сущностей в рогалике - карта, монстры, игрок, итемы, эффекты, спеллы, глобальные всякие параметры, типа уровня проклятия или погода.
...
Ну вон 40 перегруженных методов моих чуть выше... что там:
Массивы из структур, сами структуры, item`ы, рецпепты, хранилища рецептов, "собиратель" событий. все мелочи, все собрано в одном месте (т.е. классы не знают даже о том что их сохраняют записывают, их задача в игре дела делать, а сохраняет и записывает один God-класс, который знает все обо всех и всем друг :) )

Аватара пользователя
Jesus05
Сообщения: 1792
Зарегистрирован: 02 дек 2009, 07:50
Откуда: Норильск, сейчас Санкт-петербург.
Контактная информация:

Re: Сохранение в игре.

Сообщение Jesus05 » 08 фев 2017, 09:16

Максим Кич писал(а):
08 фев 2017, 08:10
Но неужели всё до сих пор настолько плохо, что проще тянуть в проект ORM и БД, пусть и урезанно-десктопные, чем сериализовать в JSON/XML?
Ну я счас поискал фреймворки для рефлексии, как-то не впечатлило...
ну вот https://github.com/RAttab/reflect
"Yet another reflection system for C++."
по примерам кода создалось впечатление что что-бы им пользоваться на такой класс:

Код: Выделить всё

struct Foo
{
    int bar(int a, int b) const { return a + b; }
    int baz;
};
надо написать маленькую оберточку?

Код: Выделить всё

reflectType(Foo)
{
    reflectPlumbing();
    reflectField(baz);
    reflectFn(bar);
}
(которая по кол-ву символов больше класса??? а зачем??)
тогда уж действительно лучше Boost взять и в каждом классе реализовать описание всех параметров, НО! тогда получиться ровно тоже самое что и я делал, только без God класса, мусор по сохранению будет разбросан по всем файлам.

Аватара пользователя
Jesus05
Сообщения: 1792
Зарегистрирован: 02 дек 2009, 07:50
Откуда: Норильск, сейчас Санкт-петербург.
Контактная информация:

Re: Сохранение в игре.

Сообщение Jesus05 » 08 фев 2017, 09:33

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

Код: Выделить всё

class MyClass
{
public:
    MyClass(QObject *parent = 0);
    ~MyClass();

    enum Priority { High, Low, VeryHigh, VeryLow };

    void setPriority(Priority priority);
    Priority priority() const;

private:
    Priority m_priority;
};
надо будет писать что-то типа:

Код: Выделить всё

class MyClass : public QObject
{
    Q_OBJECT
    Q_PROPERTY(Priority priority READ priority WRITE setPriority)
public:
    MyClass(QObject *parent = 0);
    ~MyClass();

    enum Priority { High, Low, VeryHigh, VeryLow };
    Q_ENUM(Priority)

    void setPriority(Priority priority);
    Priority priority() const;

private:
    Priority m_priority;
};
Потом правда становятся доступны многие Qt-ные штучки... типа

Код: Выделить всё

QObject *object = myinstance;
object->setProperty("priority", "VeryHigh");
или

Код: Выделить всё

QObject *object = myinstance;
const QMetaObject *metaobject = object->metaObject();
int count = metaobject->propertyCount();
for (int i=0; i<count; ++i) {
    QMetaProperty metaproperty = metaobject->property(i);
    const char *name = metaproperty.name();
    QVariant value = object->property(name);
}
Но все равно это все описывать вручную всегда :(

Аватара пользователя
Максим Кич
Администратор
Сообщения: 1560
Зарегистрирован: 03 дек 2006, 20:17
Откуда: Витебск, Беларусь
Контактная информация:

Re: Сохранение в игре.

Сообщение Максим Кич » 08 фев 2017, 09:35

Oreyn писал(а):
08 фев 2017, 08:42
Ручная сериализация не так страшна.
Там сущностей в рогалике - карта, монстры, игрок, итемы, эффекты, спеллы, глобальные всякие параметры, типа уровня проклятия или погода.
С ООП - интерфейс сериалайзабл, перегружаем функции сейв/лоад. Да придеться ручками указать все сохраняемые параметры. Или готовый велосипед взять, который это и делает.
Причём, всё ещё интереснее. Мы в любом случае все игровые объекты когда-то создаём в первый раз. И нам в любом случае надо указывать, какие параметры мы получаем, грубо говоря из ГСЧ и тут уже не удастся как-то автомагически реализовать рандомное заполнение свойства объекта просто по факту того, что мы его добавили. Если мы добавили персонажу свойство «лохматистость», то мы как минимум один раз прописываем его где-то, где эта самая лохматистость выдаётся каждому новому персу в целочисленном диапазоне 0..100 с нормальным распределением. Если у нашего персонажа есть инвентарь — то он уже где-то создаётся и наполняется какими-то стартовыми предметами на основании левой пятки ГСЧ. И там уже есть логика, которая следит за всеми ссылками. И мы можем этими же средствами создавать объекты не из случайных данных, а из сериализованных. И у нас будет один, прозрачный и предсказуемый способ, которым что-то появляется в игре. Как только у нас генерация нового объекта и десериализация сохранённого начинают ходить разными путями — начинается боль.
Oreyn писал(а):
08 фев 2017, 08:42
Циклические ссылки. Эмм, а может без них? Что там инвентарь указывает на предмет, а тот обратно на инвентарь? Перевести ссылку в айдишник из коллекции. Реализуемо.
Тут вопрос даже не в том, можно ли обойтись без циклических ссылок. Вопрос в том, зачем их хранить?
Dump the screen? [y/n]

Аватара пользователя
Jesus05
Сообщения: 1792
Зарегистрирован: 02 дек 2009, 07:50
Откуда: Норильск, сейчас Санкт-петербург.
Контактная информация:

Re: Сохранение в игре.

Сообщение Jesus05 » 08 фев 2017, 09:48

Максим Кич писал(а):
08 фев 2017, 09:35
Причём, всё ещё интереснее. Мы в любом случае все игровые объекты когда-то создаём в первый раз. И нам в любом случае надо указывать, какие параметры мы получаем ...
И у нас будет один, прозрачный и предсказуемый способ, которым что-то появляется в игре. Как только у нас генерация нового объекта и десериализация сохранённого начинают ходить разными путями — начинается боль.
Пример1: бутылек с лечебным зельем, который можно отпить 4(N) раз. создается фабрикой всегда полный, изменяет кол-во сколько его еще-раз можно выпить только по методу Use(). Это его внутреннее состояние. Но нам надо вытащить его на "свет божий" что-бы сериализовать и десериализовать.
Пример2: монстр хранит свои текущие ХП и никому их не говорит. На запросу сколько у него ХП отвечает "много, средне, мало, почти метрв". Опять для сериализации нам надо вытащить все внутренности такого монстра наружу что-бы можно было сохранять\загружать его.

Я думаю можно придумать более сложные "части" игры которые могут хранить временное состояние внутри себя, и не предполагать создание себя в половинном состоянии. Отсюда начинают расти "дополнительные" конструкторы строго для десеарилизации, методы save\load у каждого класса, или как у меня God классы и отношения дружбы между таким год классом и всеми объектами в игре.

Аватара пользователя
Apromix
Мастер
Сообщения: 1092
Зарегистрирован: 04 июл 2011, 10:44
Откуда: Украина, Черновцы
Контактная информация:

Re: Сохранение в игре.

Сообщение Apromix » 08 фев 2017, 13:01

Велисипед не так страшен в данном вопросе, как его малюют :D При решении данной проблемы даже с сериализацией приходится изобретать некий велосипед?
Изображение Изображение

altmax
Сообщения: 94
Зарегистрирован: 15 сен 2012, 11:59

Re: Сохранение в игре.

Сообщение altmax » 08 фев 2017, 13:21

Apromix писал(а):
08 фев 2017, 13:01
Велисипед не так страшен в данном вопросе, как его малюют :D При решении данной проблемы даже с сериализацией приходится изобретать некий велосипед?
А велосипед уже изобретен? Хотелось бы увидеть его. Пока сошлись на мнении, что каждый изобретает велосипед для себя.

Аватара пользователя
Oreyn
Сообщения: 297
Зарегистрирован: 07 авг 2013, 14:59

Re: Сохранение в игре.

Сообщение Oreyn » 08 фев 2017, 15:36

Jesus05 писал(а):
08 фев 2017, 09:48
Максим Кич писал(а):
08 фев 2017, 09:35
Причём, всё ещё интереснее. Мы в любом случае все игровые объекты когда-то создаём в первый раз. И нам в любом случае надо указывать, какие параметры мы получаем ...
И у нас будет один, прозрачный и предсказуемый способ, которым что-то появляется в игре. Как только у нас генерация нового объекта и десериализация сохранённого начинают ходить разными путями — начинается боль.
Пример1: бутылек с лечебным зельем, который можно отпить 4(N) раз. создается фабрикой всегда полный, изменяет кол-во сколько его еще-раз можно выпить только по методу Use(). Это его внутреннее состояние. Но нам надо вытащить его на "свет божий" что-бы сериализовать и десериализовать.
Пример2: монстр хранит свои текущие ХП и никому их не говорит. На запросу сколько у него ХП отвечает "много, средне, мало, почти метрв". Опять для сериализации нам надо вытащить все внутренности такого монстра наружу что-бы можно было сохранять\загружать его.

Я думаю можно придумать более сложные "части" игры которые могут хранить временное состояние внутри себя, и не предполагать создание себя в половинном состоянии. Отсюда начинают расти "дополнительные" конструкторы строго для десеарилизации, методы save\load у каждого класса, или как у меня God классы и отношения дружбы между таким год классом и всеми объектами в игре.
Не, не, не.
Годобжект который опрашивает кого-то на предмет инкапсулированых данных - нихт гут.
Интерфейс с перегружаемыми методами сериалайз/десериалайз, в который передается контекст слота сохранения.
Добавляешь в класс итема новое свойство - тут же лезешь в его перегруженные методы интерфейса и прописываешь что они тоже сохраняются.
Своим велосипедом, или бустом / другой ет эназер сейвилкой.
Вот это кстати кусок с таким человеческим фактором. Через полгода разработки - откуда этот чертов баг? Ааа, я забыл указать что это свойство сериализутся. А как вообще я это делал?

При сохранении дернул корень твоего графа игровых сущностей. Тот всем ниже тоже вызвал сериалайз и передал контекст куда сохранять.
Карта сохранилась - дернула предметы и существа, та в свою очередь сохранили свои инвентари, эффекты и т.д.
Загрузка в таком-же порядке.

>> Как только у нас генерация нового объекта и десериализация сохранённого начинают ходить разными путями — начинается боль.
Именно. При загрузке бутылки с зельем из инвентаря, она создается через фабрику как новый обьект такого типа, и тут же у новосозданного объекта вызывается ее перегруженный метод лоад с переданным контекстом загрузки и она восстанавливает из сейва состояние своих внутренних параметров.
UPD:
Ага, понял, кстати само первое создание объектов можно сделать этим же образом - фабрика создала, и на вход лоада подала стартовую конфигурацию объекта. Таким образом точка входа (механизм создания) один. Да еще и стартовые конфиги объектов вынес в json вместо хардкода.

Аватара пользователя
Jesus05
Сообщения: 1792
Зарегистрирован: 02 дек 2009, 07:50
Откуда: Норильск, сейчас Санкт-петербург.
Контактная информация:

Re: Сохранение в игре.

Сообщение Jesus05 » 09 фев 2017, 07:06

Наверное у нас немного разный подход идеалогически.
Я считаю - класс не должен заниматься своей сериализацией. У него другая задача.
Я не люблю многозадачные классы, если ты предметы ты должен быть предметом, а не сериализуемым предметом, или предметом-монстром-клеткой_карты-с сериализаций одновременно.
Я допускаю в такой ситуации, что-бы что-то вне класса умело его восстанавливать\сохранять.
Пусть даже это будет тупо внешне перегруженный оператор << или как еще более редко используемый <<= для возможности "ложить" класс в поток сохранялки (ну или просто в класс сохранялки) . Пусть даже код этого перегруженного метода будет в cpp или h\hpp файле с классом, но я не считаю, что он должен быть частью класса.
Адд: И да я согласен, что плохо когда кто-то кроме самого класса знает его внутренности, но здесь считаю это допустимым злом, потому что это снижает сложность класса, убирает из зоны видимости одну из навязанных ответственностей.
Адд2: Хотелось бы какой-нить фреймворк\прекомпилер который.
требовал минимального вмешательства в код такого класс:

Код: Выделить всё

class SomeCLass
{
	private:
	 int t;
	protected:
	 int y;
	public:
	 ...
}
требовал добавить 1 строчку:

Код: Выделить всё

class SomeCLass
{
	SERIALIZABLE();
	private:
	 int t;
	protected:
	 int y;
	public:
	 ...
}
После чего давал возможность делать что-то типа:

Код: Выделить всё

SomeClass t;
Serializer << t;
Serializer.saveToFile(....);
Serializer.loadFromFile(...);
Serializer >> t;
Но пока я ничего такого не встретил :(

Аватара пользователя
Jesus05
Сообщения: 1792
Зарегистрирован: 02 дек 2009, 07:50
Откуда: Норильск, сейчас Санкт-петербург.
Контактная информация:

Re: Сохранение в игре.

Сообщение Jesus05 » 09 фев 2017, 09:40

Oreyn писал(а):
08 фев 2017, 15:36
Ага, понял, кстати само первое создание объектов можно сделать этим же образом - фабрика создала, и на вход лоада подала стартовую конфигурацию объекта. Таким образом точка входа (механизм создания) один. Да еще и стартовые конфиги объектов вынес в json вместо хардкода.
Кстати, вот, а почему создание класса никто не против что-бы было вынесено в отдельный класс, а сохранение обязательно должны быть строго внутри класса, конечно фабрика обращается к открытым членам класса, но думаю допустимо создать "дополнительный" интерфейс для класса с которым могла-бы общаться только фабрика.

Код: Выделить всё

class IItem //Interface
{
	virtual use() = 0;
	virtual throw() = 0;
}

class IItemInnerInfo //Interface
{
	virtual getCountOfUse() = 0;
	virtual setCountOfUse() = 0;
	virtual getThrowDamage() = 0;
	virtual setThrowDamage() = 0;
}

class Item : public IItem, public IItemInnerInfo
{
	use();
	throw();
	
	getCountOfUse();
	setCountOfUse();
	getThrowDamage();
	setThrowDamage();
}
но это же придется много писать... а что если....

Код: Выделить всё

class IItem //Interface
{
	virtual use() = 0;
	virtual throw() = 0;
}

class ItemInnerInfo
{
	protected:
		int itemParam1;
		int itemParam2;
	public:
		getCountOfUse();
		setCountOfUse();
		getThrowDamage();
		setThrowDamage();
}

class Item : public IItem, public ItemInnerInfo
{
	use();
	throw();
}
Но тогда зачем вообще гетеры сеттеры если по сути они не будут выполнять какой-то защитной функции...

Код: Выделить всё

class IItem //Interface
{
	virtual use() = 0;
	virtual throw() = 0;
}

struct ItemInnerInfo //Без поведения, только данные, хотя если очень хочется здесь можно сделать сериализацию, тогда она будет "вне зоны" видимости основного класса.
{
	public:
		int itemParam1;
		int itemParam2;
}

class Item : public IItem, public ItemInnerInfo //Если не наследоваться от данных, то получим необходимость добавлять методы "отдай данные, возми данные"
{
	use();
	throw();
}
и по сути мы вернулись почти к тому, что я предлагал в конце этого поста

Аватара пользователя
Максим Кич
Администратор
Сообщения: 1560
Зарегистрирован: 03 дек 2006, 20:17
Откуда: Витебск, Беларусь
Контактная информация:

Re: Сохранение в игре.

Сообщение Максим Кич » 09 фев 2017, 11:15

Jesus05 писал(а):
09 фев 2017, 07:06
Наверное у нас немного разный подход идеологически.
Я считаю - класс не должен заниматься своей сериализацией. У него другая задача.
Я не люблю многозадачные классы, если ты предметы ты должен быть предметом, а не сериализуемым предметом, или предметом-монстром-клеткой_карты-с сериализаций одновременно.
Ну как бы, мне кажется, что это выплёскивание младенца вместе с водой. Предмет будет сериализуемым, потому что его пра-прадедушка был сериализуемым базовым игровым объектом. В целом, любое поведение, которое укладывается в манипулирование свойствами объекта, может быть в него инкапсулировано.
Jesus05 писал(а):
09 фев 2017, 09:40
Кстати, вот, а почему создание класса никто не против что-бы было вынесено в отдельный класс, а сохранение обязательно должны быть строго внутри класса, конечно фабрика обращается к открытым членам класса, но думаю допустимо создать "дополнительный" интерфейс для класса с которым могла-бы общаться только фабрика.
Потому что конструктор должен быть откуда-то вызван. Сохранение инициировать тоже будет отдельный класс. И в файл писать/читать будет отдельный класс. Объект должен уметь себя свернуть в строку, и «пустой» экземпляр заполнить из строки.
Jesus05 писал(а):
09 фев 2017, 09:40
Но тогда зачем вообще гетеры сеттеры если по сути они не будут выполнять какой-то защитной функции...
Во-первых, никто не мешает пользоваться ими внутри методов serialize/deserialize. Во-вторых, на одну сериализацию/десериализацию будут приходиться сотни/тысячи обращений во время обычного игрового процесса (если на каждый ход autosave не делать, или если у нас обработка логики не дублируется где-то на удалённом сервере)
Oreyn писал(а):
08 фев 2017, 15:36
Ага, понял, кстати само первое создание объектов можно сделать этим же образом - фабрика создала, и на вход лоада подала стартовую конфигурацию объекта. Таким образом точка входа (механизм создания) один. Да еще и стартовые конфиги объектов вынес в json вместо хардкода.
Именно! Но в чём я согласен с Jesus05 — разные языки имеют разный оверхед при работе с тем же JSON/XML/etc. Это тоже надо учитывать. Но в целом, у нас видение архитектуры совпадает.
Dump the screen? [y/n]

Аватара пользователя
Cfyz
Сообщения: 761
Зарегистрирован: 30 ноя 2006, 10:03
Откуда: Санкт-Петербург
Контактная информация:

Re: Сохранение в игре.

Сообщение Cfyz » 09 фев 2017, 12:15

Jesus05 писал(а):Я считаю - класс не должен заниматься своей сериализацией. У него другая задача. <...> Пусть даже это будет тупо внешне перегруженный оператор << <...> Пусть даже код этого перегруженного метода будет в cpp или h\hpp файле с классом, но я не считаю, что он должен быть частью класса.
Ага, то есть

Код: Выделить всё

struct Foo
{
    Serializer& operator<<(Serializer& s) { ... }
};
это часть класса, а вот

Код: Выделить всё

struct Foo
{
    friend Serializer& operator<<(Serializer& s, const Foo& v);
};

Serializer& operator<<(Serializer& s, const Foo& v) { ... }
это уже не часть класса? ;-)

Тут присутствует некоторая игра терминами. Класс как шаблон сущности представляет собой довольно общий концепт из набора данных и методов работы с ними. В этом контексте неважно где именно располагается реализация, если это метод для работы с данными сущности -- это часть класса.

С другой стороны часто (и, очевидно, в этом случае) под классом подразумевается строго интерфейс. Тут да, какой-нибудь void Seriazlizer::Write(const Foo&); уже интерфейсом Foo строго говоря не является. Но при этом реализация данного метода оказывается настолько сильно приколочена к Foo, что уже несправедливо слишком строго их разделять. При изменении Foo надо менять Write(const Foo&), и наоборот -- при некоторых изменениях сериализации (например способ сохранения ссылок) может потребоваться изменить Foo (добавить необходимую информацию). В этом свете самое рациональное место для размещения метода сериализации -- "внутри" Foo. Потому что это и группирует логически тесно связанный код в одном месте, и как правило упрощает работу с нутром Foo (private-protected, туда-сюда).
Cfyz теперь - наглая морда.

Аватара пользователя
Jesus05
Сообщения: 1792
Зарегистрирован: 02 дек 2009, 07:50
Откуда: Норильск, сейчас Санкт-петербург.
Контактная информация:

Re: Сохранение в игре.

Сообщение Jesus05 » 09 фев 2017, 12:37

Cfyz писал(а):
09 фев 2017, 12:15

Код: Выделить всё

struct Foo
{
    friend Serializer& operator<<(Serializer& s, const Foo& v);
};

Serializer& operator<<(Serializer& s, const Foo& v) { ... }
это уже не часть класса? ;-)

Тут присутствует некоторая игра терминами. Класс как шаблон сущности представляет собой довольно общий концепт из набора данных и методов работы с ними. В этом контексте неважно где именно располагается реализация, если это метод для работы с данными сущности -- это часть класса.
...
Когда мне нужно вывести какие-то данные в поток отданные мне сторонней библиотекой (структуру, или класс насколько я могу добраться до важных для меня частей) я пишу в своей части кода.

Код: Выделить всё

std::ostream& operator<<(std::ostream& s, const Foo& v) { ... }
и никоим образом не считаю, что это я "приклеил" костыль к чужому классу.

но согласен, что это "крестопроблемы" и это касается скорее сей чем в общем программирования.

Я все больше склоняюсь к мысли что данные класса и поведение класса это 2 разные сущности и жить они должны отдельно.

Ответить

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 9 гостей