Практически все игры используют какие-либо внешние данные. В частности, подавляющее большинство игр содержит возможности сохранения игрового процесса в его текущем состоянии и его загрузки, игры хранят во внешних файлах статистические данные, загружают из файлов дополнительные уровни. XNA содержит инструментарий, предназначенный для работы с файлами. В этой лабораторной работе мы рассмотрим файловые операции, доступные в XNA, а так же – сериализацию объектов – мощное средство, которое можно использовать для сохранения и загрузки игровых данных.
Цель работы
Научиться работать с файлами
Задачи работы
Освоить файловые операции в XNA
Освоить сериализацию объектов
Организовать загрузку и сохранение игры
Файловые операции в XNA
Создадим новый проект P10_1. Реализуем в нем демонстрацию следующих операций с файлами:
Создание нового файла.
Открытие файла и чтение информации из него.
Копирование файла.
Переименование файла.
Просмотр директории.
Удаление файла.
Для работы с файлами в XNA нужно выполнить следующие шаги:
Во-первых – получить объект типа IAsyncResult – как результат запроса о выборе устройства хранения информации.
Во-вторых – переменная типа IAsyncResult используется для создания переменной типа StorageDevice, которая представляет устройство для хранения данных.
В-третьих – переменная типа StorageDevice используется при создании объекта StorageContainer, который используется для доступа к месту хранения файлов. В случае с Windows-играми это – папка SavedGames, расположенная в папке Мои Документы текущего пользователя.
На рис. 10.1. вы можете видеть данную папку для проекта P10_1.
Рис. 10.1. Папка для сохранения игры
В листинге 10.1. вы можете найти код класса Game1.
usingSystem;usingSystem.Collections.Generic;usingMicrosoft.Xna.Framework;usingMicrosoft.Xna.Framework.Audio;usingMicrosoft.Xna.Framework.Content;usingMicrosoft.Xna.Framework.Graphics;usingMicrosoft.Xna.Framework.Input;//Классы для работы с устройствами ввода-выводаusingSystem.IO;namespace P10_1{publicclass Game1 : Microsoft.Xna.Framework.Game{ GraphicsDeviceManager graphics; SpriteBatch spriteBatch;//Для информации об используемом устройстве хранения информации StorageDevice sDev;//Для хранения результата асинхронной операции доступа к устройству IAsyncResult res;public Game1(){ graphics =new GraphicsDeviceManager(this); Content.RootDirectory="Content";}protectedoverridevoid Initialize(){// TODO: Add your initialization logic herebase.Initialize();}protectedoverridevoid LoadContent(){// Create a new SpriteBatch, which can be used to draw textures. spriteBatch =new SpriteBatch(GraphicsDevice);// TODO: use this.Content to load your game content here}protectedoverridevoid UnloadContent(){// TODO: Unload any non ContentManager content here}//Процедура для создания нового файлаvoid FileCreate(StorageDevice device){//Массив байтов для записи в файлbyte[] a =newbyte[10];for(byte i =0; i <10; i++){ a[i]= i;}//Открываем контейнер для хранения файлов//В случае с Windows-играми это - папка с соответствующим//именем в папке Мои документы текущего пользователя StorageContainer container = device.OpenContainer("P10_1");//Соединяем имя файла и имя контейнераstring filename = Path.Combine(container.Path, "savegame.sav");//Создаем новый файлif(!File.Exists(filename)){ FileStream file = File.Create(filename);//Записываем в файл массив байтов, сгенерированный выше file.Write(a, 0, 10);//закрываем файл file.Close();this.Window.Title="Файл создан";}else{this.Window.Title="Файл уже создан";}//Уничтожаем контейнер - только после этого файл будет //сохранен container.Dispose();}//Процедура для открытия существующего файлаvoid FileOpen(StorageDevice device){ StorageContainer container = device.OpenContainer("P10_1");string filename = Path.Combine(container.Path, "savegame.sav");if(File.Exists(filename)){ FileStream file = File.Open(filename, FileMode.Open);//Выведем данные, считанные из файла, в заголовок игрового окнаthis.Window.Title="Содержимое файла: ";for(int i =1; i < file.Length+1; i++)this.Window.Title=this.Window.Title+ file.ReadByte().ToString(); file.Close();}else{this.Window.Title="Отсутствует файл для чтения";} container.Dispose();}//Процедура для копирования файлаvoid FileCopy(StorageDevice device){ StorageContainer container = device.OpenContainer("P10_1");string filename = Path.Combine(container.Path, "savegame.sav");if(File.Exists(filename)){string copyfilename = Path.Combine(container.Path, "copysave.bak"); File.Copy(filename, copyfilename, true);this.Window.Title="Файл скопирован";}else{this.Window.Title="Файл не существует";} container.Dispose();}//Процедура для переименования файлаvoid FileRename(StorageDevice device){ StorageContainer container = device.OpenContainer("P10_1");//Старое имя файлаstring oldfilename = Path.Combine(container.Path, "copysave.bak");//Новое имя файлаstring newfilename = Path.Combine(container.Path, "rencopysave.sav");//Если в папке нет файла с новым именем файла и есть файл //со старым именем - переименовываемif(!File.Exists(newfilename)&File .Exists(oldfilename )){ File.Move(oldfilename, newfilename);this.Window.Title="Скопированный файл переименован";}else{this.Window.Title="Такой файл уже есть или отсутствует файл для переименования";} container.Dispose();}//Просмотр содержимого папкиvoid FileEnumerate(StorageDevice device){ StorageContainer container = device.OpenContainer("P10_1");string FNForComp="";//Получить список файлов ICollection<string> FileList = Directory.GetFiles(container.Path);//Для каждого имени файла проверим, соответствует ли оно//заданному имени, предположим, что папка не содержит заданного файлаthis.Window.Title="Папка не содержит файл rencopysave.sav";foreach(string filename in FileList){//Извлечем из строки с полным путем к файлу//имя файла FNForComp = Path.GetFileName(filename);//Если имя файла равно искомому - выведем //соответствующее сообщениеif(FNForComp =="rencopysave.sav"){this.Window.Title="Папка содержит файл rencopysave.sav";}} container.Dispose();}//удаление файлаvoid FileDelete(StorageDevice device){ StorageContainer container = device.OpenContainer("P10_1");string filename = Path.Combine(container.Path, "rencopysave.sav");//Удаляем файл если он существуетif(File.Exists(filename)){ File.Delete(filename);this.Window.Title="Файл rencopysave.sav удален";} container.Dispose();}protectedoverridevoid Update(GameTime gameTime){ KeyboardState kb = Keyboard.GetState();//Инициируем процедуру выбора устройства, в случае с Windows//автоматически выбирается папка SavedGames в папке Мои документы//текущего пользователя res = Guide.BeginShowStorageDeviceSelector(PlayerIndex.One, null, null);//Сохраняем найденное устройство - позже мы используем его //в процедурах работы с файлами sDev = Guide.EndShowStorageDeviceSelector(res);//При нажатии на клавишу Q вызываем процедуру сохранения файлаif(kb.IsKeyDown(Keys.Q)){ FileCreate(sDev);}//При нажатии на W вызываем процедуру открытия файлаif(kb.IsKeyDown(Keys.W)){ FileOpen(sDev);}//При нажатии на E вызываем процедуру копирования файлаif(kb.IsKeyDown(Keys.E)){ FileCopy(sDev);}//При нажатии на A вызываем процедуру переименования файлаif(kb.IsKeyDown(Keys.A)){ FileRename(sDev);}//При нажатии на S вызываем процедуру просмотра каталогаif(kb.IsKeyDown(Keys.S)){ FileEnumerate(sDev);}//При нажатии D вызываем процедуру удаления файлаif(kb.IsKeyDown(Keys.D)){ FileDelete(sDev);}base.Update(gameTime);}protectedoverridevoid Draw(GameTime gameTime){ graphics.GraphicsDevice.Clear(Color.CornflowerBlue);// TODO: Add your drawing code herebase.Draw(gameTime);}}}
В обычных файлах можно хранить различные данные, в том числе – информацию об уровнях, информацию для сохранения игры и т.д. Однако при таком подходе вся работа по восстановлению необходимых данных ложится на программиста. Удобнее было бы сохранять и восстанавливать не отдельные переменные или поля объектов, а целые объекты, которые хранят множество полей, используемых в игре. Сделать это позволяет сериализация объектов.
Сериализация объектов: сохранение и загрузка игры
Сериализация объектов - это, другими словами – сохранение объекта в виде файла. Сериализованный объект можно восстановить в ходе операции десериализации. В случае с компьютерными играми сериализацию удобно использовать для сохранения игровой информации. Для того, чтобы эффективно использовать инструменты сериализации в играх, нужно заранее уяснить, какие именно объекты будут сериализованы. Желательно выделить для сериализации особые объекты, которые будут содержать лишь ту информацию, сохранение которой необходимо для успешного продолжения игрового процесса при десериализации соответствующего объекта, то есть – загрузке игры.
Игра, предусматривающая возможность сохранения игрового состояния, должна предусматривать как минимум два режима работы. Первый – когда игра начинается сначала – в таком случае поля класса, предназначенного для сериализации, заполняются некоторыми заранее заданными значениями. В ходе игры поля класса меняются, отражая изменения, произошедшие в ходе игрового процесса. Далее, после того, как объект сериализован, игра может либо начинаться сначала, либо – восстанавливать свое состояние на момент сериализации. Рассмотрим пример сохранения и восстановления состояния игры на примере проекта P10_2.
Игровой процесс приводимого здесь проекта заключается в том, что пользователь должен кликать мышью по плиткам, которые исчезают после щелчка по ним. Задача – создать механизм, который позволит сохранять состояние игрового экрана в файле и восстанавливать это состояние. Мы не будем приводить здесь разработку игровой логики, меню и других подсистем игры. Сосредоточимся на сохранении и загрузке игры.
На рис. 10.2. вы можете видеть окно Project Explorer для проекта P10_2.
Рис. 10.2. Окно Project Explorer для проекта P10_2
Класс Game1 – это стандартный игровой класс.
GameData – класс, содержащий игровые данные и предназначенный для сериализации.
gBaseClass, Wall – классы для визуализации игровых объектов.
В листинге 10.2. вы можете видеть код класса GameData.
usingSystem;usingSystem.Collections.Generic;usingSystem.Text;namespace P10_2{//Атрибут, указывающий на то, что класс может быть сериализован[Serializable]class GameData{//Поле класса, которое содержит важные игровые данныеpublicbyte[,] array;//Конструктор классаpublic GameData (byte[,] arr){ //array = new byte [8,10]; array = arr;}}}
Обратите внимание на атрибут Serializable – он указывает на то, что класс может быть сериализован. От набора полей этого класса и от данных, хранящихся в них, зависит размер файла, в котором будет храниться игровая информация. В нашем случае класс содержит массив типа Byte, который будет представлять собой состояние игрового окна на момент сохранения игры.
Рассмотрим классы gBaseClass и Wall (листинг 10.3., 10.4.) Эти классы мы уже использовали в проекте P5_1. Класс gBaseClass содержит механизмы для рисования компонента и расположения его на экране, а класс Wall – потомок класса gBaseClass – используется для управления поведением объекта.
usingSystem;usingSystem.Collections.Generic;usingMicrosoft.Xna.Framework;usingMicrosoft.Xna.Framework.Audio;usingMicrosoft.Xna.Framework.Content;usingMicrosoft.Xna.Framework.Graphics;usingMicrosoft.Xna.Framework.Input;//Классы для работы с устройствами ввода-выводаusingSystem.IO;//Классы для работы механизмов сериализацииusingSystem.Runtime.Serialization;usingSystem.Runtime.Serialization.Formatters.Binary;namespace P10_2{/// /// This is the main type for your game/// publicclass Game1 : Microsoft.Xna.Framework.Game{ GraphicsDeviceManager graphics; SpriteBatch spriteBatch;//Для текстуры стены Texture2D txtWall;//Прямоугольник для создания объектов Wall Rectangle recSprite =new Rectangle(0, 0, 64, 64);//Массив объектов Wall Wall [,] arrayOfWalls;//Объект, хранящий игровые данные и подлежащий сериализации//Массив в этом объекте используется при загрузке игры и //при сохранении игры, в игровом процессе используется//массив объектов Wall GameData MyObj;//Переменная для хранения состояния мыши MouseState mouse;//устройство для хранения информации StorageDevice sDev;//результат операции доступа к устройству IAsyncResult res;public Game1(){ graphics =new GraphicsDeviceManager(this); Content.RootDirectory="Content";}protectedoverridevoid Initialize(){//Разрешение экрана 640х512 graphics.PreferredBackBufferWidth=640; graphics.PreferredBackBufferHeight=512; graphics.ApplyChanges();//Сделать указатель мыши видимымthis.IsMouseVisible= true;base.Initialize();}protectedoverridevoid LoadContent(){// Create a new SpriteBatch, which can be used to draw textures. spriteBatch =new SpriteBatch(GraphicsDevice); Services.AddService(typeof(SpriteBatch), spriteBatch); txtWall = Content.Load<Texture2D>("wall");//В этом массиве лишь единицы - это значит//что при запуске все игровое поле заполнено//изображениями стенbyte[,] arrayForGame =newbyte[8, 10]{{1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 1, 1, 1, 1, 1, 1, 1},{1, 1, 1, 1, 1, 1, 1, 1, 1, 1},{1, 1, 1, 1, 1, 1, 1, 1, 1, 1},};//Новый массив стен arrayOfWalls =new Wall[8, 10];//Создаем объект для хранения данных,//передавая ему массив MyObj =new GameData(arrayForGame);//вызываем процедуру создания и //вывода на экран игровых объектов AddSprites ();}//Создание игровых объектовvoid AddSprites(){//Просматриваем массив array в объекте типа GameDatafor(int i =0; i <8; i++){for(int j =0; j <10; j++){if(MyObj.array[i, j]==1){//Если в массиве единица - создаем новый объект в текущей ячейке//массива arrayOfWalls и добавляем в список игровых компонентов arrayOfWalls[i, j]=new Wall(this, ref txtWall, new Vector2(j, i), recSprite); Components.Add(arrayOfWalls[i, j]);}}}}//Процедура очистки массива игровых компонентовvoid ClearAll(){for(int i =0; i <8; i++){for(int j =0; j <10; j++){if(arrayOfWalls[i, j]!=null){ arrayOfWalls[i, j].Dispose(); arrayOfWalls[i, j]= null;}}}}//Процедура проверки массива игровых компонентов и отражения//его состояния в массиве объекта типа GameData//в нашем случае, если ячейка массива игровых компонентов содержит//объект, в массив объекта GameData записывают 1, если нет - 0void SetWallToArray(){for(int i =0; i <8; i++){for(int j =0; j <10; j++){if(arrayOfWalls[i, j]==null){ MyObj.array[i, j]=0;}else{ MyObj.array[i, j]=1;}}}}//Процедура сериализации объектаvoid serializ(){//Контейнер для хранения данных StorageContainer container = sDev.OpenContainer("P10_2");//Полное имя файла - комбинация адреса контейнера и имениstring filename = Path.Combine(container.Path, "savegame.sav");//Объект, предназначенный для сериализации и десериализации других объектов IFormatter formatter =new BinaryFormatter();//Создаем новый поток для записи файла Stream stream =new FileStream(filename, FileMode.Create, FileAccess.Write, FileShare.None);//Сериализуем объект MyObj в поток stream formatter.Serialize(stream, MyObj);//Закрываем поток stream.Close();//Уничтожаем контейнер container.Dispose();//выводим сообщение в заголовок игрового окнаthis.Window.Title="Игра сохранена";}//Процедура десериализации объекта//похожа на процедуру десериализации//при десериализации файл открывают для чтения и //десериализуют в объектvoid deserializ(){ StorageContainer container = sDev.OpenContainer("P10_2");string filename = Path.Combine(container.Path, "savegame.sav"); IFormatter formatter =new BinaryFormatter();//Новый поток для чтения файла Stream stream =new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read);//Получаем данные из потока и приводим их к типу MyObj MyObj =(GameData)formatter.Deserialize(stream); stream.Close(); container.Dispose();this.Window.Title="Игра загружена";}protectedoverridevoid UnloadContent(){// TODO: Unload any non ContentManager content here}protectedoverridevoid Update(GameTime gameTime){//получим состояние клавиатуры KeyboardState kb = Keyboard.GetState();//Сохраняем игру при нажатой клавише Aif(kb.IsKeyDown(Keys.A)){//Отразим состояние игровго окна в массиве объекта типа GameData SetWallToArray();//Получим устройство для хранения данных res = Guide.BeginShowStorageDeviceSelector(PlayerIndex.One, null, null); sDev = Guide.EndShowStorageDeviceSelector(res);//Вызовем процедуру сериализации serializ ();}//Загружаем игру при нажатой клавише Wif(kb.IsKeyDown(Keys .W)){//Очищаем игровой экран ClearAll();//Получим устройство для хранения данных res = Guide.BeginShowStorageDeviceSelector(PlayerIndex.One, null, null); sDev = Guide.EndShowStorageDeviceSelector(res);//Десериализуем объект из файла deserializ();//Сконструируем игровой экран на основе десериализованного //объекта MyObj AddSprites();}//Работа с мышью mouse = Mouse.GetState();if(mouse.LeftButton== ButtonState.Pressed){for(int i =0; i <8; i++){for(int j =0; j <10; j++){//Если в текущей ячейке массива есть "стена"//проверим, находился ли указатель мыши в пределах объекта//соответствующего текущей ячейке. Если да - уничтожим объектif(arrayOfWalls[i, j]!=null){if(arrayOfWalls[i, j].sprPosition.X+ arrayOfWalls[i, j].sprRectangle.Width> mouse.X&& arrayOfWalls[i, j].sprPosition.X< mouse.X&& arrayOfWalls[i, j].sprPosition.Y+ arrayOfWalls[i, j].sprRectangle.Height> mouse.Y&& arrayOfWalls[i, j].sprPosition.Y< mouse.Y){ arrayOfWalls[i, j].Dispose(); arrayOfWalls[i, j]= null;}}}}}base.Update(gameTime);}protectedoverridevoid Draw(GameTime gameTime){ graphics.GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin();base.Draw(gameTime); spriteBatch.End();}}}
На рис. 10.3. вы можете видеть игровой экран проекта P10_2.
Рис. 10.3. Игровой экран проекта P10_2
Вопросы
1) В какой папке на ПК по умолчанию сохраняются файлы, которые создаются в процессе работы игрового проекта?
a. В корневом каталоге диска D
b. В папке, где расположен исполняемый файл игры
c. В папке, соответствующей игровому проекту, расположенной по адресу Мои документы\SavedGames
d. В корневом каталоге диска C
2) Какой тип имеет переменная, задающая контейнер для хранения игровых файлов?
a. File
b. StorageContainer
c. FileStream
d. StorageDevice
3) Какой тип имеет переменная, задающая устройство, используемое для сохранения файлов
a. File
b. StorageContainer
c. FileStream
d. StorageDevice
4) Что такое сериализация объекта?
a. Уничтожение объекта
b. Сохранение объекта в виде файла
c. Восстановление состояния объекта из файла
d. Создание нового объекта
5) Что такое десериализация объекта?
a. Уничтожение объекта
b. Сохранение объекта в виде файла
c. Восстановление состояния объекта из файла
d. Создание нового объекта
6) Какой атрибут должен иметь класс, подлежащий сериализации?
a. [Serializable]
b. [AttributeUsage]
c. [Author]
d. [Deserializable]
7) При проектировании класса, подлежащего сериализации, рекомендуется
a. Включать в класс как можно больше свойств, которые хранят всю информацию об игре
b. Включать в класс лишь ту информацию, которая необходима для реализации функций сохранения и загрузки игры
8) Какой класс содержит статические методы, которые удобно использовать при работе с именами файлов
a. String
b. FileStream
c. Path
d. File
2436 Прочтений • [Работа с файлами] [08.08.2012] [Комментариев: 0]