Разработка компьютерных игр для Windows Phone 7 с использованием технологий Silverlight и XNA

2D-графика в XNA

Разбить на страницы
Показывать лекцию целиком

Цель работы: Освоить основные концепции и приемы работы с двумерными изображениями в XNA

8.1. Система координат

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

(рис 8.1) Традиционная система координат

В данной системе координат начало координат расположено в левом нижнем углу системы – именно здесь находится точка 0,0. Координаты по оси Х возрастают слева направо, по оси Y – снизу вверх.

При разработке компьютерных игр используется экранная система координат. Главное отличие этой системы от традиционной заключается в том, что начало координат расположено в левом верхнем углу экрана. Координаты по оси Х возрастают слева направо, а вот ось Y оказывается перевернутой – ее координаты возрастают сверху вниз. На рис. 8.2 вы можете видеть эту систему координат, в которой нарисован тот же треугольник.

(рис 8.2) Экранная система координат

Границы экранной системы координат – это разрешение экрана устройства. Экран смартфона при разработке для XNA, имеет разрешение 800х480 – это разрешение на данный момент является основным, однако, платформа Windows Phone предусматривает и другие разрешения. Экран может быть развернут либо в альбомном режиме (800х480), либо в портретном (480х800).

8.2. Вывод двумерного изображения на экран

Прежде чем заниматься работой с изображениями в XNA, эти изображения, для начала, нужно создать. Создадим простое изображение в стандартном графическом редакторе Windows – Paint. Для запуска редактора воспользуемся командой Пуск > Все программы > Стандартные > Paint. Нарисуем в редакторе прямоугольник (рис. 8.3), сохраним его под именем P3_1.png. Как следует из расширения, мы сохранили рисунок в формате PNG.

(рис 8.3) Создание простого изображения в редакторе Paint

Создадим новый стандартный игровой проект с именем P3_1. В панели Обозреватель решений щелкнем правой кнопкой мыши по проекту P3_1Content (Content) и в появившемся контекстном меню выберем пункт Добавить > Существующий элемент. Появится стандартное окно для работы с файлами. Найдем с его помощью графический файл и добавим в проект. В результате панель Обозреватель решений должна выглядеть так, как изображено на рис. 8.4.

(рис 8.4) Изображение, добавленное в проект

Обратите внимание на то, что при добавлении изображения в проект, оно физически копируется в папку Content, которая находится в папке, содержащей файлы проекта.

Файл, добавленный в проект, имеет то же самое имя, которое он имел при сохранении. Выделив этот файл, мы можем просмотреть его свойства – в окне Свойства. Это окно можно видеть на том же рис. 8.4. В данном случае обратите внимание на параметр Имя актива – здесь мы можем видеть имя изображения в проекте. Как и можно ожидать, оно носит имя P3_1.

Теперь выполним действия, необходимые для вывода нашего изображения на игровой экран. Для этого в раздел объявления свойств класса добавим две переменные (листинг 8.1.).

private Texture2D MySprite;
private Vector2 position = new Vector2(10,10);

Первая переменная – MySprite – имеет тип Texture2D. Этот тип данных предназначен для хранения двумерных изображений, или текстур. Именно в эту переменную мы будем загружать изображение, которое ранее добавили в проект.

Вторая переменная - position – нужна для хранения позиции, в которую наша текстура будет выведена на экран. Мы сразу же инициализируем эту переменную значениями 10,10 – это координаты вывода изображения. Координаты привязаны к левому верхнему углу экрана. То есть – именно его левый верхний угол будет находиться в позиции 150,200 при выводе. Если бы мы инициализировали эту переменную значениями 0,0, изображение было бы выведено в начале координат – то есть – его верхний угол находился бы в позиции 0,0, а само оно располагалось бы в верхнем левом углу игрового поля.

Обратите внимание, без дополнительных настроек речь идет о ландшафтной ориентации экрана.

Теперь загрузим текстуру в переменную MySprite. Для этого модифицируем метод LoadContent следующим образом (листинге 8.2.).

        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
            MySprite = Content.Load<Texture2D>("P3_1");
        }

Здесь, для наглядности, удалены стандартные комментарии. Итак, для загрузки мы пользуемся методом Load объекта Content. Причем, обратите внимание на то, что метод Load вызывается с указанием типа загружаемого игрового ресурса. В нашем случае это – двумерное изображение, поэтому тип ресурса указывается как Texture2D. Несложно заметить, что этот тип соответствует типу переменной MySprite.

Теперь настала очередь вывода изображения на игровой экран. Для этого нам понадобится модифицировать метод Draw. В листинге 8.3 приведен его код.

protected override void Draw(GameTime gameTime)
        {
        graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
        
        spriteBatch.Begin();
        spriteBatch.Draw(MySprite, position, Color.White);
        spriteBatch.End();

        base.Draw(gameTime);
        }

Метод Begin() объекта spriteBatch подготавливает графическое устройство к выводу изображения. Метод Draw того же объекта принимает в качестве параметров переменную типа Texture2D (MySprite), переменную типа Vector2 (position) и цвет, в соответствии с которым будет изменен оттенок изображения. В данном случае это белый цвет – это значит, что цвета изображения останутся неизменными. Метод End() завершает процедуру вывода – изображения выводятся на экран.

Метод Draw базового класса в данном случае не используется для вывода изображений. Мы рассмотрим его роль в выводе изображений ниже.

Запустив программу в эмуляторе, мы получим такое изображение (рис. 8.5.):

(рис 8.5) Запуск программы в эмуляторе

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

(рис 8.6) Поворот экрана эмулятора

Похожим образом выглядит запуск нашей программы на устройстве, однако, здесь нужно учитывать то, что по умолчанию на экране отображается строка состояния. В итоге, мы работаем с тем же разрешением, но окно вывода занимает не весь экран, масштабируясь для сохранения пропорций, рис. 8.7. Строку состояния можно скрыть.

(рис 8.7) Запуск программы на устройстве

Обратите внимание на то, что экран отображается правильно (то есть – начало системы координат совпадает с левым верхним углом экрана устройства) при любом расположении устройства (то есть – и при повороте по часовой стрелки относительно вертикального расположения экрана, и при повороте против часовой стрелки) – таким образом устройство самостоятельно разворачивает экран в соответствии с настройками по умолчанию.

Мы рассмотрели простейший способ вывода изображения. Однако использование такого способа вывода изображений при разработке реальных игровых проектов не слишком удобно. Дело в том, что игровой объект, который стоит за каждым изображением, может иметь множество свойств. Например – скорость и направление движения. Так же количество объектов в игре может меняться. К тому же, не очень удобно хранить множество свойств различных игровых объектов, используя область объявления свойств основного игрового проекта. Удобнее было бы выделить данные об игровых объектах в отдельный класс и пользоваться объектами, сгенерированными на основе этого класса для вывода изображений и организации игровой логики. Собственно говоря, на практике так и поступают, создавая отдельные игровые компоненты (классы) для представления игровых объектов. Как правило, в реальных проектах создается целая система классов игровых объектов, связанных отношениями наследования.

Способ, описанный выше, может использоваться для вывода некоторых объектов, например – так можно вывести фоновый рисунок.

Рассмотрим подход, который позволяет упростить подход к выводу изображений методом, описанным выше.

8.3. Разработка класса для хранения графической информации

Создадим новый стандартный проект XNA-игры с именем P3_2. Добавим в проект изображение, которое мы создали выше (P3_1.png).

Теперь подумаем, каким должен быть класс >для представления игрового объекта. Сейчас нам нужен класс, объект которого сможет хранить текстуру спрайта и информацию о его позиции на экране. При необходимости этот список можно расширить – причем – как свойствами – так и методами. Но над подобным проектом мы будем работать на одном из следующих занятий. Ограничимся пока двумя вышеназванными свойствами.

Щелкнем по значку проекта P3_2 в панели Обозреватель проектов и выберем в появившемся меню пункт Добавить > Класс. Появится окно добавления нового элемента, в котором будет выделен шаблон пустого класса (рис. 8.8).

(рис 8.8) Добавляем в проект новый класс

Зададим имя новому классу – пусть это будет spriteClass, и нажмем кнопку Добавить. В проект будет добавлен новый пустой класс. Вот как выглядит наш проект после всех перечисленных действий (рис. 8.9).

(рис 8.9) Внешний вид окна проекта после добавления нового класса

Модифицируем код класса spriteClass следующим образом.

Во-первых, в раздел подключения пространств имен добавим директивы using, содержащие те пространства имен, которые содержат объявления типов данных, необходимых для хранения текстуры и ее позиции.

Во-вторых – создадим необходимые свойства класса.

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

Вот как выглядит код класса после всех модификаций (листинг 8.4.):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;


namespace P3_2
{
    class spriteClass
    {
        public Texture2D spTexture;
        public Vector2 spPosition;

        public spriteClass(Texture2D newSpTexture, Vector2 newSpPosition)
        {
            spTexture = newSpTexture;
            spPosition = newSpPosition;
        }

    }
}

Здесь мы подключили к нашему классу пространство имен Microsoft.Xna.Framework.Graphics – оно нужно нам, так как содержит объявление типа данных Texture2D. Пространство имен Microsoft.Xna.Framework содержит объявление типа данных Vector2.

В раздел объявления свойств класса мы добавили две переменных. Переменная spTexture типа Texture2D используется для хранения текстуры, переменная spPosition типа Vector2 – для хранения данных о положении объекта на экране. В конструкторе класса мы инициализируем эти переменные значениями соответствующих типов, переданными при создании объекта типа spriteClass в основном тексте программы.

Свойства класса объявлены с модификаторами доступности public. Это значит, что мы сможем модифицировать эти свойства извне. В данном случае такой подход оправдан. Однако надо отметить, что при конструировании классов рекомендуется защищать свойства от прямой модификации или чтения. Для организации доступа к таким свойствам можно создавать дополнительные свойства-методы, пользуясь директивами get/set.

Теперь внесем изменения в основной текст игры, который расположен в файле Game1.cs. В листинге 8.5 приведены эти изменения с комментариями, указывающими на их местонахождение.

/// Объект mySpriteObj для хранения данных об изображении для вывода на экран
spriteClass mySpriteObj;
/// Добавляем в метод LoadContent команду создания нового объекта типа spriteClass. 
Передаем в качестве текстуры изображения текстуру, загруженную с помощью команды Content.Load, 
в качестве позиции – новый объект типа Vector2
mySpriteObj = new spriteClass(Content.Load<Texture2D>("P3_1"), new Vector2(10f, 10f));
/// В метод Draw добавляем соответствующие вызовы методов объекта spriteBatch. 
Обратите внимание на то, что при вызове метода Draw мы пользуемся свойствами объекта mySpriteObj.
spriteBatch.Begin();
spriteBatch.Draw(mySpriteObj.spTexture, mySpriteObj.spPosition, Color.White);
spriteBatch.End();
/// В метод UnloadContent добавляем команды для освобождения системных ресурсов.
mySpriteObj.spTexture.Dispose();
spriteBatch.Dispose();

При запуске данного примера мы получим тот же графический вывод, который получили при проверке предыдущего.

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

Однако и этот подход не является единственно возможным. Еще более интересный способ вывода изображений и создания игровых объектов – это создание игровых объектов на базе игровых компонентов.

8.4. Разработка игрового компонента

Создадим новый игровой проект, назовем его P3_3. Щелкнем правой кнопкой мыши по значку проекта в панели Обозреватель проектов, выберем пункт Добавить > Создать элемент. В появившемся окне выберем миниатюру Игровой компонент, зададим имя для компонента – spriteComp и нажмем на кнопку Добавить. В проект будет добавлен новый игровой компонент. В листинге 8.6 вы можете видеть полный код этого компонента.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;


namespace P3_3
{
    /// <summary>
    /// Это игровой компонент, реализующий интерфейс IUpdateable.
    /// </summary>
    public class spriteComp : Microsoft.Xna.Framework.GameComponent
    {
        public spriteComp(Game game)
            : base(game)
        {
            // ЗАДАЧА: здесь создаются дочерние компоненты
        }

        /// <summary>
        /// Позволяет игровому компоненту выполнить необходимую инициализацию перед\r\запуском. 
         Здесь можно запросить нужные службы и загрузить контент.
        /// 
        /// </summary>
        public override void Initialize()
        {
            // ЗАДАЧА: добавьте здесь код инициализации

            base.Initialize();
        }

        /// <summary>
        /// Позволяет игровому компоненту обновиться.
        /// </summary>
        /// >param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
        public override void Update(GameTime gameTime)
        {
            // ЗАДАЧА: добавьте здесь код обновления

            base.Update(gameTime);
        }
    }
}

Можно отметить, что игровой компонент представляет собой класс, который наследует класс Microsoft.Xna.Framework.GameComponent. Наследование класса GameComponent подходит для создания игровых компонентов, которые не имеют графических представлений. Мы же хотим создать компонент, который планируется визуализировать. Поэтому нужно заменить GameComponent на DrawableGameComponent.

Обратите внимание на то, что созданный компонент имеет несколько методов. Так, метод spriteComp – это конструктор класса, метод Initialise предназначен для инициализации компонента, метод Update – это игровой цикл компонента.

Здесь не хватает еще одного метода – метода Draw. Добавим этот метод в класс (после ввода текста public override void нам автоматически предложат список, из которого достаточно выбрать Draw и нажать Enter – набор входных переменных и структура метода будет создана автоматически. В листинге 8.7 представлен измененный код компонента, готовый для дальнейшей работы.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;


namespace P3_3
{
    /// <summary>
    /// Это игровой компонент, реализующий интерфейс IUpdateable.
    /// </summary>
    public class spriteComp : Microsoft.Xna.Framework.DrawableGameComponent
    {
        public spriteComp(Game game)
            : base(game)
        {
            // ЗАДАЧА: здесь создаются дочерние компоненты
        }

        /// <summary>
        /// Позволяет игровому компоненту выполнить необходимую инициализацию перед\r\запуском.  
        Здесь можно запросить нужные службы и загрузить контент.
        /// 
        /// </summary>
        public override void Initialize()
        {
            // ЗАДАЧА: добавьте здесь код инициализации

            base.Initialize();
        }

        /// <summary>
        /// Позволяет игровому компоненту обновиться.
        /// </summary>
        /// >param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
        public override void Update(GameTime gameTime)
        {
            // ЗАДАЧА: добавьте здесь код обновления

            base.Update(gameTime);
        }
        public override void Draw(GameTime gameTime)
        {
            base.Draw(gameTime);
        }
    }
}

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

Создадим новый графический файл. Воспользуемся для этого графическим редактором Adobe Photoshop CS. На рис. 8.10 вы можете видеть изображение в окне редактора. Обратите внимание на то, что один графический файл содержит несколько изображений, а так же на то, что фон сделан прозрачным – такая техника позволяет выводить в игровое окно рисунки сложной формы.

(рис 8.10) Создание изображений в Adobe Photoshop

Здесь мы создали три изображения.

Сохраним изображение в виде PNG-файла. Такие файлы поддерживают прозрачность фона. Теперь нам нужно решить еще одну задачу – определить координаты отдельных спрайтов в нашем большом изображении. Это можно сделать следующим образом. Откроем сохраненное изображение в каком-нибудь графическом редакторе, можно – в том же Photoshop CS – мы рассмотрим вопрос определения координат именно для этой программы.

Увеличим масштаб изображения, переключимся на панель Info, и, наведя мышь на пиксель левого верхнего угла изображения, запишем отобразившиеся координаты указателя (рис. 8.11). В нашем случае это (18, 9). Обратите внимание на то, что нас интересует координата, выраженная в пикселях, для правильного определения координаты в данном случае нам нужно настроить параметр Ruler Units (окно Info Options, где производится настройка, можно найти в меню палитры) на измерение в пикселях (Pixels).

(рис 8.11) Определение координат объектов в Photoshop

Точно так же узнаем координату правого нижнего угла изображения. В нашем случае это – (35, 97). Для дальнейшей работы нам понадобится знание координаты левой верхней точки прямоугольника, ограничивающего спрайт, а так же – его ширина и высота. В нашем случае ширина составляет 17 пикселей (35– 18), высота – 88 пикселей (97 – 9). В итоге получаем следующие параметры прямоугольника, в который заключен наш спрайт:

Х: 18
Y: 9
Width: 17
Height: 88

Ниже эти данные понадобятся нам.

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

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;


namespace P3_3
{
    /// <summary>
    /// Это игровой компонент, реализующий интерфейс IUpdateable.
    /// </summary>
    public class spriteComp : Microsoft.Xna.Framework.DrawableGameComponent
    {
        private Texture2D sprTexture;
        private Rectangle sprRectangle;
        private Vector2 sprPosition;

        public spriteComp(Game game, ref Texture2D newTexture, 
            Rectangle newRectangle, Vector2 newPosition
)
            : base(game)
        {
            sprTexture = newTexture;
            sprRectangle = newRectangle;
            sprPosition = newPosition;

            // ЗАДАЧА: здесь создаются дочерние компоненты
        }

        /// <summary>
        /// Позволяет игровому компоненту выполнить необходимую инициализацию перед\r\запуском.  
        Здесь можно запросить нужные службы и загрузить контент.
        /// 
        /// </summary>
        public override void Initialize()
        {
            // ЗАДАЧА: добавьте здесь код инициализации

            base.Initialize();
        }

        /// <summary>
        /// Позволяет игровому компоненту обновиться.
        /// </summary>
        /// >param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
        public override void Update(GameTime gameTime)
        {
            // ЗАДАЧА: добавьте здесь код обновления

            base.Update(gameTime);
        }
        public override void Draw(GameTime gameTime)
        {
            SpriteBatch sprBatch =
                (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));

            sprBatch.Draw(sprTexture, sprPosition, sprRectangle, Color.White);

            base.Draw(gameTime);
        }
    }
}

Рассмотрим изменения, внесенные в компонент.

Мы добавили три переменных – sprTexture, sprRectangle, sprPosition – для хранения текстуры, прямоугольника, показывающего координаты спрайта в текстуре, и позиции вывода спрайта на экран.

Далее, мы модифицировали конструктор класса, добавив в список передаваемых параметров следующие переменные:

ref Texture2D newTexture – передача текстуры. Параметр передается по ссылке, то есть объект получает не копию текстуры, а лишь ссылку на нее – это экономит системные ресурсы.

Rectangle newRectangle – прямоугольник, задающий координаты спрайта в изображении.

Vector2 newPosition – позиция спрайта на игровом экране.

В теле конструктора мы инициализируем соответствующие свойства класса переданными параметрами.

Далее, мы дорабатываем код метода Draw. Здесь нам предстоит наиболее сложная часть модификации. Дело в том, в игре, для которой мы создаем компонент, уже есть все средства для вывода изображений. Поэтому разумнее всего воспользоваться уже существующим объектом типа SpriteBatch для того, чтобы с его помощью вывести изображение на экран. Для того, чтобы сделать однажды созданный SpriteBatch доступным для всех компонентов игры, существует специальная методика. Есть такое понятие, как игровые сервисы, которые доступны всем компонентам. Сначала мы добавим созданный в классе Game1 объект типа SpriteBatch в список игровых сервисов (мы рассмотрим это ниже), после чего сможем извлечь этот объект в игровом компоненте. Мы так и поступаем – следующая команда создает новый объект sprBatch, который содержит ссылку на исходный объект типа SpriteBatch, который ранее, в классе Game1, добавлен в список игровых ресурсов.

SpriteBatch sprBatch =  (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));

После того, как у нас появилась ссылка на исходный объект типа SpriteBatch, мы можем вывести изображение, используя перегруженный метод Draw такого объекта. Ранее мы пользовались этим методом, передавая ему текстуру, позицию текстуры в игровом окне и оттенок текстуры. Теперь же к списку передаваемых ему параметров добавляется прямоугольник, задающий позицию спрайта в файле текстуры. Этот вызов выглядит так:

sprBatch.Draw(sprTexture, sprPosition, sprRectangle, Color.White);

Выше мы выводили спрайт на экран, сначала вызвав метод Begin() объекта типа SpriteBatch, потом – вызывали метод Draw(), потом – метод End(), завершающий вывод. Здесь мы не вызываем этих методов. Дело в том, что данные методы будут вызваны в методе Draw(), который вызывает основная игра (объект типа Game1). В код класса Game1 внесены некоторые изменения, которые мы сейчас рассмотрим. В листинге 8.9 вы можете видеть этот код.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Input.Touch;
using Microsoft.Xna.Framework.Media;

namespace P3_3
{
    /// <summary>
    /// Это главный тип игры
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        spriteComp gameObject;
        Texture2D texture;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            // Частота кадра на Windows Phone по умолчанию — 30 кадров в секунду.
            TargetElapsedTime = TimeSpan.FromTicks(333333);

            // Дополнительный заряд аккумулятора заблокирован.
            InactiveSleepTime = TimeSpan.FromSeconds(1);
        }

        /// <summary>
        /// Позволяет игре выполнить инициализацию, необходимую перед запуском.
        /// Здесь можно запросить нужные службы и загрузить неграфический
        /// контент.  Вызов base.Initialize приведет к перебору всех компонентов и
        /// их инициализации.
        /// </summary>
        protected override void Initialize()
        {
            // ЗАДАЧА: добавьте здесь логику инициализации

            base.Initialize();
        }

        /// <summary>
        /// LoadContent будет вызываться в игре один раз; здесь загружается
        /// весь контент.
        /// </summary>
        protected override void LoadContent()
        {
            // Создайте новый SpriteBatch, который можно использовать для отрисовки текстур.
            spriteBatch = new SpriteBatch(GraphicsDevice);
            Services.AddService(typeof(SpriteBatch), spriteBatch);
            texture = Content.Load<Texture2D>("BallandBats");
            CreateNewObject();

            // ЗАДАЧА: используйте здесь this.Content для загрузки контента игры
        }

        protected void CreateNewObject()
        {
            gameObject = new spriteComp(this, ref texture,
                new Rectangle(18, 9, 17, 88), new Vector2(100, 150));
            Components.Add(gameObject);
        }


        /// <summary>
        /// UnloadContent будет вызываться в игре один раз; здесь выгружается
        /// весь контент.
        /// </summary>
        protected override void UnloadContent()
        {
            // ЗАДАЧА: выгрузите здесь весь контент, не относящийся к ContentManager
        }

        /// <summary>
        /// Позволяет игре запускать логику обновления мира,
        /// проверки столкновений, получения ввода и воспроизведения звуков.
        /// </summary>
        /// >param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
        protected override void Update(GameTime gameTime)
        {
            // Позволяет выйти из игры
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            // ЗАДАЧА: добавьте здесь логику обновления

            base.Update(gameTime);
        }

        /// <summary>
        /// Вызывается, когда игра отрисовывается.
        /// </summary>
        /// >param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // ЗАДАЧА: добавьте здесь код отрисовки
            spriteBatch.Begin();
            base.Draw(gameTime);
            spriteBatch.End();
        }
    }
}

Для начала мы добавили новые переменные, одна из которых (типа spriteComp) служит для хранения объекта класса spriteComp, а вторая (типа Texture2D) служит для хранения текстуры. Так как текстура содержит несколько спрайтов, переменная Texture2D может понадобиться при создании нескольких объектов, поэтому имеет смысл создать эту переменную, один раз загрузить в нее изображение и пользоваться ей.

В методе LoadContent() мы, как обычно, создаем объект типа SpriteBatch, и, с помощью команды Services.AddService(typeof(SpriteBatch), spriteBatch); добавляем объект SpriteBatch к списку игровых сервисов. Именно после этой команды spriteBatch можно вызывать в игровых компонентах и выводить их изображений.

Далее в этом методе мы загружаем текстуру с изображениями в переменную texture и вызываем метод CreateNewObject();. Этот метод мы создали самостоятельно – мы используем его для создания объекта gameObject.

gameObject  = new spriteComp (this, ref texture, new Rectangle (18,9,17,88), new Vector2 (100,150));

При создании этого объекта мы передаем конструктору класса spriteComp объект игры (this), ссылку на текстуру, объект Rectangle – обратите внимание на то, что мы создаем новый объект Rectangle, который инициализирован значениями координаты нужного нам спрайта в файле текстуры, вычисленными ранее. Так же мы передаем конструктору новый объект типа Vector2, который содержит координаты спрайта для вывода на игровой экран.

В методе CreateNewObject() мы разместили еще одну команду – Components.Add(gameObject );. С ее помощью мы добавляем в список компонентов игры только что созданный объект. Это очень важная команда – благодаря ей при исполнении команды base.Draw(gameTime); будет обработан метод Draw нашего игрового компонента, и изображения, которые он выведет, будут выведены на экран.

В методе Draw() класса Game1 мы используем такой код:

spriteBatch.Begin();
base.Draw(gameTime);
spriteBatch.End();

Команда spriteBatch.Begin(); готовит графическое устройтство для вывода изображений, после этого осуществляется вывод изображений – по вызову base.Draw(gameTime); обрабатываются соответствующие методы игровых компонентов, унаследованных от DrawableGameComponent. Напомню, что в методе Draw() нашего компонента spriteComp есть лишь вызов метода Draw() объекта класса SpriteBatch, без вызовов Begin() и End(). В результате, после того, как компонент выведет изображение, вывод завершается командой End() объекта spriteBatch в методе Draw() класса Game1 и изображение появляется на экране.

Такой механизм работы позволяет создавать множество компонентов, добавлять их в список компонентов игры и не заботиться о выводе каждого из них в методе Draw() класса Game1. После того, как компонент добавлен в список компонентов игры, вывод его графического представления осуществляется автоматически. Можно сказать, что метод base.Draw(gameTime); "просматривает" все зарегистрированные компоненты и выводит их на экран.

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

Вот как выглядит игровой экран после вывода в него изображения (рис. 8.12.

(рис 8.12) Игровой экран после вывода изображения

Здесь мы рассмотрели различные подходы к выводу изображений. Однако можно заметить, что вывод изображения, которое ассоциируется с игровым объектом – это лишь одна из задач, которую приходится решать при разработке игры. Другие задачи – например – перемещение объекта, проверка, не столкнулся ли объект с другими объектами – так же можно решать с использованием различных подходов. Можно заметить, что наиболее сложный с точки зрения реализации подход – разработка отдельного игрового компонента, является одновременно и наиболее удобным для реализации различных возможностей игрового объекта. Позже, дорабатывая функциональность игровых объектов, мы будем пользоваться именно этим подходом.

8.5. Вывод нескольких изображений

Рассмотрим вывод нескольких изображений в игровое окно. Как мы уже говорили, с использованием зарегистрированных игровых компонентов, унаследованных от DrawableGameComponent, вывод автоматизирован. Однако это не значит, что использовать более простые методы вывода не нужно.

Например, рассмотрим вывод фонового изображения в уже созданном проекте P2_3. Выше мы создали изображения, которые можно использовать для создания собственной версии игры Pong. Точно так же создадим изображение, которое будет служить фоном для игры. Стандартный игровой экран имеет разрешение 800х480 пикселей. Создадим изображение такого размера (назовем его background.png), загрузим его в проект. Для этого добавим в класс новую переменную

Texture2D backTexture;

После этого, в методе LoadContent(), загрузим изображение в эту переменную командой

backTexture = Content.Load<Texture2D>("Background");

Теперь выведем изображение с помощью метода Draw() объекта spriteBatch.

spriteBatch.Draw(backTexture, new Rectangle(0, 0, 800, 480), Color.White);

Поместим эту команду между вызовами Begin() и End() перед командой base.Draw(gameTime);

В результате окно игры приобретет такой вид (рис. 8.13).

(рис 8.13) Игровой экран после вывода изображения игрового объекта и фона

Как видите, спрайт игрового объекта выведен поверх фонового изображения.

Подход, требующий соблюдения очередности вывода изображений для регулирования их наложения, не очень удобен. Однако XNA Framework предусматривает специальные инструменты для настройки взаимного расположения объектов. В частности, при выводе спрайта можно указывать "глубину" (depth), на которой он расположен.

8.6. Выводы

В данной лабораторной работе мы рассмотрели основы работы с графикой в XNA-играх для Windows Phone 7

8.7. Задание

Нарисуйте несколько изображений в любом известном вам графическом редакторе. Выведите эти изображения в игровое окно XNA, пользуясь методикой, изложенной в пункте о выводе нескольких изображений на экран. Если вы разрабатываете двумерную игру, займитесь подготовкой изображений для нее, проверьте, как выбранные изображения выглядят при выводе их на экран эмулятора или телефона.

8.8. Дополнительные материалы

К данной лабораторной работе прилагается видеоурок

и демонстрационный проект.

Страницы:

Цель работы: Освоить основные концепции и приемы работы с двумерными изображениями в XNA

8.1. Система координат

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

(рис 8.1) Традиционная система координат

В данной системе координат начало координат расположено в левом нижнем углу системы – именно здесь находится точка 0,0. Координаты по оси Х возрастают слева направо, по оси Y – снизу вверх.

При разработке компьютерных игр используется экранная система координат. Главное отличие этой системы от традиционной заключается в том, что начало координат расположено в левом верхнем углу экрана. Координаты по оси Х возрастают слева направо, а вот ось Y оказывается перевернутой – ее координаты возрастают сверху вниз. На рис. 8.2 вы можете видеть эту систему координат, в которой нарисован тот же треугольник.

(рис 8.2) Экранная система координат

Границы экранной системы координат – это разрешение экрана устройства. Экран смартфона при разработке для XNA, имеет разрешение 800х480 – это разрешение на данный момент является основным, однако, платформа Windows Phone предусматривает и другие разрешения. Экран может быть развернут либо в альбомном режиме (800х480), либо в портретном (480х800).

8.2. Вывод двумерного изображения на экран

Прежде чем заниматься работой с изображениями в XNA, эти изображения, для начала, нужно создать. Создадим простое изображение в стандартном графическом редакторе Windows – Paint. Для запуска редактора воспользуемся командой Пуск > Все программы > Стандартные > Paint. Нарисуем в редакторе прямоугольник (рис. 8.3), сохраним его под именем P3_1.png. Как следует из расширения, мы сохранили рисунок в формате PNG.

(рис 8.3) Создание простого изображения в редакторе Paint

Создадим новый стандартный игровой проект с именем P3_1. В панели Обозреватель решений щелкнем правой кнопкой мыши по проекту P3_1Content (Content) и в появившемся контекстном меню выберем пункт Добавить > Существующий элемент. Появится стандартное окно для работы с файлами. Найдем с его помощью графический файл и добавим в проект. В результате панель Обозреватель решений должна выглядеть так, как изображено на рис. 8.4.

(рис 8.4) Изображение, добавленное в проект

Обратите внимание на то, что при добавлении изображения в проект, оно физически копируется в папку Content, которая находится в папке, содержащей файлы проекта.

Файл, добавленный в проект, имеет то же самое имя, которое он имел при сохранении. Выделив этот файл, мы можем просмотреть его свойства – в окне Свойства. Это окно можно видеть на том же рис. 8.4. В данном случае обратите внимание на параметр Имя актива – здесь мы можем видеть имя изображения в проекте. Как и можно ожидать, оно носит имя P3_1.

Теперь выполним действия, необходимые для вывода нашего изображения на игровой экран. Для этого в раздел объявления свойств класса добавим две переменные (листинг 8.1.).

private Texture2D MySprite;
private Vector2 position = new Vector2(10,10);

Первая переменная – MySprite – имеет тип Texture2D. Этот тип данных предназначен для хранения двумерных изображений, или текстур. Именно в эту переменную мы будем загружать изображение, которое ранее добавили в проект.

Вторая переменная - position – нужна для хранения позиции, в которую наша текстура будет выведена на экран. Мы сразу же инициализируем эту переменную значениями 10,10 – это координаты вывода изображения. Координаты привязаны к левому верхнему углу экрана. То есть – именно его левый верхний угол будет находиться в позиции 150,200 при выводе. Если бы мы инициализировали эту переменную значениями 0,0, изображение было бы выведено в начале координат – то есть – его верхний угол находился бы в позиции 0,0, а само оно располагалось бы в верхнем левом углу игрового поля.

Обратите внимание, без дополнительных настроек речь идет о ландшафтной ориентации экрана.

Теперь загрузим текстуру в переменную MySprite. Для этого модифицируем метод LoadContent следующим образом (листинге 8.2.).

        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);
            MySprite = Content.Load<Texture2D>("P3_1");
        }

Здесь, для наглядности, удалены стандартные комментарии. Итак, для загрузки мы пользуемся методом Load объекта Content. Причем, обратите внимание на то, что метод Load вызывается с указанием типа загружаемого игрового ресурса. В нашем случае это – двумерное изображение, поэтому тип ресурса указывается как Texture2D. Несложно заметить, что этот тип соответствует типу переменной MySprite.

Теперь настала очередь вывода изображения на игровой экран. Для этого нам понадобится модифицировать метод Draw. В листинге 8.3 приведен его код.

protected override void Draw(GameTime gameTime)
        {
        graphics.GraphicsDevice.Clear(Color.CornflowerBlue);
        
        spriteBatch.Begin();
        spriteBatch.Draw(MySprite, position, Color.White);
        spriteBatch.End();

        base.Draw(gameTime);
        }

Метод Begin() объекта spriteBatch подготавливает графическое устройство к выводу изображения. Метод Draw того же объекта принимает в качестве параметров переменную типа Texture2D (MySprite), переменную типа Vector2 (position) и цвет, в соответствии с которым будет изменен оттенок изображения. В данном случае это белый цвет – это значит, что цвета изображения останутся неизменными. Метод End() завершает процедуру вывода – изображения выводятся на экран.

Метод Draw базового класса в данном случае не используется для вывода изображений. Мы рассмотрим его роль в выводе изображений ниже.

Запустив программу в эмуляторе, мы получим такое изображение (рис. 8.5.):

(рис 8.5) Запуск программы в эмуляторе

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

(рис 8.6) Поворот экрана эмулятора

Похожим образом выглядит запуск нашей программы на устройстве, однако, здесь нужно учитывать то, что по умолчанию на экране отображается строка состояния. В итоге, мы работаем с тем же разрешением, но окно вывода занимает не весь экран, масштабируясь для сохранения пропорций, рис. 8.7. Строку состояния можно скрыть.

(рис 8.7) Запуск программы на устройстве

Обратите внимание на то, что экран отображается правильно (то есть – начало системы координат совпадает с левым верхним углом экрана устройства) при любом расположении устройства (то есть – и при повороте по часовой стрелки относительно вертикального расположения экрана, и при повороте против часовой стрелки) – таким образом устройство самостоятельно разворачивает экран в соответствии с настройками по умолчанию.

Мы рассмотрели простейший способ вывода изображения. Однако использование такого способа вывода изображений при разработке реальных игровых проектов не слишком удобно. Дело в том, что игровой объект, который стоит за каждым изображением, может иметь множество свойств. Например – скорость и направление движения. Так же количество объектов в игре может меняться. К тому же, не очень удобно хранить множество свойств различных игровых объектов, используя область объявления свойств основного игрового проекта. Удобнее было бы выделить данные об игровых объектах в отдельный класс и пользоваться объектами, сгенерированными на основе этого класса для вывода изображений и организации игровой логики. Собственно говоря, на практике так и поступают, создавая отдельные игровые компоненты (классы) для представления игровых объектов. Как правило, в реальных проектах создается целая система классов игровых объектов, связанных отношениями наследования.

Способ, описанный выше, может использоваться для вывода некоторых объектов, например – так можно вывести фоновый рисунок.

Рассмотрим подход, который позволяет упростить подход к выводу изображений методом, описанным выше.

8.3. Разработка класса для хранения графической информации

Создадим новый стандартный проект XNA-игры с именем P3_2. Добавим в проект изображение, которое мы создали выше (P3_1.png).

Теперь подумаем, каким должен быть класс >для представления игрового объекта. Сейчас нам нужен класс, объект которого сможет хранить текстуру спрайта и информацию о его позиции на экране. При необходимости этот список можно расширить – причем – как свойствами – так и методами. Но над подобным проектом мы будем работать на одном из следующих занятий. Ограничимся пока двумя вышеназванными свойствами.

Щелкнем по значку проекта P3_2 в панели Обозреватель проектов и выберем в появившемся меню пункт Добавить > Класс. Появится окно добавления нового элемента, в котором будет выделен шаблон пустого класса (рис. 8.8).

(рис 8.8) Добавляем в проект новый класс

Зададим имя новому классу – пусть это будет spriteClass, и нажмем кнопку Добавить. В проект будет добавлен новый пустой класс. Вот как выглядит наш проект после всех перечисленных действий (рис. 8.9).

(рис 8.9) Внешний вид окна проекта после добавления нового класса

Модифицируем код класса spriteClass следующим образом.

Во-первых, в раздел подключения пространств имен добавим директивы using, содержащие те пространства имен, которые содержат объявления типов данных, необходимых для хранения текстуры и ее позиции.

Во-вторых – создадим необходимые свойства класса.

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

Вот как выглядит код класса после всех модификаций (листинг 8.4.):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework;


namespace P3_2
{
    class spriteClass
    {
        public Texture2D spTexture;
        public Vector2 spPosition;

        public spriteClass(Texture2D newSpTexture, Vector2 newSpPosition)
        {
            spTexture = newSpTexture;
            spPosition = newSpPosition;
        }

    }
}

Здесь мы подключили к нашему классу пространство имен Microsoft.Xna.Framework.Graphics – оно нужно нам, так как содержит объявление типа данных Texture2D. Пространство имен Microsoft.Xna.Framework содержит объявление типа данных Vector2.

В раздел объявления свойств класса мы добавили две переменных. Переменная spTexture типа Texture2D используется для хранения текстуры, переменная spPosition типа Vector2 – для хранения данных о положении объекта на экране. В конструкторе класса мы инициализируем эти переменные значениями соответствующих типов, переданными при создании объекта типа spriteClass в основном тексте программы.

Свойства класса объявлены с модификаторами доступности public. Это значит, что мы сможем модифицировать эти свойства извне. В данном случае такой подход оправдан. Однако надо отметить, что при конструировании классов рекомендуется защищать свойства от прямой модификации или чтения. Для организации доступа к таким свойствам можно создавать дополнительные свойства-методы, пользуясь директивами get/set.

Теперь внесем изменения в основной текст игры, который расположен в файле Game1.cs. В листинге 8.5 приведены эти изменения с комментариями, указывающими на их местонахождение.

/// Объект mySpriteObj для хранения данных об изображении для вывода на экран
spriteClass mySpriteObj;
/// Добавляем в метод LoadContent команду создания нового объекта типа spriteClass. 
Передаем в качестве текстуры изображения текстуру, загруженную с помощью команды Content.Load, 
в качестве позиции – новый объект типа Vector2
mySpriteObj = new spriteClass(Content.Load<Texture2D>("P3_1"), new Vector2(10f, 10f));
/// В метод Draw добавляем соответствующие вызовы методов объекта spriteBatch. 
Обратите внимание на то, что при вызове метода Draw мы пользуемся свойствами объекта mySpriteObj.
spriteBatch.Begin();
spriteBatch.Draw(mySpriteObj.spTexture, mySpriteObj.spPosition, Color.White);
spriteBatch.End();
/// В метод UnloadContent добавляем команды для освобождения системных ресурсов.
mySpriteObj.spTexture.Dispose();
spriteBatch.Dispose();

При запуске данного примера мы получим тот же графический вывод, который получили при проверке предыдущего.

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

Однако и этот подход не является единственно возможным. Еще более интересный способ вывода изображений и создания игровых объектов – это создание игровых объектов на базе игровых компонентов.

8.4. Разработка игрового компонента

Создадим новый игровой проект, назовем его P3_3. Щелкнем правой кнопкой мыши по значку проекта в панели Обозреватель проектов, выберем пункт Добавить > Создать элемент. В появившемся окне выберем миниатюру Игровой компонент, зададим имя для компонента – spriteComp и нажмем на кнопку Добавить. В проект будет добавлен новый игровой компонент. В листинге 8.6 вы можете видеть полный код этого компонента.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;


namespace P3_3
{
    /// <summary>
    /// Это игровой компонент, реализующий интерфейс IUpdateable.
    /// </summary>
    public class spriteComp : Microsoft.Xna.Framework.GameComponent
    {
        public spriteComp(Game game)
            : base(game)
        {
            // ЗАДАЧА: здесь создаются дочерние компоненты
        }

        /// <summary>
        /// Позволяет игровому компоненту выполнить необходимую инициализацию перед\r\запуском. 
         Здесь можно запросить нужные службы и загрузить контент.
        /// 
        /// </summary>
        public override void Initialize()
        {
            // ЗАДАЧА: добавьте здесь код инициализации

            base.Initialize();
        }

        /// <summary>
        /// Позволяет игровому компоненту обновиться.
        /// </summary>
        /// >param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
        public override void Update(GameTime gameTime)
        {
            // ЗАДАЧА: добавьте здесь код обновления

            base.Update(gameTime);
        }
    }
}

Можно отметить, что игровой компонент представляет собой класс, который наследует класс Microsoft.Xna.Framework.GameComponent. Наследование класса GameComponent подходит для создания игровых компонентов, которые не имеют графических представлений. Мы же хотим создать компонент, который планируется визуализировать. Поэтому нужно заменить GameComponent на DrawableGameComponent.

Обратите внимание на то, что созданный компонент имеет несколько методов. Так, метод spriteComp – это конструктор класса, метод Initialise предназначен для инициализации компонента, метод Update – это игровой цикл компонента.

Здесь не хватает еще одного метода – метода Draw. Добавим этот метод в класс (после ввода текста public override void нам автоматически предложат список, из которого достаточно выбрать Draw и нажать Enter – набор входных переменных и структура метода будет создана автоматически. В листинге 8.7 представлен измененный код компонента, готовый для дальнейшей работы.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;


namespace P3_3
{
    /// <summary>
    /// Это игровой компонент, реализующий интерфейс IUpdateable.
    /// </summary>
    public class spriteComp : Microsoft.Xna.Framework.DrawableGameComponent
    {
        public spriteComp(Game game)
            : base(game)
        {
            // ЗАДАЧА: здесь создаются дочерние компоненты
        }

        /// <summary>
        /// Позволяет игровому компоненту выполнить необходимую инициализацию перед\r\запуском.  
        Здесь можно запросить нужные службы и загрузить контент.
        /// 
        /// </summary>
        public override void Initialize()
        {
            // ЗАДАЧА: добавьте здесь код инициализации

            base.Initialize();
        }

        /// <summary>
        /// Позволяет игровому компоненту обновиться.
        /// </summary>
        /// >param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
        public override void Update(GameTime gameTime)
        {
            // ЗАДАЧА: добавьте здесь код обновления

            base.Update(gameTime);
        }
        public override void Draw(GameTime gameTime)
        {
            base.Draw(gameTime);
        }
    }
}

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

Создадим новый графический файл. Воспользуемся для этого графическим редактором Adobe Photoshop CS. На рис. 8.10 вы можете видеть изображение в окне редактора. Обратите внимание на то, что один графический файл содержит несколько изображений, а так же на то, что фон сделан прозрачным – такая техника позволяет выводить в игровое окно рисунки сложной формы.

(рис 8.10) Создание изображений в Adobe Photoshop

Здесь мы создали три изображения.

Сохраним изображение в виде PNG-файла. Такие файлы поддерживают прозрачность фона. Теперь нам нужно решить еще одну задачу – определить координаты отдельных спрайтов в нашем большом изображении. Это можно сделать следующим образом. Откроем сохраненное изображение в каком-нибудь графическом редакторе, можно – в том же Photoshop CS – мы рассмотрим вопрос определения координат именно для этой программы.

Увеличим масштаб изображения, переключимся на панель Info, и, наведя мышь на пиксель левого верхнего угла изображения, запишем отобразившиеся координаты указателя (рис. 8.11). В нашем случае это (18, 9). Обратите внимание на то, что нас интересует координата, выраженная в пикселях, для правильного определения координаты в данном случае нам нужно настроить параметр Ruler Units (окно Info Options, где производится настройка, можно найти в меню палитры) на измерение в пикселях (Pixels).

(рис 8.11) Определение координат объектов в Photoshop

Точно так же узнаем координату правого нижнего угла изображения. В нашем случае это – (35, 97). Для дальнейшей работы нам понадобится знание координаты левой верхней точки прямоугольника, ограничивающего спрайт, а так же – его ширина и высота. В нашем случае ширина составляет 17 пикселей (35– 18), высота – 88 пикселей (97 – 9). В итоге получаем следующие параметры прямоугольника, в который заключен наш спрайт:

Х: 18
Y: 9
Width: 17
Height: 88

Ниже эти данные понадобятся нам.

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

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;


namespace P3_3
{
    /// <summary>
    /// Это игровой компонент, реализующий интерфейс IUpdateable.
    /// </summary>
    public class spriteComp : Microsoft.Xna.Framework.DrawableGameComponent
    {
        private Texture2D sprTexture;
        private Rectangle sprRectangle;
        private Vector2 sprPosition;

        public spriteComp(Game game, ref Texture2D newTexture, 
            Rectangle newRectangle, Vector2 newPosition
)
            : base(game)
        {
            sprTexture = newTexture;
            sprRectangle = newRectangle;
            sprPosition = newPosition;

            // ЗАДАЧА: здесь создаются дочерние компоненты
        }

        /// <summary>
        /// Позволяет игровому компоненту выполнить необходимую инициализацию перед\r\запуском.  
        Здесь можно запросить нужные службы и загрузить контент.
        /// 
        /// </summary>
        public override void Initialize()
        {
            // ЗАДАЧА: добавьте здесь код инициализации

            base.Initialize();
        }

        /// <summary>
        /// Позволяет игровому компоненту обновиться.
        /// </summary>
        /// >param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
        public override void Update(GameTime gameTime)
        {
            // ЗАДАЧА: добавьте здесь код обновления

            base.Update(gameTime);
        }
        public override void Draw(GameTime gameTime)
        {
            SpriteBatch sprBatch =
                (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));

            sprBatch.Draw(sprTexture, sprPosition, sprRectangle, Color.White);

            base.Draw(gameTime);
        }
    }
}

Рассмотрим изменения, внесенные в компонент.

Мы добавили три переменных – sprTexture, sprRectangle, sprPosition – для хранения текстуры, прямоугольника, показывающего координаты спрайта в текстуре, и позиции вывода спрайта на экран.

Далее, мы модифицировали конструктор класса, добавив в список передаваемых параметров следующие переменные:

ref Texture2D newTexture – передача текстуры. Параметр передается по ссылке, то есть объект получает не копию текстуры, а лишь ссылку на нее – это экономит системные ресурсы.

Rectangle newRectangle – прямоугольник, задающий координаты спрайта в изображении.

Vector2 newPosition – позиция спрайта на игровом экране.

В теле конструктора мы инициализируем соответствующие свойства класса переданными параметрами.

Далее, мы дорабатываем код метода Draw. Здесь нам предстоит наиболее сложная часть модификации. Дело в том, в игре, для которой мы создаем компонент, уже есть все средства для вывода изображений. Поэтому разумнее всего воспользоваться уже существующим объектом типа SpriteBatch для того, чтобы с его помощью вывести изображение на экран. Для того, чтобы сделать однажды созданный SpriteBatch доступным для всех компонентов игры, существует специальная методика. Есть такое понятие, как игровые сервисы, которые доступны всем компонентам. Сначала мы добавим созданный в классе Game1 объект типа SpriteBatch в список игровых сервисов (мы рассмотрим это ниже), после чего сможем извлечь этот объект в игровом компоненте. Мы так и поступаем – следующая команда создает новый объект sprBatch, который содержит ссылку на исходный объект типа SpriteBatch, который ранее, в классе Game1, добавлен в список игровых ресурсов.

SpriteBatch sprBatch =  (SpriteBatch)Game.Services.GetService(typeof(SpriteBatch));

После того, как у нас появилась ссылка на исходный объект типа SpriteBatch, мы можем вывести изображение, используя перегруженный метод Draw такого объекта. Ранее мы пользовались этим методом, передавая ему текстуру, позицию текстуры в игровом окне и оттенок текстуры. Теперь же к списку передаваемых ему параметров добавляется прямоугольник, задающий позицию спрайта в файле текстуры. Этот вызов выглядит так:

sprBatch.Draw(sprTexture, sprPosition, sprRectangle, Color.White);

Выше мы выводили спрайт на экран, сначала вызвав метод Begin() объекта типа SpriteBatch, потом – вызывали метод Draw(), потом – метод End(), завершающий вывод. Здесь мы не вызываем этих методов. Дело в том, что данные методы будут вызваны в методе Draw(), который вызывает основная игра (объект типа Game1). В код класса Game1 внесены некоторые изменения, которые мы сейчас рассмотрим. В листинге 8.9 вы можете видеть этот код.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Input.Touch;
using Microsoft.Xna.Framework.Media;

namespace P3_3
{
    /// <summary>
    /// Это главный тип игры
    /// </summary>
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        GraphicsDeviceManager graphics;
        SpriteBatch spriteBatch;
        spriteComp gameObject;
        Texture2D texture;

        public Game1()
        {
            graphics = new GraphicsDeviceManager(this);
            Content.RootDirectory = "Content";

            // Частота кадра на Windows Phone по умолчанию — 30 кадров в секунду.
            TargetElapsedTime = TimeSpan.FromTicks(333333);

            // Дополнительный заряд аккумулятора заблокирован.
            InactiveSleepTime = TimeSpan.FromSeconds(1);
        }

        /// <summary>
        /// Позволяет игре выполнить инициализацию, необходимую перед запуском.
        /// Здесь можно запросить нужные службы и загрузить неграфический
        /// контент.  Вызов base.Initialize приведет к перебору всех компонентов и
        /// их инициализации.
        /// </summary>
        protected override void Initialize()
        {
            // ЗАДАЧА: добавьте здесь логику инициализации

            base.Initialize();
        }

        /// <summary>
        /// LoadContent будет вызываться в игре один раз; здесь загружается
        /// весь контент.
        /// </summary>
        protected override void LoadContent()
        {
            // Создайте новый SpriteBatch, который можно использовать для отрисовки текстур.
            spriteBatch = new SpriteBatch(GraphicsDevice);
            Services.AddService(typeof(SpriteBatch), spriteBatch);
            texture = Content.Load<Texture2D>("BallandBats");
            CreateNewObject();

            // ЗАДАЧА: используйте здесь this.Content для загрузки контента игры
        }

        protected void CreateNewObject()
        {
            gameObject = new spriteComp(this, ref texture,
                new Rectangle(18, 9, 17, 88), new Vector2(100, 150));
            Components.Add(gameObject);
        }


        /// <summary>
        /// UnloadContent будет вызываться в игре один раз; здесь выгружается
        /// весь контент.
        /// </summary>
        protected override void UnloadContent()
        {
            // ЗАДАЧА: выгрузите здесь весь контент, не относящийся к ContentManager
        }

        /// <summary>
        /// Позволяет игре запускать логику обновления мира,
        /// проверки столкновений, получения ввода и воспроизведения звуков.
        /// </summary>
        /// >param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
        protected override void Update(GameTime gameTime)
        {
            // Позволяет выйти из игры
            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)
                this.Exit();

            // ЗАДАЧА: добавьте здесь логику обновления

            base.Update(gameTime);
        }

        /// <summary>
        /// Вызывается, когда игра отрисовывается.
        /// </summary>
        /// >param name="gameTime">Предоставляет моментальный снимок значений времени.</param>
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.CornflowerBlue);

            // ЗАДАЧА: добавьте здесь код отрисовки
            spriteBatch.Begin();
            base.Draw(gameTime);
            spriteBatch.End();
        }
    }
}

Для начала мы добавили новые переменные, одна из которых (типа spriteComp) служит для хранения объекта класса spriteComp, а вторая (типа Texture2D) служит для хранения текстуры. Так как текстура содержит несколько спрайтов, переменная Texture2D может понадобиться при создании нескольких объектов, поэтому имеет смысл создать эту переменную, один раз загрузить в нее изображение и пользоваться ей.

В методе LoadContent() мы, как обычно, создаем объект типа SpriteBatch, и, с помощью команды Services.AddService(typeof(SpriteBatch), spriteBatch); добавляем объект SpriteBatch к списку игровых сервисов. Именно после этой команды spriteBatch можно вызывать в игровых компонентах и выводить их изображений.

Далее в этом методе мы загружаем текстуру с изображениями в переменную texture и вызываем метод CreateNewObject();. Этот метод мы создали самостоятельно – мы используем его для создания объекта gameObject.

gameObject  = new spriteComp (this, ref texture, new Rectangle (18,9,17,88), new Vector2 (100,150));

При создании этого объекта мы передаем конструктору класса spriteComp объект игры (this), ссылку на текстуру, объект Rectangle – обратите внимание на то, что мы создаем новый объект Rectangle, который инициализирован значениями координаты нужного нам спрайта в файле текстуры, вычисленными ранее. Так же мы передаем конструктору новый объект типа Vector2, который содержит координаты спрайта для вывода на игровой экран.

В методе CreateNewObject() мы разместили еще одну команду – Components.Add(gameObject );. С ее помощью мы добавляем в список компонентов игры только что созданный объект. Это очень важная команда – благодаря ей при исполнении команды base.Draw(gameTime); будет обработан метод Draw нашего игрового компонента, и изображения, которые он выведет, будут выведены на экран.

В методе Draw() класса Game1 мы используем такой код:

spriteBatch.Begin();
base.Draw(gameTime);
spriteBatch.End();

Команда spriteBatch.Begin(); готовит графическое устройтство для вывода изображений, после этого осуществляется вывод изображений – по вызову base.Draw(gameTime); обрабатываются соответствующие методы игровых компонентов, унаследованных от DrawableGameComponent. Напомню, что в методе Draw() нашего компонента spriteComp есть лишь вызов метода Draw() объекта класса SpriteBatch, без вызовов Begin() и End(). В результате, после того, как компонент выведет изображение, вывод завершается командой End() объекта spriteBatch в методе Draw() класса Game1 и изображение появляется на экране.

Такой механизм работы позволяет создавать множество компонентов, добавлять их в список компонентов игры и не заботиться о выводе каждого из них в методе Draw() класса Game1. После того, как компонент добавлен в список компонентов игры, вывод его графического представления осуществляется автоматически. Можно сказать, что метод base.Draw(gameTime); "просматривает" все зарегистрированные компоненты и выводит их на экран.

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

Вот как выглядит игровой экран после вывода в него изображения (рис. 8.12.

(рис 8.12) Игровой экран после вывода изображения

Здесь мы рассмотрели различные подходы к выводу изображений. Однако можно заметить, что вывод изображения, которое ассоциируется с игровым объектом – это лишь одна из задач, которую приходится решать при разработке игры. Другие задачи – например – перемещение объекта, проверка, не столкнулся ли объект с другими объектами – так же можно решать с использованием различных подходов. Можно заметить, что наиболее сложный с точки зрения реализации подход – разработка отдельного игрового компонента, является одновременно и наиболее удобным для реализации различных возможностей игрового объекта. Позже, дорабатывая функциональность игровых объектов, мы будем пользоваться именно этим подходом.

8.5. Вывод нескольких изображений

Рассмотрим вывод нескольких изображений в игровое окно. Как мы уже говорили, с использованием зарегистрированных игровых компонентов, унаследованных от DrawableGameComponent, вывод автоматизирован. Однако это не значит, что использовать более простые методы вывода не нужно.

Например, рассмотрим вывод фонового изображения в уже созданном проекте P2_3. Выше мы создали изображения, которые можно использовать для создания собственной версии игры Pong. Точно так же создадим изображение, которое будет служить фоном для игры. Стандартный игровой экран имеет разрешение 800х480 пикселей. Создадим изображение такого размера (назовем его background.png), загрузим его в проект. Для этого добавим в класс новую переменную

Texture2D backTexture;

После этого, в методе LoadContent(), загрузим изображение в эту переменную командой

backTexture = Content.Load<Texture2D>("Background");

Теперь выведем изображение с помощью метода Draw() объекта spriteBatch.

spriteBatch.Draw(backTexture, new Rectangle(0, 0, 800, 480), Color.White);

Поместим эту команду между вызовами Begin() и End() перед командой base.Draw(gameTime);

В результате окно игры приобретет такой вид (рис. 8.13).

(рис 8.13) Игровой экран после вывода изображения игрового объекта и фона

Как видите, спрайт игрового объекта выведен поверх фонового изображения.

Подход, требующий соблюдения очередности вывода изображений для регулирования их наложения, не очень удобен. Однако XNA Framework предусматривает специальные инструменты для настройки взаимного расположения объектов. В частности, при выводе спрайта можно указывать "глубину" (depth), на которой он расположен.

8.6. Выводы

В данной лабораторной работе мы рассмотрели основы работы с графикой в XNA-играх для Windows Phone 7

8.7. Задание

Нарисуйте несколько изображений в любом известном вам графическом редакторе. Выведите эти изображения в игровое окно XNA, пользуясь методикой, изложенной в пункте о выводе нескольких изображений на экран. Если вы разрабатываете двумерную игру, займитесь подготовкой изображений для нее, проверьте, как выбранные изображения выглядят при выводе их на экран эмулятора или телефона.

8.8. Дополнительные материалы

К данной лабораторной работе прилагается видеоурок

и демонстрационный проект.

Вернуться к учебному плану