netlib.narod.ru | < Назад | Оглавление | Далее > |
Если вы следовали направлению, заданному в предыдущей главе, то сейчас знаете, как создать простой пустой проект Windows Game, используя XNA Game Studio. В этом разделе вы создадите базовый проект, который отображает на экране два простых двухмерных изображения. Вы узнаете как перемещать их и как сделать, чтобы они отскакивали от границ окна и друг от друга.
В жаргоне игрового программирования используется много терминов, описывающих детали использования двухмерной графики в игре. Давайте взглянем на наиболее часто встречающиеся:
Спрайт (sprite). Спрайт — это двухмерное изображение, которым можно управлять независимо от остальной части сцены. Этот термин часто используется для описания отображаемых изображений или класса, используемого в игре для отображения изображений (который включает свойства, такие как скорость, местоположение, ширина и высота и т.д.). Поскольку компьютер всегда рисует двухмерное изображение как прямоугольник, спрайты обычно содержат прозрачные области, что создает иллюзию непрямоугольного рисования. Термин «анимированный спрайт» ссылается на спрайт, чье изображение меняется через предопределенные интервалы времени для создания иллюзии движения (такой, как идущий человек или вращающееся колесо).
Текстура (texture). Этот термин означает двухмерное изображение, загружаемое поверх трехмерной модели, которое видимо с любой точки просмотра, в зависимости от местоположения модели и местоположения камеры, используемой для визуализации сцены. Вы можете использовать текстуры чтобы создавать иллюзию высокодетализированных моделей, накладывая детализированную текстуру на простую трехмерную модель.
Щит (billboard). Это также термин, используемый в трехмерном мире и представляющий текстуру, накладываемую на специальную плоскость, которая всегда перпендикулярна оси камеры. Использование выглядящих трехмерными изображений на щитах — это эффективная техника создания игровых компонентов, таких как деревья, дорожные знаки или факелы на стенах, без необходимости создания высокодетализированных моделей. Это позволяет создавать более детализированные сцены, обходясь той же мощью оборудования визуализации.
Фон (background). Сцена двухмерной игры обычно состоит из фонового изображения и множества спрайтов, отображаемых поверх него. Когда фоновое изображение перемещается, у вас есть прокручиваемый фон (scrolling background), что является основной характеристикой игр, называемых скроллерами (scrollers). Также следует упомянуть параллаксную прокрутку (parallax scrolling), специальную технику прокрутки, в которой у двухмерной игры есть несколько прокручиваемых фонов с разной скоростью прокрутки, что создает иллюзию трехмерного окружения. Например, когда персонаж игрока перемещается влево, деревья и кусты за ним перемещаются со скоростью игрока, далекие горы на заднем плане перемещаются медленнее, а облака в небе перемещаются совсем медленно.
Блоки (tiles). Это небольшие изображения, используемые как мозаика для составления большего изображения, обычно фона уровня. Например, платформенные игры обычно используют блоки для создания различных уровней с платформами из одних и тех же базовых изображений. Термин блочная карта (tiled map) часто используется для описания игрового уровня, созданного из блоков, и иногда означает файл с информацией, необходимой для создания таких уровней из блоков. Классический пример использования блоков — построение ландшафта. Ролевые игры (RPG) обычно предоставляют программу редактора уровней, в которой пользователь может строить уровни, выбирая в приложении различные блоки и объединяя их вместе.
Хотя двухмерные игры используют много математических концепций, в этом разделе мы рассмотрим только самые основные. Однако, если вы поймете концепции представленные в этой главе, то сможете полагаться на эти знания, создавая ваши двухмерные игры, и легко изучить другие связанные концепции.
Вы создадите простую программу XNA представляющую концепции рисования спрайтов, перемещения их по экрану и столкновения спрайтов друг с другом и с границами игрового окна. Однако перед тем как начать кодировать, давайте поговорим о двухмерной системе координат и экранных координатах.
Возможно, в школе вы слышали о двухмерной системе координат (2-D coordinate system), когда создавали простые графики на уроках геометрии. Чтобы вы вспомнили, на рис. 2.1 показан треугольник, каждая вершина которого представлена в двухмерной системе координат. Проанализируйте координаты вершин, чтобы убедиться, что вы понимаете концепцию.
Рис. 2.1. Треугольник в двухмерной системе координат
Главное различие между системой координат, представленной на рис. 2.1 и координатами, применяемыми при создании двухмерных игр, — называемых «экранные координаты», — в том, что начало координат находится не в нижнем левом, а в верхнем левом углу, как показано на рис. 2.2. Сравните два рисунка, чтобы понять, как это различие влияет на определение вершин: самой верхней вершине на экране соответствует наименьшее значение координаты Y.
Рис. 2.2. Тот же треугольник в экранных координатах
Еще один момент заключается в том, что экранные координаты напрямую связаны с разрешением экрана. Так, если ваш монитор сконфигурирован под разрешение 800 × 600, это значит, что в оси X — 800 пикселов (каждый пиксель это независимая точка на экране), а по оси Y — 600 пикселов, как показано на рис. 2.2.
Давайте сейчас создадим простой пример в XNA, отображающий спрайт в заданной позиции экрана.
Начнем с создания нового проекта или откроем пустой проект, созданный вами в предыдущей главе.
Чтобы сгруппировать изображение спрайта и некоторые связанные с ним свойства (такие, как местоположение, размер и скорость), создадим простой класс, который позже в этой главе будем расширять по мере исследования новых концепций. Следующий фрагмент кода представляет простой класс спрайта, включающий следующие свойства:
texture: Хранит изображение спрайта, используя класс XNA Texture2D. Этот класс имеет много свойств и методов, помогающих иметь дело с текстурами; вы увидите некоторые из них в главах 3 и 4. В этом классе текстура хранится в виде двухмерной сетки текселей (texels). Подобно пикселям, являющимся наименьшим элементом, который может быть нарисован на экране, тексели являются наименьшими элементами, которые хранит видеокарта и состоят из цвета и значения прозрачности.
size: Хранит размер спрайта, используя класс XNA Vector2. У этого класса есть два свойства, X и Y, которые используются для хранения ширины и высоты изображения.
position: Хранит местоположение спрайта, используя класс XNA Vector2. Свойства X и Y класса хранят экранные координаты спрайта.
class clsSprite { public Texture2D texture; // Текстура спрайта public Vector2 position; // Местоположение спрайта на экране public Vector2 size; // Размер спрайта в пикселях public clsSprite (Texture2D newTexture, Vector2 newPosition, Vector2 newSize) { texture = newTexture; position = newPosition; size = newSize; } }
Сейчас этот класс хранит только свойства спрайта и не содержит никаких методов. Поскольку наша цель здесь сохранить код простым, мы не будем создавать свойства, используя структуры get/set, хотя, создавая свойства в ваших играх, желательно поступать именно так. Следующий фрагмент кода показывает, как использовать такие структуры, на тот случай, если вы сами захотите улучшить код.
int _gameLevel; // Хранит текущий уровень игры public static int GameLevel { get { return _gameLevel; } set { _gameLevel = value; } }
Первый этап создания спрайта — добавление в вашу игру нового изображения, чтобы вы могли использовать его через конвейер содержимого. Зайдите на сайт XNA Creator's Club (http://creators.xna.com) и сохраните логотип XNA, отображающийся на главной странице сайта (или загрузите изображение напрямую по ссылке http://creators.xna.com/themes/default/ images/common/xna_thumbnail.png). Как только изображение будет на вашем жестком диске, добавьте его в проект, щелкнув левой кнопкой мыши по окну Solution Explorer, как показано на рис. 2.3, и выбрав в появившемся меню Add, а затем Existing Item, после чего выберите изображение, которое только что загрузили.
Рис. 2.3. Добавление изображения к игровому проекту
После добавления изображения к игровому решению, выберите имя изображения в окне Solution Explorer и нажмите F4. Вам будет представлено (если оно еще не было видимо) окно Properties для только что добавленного изображения, которое показано на рис. 2.4.
Рис. 2.4. Свойства изображения
В окне Properties представлена различная информация, такая как импортер содержимого и обработчик содержимого, используемые для данного содержимого (также называемого ресурсом, asset). Если вы не помните эти концепции, обратитесь к предыдущей главе, чтобы освежить знания! В окне вы также видите свойство Asset Name, которое определяет, как ваш код будет ссылаться на это содержимое.
Когда у вас есть изображение, следующий этап заключается в добавлении кода для рисования его на экране. Для этого вам потребуется SpriteBatch (класс XNA рисующий спрайты на экране) и текстура, которая будет использоваться как изображение спрайта (в данном случае вы загружаете эту текстуру в ваш класс clsSprite).
В новом проекте Windows Game для вас уже создан объект SpriteBatch, так что начнем с создания объекта ClsSprite в классе Game1. Включите его определение в начало класса сразу после объектов устройства и пакета спрайтов, которые автоматически созданы для вас. У вас должно получиться что-то похожее на следующий фрагмент кода:
public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; // Устройство SpriteBatch spriteBatch; // Визуализатор спрайтов clsSprite mySprite; // Мой класс спрайта
Очевидно, вам необходимо создать этот объект с правильными данными, прежде чем использовать его. Вы делаете это в методе LoadContent, поскольку, как говорилось в предыдущей главе, это правильное место для добавления графической инициализации. Поскольку проект уже создает объект SpriteBatch, вам необходимо только создать объект clsSprite:
protected override void LoadContent() { // Загружаем двухмерную текстуру спрайта mySprite = new clsSprite(Content.Load<Texture2D>("xna_thumbnail"), new Vector2(0f, 0f), new Vector2(64f, 64f)); // Создаем SpriteBatch для визуализации спрайтов spriteBatch = new SpriteBatch(graphics.GraphicsDevice); }
Хотя вы добавили только одну строку кода, произошло много вещей. Давайте посмотрим: вы создали ваш объект класса спрайта, используя диспетчер содержимого для загрузки Texture2D на основании имени ресурса изображения, xna_thumbnail. Вы также определили местоположение спрайта как (0, 0) и выбрали размер спрайта: 64 пикселя в ширину и 64 пикселя в высоту.
Что касается создания SpriteBatch, стоит отметить, что вы передаете в качестве параметра графическое устройство. В предыдущей главе мы упоминали, что устройство (представленное здесь переменной graphics) это ваша точка входа на уровень обработки графики, и через нее вы выполняете любую графическую операцию. Здесь вы информируете SpriteBatch, какое устройство должно использоваться для рисования спрайтов; позднее в этой главе вы также используете устройство для изменения размеров окна программы.
Хорошей программистской практикой является уничтожение всего, что вы создали, когда программа заканчивается. Для этого вам надо освободить созданные в методе LoadContent clsSprite и SpriteBatch. Как вы, возможно, догадались, это делается в методе UnloadContent. Код для освобождения объектов таков:
protected override void UnloadContent() { // Освобождаем ранее выделенные ресурсы mySprite.texture.Dispose(); spriteBatch.Dispose(); }
И, наконец, вам надо добавить код для рисования спрайта с использованием созданного вами объекта SpriteBatch. Вы используете SpriteBatch, как говорит его имя, для рисования пакета спрайтов, группируя один или несколько вызовов его метода Draw внутри блока, начинаемого вызовом Begin и завершаемого вызовом метода End, как показано ниже:
protected override void Draw(GameTime gameTime) { graphics.GraphicsDevice.Clear(Color.CornflowerBlue); spriteBatch.Begin(); spriteBatch.Draw(mySprite.texture, mySprite.position, Color.White); spriteBatch.End(); base.Draw(gameTime); }
Существует много перегрузок для метода Draw, позволяющих вам рисовать только часть исходной текстуры, масштабировать или вращать изображение и т.д. Вы используете простейшую из них, которая получает три аргумента: текстуру для рисования, местоположение в экранных координатах (оба берутся из вашего объекта clsSprite) и цвет канала модуляции, используемый для придания изображению оттенков. Использование в этом последнем параметре любого цвета, отличающегося от белого, приведет к рисованию изображения, в котором его оригинальные цвета смешаны с заданным цветным тоном.
Следует отметить, что метод Begin также может получать параметры, которые будут использованы при визуализации каждого спрайта в блоке. Например, если в текстуре есть информация о прозрачности, вы должны сообщить SpriteBatch, что ее следует учитывать при рисовании, заменив строку кода с вызовом Begin на следующую:
spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
Запуск программы сейчас приведет к появлению окна с расположенным в верхнем левом углу спрайтом — это позиция (0, 0) окна программы — как показано на рис. 2.5.
Рис. 2.5. Спрайт визуализированный в позиции (0, 0) окна программы
Если вы хотите изменить размер окна (например, на 400 × 400), вы должны проинформировать о новом размере устройство (через объект graphics) в методе LoadContent, добавив следующие строки кода:
graphics.PreferredBackBufferWidth = 400; graphics.PreferredBackBufferHeight = 400; graphics.ApplyChanges();
Фактически, в этих строках вы меняете ширину и высоту вторичного буфера (back buffer), который отражает размеры окна, поскольку вы работаете в оконном режиме. Вторичный буфер — это часть техники используемой для рисования игровой сцены без мерцания изображения, называемой двойная буферизация (double buffering). В двойной буферизации у вас есть два места, или буфера, для рисования и отображения игровой сцены: пока первый из них показывается игроку, во втором, невидимом (вторичном буфере) выполняется рисование. Когда рисование завершено содержимое вторичного буфера перемещается на экран, так что игрок не будет видеть только часть сцены, если ее рисование занимает длительное время (раздражающий визуальный эффект, известный как «мерцание»).
К счастью, вам не надо беспокоиться о таких деталях, потому что XNA скрывает эти сложности от вас. Но всегда хорошо понимать, почему свойство называется PreferredBackBufferWidth, а не как-нибудь вроде PreferredWindowsWidth!
Поскольку создавая двухмерные игры вы работаете непосредственно с экранными координатами, перемещать спрайт просто: вам надо только рисовать его в различных позициях. Увеличивая координату X местоположения спрайта вы будете перемещать его вправо; уменьшая — будете перемещать спрайт влево. Если вы хотите перемещать спрайт на экране вниз, вам надо увеличивать координату Y, а вверх спрайт перемещается путем уменьшения координаты Y. Помните, что точка с экранными координатами (0, 0) соответствует верхнему левому углу окна.
Базовый игровой проект XNA Framework предоставляет конкретное место для размещения игровых вычислений: переопределяемый метод Update.
Вы можете перемещать спрайт просто добавив одну строку кода, увеличивающую координату X спрайта, как в показанном ниже фрагменте:
mySprite1.position.X += 1;
Поскольку вы используете свойство position спрайта, когда визуализируете его в методе Draw, добавив эту строку вы увидите, что спрайт перемещается поперек окна вправо, пока не исчезнет с экрана.
Чтобы создать более похожий на игру спрайт, давайте сделаем что-нибудь более сложное. Сперва создадим в классе clsSprite новое свойство velocity, которое будет определять скорость перемещения спрайта по осям X и Y. Затем модифицируем конструктор класса для получения и сохранения размеров экрана, чтобы мы могли добавить метод, перемещающий спрайт с заданной скоростью так, чтобы он не выходил за границы экрана. Итак, удалим строку кода, меняющую координату X спрайта, и выполним следующие три простых шага:
Изменяем конструктор класса спрайта и меняем код создания спрайта в классе Game1. В файле clsSprite.cs выполните следующие изменения в конструкторе класса:
private Vector2 screenSize; public clsSprite (Texture2D newTexture, Vector2 newPosition, Vector2 newSize, int ScreenWidth, int ScreenHeight) { texture = newTexture; position = newPosition; size = newSize; screenSize = new Vector2(ScreenWidth, ScreenHeight); }
Теперь соответствующим образом измените код создания спрайта из файла Game1.cs, находящийся в методе LoadContent.
mySprite1 = new clsSprite(Content.Load<Texture2D>("xna_thumbnail"), new Vector2(0f, 0f), new Vector2(64f, 64f), graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight);
Создайте в классе спрайта новое свойство velocity:
public Vector2 velocity;
Вы можете установить эту скорость равной (1, 1) в методе LoadContent после кода создания спрайта, проинформировав спрайт, что он должен перемещаться на один пиксель по осям X и Y за каждое обновление. В результате спрайт будет перемещаться на экране по диагонали.
mySprite1.velocity = new Vector2(1, 1);
У вас есть границы экрана и у вас есть скорость. теперь вам надо создать метод — давайте назовем его Move — в классе спрайта, который перемещает спрайт в соответствии с его скоростью, учитывая границы экрана. Код этого метода такой:
public void Move() { // Если мы вышли за границы экрана, инвертируем скорость // Проверяем правую границу if(position.X + size.X + velocity.X > screenSize.X) velocity.X = -velocity.X; // Проверяем нижнюю границу if (position.Y + size.Y + velocity.Y > screenSize.Y) velocity.Y = -velocity.Y; // Проверяем левую границу if (position.X + velocity.X < 0) velocity.X = -velocity.X; // Проверяем верхнюю границу if (position.Y + velocity.Y < 0) velocity.Y = -velocity.Y; // Поскольку мы настроили скорость, // просто прибавляем ее к текущему местоположению position += velocity; }
Поскольку и местоположение спрайта и его скорость представлены классом Vector2, вы должны просто сложить векторы, чтобы изменить местоположение спрайта. Однако, поскольку вы не хотите прибавлять скорость, если это приведет к выходу спрайта за границы экрана, вы добавляете код для инвертирования скорости в таких ситуациях.
Взгляните на показанный выше код: проверка для левой и верхней границ экрана выполняется напрямую, поскольку местоположение спрайта определяет координаты его верхнего левого угла. Однако, при проверке не вышел ли спрайт за правую границу экрана, вы прибавляете к координате X спрайта его ширину. Когда проверяется выход спрайта за нижнюю границу экрана вы должны сложить высоту спрайта и его координату Y. Тщательно прочитайте код, чтобы убедиться, что вы понимаете проверки, а затем запустите код. Спрайт будет перемещаться по экрану и отражаться от границ окна!
Отражение спрайта от границ окна это уже простейший вариант обнаружения столкновений, но в двухмерных играх вы обычно хотите обнаруживать столкновения между спрайтами.
Если вы введете запрос «алгоритм проверки столкновений» в поисковом движке в Интернете, то увидите тысячи страниц, представляющих множество разных алгоритмов обнаружения столкновений в двухмерных и трехмерных системах. Мы здесь не будем настолько углубляться в детали; мы только представим простой пример, который поможет вам понять концепцию. Позже, в главе 3, вы увидите некоторые алгоритмы в действии.
При проверке столкновений обычно неразумно проверять каждый отдельный пиксель спрайта, сравнивая его с пикселями другого спрайта, поэтому алгоритмы проверки столкновений базируются на приближенном представлении формы объекта с несколькими легко вычисляемыми формулами. Наиболее известный алгоритм обнаружения столкновений известен как ограничивающие прямоугольники (bounding boxes), и выполняет аппроксимацию формы объекта с помощью одного или нескольких прямоугольников. На рис 2.6 показано спрайт самолета, чья форма приближенно представлена двумя прямоугольниками.
Рис. 2.6. Два прямоугольника могут использоваться чтобы проверять столкновения спрайта самолета
Простейшая реализация проверки с ограничивающими прямоугольниками заключается в проверке находятся ли координаты X и Y верхнего левого угла первого прямоугольника (который охватывает первый проверяемый спрайт) внутри второго прямоугольника (который охватывает второй проверяемый объект). Другими словами, проверяем, что X и Y первого проверяемого прямоугольника меньше или равны соответствующих X и Y другого прямоугольника плюс ширина другого прямоугольника.
Реализуем в вашем классе clsSprite метод (с именем Collides), который получает в качестве параметра спрайт и проверяет полученный спрайт, сравнивая его с текущим. Если обнаружено столкновение, метод возвращает true.
public bool Collides(clsSprite otherSprite) { // Проверяем столкновение двух спрайтов if (this.position.X + this.size.X > otherSprite.position.X && this.position.X < otherSprite.position.X + otherSprite.size.X && this.position.Y + this.size.Y > otherSprite.position.Y && this.position.Y < otherSprite.position.Y + otherSprite.size.Y) return true; else return false; }
Сравните этот код с графическим примером на рис. 2.7, чтобы убедиться, что вы поняли алгоритм.
Рис. 2.7. Два неперекрывающихся прямоугольника
Согласно примеру кода, два прямоугольника могут перекрываться только если обе координаты X и Y прямоугольника 2 находятся внутри диапазона (от X до X + ширина, от Y до Y + высота) прямоугольника 1. Взглянув на рисунок, вы увидите, что координата Y прямоугольника 2 не больше, чем координата Y плюс высота прямоугольника 1. Это означает, что ваши прямоугольники могут пересекаться. Но, проверяя координату X прямоугольника 2, вы увидите, что она больше чем координата X плюс ширина прямоугольника 1, а значит столкновение невозможно.
На рис. 2.8 у вас есть столкновение.
Рис. 2.8. Два перекрывающихся прямоугольника
В этом случае вы можете проверить, что обе координаты X и Y прямоугольника 2 находятся внутри диапазона прямоугольника 1. В примере кода вы также выполняете противоположную проверку, глядя, находятся ли координаты X и Y прямоугольника 1 внутри диапазона прямоугольника 2. Поскольку вы проверяете только одну точку, возможно, что верхний левый угол прямоугольника 2 будет находиться вне прямоугольника 1, а верхний левый угол прямоугольника 1 будет внутри прямоугольника 2.
Для проверки вашего метода создадим второй, неподвижный спрайт в середине окна. Для этого вам надо повторить код создания спрайта и добавить код проверки столкновений в метод Update класса Game1.
Во-первых, добавим объявление переменной спрайта в начало класса Game1, следом за предыдущим объявлением спрайта.
clsSprite mySprite2;
Теперь в метод LoadContent добавим код для создания спрайта:
mySprite2 = new clsSprite(Content.Load<Texture2D>("xna_thumbnail"), new Vector2(200f, 200f), new Vector2(64f, 64f), graphics.PreferredBackBufferWidth, graphics.PreferredBackBufferHeight);
А в метод UnloadContent добавим код, освобождающий его:
mySprite2.texture.Dispose();
И, наконец, в метод Draw добавляем код для рисования нового спрайта. Код для рисования двух спрайтов таков:
spriteBatch.Begin(SpriteBlendMode.AlphaBlend); spriteBatch.Draw(mySprite1.texture, mySprite1.position, Color.White); spriteBatch.Draw(mySprite2.texture, mySprite2.position, Color.White); spriteBatch.End();
Если запустить программу сейчас, вы увидите два спрайта, но они пока не отражаются друг от друга. Вы реализуете отражение, добавив вызов метода Collides в метод Update, как показано ниже:
// Перемещение спрайта mySprite1.Move(); // Проверка столкновений if (mySprite1.Collides(mySprite2)) mySprite1.velocity *= -1;
В этом коде вы инвертируете скорость mySprite1, умножая ее на –1. Это меняет знаки у членов X и Y свойства Vector2 velocity.
Запустив код сейчас вы увидите, что спрайты перемещаются и отражаются друг от друга и от границ окна, как показано на рис. 2.9.
Рис. 2.9. Теперь спрайты перемещаются и сталкиваются
netlib.narod.ru | < Назад | Оглавление | Далее > |