В этой главе мы начнем работать с двухмерными изображениями и создадим два проекта. В первом проекте мы загрузим в игру и выведем на экран простое двухмерное изображение, или, как принято говорить, спрайт. Общая структура классов этого проекта останется неизменной, такой, как она была в предыдущей главе. Весь код по загрузке и выводу изображения на экран будет написан непосредственно в классе Game1. После того как вы разберетесь и поймете, как загружаются и рисуются двухмерные изображения на экране, мы создадим второй более сложный проект. Идея второго проекта (DrawSpriteClass) также заключается в загрузке и выводе на экран спрайта, но в этом случае структура проекта будет значительно улучшена. В частности, добавится новый класс Sprite, отвечающий за создание и загрузку вухмерных изображений. Впоследствии с каждой новой главой класс Sprite будет постоянно обновляться и улучшаться, обрастая новыми функциональными возможностями. 6.1. Система координат Двухмерная система координат, предназначенная для представления графики в играх, перевернута по отношению к обычной декартовой системе координат. Начало этой системы координат находится в левом верхнем углу экрана телевизора или монитора. Положительная ось Х проходит по верхней кромке экрана слева направо, а положительная ось Y – сверху вниз по левой боковой стороне экрана. Отрицательная часть обеих осей координат лежит за областью экрана, как это показано на рис. 6.1. Игровая графика, которая не попадает в область экрана, отсекается системными сервисами и не участвует в построении сцены.
Рис. 6.1. Система координат
Ключевым понятием в двухмерной графике является спрайт. Спрайт – это простое двухмерное изображение, нарисованное любом графическом редакторе и сохраненное в одном из графических форматов. В XNA Game Studio Express реализована возожность работы с форматами BMP, JPG, PNG, TGA и DDS. Это самые распространенные и чаще всего используемые в играх графические форматы. Отображая на экране телевизора спрайт, необходимо понимать следующее условие. Любой спрайт – это изображение, которое заключено в прямоугольник. Рисуя спрайт на экране, его начальной точкой отчета всегда будет оставаться верхний левый угол этого самого прямоугольника (рис. 6.2). Поэтому если вы определяете, допустим, столкновения между спрайтами, то вам необходимо знать ширину и высоту изображения, чтобы прибавить эти значения к начальной точке координат спрайта (левый верхний угол). Соответственно середина спрайта будет находиться в половине ширины и высоты графического изображения (рис. 6.2).
Рис. 6.2. Представление спрайта на экране 6.2. Проект DrawSprite В этом проекте нам предстоит нарисовать спрайт на экране, или, иначе, вывести определенное изображение в заданном месте экрана. Что для этого нужно? Прежде всего необходимо само изображение в формате, понятном XNA Game Studio Express. Такое изображение можно создать в любом графическом редакторе. Затем в арисованном изображении нужно вырезать фон, чтобы при выводе спрайта на экран вокруг исходного изображения фона не было видно. Делается это с помощью графического редактора, и чаще всего для этих целей используется Adobe Photoshop, который уже давно стал своего рода промышленным стандартом. После того как вы подготовили изображение, можно приступать к работе с исходным кодом. Создайте новый проект (у нас это DrawSprite) и скопируйте в него программный код из предыдущего примера пятой главы. Либо можно просто модернизировать предыдущей проект или открыть готовую программу с диска, которая находится в папке Code\Chapter6\DrawSprite. Все изображения игры, а также модели, шрифты, звуковые файлы можно хранить прямо в корневом каталоге проекта, но значительно лучше организовать для этих целей простую и понятную иерархию каталогов. В справочной информации по XNA Game Studio Express предлагается следующая структура папок для хранения в проекте различных компонентов игры. * Папка Content – это большая общая папка, в которой хранится весь так называемый игровой контент, или компоненты игры. Создав такую папкув каталоге проекта, впоследствии в ней вы будете создавать дополнительные папки для различных файлов, например графических изображений, моделей, звуковых файлов, шрифтов и т. д. * Папка Textures – это вложенная папка в папке Content, которая содержит все графические изображения, будь то спрайты, текстуры, игровые карты и т. д. * Папка Models – это также вложенная подпапка в папке Content, где содержатся все трехмерные модели проекта. Дополнительно если при создании модели текстурная составляющая этой модели была прописана под один каталог модели, то можно хранить необходимые текстуры прямо в этой папке. К этим двум подпапкам папки Content можно добавлять любое количество папок для хранения, например, звуковых файлов, шрифтов, эффектов вплоть до создания папок для каждого уровня или нескольких однотипных файлов исходного кода. Конечно, на самом деле все эти разделения по папкам – дело личных предпочтений, и никто вам не мешает хранить весь контент игры в корневом каталоге, но в книге используется именно такая модель разделения файлов по папкам. Для того чтобы создать структуру папок, необходимо в текущем проекте, в панели Solution Explorer щелкнуть правой кнопкой мыши на названии проекта и в контекстном меню выбрать команды Add -> New Folder. По выполнении этой команды в каталоге проекта сформируется новая папка, которой необходимо дать название Content. Затем в этой папке создайте еще одну папку, но уже с названием Textures, где у нас и будет находиться графический файл sprite.png (рис. 6.3).
Рис. 6.3. Папка Content
Чтобы работать с изображениями в проектах, необходимо эти изображения добавить в ваш проект. При этом простое добавление спрайта в каталог с программой никаких результатов не даст, необходимо явно добавить спрайт в проект. Для явного добавления файлов в текущий проект в панели Solution Explorer щелкните правой кнопкой мыши на названии папки Textures и в контекстном меню выберите команды Add -> Exiting Item. Откроется диалоговое окно Add Exiting Item (рис. 6.4). В этом окне в списке Files of Types нужно выбрать вид файла, который требуется добавить в проект. Все графические изображения и модели принадлежат к типу Content Pipeline. Выберите этот тип, найдите спрайт, где бы он у вас на компьютере ни находился, выделите его курсором и нажмите кнопку Add. Вне зависимости от того, в какой директории лежал графический файл, Visual C# Express скопирует его в выбранную папку проекта.
Рис. 6.4. Добавляем файл в проект
В нашем проекте изображение sprite.png представляет собой девушку, которая падает, а точнее будет падать сверху вниз (рис. 6.5). Рисунок был нарисован специально под нашу задачу, поэтому девушка представлена в такой необычной позе. Изображение девушки заключено в прямоугольник, но при этом белый фон рисунка вырезан средствами Photoshop. Сейчас изображение статично и состоит из одного фрейма, или одного кадра, но в следующей главе мы дорисуем еще несколько фреймов для создания иллюзии анимации.
Рис. 6.5. Статичное изображение девушки
Добавив спрайт в проект, можно приступать к работе над исходным кодом класса Game1. В глобальных переменных к уже имеющимся объявлениям добавляем следующие три строки кода: Texture2D spriteTexture; Vector2 spritePosition; SpriteBatch spriteBatch; В первой строке кода объявляется объект spriteTexture класса Texture2D. Это системный класс, позволяющий работать с двухмерными текстурами. Объект этого класса spriteTexture как раз и будет представлять или содержать графический файл sprite.png. Объект spriteTexture отвечает только за загрузку в программу изображений, а с помощью объекта spriteBatch класса SpriteBatch происходит рисование спрайтов на экране. Во второй строке кода этого блока объявляется структурная переменная spritePosition структуры Vector2. Эта структура дает возможность задавать сразу две координаты по осям X и Y. С помощью переменной spritePosition мы сможем задать точку вывода спрайта на экран. Платформа XNA также имеет в своем составе еще структуры Vector3 и Vector4 для работы с тремя и четырьмя координатами соответственно (X, Y, Z и W). В этом случае ось Z представляет буфер глубины, а величина W – это коэффициент перспективы. В последней строке этого блока создается еще один объект spriteBatch класса SpriteBatch. Класс SpriteBatch имеет в своем составе сервисы, необходимые для вывода или рисования графических изображений на экране. Далее в методе LoadGraphicsContent() создадим, или инициализируем (кто к чему привык), объект spriteBatch и загрузим изображение девушки в программу. protected override void LoadGraphicsContent(bool loadAllContent) { if(loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); spriteTexture = content.Load(«Content\\Textures\\sprite»); } } В этом примере создание объекта spriteBatch происходит типичным, или стандартным, образом. Эта конструкция кода имеет место во всех программах. В следующей строке кода метода LoadGraphicsContent() происходит загрузка изображения sprite.png в программу. Для этих целей используется метод Load(), а в качестве его параметра указывается полный путь к графическому изображению. В этой записи два слэша обозначают одну из папок в рабочем каталоге проекта. Если вы хотите загрузить текстуру, например, из корневого каталога, то достаточно указать просто имя файла без слэшей, если, конечно, ваш файл действительно находится в корневом каталоге проекта. Загрузка графического файла в программу происходит посредством сервисов XNA Content Pipeline. В строке загрузки графического файла указывается только название файла без его расширения. Загрузчику XNA Content Pipeline абсолютно все равно, в каком формате выполнено изображение, главное, чтобы это были форматы, с которыми он умеет работать (BMP, JPG, PNG, TGA и DDS), а все остальное – дело его техники. После компиляции приложения XNA Content Pipeline преобразует все графические ресурсы игры в свой специфический формат XNB, и программа будет читать уже эти преобразованные файлы. Далее в методе Initialize() мы зададим две координаты по осям X и Y для вывода спрайта на экран. protected override void Initialize() { spritePosition.X = 300; spritePosition.Y = 200; /* можно задать также вывод изображения в центре экрана: * spritePosition.X = graphics.PreferredBackBufferWidth / 2; * spritePosition.Y = graphics.PreferredBackBufferHeight / 2; */ base.Initialize(); } Начальная точка координат для спрайта – это его верхний левый угол. Переменная spritePosition представляет собой вектор, который позволяет задавать координаты по двум осям. Обращение к осям X и Y происходит посредством оператора точка. Дополнительно можно использовать и другую конструкцию кода с явным созданием объекта spritePosition следующим образом (в центре экрана). spritePosition = new Vector2(graphics.PreferredBackBufferWidth / 2, graphics.PreferredBackBufferHeight / 2); Эта запись идентична предыдущей записи, а деление этих значений на 2 позволяет найти центр экрана (но не центр спрайта). В ином случае можно было записать следующие строки: spritePosition.X = 512; // 1280/2 spritePosition.Y = 384; // 720/2 Если не определять координаты для вывода спрайта на экран, то по умолчанию значение положения спрайта задается двумя нулями, то есть верхний левый угол экрана. После всех этих действий мы будем иметь загруженный в программу спрайт, а точнее механизм, который будет загружать в программу спрайт и точку вывода спрайта на экране. Теперь нам осталось только нарисовать/вывести/отобразить спрайт на экране. Для этих целей служит метод Draw(), код которого изменяется следующим образом. protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(spriteTexture, spritePosition, Color.White); spriteBatch.End(); base.Draw(gameTime); } Методы Begin() и End() класса SpriteBatch задают начало и окончание представления всей двухмерной сцены на экране телевизора. Между двумя вызовами этих методов необходимо производить вывод, или рисование, всех графических изображений на экране. На каждый вызов метода Begin() должен непременно следовать вызов метода End(). Это обязательное условие! Между вызовами методов Begin() и End() происходит вызов метода Draw() класса SpriteBatch. spriteBatch.Draw(spriteTexture, spritePosition, Color.White); Первым параметром этого метода является спрайт, представленный объектом spriteTexture. Второй параметр задает расположение спрайта на экране по осям X и Y, а последний параметр устанавливает цветовую составляющую спрайта, или то, каким цветом будет закрашен спрайт. Сейчас используется белый цвет, что означает рисование спрайта в оригинале без какой-либо дополнительной ретуши цветом. В этом параметре для окраски спрайтов можно использовать любые доступные цвета. После компиляции и запуска проекта DrawSprite на экране телевизора отобразится графическое изображение. В листинге 6.1 класса Game1 проекта DrawSprite находится полный исходный код рассматриваемого примера. //========================================================================= /// /// Листинг 6.1 /// Исходный код к книге: /// «Программирование игр для приставки Xbox 360 в XNA Game Studio Express» /// Автор книги: Горнаков С. Г. /// Глава 6 /// Проект: DrawSprite /// Класс: Game1 /// Вывод спрайта на экран телевизора /// //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace DrawSprite { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; Texture2D spriteTexture; Vector2 spritePosition; SpriteBatch spriteBatch; /// /// Конструктор /// public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1280; graphics.PreferredBackBufferHeight = 720; } /// /// Инициализация /// protected override void Initialize() { spritePosition.X = 300; spritePosition.Y = 200; base.Initialize(); } /// /// Загрузка компонентов игры /// protected override void LoadGraphicsContent(bool loadAllContent) { if(loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); spriteTexture = content.Load(«Content\\Textures\\sprite»); } } /// /// Освобождаем ресурсы /// protected override void UnloadGraphicsContent(bool unloadAllContent) { … } /// /// Обновляем состояние игры /// protected override void Update(GameTime gameTime) { … } /// /// Рисуем на экране /// protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(spriteTexture, spritePosition, Color.White); spriteBatch.End(); base.Draw(gameTime); } } } 6.3. Проект DrawSpriteClass В первом проекте, работая с изображением, мы использовали класс Game1, в котором обеспечивалась загрузка спрайта в программу. Такой подход оправдан для небольших демонстрационных примеров, где программный код может составлять несколько десятков строк. В больших играх этот подход не годится, поскольку количество строк исходного кода может составлять несколько тысяч и в итоге можно элементарно запутаться в своем коде, не говоря уже о сторонних программистах. Поэтому перед созданием игры всегда необходимо тщательно продумывать структуру будущих классов. Тем более что язык программирования C# тем и хорош, что он объектно-ориентированный и все его лучшие качества заключены именно в этом стиле программирования. В новом проекте DrawSpriteClass мы создадим дополнительный класс Sprite, который будет отвечать за все спрайты, загружаемые в игру. Далее с каждой новой главой функционал класса Sprite будет пополняться новыми методами, что позволит нам разработать универсальный класс для работы с графическими изображениями, который в дальнейшем вы можете усовершенствовать по своему усмотрению. Проект DrawSpriteClass основан на исходном коде предыдущего проекта DrawSprite, но с некоторыми дополнениями и переработками. Проект будет включать в себя классы Program, Game1 и Sprite. Исходный код класса Sprite находится в отдельном файле Sprite.cs. Этот файл необходимо добавить в проект, а точнее создать в проекте еще один дополнительный класс с названием Sprite. Делается это следующим образом. В открытом проекте в панели Solution Explorer щелкните правой кнопкой мыши на названии проекта и в контекстном меню выберите команды Add -> New Item. В появившемся диалоговом окне Add New Item – DrawSpriteClass выделите курсором мыши шаблон Class и в поле Name задайте имя будущему классу (рис. 6.6). После этого в рабочем каталоге проекта инструментарий Visual C# сформирует новый файл под названием Sprite.cs.
Рис. 6.6. Добавляем в проект новый класс Sprite 6.3.1. Класс Sprite проекта DrawSpriteClass Рассмотрим исходный код класса Sprite (листинг 6.2), а затем перейдем к его подробному анализу. //========================================================================= /// /// Листинг 6.2 /// Исходный код к книге: /// «Программирование игр для приставки Xbox 360 в XNA Game Studio Express» /// Автор книги: Горнаков С. Г. /// Глава 6 /// Проект: DrawSpriteClass /// Класс: Sprite /// Создаем класс Sprite /// //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace DrawSpriteClass { public class Sprite { public Texture2D spriteTexture; public Vector2 spritePosition; /// /// Конструктор /// public Sprite() { } /// /// Загрузка спрайта в игру /// public void Load(ContentManager content, String stringTexture) { spriteTexture = content.Load(stringTexture); } /// /// Рисуем спрайт /// public void DrawSprite(SpriteBatch spriteBatch) { spriteBatch.Draw(spriteTexture, spritePosition, Color.White); } } } В сформированном файле Sprite.cs в области подключения библиотечных классов добавляются дополнительные библиотеки, которых при формировании шаблона Visual C# не было. Затем в исходном коде класса Sprite идет объявление двух объектов. public Texture2D spriteTexture; public Vector2 spritePosition; Первый объект spriteTexture класса Texture2D будет содержать загруженное в программу изображение, а второй объект spritePosition класса Vector2 – задавать местоположение спрайта на экране. Здесь наблюдается прямая аналогия с примером, рассмотренным в предыдущем проекте, но все объявления и действия по загрузке изображений будут происходить непосредственно в классе Sprite. Конструктор класса Sprite остается без реализации. В следующей главе мы будем изучать спрайтовую анимацию и создадим дополнительный конструкто для класса Sprite, направленный на создание объекта с возможностью анимации. В итоге у нас получатся два разных конструктора: один – для создания неанимированных объектов, а другой – для анимированных изображений. Затем в исходном коде файла Sprite.cs следует описание метода Load(), который в качестве параметров принимает объект content класса ContentManager и объект stringTexture класса String. public void Load(ContentManager content, String stringTexture) { spriteTexture = content.Load(stringTexture); } Объект content необходим для вызова метода Load(), а значит, и для возможности загрузки в программу изображения непосредственно через класс Sprite. Второй объект stringTexture будет содержать строку текста, в которой мы будем передавать путь к загружаемому изображению. На этом этапе можно было сразу прописать путь к графическому файлу, но тогда метод Load() можно будет использовать только один раз для загрузки одного жестко заданного изображения. В нашем случае метод Load() универсален и годится для загрузки любого спрайта. Достаточно лишь вызвать этот метод в классе Game1 и в качестве параметров передать объект content и указать путь к спрайту. После метода Load() идет реализация метода DrawSprite(), отвечающего за рисование спрайта на экране. public void DrawSprite(SpriteBatch spriteBatch) { spriteBatch.Draw(spriteTexture, spritePosition, Color.White); } В этом методе происходит вызов системного метода Draw(), а в качестве параметров выступают спрайт, позиция на экране и цветовая составляющая. В сам метод DrawSprite() в качестве параметра передается объект spriteBatch класса SpriteBatch, для возможности вызова метода Draw() непосредственно через класс Sprite. На этом реализация класса Sprite закончена, давайте перейдем к рассмотрению исходного кода класса Game1. 6.3.2. Класс Game1 проекта DrawSpriteClass Исходный код класса Game1 представлен в листинге 6.3. //========================================================================= /// /// Листинг 6.3 /// Исходный код к книге: /// «Программирование игр для приставки Xbox 360 в XNA Game Studio Express» /// Автор книги: Горнаков С. Г. /// Глава 6 /// Проект: DrawSpriteClass /// Класс: Game1 /// Создаем класс Sprite /// //========================================================================= #region Using Statements using System; using System.Collections.Generic; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Audio; using Microsoft.Xna.Framework.Content; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework.Storage; #endregion namespace DrawSpriteClass { public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; ContentManager content; SpriteBatch spriteBatch; Sprite sprite; /// /// Конструктор /// public Game1() { graphics = new GraphicsDeviceManager(this); content = new ContentManager(Services); graphics.PreferredBackBufferWidth = 1280; graphics.PreferredBackBufferHeight = 720; sprite = new Sprite(); } /// /// Инициализация /// protected override void Initialize() { sprite.spritePosition = new Vector2(300, 200); base.Initialize(); } /// /// Загрузка компонентов игры /// protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { Проект DrawSpriteClass spriteBatch = new SpriteBatch(graphics.GraphicsDevice); sprite.Load(content, «Content\\Textures\\sprite»); } } /// /// Освобождаем ресурсы /// protected override void UnloadGraphicsContent(bool unloadAllContent) { … } /// /// Обновляем состояние игры /// protected override void Update(GameTime gameTime) { … } /// /// Рисуем на экране /// protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(SpriteBlendMode.AlphaBlend); sprite.DrawSprite(spriteBatch); spriteBatch.End(); base.Draw(gameTime); } } } Заметьте, что код из предыдущего примера по загрузке спрайта в классе Game1 был удален, потому что загрузка изображения теперь происходит через класс Sprite. В глобальных переменных класса Game1 добавляется объявление нового объекта sprite класса Sprite. Sprite sprite; А в конструкторе класса Game1 происходит создание этого объекта строкой кода. sprite = new Sprite(); Затем в методе LoadGraphicsContent() через объект sprite вызывается метод Load() класса Sprite, который загружает изображение в игру. В качестве второго параметра этого метода передается путь к графическому файлу sprite.png. protected override void LoadGraphicsContent(bool loadAllContent) { if (loadAllContent) { spriteBatch = new SpriteBatch(graphics.GraphicsDevice); sprite.Load(content, «Content\\Textures\\sprite»); } } Потом в методе Initialize() через объект sprite мы обращаемся к объекту spritePosition и задаем начальную точку вывода изображения на экран монитора. protected override void Initialize() { sprite.spritePosition = new Vector2(300, 200); base.Initialize(); } И в конце исходного кода класса Game1 в методе Draw() вызываем методn DrawSprite() класса Sprite для вывода спрайта на экран в заданном месте. sprite.DrawSprite(spriteBatch); На этом этапе это все улучшения и новшества, добавленные в исходный код будущей игры. После запуска игры вы увидите на экране девушку. Рассматривае-мый пример находится на диске в папке Code\Chapter6\DrawSpriteClass. Откомпилируйте и запустите программу на приставке Xbox 360, а также поэкспериментируйте с выбором различных значений для задания позиций спрайта на экране телевизора или монитора. Далее >>