Здравствуйте уважаемые пользователи портала GcUp.Ru! В этой статье я расскажу Вам, как добавить физические законы в XNA GameStudio. Весь приведенный ниже код проверен, и отлично функционирует в версии 4.0. Но, так же он должен работать и в других версиях (3.0 и 3.1; на счет 2.0 не уверен). Перед знакомством с этим уроком рекомендую прочитать мои предыдущие статьи, так как этот урок является их продолжением, а это значит, что Вам понадобятся те знания, которые Вы получили из этих статей.
Так же перед прочтением данного урока Вы должны обладать базовыми знаниями языка программирования C#, и иметь программу Microsoft Visual C# Express необходимой Вам версии (для XNA 3.1 - Microsoft Visual C# Express 2008, а для XNA 4.0 - Microsoft Visual C# Express 2010) с установленной библиотекой XNA GameStudio.
Итак, если Вы имеете все вышеприведенное, тогда приступим!
Немного теории:
Итак, для начала давайте разберемся, для чего же нам нужна физика в нашей игре. Так как на протяжении всей серии статей мы создавали платформер, физика нам пригодятся, когда наш персонаж будет прыгать или падать. Ведь основная идея этого жанра – прыгать по платформам! Вот для этого нам и понадобится игровая физика. Есть много способов ее создания. Можно, зная физические формулы, рассчитать игровую физику, которая будет максимально приближена к жизни. Это самый сложный способ. Можно использовать для этой цели физический движок. Сам по себе такой движок – это программа, в которой как раз расписаны все эти формулы в виде программного кода, т.е. физика так же получается максимально приближенной к жизни. Но в отличие от первого способа, этот гораздо проще. Но, тем не менее, он довольно сложен, и, если сложится, о физических движках мы поговорим в отдельной серии статей. Третий (и самый простой) способ – это приблизительная имитация физических законов. Например, если под героем нет платформы, он опускается вниз, если нажата некая клавиша, персонаж поднимается вверх некоторое количество времени (совершает прыжок) и т.п. В этой статье мы будем пользоваться третьим способом создания игровой физики, так как он самый простой, но и самый популярный среди начинающих игроделов.
Итак, теперь пора переходить к практике!
Создание физики падения:
Для начала откройте проект, который мы создавали в статье «XNA для начинающих: рисование спрайтов, анимация и бег» и создайте в нем игровой уровень (впрочем, можно и наоборот). Я создал новый проект и назвал его Physics, в который добавил и персонажа, и уровень. В итоге у меня получился герой, который не падает вниз, но зато не может выходить за край игрового окна, и игровой уровень, состоящий из блоков. Вот полный код моего класса Game1:
Code
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; using Microsoft.Xna.Framework.Net; using Microsoft.Xna.Framework.Storage;
namespace Physics { /// <summary> /// This is the main type for your game /// </summary> public class Game1 : Microsoft.Xna.Framework.Game { GraphicsDeviceManager graphics; SpriteBatch spriteBatch; Texture2D boxTexture1; Texture2D boxTexture2;
List<Box> boxs; KeyboardState states;
Sprites hero; Sprites runAnimation;
public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; hero = new Sprites(); runAnimation = new Sprites(10);
hero.rect = new Rectangle(300, 210, 96, 96); runAnimation.rect = new Rectangle(300, 210, 96, 96);
}
/// <summary> /// Allows the game to perform any initialization it needs to before starting to run. /// This is where it can query for any required services and load any non-graphic /// related content. Calling base.Initialize will enumerate through any components /// and initialize them as well. /// </summary> protected override void Initialize() { // TODO: Add your initialization logic here
base.Initialize(); }
/// <summary> /// LoadContent will be called once per game and is the place to load /// all of your content. /// </summary> protected override void LoadContent() { // Create a new SpriteBatch, which can be used to draw textures. spriteBatch = new SpriteBatch(GraphicsDevice);
for (int i = 0; i < map.GetLength(0); i++) { for (int j = 0; j < map.GetLength(1); j++) { Rectangle rect = new Rectangle(x, y, 30, 30); int a = map[i, j];
if (a == 1) {
Box box = new Box(boxTexture1, rect); boxs.Add(box); }
else if (a == 2) {
Box box = new Box(boxTexture2, rect); boxs.Add(box); } x += 30; }
x = 0; y += 30; } }
/// <summary> /// UnloadContent will be called once per game and is the place to unload /// all content. /// </summary> protected override void UnloadContent() { // TODO: Unload any non ContentManager content here }
/// <summary> /// Allows the game to run logic such as updating the world, /// checking for collisions, gathering input, and playing audio. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Update(GameTime gameTime) { // Allows the game to exit if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit();
states = Keyboard.GetState();
// TODO: Add your update logic here
runAnimation.Update(gameTime);
base.Update(gameTime); }
/// <summary> /// This is called when the game should draw itself. /// </summary> /// <param name="gameTime">Provides a snapshot of timing values.</param> protected override void Draw(GameTime gameTime) { GraphicsDevice.Clear(Color.CornflowerBlue);
spriteBatch.Begin(); foreach (Box box in boxs) { box.Draw(spriteBatch); }
spriteBatch.End();
spriteBatch.Begin();
if (states.IsKeyDown(Keys.Left)) { runAnimation.DrawAnimation(spriteBatch, true, false);
Теперь давайте добавим физику падения. Для начала нам нужно привести размеры герой к таким значениям, чтобы нам легко было размещать его на уровне. Как Вы помните из предыдущего урока, все наше игровое поле поделено на ячейки размером 30x30 пикселей. Значит, нам нужно привести размеры нашего героя к таким значениям, которые будут делиться на 30. Лучше всего подходят размеры 60x60 пикселей. Для этого изменим строчки:
Code
hero.rect = new Rectangle(300, 210, 96, 96); runAnimation.rect = new Rectangle(300, 210, 96, 96);
Code
hero.rect = new Rectangle(300, 210, 60, 60); runAnimation.rect = new Rectangle(300, 210, 60, 60);
Как Вы помните, этим мы изменили размеры прямоугольника нашего героя (т.е. изменились и размеры текстуры). Теперь давайте посмотрим на размеры нашего игрового уровня. В высоту он имеет 330 пикселей. Но наш герой должен упасть не в самый низ игрового окна, а на землю. Т.е. остановить падение он должен на 300 пикселях:
Но так как координаты героя задаются по левому верхнему углу, нам нужно тормозить героя еще на 60 пикселей (высота спрайта) раньше. Т.е. для того, чтобы заставить нашего персонажа падать, но не дать ему упасть ниже земли, нам нужно задать такое условие:
«Если координата Y персонажа меньше 240 (240 = 330 – 30 – 60), нужно увеличивать ее».
Для этого в конец метода Update, класса Sprites добавим такой код:
Code
if (rect.Y < 240) rect.Y += 7;
Как Вы видите, наш герой падает. Но падает как «бревно», т.е. без каких либо движений. Давайте добавим ему анимацию падения. Для этого добавим файл с этой анимацией в папку Textures (в разделе Content). Назовем его fall. Теперь перейдем в класс Game1 и создадим объект класса Sprites, который так же назовем fall.
Перед конструктором добавим:
Code
Sprites fall;
Далее в конструкторе инициализируем его:
Code
fall = new Sprites(10); fall.rect = new Rectangle(300, 10, 60, 60);
Далее перейдем в метод LoadContent() и добавим туда:
Code
fall.LoadContent(Content, "Textures//fall");
В метод Update добавим строку
Code
fall.Update(gameTime);
И в метод Draw() после строк
Code
else if (states.IsKeyDown(Keys.Right)) { runAnimation.DrawAnimation(spriteBatch, false, true);
}
Добавим
Code
else if ((fall.isFall) { fall.rect = runAnimation.rect; fall.DrawAnimation(spriteBatch, false, true);
Хотя объявление public полей не является хорошим стилем ООП, но не станем вдаваться в красоту кода, чтобы не тратить время. Ведь цель урока – сделать игровую физику! Но дам совет на будущее: По возможно старайтесь не объявлять public переменных, а пользоваться свойствами (если интересно, в ЛС, хотя, об этом можно прочесть в любой книге по C#)
Далее перейдем в метод Update() (класса Sprites) и изменим строки:
Теперь запустите проект и проверьте, как работает наша анимация:
Ну, с падением мы закончили. Теперь давайте поговорим о самом трудном, а именно о прыжке!
Физика прыжка:
Немного теории:
Итак, теперь давайте поговорим непосредственно о прыжке. Для начала нам нужно придумать алгоритм, по которому мы реализуем данный прыжок. Сначала мы введем переменную, которая будет отвечать за него. Как Вы увидели из предыдущей части статьи, чтобы персонаж падал, нужно увеличивать его Y координату. Соответственно, чтобы наш персонаж подпрыгивал, нужно уменьшать ее.
Но, если мы сделаем условие, в котором при нажатии на клавишу будет уменьшаться Y координата, тогда наш герой просто улетит. Выходом из этой ситуации является такое условие: «Если клавиша нажата, Y координата уменьшается только в течении некоторого промежутка времени».
Как только этот промежуток пройдет, автоматически включится алгоритм падения и наш герой приземлится.
Однако, для того, чтобы невозможно было часто кликать клавишу прыжка и прыгать сколь угодно высоко, мы применим небольшую хитрость. Нажатие клавиши не будет напрямую отвечать за прыжок. Оно лишь будет запускать определенный процесс, который и будет описывать наш прыжок. И пока этот процесс идет (т.е. пока игрок не приземлился на землю), нажатие клавиш на него влиять не будет. Собственно, теперь можно приступать к реализации.
Создание прыжка:
Итак, для начала давайте немного изменим код метода Draw() класса Game1. Замените код, отвечающий за рисование героя, на этот:
Code
spriteBatch.Begin();
if ((fall.isFall)) { fall.rect = runAnimation.rect; fall.DrawAnimation(spriteBatch, false, true);
}
else if (states.IsKeyDown(Keys.Left)) { runAnimation.DrawAnimation(spriteBatch, true, false);
} else if (states.IsKeyDown(Keys.Right)) { runAnimation.DrawAnimation(spriteBatch, false, true); }
Это мы сделали для того, чтобы при падении нельзя было включить анимацию бега. Теперь перейдем в класс Sprites и перед конструктором объявим несколько переменных:
Как Вы уже знаете, первые две отвечают за нажатие клавиш, причем, если их использовать в связке, то нажатие получается однократным. Его мы будем использовать для того, чтобы не приходилось держать клавишу прыжка нажатой в процессе самого прыжка. Следующая переменная (isJump) отвечает за тот самый процесс, который описывался в теоретической части. Так как его запускает нажатие клавиши, пока данная переменная равна false. Последние две переменные отвечают за время, в процессе которого будет происходить наш прыжок. Как Вы можете заметить, последняя переменная отвечает за количество секунд прыжка. В нашем случае герой будет подниматься меньше половины секунды, но Вы свободно можете изменить данное значение, если в Вашей игре, к примеру, очень маленькая гравитация.
Теперь давайте перейдем в метод Update() этого же класса и для начала инициализируем некоторые из вышеобъявленных переменных. В самом начале метода добавьте:
Code
state = Keyboard.GetState();
А в самом конце:
Code
Oldstate = state;
Теперь давайте запустим наш процесс, который описывался в «теории». После строк:
Code
if (next.Left > 0 && next.Right < 600) rect = next;
Добавьте:
Code
if (state.IsKeyDown(Keys.Up) && Oldstate.IsKeyUp(Keys.Up)) { isJump = true; }
Далее мы просто проверим: «Если процесс запущен и количество прошедших секунд не превышает предел (время прыжка еще не вышло), то мы будем увеличивать наше время и делать прыжок. Если же время вышло, а процесс все еще запущен, то нужно его остановить, а так же включить падение». Заменим строки:
Все, можно сказать, что прыжок написан. Нажав кнопку «Вверх», Вы сможете подпрыгнуть. Однако у такого прыжка есть несколько минусов. Одни из них мы устраним сейчас, а вот другой будет Вашим домашним заданием. Первый минус состоит в том, что в какую сторону бы мы не прыгали, анимация падения повернута лишь вправо. Давайте исправим это. Перейдем в класс Game1 и объявим переменную
Code
bool right;
в начале класса.
Далее перейдем в метод Draw() и изменим код рисования спрайта таким образом:
Code
spriteBatch.Begin();
if ((fall.isFall)) { fall.rect = runAnimation.rect; if (right) { fall.DrawAnimation(spriteBatch, false, true); } else fall.DrawAnimation(spriteBatch, true, false); }
else if (states.IsKeyDown(Keys.Left)) { runAnimation.DrawAnimation(spriteBatch, true, false);
right = false;
} else if (states.IsKeyDown(Keys.Right)) { runAnimation.DrawAnimation(spriteBatch, false, true); right = true;
Этим мы говорим, что «Если персонаж повернут вправо, рисовать его в правую сторону. Иначе рисовать в левую». Второй минус состоит в том, что при подпрыгивании наш герой не изменяет свой спрайт, а рисуется в обычной стойке. Я полагаю, что если Вы последовательно изучали все мои статьи, то сможете выполнить это задание. Вам нужно добавить новый спрайт в игру и создать условие: «Если персонаж подпрыгнул, рисовать спрайт подпрыгивания». Желаю удачи!
Послесловие:
Если Вы читаете эту заключительную часть статьи, то хочу поблагодарить Вас за то, что Вы не пожалели своего времени и все же прочитали этот урок. Если у Вас будут какие-то проблемы с созданием 2D физики платформера в XNA – обращайтесь. Помогу по мере сил. Надеюсь, что этот урок помог вам в освоении этой не совсем простой, но очень интересной среды разработки!
Также если вы считаете, что данный материал мог быть интересен и полезен кому-то из ваших друзей, то вы бы могли посоветовать его, отправив сообщение на e-mail друга:
Игровые объявления и предложения:
Если вас заинтересовал материал «XNA для начинающих: игровая физика», и вы бы хотели прочесть что-то на эту же тему, то вы можете воспользоваться списком схожих материалов ниже. Данный список сформирован автоматически по тематическим меткам раздела.
Предлагаются такие схожие материалы:
Если вы ведёте свой блог, микроблог, либо участвуете в какой-то популярной социальной сети, то вы можете быстро поделиться данной заметкой со своими друзьями и посетителями.
Фил, проще сначала немного разобраться в теме... Во-первых, в данной статье и уроке с того сайта, кроме спрайтов, нет ничего общего. Код различается полностью, как теория, так и реализация. А во-вторых, данные спрайты - исходники самого популярного стартер-кита на XNA, который входит в поставку XNA GS. Код и спрайты из этого стартер-кита бесплатно доступен любому разработчику, поэтому я не вижу причин, почему бы мне не использовать эти спрайты. Видимо, автор уроков из ссылки посчитал так же...
В каком смысле? В любой игре известны и пол и потолок =). Даже если позиция всех блоков рандомна, то пишется алгоритм сзаимодействия с каждым, а далее проверяется пересечение. Так не бывает, чтобы небыло известно ничего
Но, кажется я понял, что ты имеешь ввиду. Это все будет в статье: "Взаимодействие с картой"
ПОправляю себя же: Да, с границами экрана оговорился. А вот пол и потолок не всегда в одном месте находятся, и проверять столкновение с полом как "if y > 400" - не самый хороший способ, особенно если он "многоэтажный". Да, ты всё правильно : )
Кстати, хочу спросить совета у читателей данной серии статей. Что писать в следующем уроке? Реализацию движения камеры, так сказать, удлиннение уровня или взаимодействие героя с уровнем? Эти статьи будут точно написаны, но вот в каком порядке? Что Вам более необходимо?
P.S. В статье про камеру постараюсь затронуть тему матриц, что будет полезно и в 3D
Благодарю за Ваше мнение. Хотя, "Удлинение" и "Камеру" я планирую объединить в одну статью . Не люблю писать статьи по двадцать слов и две строчки кода
Плоховенько, лучше бы нормальную физику создал, но видимо не можешь. Таким мотивом в виду камеры будешь использовать перемещение всех объектов в Update. Не впечатлило, начинать надо с менеджера экранов.
Статья ориентирована в большей степени на новичков и поэтому хорошая физика тут ни к чему. Главное, что оно работает и выглядит, как надо. А при помощи какой формулы это реализовано - не суть. Но, если Вы можете подсказать более простой и понятный вариант, буду рад выслушать.
А на счет камеры - нет, конечно. Есть такая замечательная вещь, как матрица переноса и матрица вида. Попробую что-нибудь с ними сделать
P.S. на счет "не можешь", тут не соглашусь =). В девятом классе учился, пятерку по физике имел =). Но зачем грузить новичков формулами? Я просто показываю самый простой и понятный вариант =)
Не нравится мне, когда статьи для новичков пишутся новичками, нет советов со ссылкой на опыт.
Про камеру ладно, но вот такой подход к физике очень и очень не правильный, потом этим новичкам приходится переучиваться. Проще объяснить новичку удобную вещь, объяснить принципы программирования игр - дать почву для роста, а этот пример баловство. Знакомство с XNA надо начинать с других вещей. (имхо)