Сериализация данных в игровых приложениях. (Serialization XNA)
По мере продвижения в изучении этой серии статей вы уже освоили некоторые азы создания игр. Теперь я хочу рассказать вам о сериализации данных. Что же это такое и для чего это нам нужно? Вообще пугающее новичков понятие сериализация используется не только в играх, а и во многих не игровых приложениях. Суть процесса сериализации заключается в следующем. Во время выполнения приложения, например игры, ваша программа использует некоторые переменные содержащие некоторые значения. Эти значения постоянно меняются, изменяются и поведения целых классов в зависимости от значений переменных. Например количество патронов, жизней, енергии или очков игрового персонажа. Понятное дело, чтобы насобирать максимум, скажем, жизней - игроку пришлось просидеть за игрой несколько часов. Игра в момент своего старта конечно же, присваевает игроку некоторое число жизней, которое вы укажите в своей программе, но в процессе тяжелых игровых событий персонаж это число ,допустим, удвоил. Если теперь он выйдет из игры, то потом начнет играть снова, он обнаружит что все что нажито честным и непосильным трудом сгорело, количество заветных жизней такое как было сначала. Все кто когда либо играл в компьютеоные игры, наверняка слышал термин сохранение игры. В любой момент времени, нажав на определенную клавишу, игра "сохраняется" в некий файл сохранения, имеющий как правило расширение .sav. После чего во время повторного старта игры, персонаж оказывается в том игровом месте и с такими же регалиями, которые он сохранял. Вопрос - откуда программа загрузила данные, которые разработчики нигде не определили? Ответ напрашивается сам собой. Конечно же из файла сохранения. Пока вроде все просто. Мы теоретически можем себе представить как из текстового файла подгрузить например имя игрока. Но как подгрузить n-мерный массив, или готовый класс? Несомненно найдутся люди, которые скажут, что способ такой есть, и написав специальную логику, которая на лету будет разбирать текст, конвертировать всевозможные значения и типы, в итоге все таки справится с этой задачей. Но мы пойдем другим путем. И путь этот назывется сериализация. Иными словами сериализация - это способ сохранения в бинарный файл целых классов, структур, перечислений, делегатов. Антипод сереиализации - десериализация. Это как вы наверное уже догадались обратный процесс, то есть извлечение из бинарного файла тех же классов, структур, делегатов и перечислений.
На практике все выше сказанное можно реализовать например вот так. Заведем новый пример с названием myTerrainSerialize. Это будет слегка модернизированый вариант примера myTerrain. Если помните в нем мы строили ландшафт на основе карты высот. Значения высоты вершины считывалось из нее же. В этом примере мы отделим код заполнения данными массива float[,] heightData в новый класс с названием Configurator. Выглядеть он будет так.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
namespace myTerrainSerialize
{
[Serializable]
public class Configurator
{
public int WIDTH;
public int HEIGHT;
public float[,] heightData;
public Configurator(Game1 game)
{
LoadHeightData(game.Content.Load<Texture2D>("HeightMap"));
}
public void LoadHeightData(Texture2D heightMap)
{
// установили значение
// сторон карты высот в
// глобальные переменные
WIDTH = heightMap.Width;
HEIGHT = heightMap.Height;
// создали массив цветов для хранения
// данных карты высот с размерностью
// высоты умноженной на ширину
Color[] heightMapColors = new Color[WIDTH * HEIGHT];
// загружаем данные из карты высот
// в массив цветов
heightMap.GetData(heightMapColors);
// создали массив значений высот
heightData = new float[WIDTH, HEIGHT];
// в цикле
for (int x = 0; x < WIDTH; x++)
{
for (int y = 0; y < HEIGHT; y++)
{
// если карта высот ч/б то не важно из какого канала
// брать значение. Мы возьмем из красного
heightData[x, y] = heightMapColors[x + y * WIDTH].R;
}
}
}
}
}
Перед тем как обьявить новый класс мы написали строчку [Serializable].
Это означает, что класс готов к сериализации. Он имеет массив
public float[,] heightData;
который нам понадобится в дальнейшем при создании вершин. При первом запуске программы этот массив заполнится данными, после чего весь класс будет сохранен в файл savegame.sav ,который будет создан по пути
C:\Users\имя пользователя\Documents\SavedGames\XNAGame\Player1.
Помимо этого массива, класс конфигуратора игры может хранить множество необходимых переменных, с их количеством и назначением вы можете поэксперриментировать самостоятельно. Продолжим далее. Теперь у нас есть то, что подлежит сохранению, но пока еще нет самого механизма сохранения и извлечения данных. Для этих целей создадим класс с названием Serializator.
class Serializator
{
Game1 game;
static StorageDevice HardDiskDevice;
Configurator ourObj;
public Serializator(Game1 game)
{
this.game = game;
HardDiskDevice = Guide.EndShowStorageDeviceSelector(Guide.BeginShowStorageDeviceSelector(PlayerIndex.One, null, null));
ourObj = new Configurator(game);
//serialize();
}
//Процедура сериализации объекта
void serialize()
{
//Контейнер для хранения данных
StorageContainer container = HardDiskDevice.OpenContainer("XNAGame");
//Полное имя файла - комбинация адреса контейнера и имени
string filename = Path.Combine(container.Path, "savegame.sav");
//Объект, предназначенный для сериализации и десериализации других объектов
IFormatter formatter = new BinaryFormatter();
//Создаем новый поток для записи файла
Stream stream = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None);
//Сериализуем объект ourObj в поток stream
formatter.Serialize(stream, ourObj);
//Закрываем поток
stream.Close();
//Уничтожаем контейнер
container.Dispose();
}
//метод возвращающий массив высот из файла
public static float[,] getdata()
{
StorageContainer container = HardDiskDevice.OpenContainer("XNAGame");
IFormatter formatter = new BinaryFormatter();
//Новый поток для чтения файла
Stream stream = new FileStream(Path.Combine(container.Path, "savegame.sav"), FileMode.Open, FileAccess.Read, FileShare.Read);
//Получаем данные из потока и приводим их к типу MyObj
Configurator MyObj = (Configurator)formatter.Deserialize(stream);
stream.Close();
container.Dispose();
return MyObj.heightData;
}
}
При первом запуске приложения мы не будем комментировать вызов метода serialize();
Иначе произойдет ошибка свидетельствующая о том, что в указанной директории еще нет искомого файла. Но в последующих запусках приложения, этот вызов комментируется. И когда класс Terrain потребует от Serializatora массив высот
heightData = Serializator.getdata();
то метод getdata() вернет ему необходимый массив взятый из класса Configurator, который был извлечен именно из файла savegame.sav.
По мере продвижения в изучении этой серии статей вы уже освоили некоторые азы создания игр. Теперь я хочу рассказать вам о сериализации данных. Что же это такое и для чего это нам нужно? Вообще пугающее новичков понятие сериализация используется не только в играх, а и во многих не игровых приложениях. Суть процесса сериализации заключается в следующем. Во время выполнения приложения, например игры, ваша программа использует некоторые переменные содержащие некоторые значения. Эти значения постоянно меняются, изменяются и поведения целых классов в зависимости от значений переменных. Например количество патронов, жизней, енергии или очков игрового персонажа. Понятное дело, чтобы насобирать максимум, скажем, жизней - игроку пришлось просидеть за игрой несколько часов. Игра в момент своего старта конечно же, присваевает игроку некоторое число жизней, которое вы укажите в своей программе, но в процессе тяжелых игровых событий персонаж это число ,допустим, удвоил. Если теперь он выйдет из игры, то потом начнет играть снова, он обнаружит что все что нажито честным и непосильным трудом сгорело, количество заветных жизней такое как было сначала. Все кто когда либо играл в компьютеоные игры, наверняка слышал термин сохранение игры. В любой момент времени, нажав на определенную клавишу, игра "сохраняется" в некий файл сохранения, имеющий как правило расширение .sav. После чего во время повторного старта игры, персонаж оказывается в том игровом месте и с такими же регалиями, которые он сохранял. Вопрос - откуда программа загрузила данные, которые разработчики нигде не определили? Ответ напрашивается сам собой. Конечно же из файла сохранения. Пока вроде все просто. Мы теоретически можем себе представить как из текстового файла подгрузить например имя игрока. Но как подгрузить n-мерный массив, или готовый класс? Несомненно найдутся люди, которые скажут, что способ такой есть, и написав специальную логику, которая на лету будет разбирать текст, конвертировать всевозможные значения и типы, в итоге все таки справится с этой задачей. Но мы пойдем другим путем. И путь этот назывется сериализация. Иными словами сериализация - это способ сохранения в бинарный файл целых классов, структур, перечислений, делегатов. Антипод сереиализации - десериализация. Это как вы наверное уже догадались обратный процесс, то есть извлечение из бинарного файла тех же классов, структур, делегатов и перечислений.
На практике все выше сказанное можно реализовать например вот так. Заведем новый пример с названием myTerrainSerialize. Это будет слегка модернизированый вариант примера myTerrain. Если помните в нем мы строили ландшафт на основе карты высот. Значения высоты вершины считывалось из нее же. В этом примере мы отделим код заполнения данными массива float[,] heightData в новый класс с названием Configurator. Выглядеть он будет так.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
namespace myTerrainSerialize
{
[Serializable]
public class Configurator
{
public int WIDTH;
public int HEIGHT;
public float[,] heightData;
public Configurator(Game1 game)
{
LoadHeightData(game.Content.Load<Texture2D>("HeightMap"));
}
public void LoadHeightData(Texture2D heightMap)
{
// установили значение
// сторон карты высот в
// глобальные переменные
WIDTH = heightMap.Width;
HEIGHT = heightMap.Height;
// создали массив цветов для хранения
// данных карты высот с размерностью
// высоты умноженной на ширину
Color[] heightMapColors = new Color[WIDTH * HEIGHT];
// загружаем данные из карты высот
// в массив цветов
heightMap.GetData(heightMapColors);
// создали массив значений высот
heightData = new float[WIDTH, HEIGHT];
// в цикле
for (int x = 0; x < WIDTH; x++)
{
for (int y = 0; y < HEIGHT; y++)
{
// если карта высот ч/б то не важно из какого канала
// брать значение. Мы возьмем из красного
heightData[x, y] = heightMapColors[x + y * WIDTH].R;
}
}
}
}
}
Перед тем как обьявить новый класс мы написали строчку [Serializable].
Это означает, что класс готов к сериализации. Он имеет массив
public float[,] heightData;
который нам понадобится в дальнейшем при создании вершин. При первом запуске программы этот массив заполнится данными, после чего весь класс будет сохранен в файл savegame.sav ,который будет создан по пути
C:\Users\имя пользователя\Documents\SavedGames\XNAGame\Player1.
Помимо этого массива, класс конфигуратора игры может хранить множество необходимых переменных, с их количеством и назначением вы можете поэксперриментировать самостоятельно. Продолжим далее. Теперь у нас есть то, что подлежит сохранению, но пока еще нет самого механизма сохранения и извлечения данных. Для этих целей создадим класс с названием Serializator.
class Serializator
{
Game1 game;
static StorageDevice HardDiskDevice;
Configurator ourObj;
public Serializator(Game1 game)
{
this.game = game;
HardDiskDevice = Guide.EndShowStorageDeviceSelector(Guide.BeginShowStorageDeviceSelector(PlayerIndex.One, null, null));
ourObj = new Configurator(game);
//serialize();
}
//Процедура сериализации объекта
void serialize()
{
//Контейнер для хранения данных
StorageContainer container = HardDiskDevice.OpenContainer("XNAGame");
//Полное имя файла - комбинация адреса контейнера и имени
string filename = Path.Combine(container.Path, "savegame.sav");
//Объект, предназначенный для сериализации и десериализации других объектов
IFormatter formatter = new BinaryFormatter();
//Создаем новый поток для записи файла
Stream stream = new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None);
//Сериализуем объект ourObj в поток stream
formatter.Serialize(stream, ourObj);
//Закрываем поток
stream.Close();
//Уничтожаем контейнер
container.Dispose();
}
//метод возвращающий массив высот из файла
public static float[,] getdata()
{
StorageContainer container = HardDiskDevice.OpenContainer("XNAGame");
IFormatter formatter = new BinaryFormatter();
//Новый поток для чтения файла
Stream stream = new FileStream(Path.Combine(container.Path, "savegame.sav"), FileMode.Open, FileAccess.Read, FileShare.Read);
//Получаем данные из потока и приводим их к типу MyObj
Configurator MyObj = (Configurator)formatter.Deserialize(stream);
stream.Close();
container.Dispose();
return MyObj.heightData;
}
}
При первом запуске приложения мы не будем комментировать вызов метода serialize();
Иначе произойдет ошибка свидетельствующая о том, что в указанной директории еще нет искомого файла. Но в последующих запусках приложения, этот вызов комментируется. И когда класс Terrain потребует от Serializatora массив высот
heightData = Serializator.getdata();
то метод getdata() вернет ему необходимый массив взятый из класса Configurator, который был извлечен именно из файла savegame.sav.
Как вы сами убедились, сериализация - мощнейший инструмент который наверняка вам не раз пригодится.
Комментариев нет:
Отправить комментарий