Не так давно публике была представлена платформа Windows Phone. Платформа очень интересная в плане разработки, т.к. присутствует поддержка .Net Framework, мультизадачность и XNA Framework, причем отличия у XNA от десктопной версии минимальны.
Вначале ложка дегтя: выяснилось, что пока на Windows Phone не будет полноценной поддержки собственных шейдеров, но обещается ряд предустановленных с широкими возможностями настройками. Что-ж, не будем огорчаться, всему свое время.
Для работы нам понадобится Windows Phone Developer Tools
Перед тем как читать дальше рекомендую просмотреть статьи по ссылкам, в них содержатся базовые сведения по архитектуре игр на XNA:
Итак, создаем новое решение:
Начнем с вывода текста на экран, для этого добавим в проект новый файл шрифта:
Откроем этот файл и добавим в секцию запись для кириллицы:
<CharacterRegions>
<CharacterRegion>
<Start> </Start>
<End>~</End>
</CharacterRegion>
<CharacterRegion>
<Start>А</Start>
<End>я</End>
</CharacterRegion>
</CharacterRegions>
Также увеличим размер шрифта.
Код загрузки шрифта и вывод текста на экран:
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>("Arial");
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin();
spriteBatch.DrawString(font, "Привет Хабрахабр!", new Vector2(120, 400), Color.White);
spriteBatch.End();
base.Draw(gameTime);
}
Поворот экрана
Но вот какая незадача: ориентация экрана у нас не переключается при смене ориентации девайса в пространстве, а играть на экранчике с соотношением сторон 480/800 не очень удобно. Пока в XNA отсутствуют средства для разворота экрана, придется выкручиваться самим. В этом нам поможет RenderTarget2D. Фактически это текстура заданного нами размера, в которую мы можем рисовать, а потом так же выводить на экран. Изменим функцию Draw: теперь отрисовка спрайтов идет не на экран, а в renderTarget.
protected override void Initialize()
{
base.Initialize();
renderTarget = new RenderTarget2D(GraphicsDevice, 800, 480, false, SurfaceFormat.Color, DepthFormat.Depth24Stencil8);
}
private void DrawSprites(GameTime gameTime) //В этой функции мы рисуем в renderTarget размером 800x480
{
spriteBatch.Begin();
spriteBatch.DrawString(font, "Привет Хабрахабр!", new Vector2(40, 40), Color.White);
spriteBatch.End();
}
protected override void Draw(GameTime gameTime)
{
GraphicsDevice.SetRenderTarget(renderTarget);
GraphicsDevice.Clear(Color.Black); //Очищаем renderTarget
DrawSprites(gameTime); //Рисуем в него
GraphicsDevice.SetRenderTarget(null); //Возвращаем стандартный
GraphicsDevice.Clear(Color.Black);
spriteBatch.Begin(); //И здесь рисуем повернутую и отраженную текстуру из нашего renderTarget
spriteBatch.Draw((Texture2D)renderTarget, Vector2.Zero, null, Color.White, -MathHelper.PiOver2, new Vector2(800, 0), 1, SpriteEffects.FlipHorizontally | SpriteEffects.FlipVertically, 0);
spriteBatch.End();
base.Draw(gameTime);
}
Что мы получили:
Наша «игра» определенно нуждается в стартовой заставке: добавим к проекту title.png. Мы будем выводить эту картинку на экран 4 секунды после запуска игры.
Texture2D title;
protected override void LoadContent()
{
spriteBatch = new SpriteBatch(GraphicsDevice);
font = Content.Load<SpriteFont>("Arial");
title = Content.Load<Texture2D>("Title");
}
private void DrawSprites(GameTime gameTime)
{
spriteBatch.Begin();
if (gameTime.TotalGameTime.TotalSeconds > 4)
spriteBatch.DrawString(font, "Привет Хабрахабр!", new Vector2(40, 40), Color.White);
else
spriteBatch.Draw(title, Vector2.Zero, Color.White);
spriteBatch.End();
}
3D графика
Нам понадобится класс камеры, отвечающий за работу с матрицами и обработку пользовательского ввода, про создание камеры можете почитать на gamedev.ru. В рамках этой статьи интерес представляет только функция UpdateTouch, отвечающая за работу с сенсорным вводом. Исходный код камеры целиком можно посмотреть по этой ссылке. Рекомендую выносить логику, классы для хранения данных в отдельную Dll. Для этого добавим к решению новый проект «WindowsPhoneGameDemoObjects»
private void UpdateTouch(GameTime gameTime)
{
TouchCollection tc = TouchPanel.GetState(); //Получаем все касания
if (tc.Count > 0)
{
TouchLocation current = tc[0]; //Получаем первое касание
TouchLocation prev;
if (current.TryGetPreviousLocation(out prev)) //Если это касание было и в прошлом цикле обновления
{
//То рассчитываем приращения углов
Angle.X -= MathHelper.ToRadians((current.Position.X - prev.Position.X) * turnSpeed); // pitch
Angle.Y += MathHelper.ToRadians((current.Position.Y - prev.Position.Y) * turnSpeed); // yaw
while (Angle.Y > MathHelper.Pi * 2)
Angle.Y -= MathHelper.Pi * 2;
while (Angle.Y < -MathHelper.Pi * 2)
Angle.Y += MathHelper.Pi * 2;
if (Angle.X > maxPitch)
Angle.X = maxPitch;
if (Angle.X < -maxPitch)
Angle.X = -maxPitch;
float time = (float)gameTime.ElapsedGameTime.TotalSeconds;
Vector3 forward = -Vector3.Normalize(new Vector3((float)Math.Sin(-Angle.Y),
(float)Math.Sin(Angle.X),
(float)Math.Cos(-Angle.Y)));
if (DenyVerticalMovement)
{
forward = new Vector3(forward.X, 0, forward.Z);
forward.Normalize();
}
Position += forward * movementSpeed * time;
}
else
touchStartTime = gameTime;
}
}
Отмечу, что камера унаследована от DrawableGameComponent — т.е. если мы добавим камеру в список компонентов нашей игры (this.Components.Add(camera);) то метод Update будет вызываться автоматически. Напишем классы объекта и сцены и добавим в проект модель, которую будем рендерить.
public class Entity
{
public Matrix World //Матрица мира объкта
{
get
{
return Matrix.CreateScale(Scale)
* Matrix.CreateRotationY(Rotation.Y)
* Matrix.CreateRotationX(Rotation.X)
* Matrix.CreateRotationZ(Rotation.Z)
* Matrix.CreateTranslation(Position)
;
}
}
public Model Model; //Модель объекта
public Vector3 Scale = Vector3.One; //Названия переменных говорят сами за себя :)
public Vector3 Rotation;
public Vector3 Position;
public void Draw(Camera camera)
{
Matrix[] transforms = new Matrix[Model.Bones.Count]; //Получаем матрицы трансформаций костей модели
Model.CopyAbsoluteBoneTransformsTo(transforms);
foreach (ModelMesh mesh in Model.Meshes) //И в цикле отрисовываем все элементы модели
{
foreach (BasicEffect be in mesh.Effects)
{
be.EnableDefaultLighting();
be.PreferPerPixelLighting = true;
be.Projection = camera.Projection;
be.View = camera.View;
be.World = World * mesh.ParentBone.Transform;
}
mesh.Draw();
}
}
}
public class Scene
{
public List<Entity> Entities = new List<Entity>();
public void Load(ContentManager content)
{
Entities.Add(new Entity() { Model = content.Load<Model>("Scene\\Scene") });
}
public void Draw(Camera camera)
{
foreach (Entity e in Entities)
e.Draw(camera);
}
}
Практически все готово, осталось лишь решить одну неприятную проблему. Дело в том, что объект SpriteBatch ведет себя некультурно — меняет настройки рендеринга, а после отрисовки не возвращает исходные. Чтобы решить эту проблему будем сохранять интересующие нас настройки и устанавливать их обратно после завершения работы SpriteBatch-а.
Классно? Да, но не каждому игроку захочется ощущать себя бесплотной тенью, нам не хватает коллизий. А что нужно чтобы обрабатывать коллизии? Правильно, геометрия. Только брать ее на ходу из данных моделей не очень-то удобно, поэтому напишем свой ContentProcessor. Это штука, которая специальным образом обрабатывает во время сборки проекта игровые ресурсы.
В этом ContentProcessor-е мы будем вычленять из моделей данные о геометрии и хранить их отдельно. Добавим к решению новый проект «Content Pipeline Extension Library», и напишем класс ModelCollisionProcessor, отвечающий за обработку 3D моделей, Данный код взят из msdn-овских примеров, вся работа которую он выполняет — добавление к модели списка полигонов. Список полигонов модели хранится в ее свойстве Tag. В этом свойстве мы можем хранить произвольные данные.
Чтобы задействовать написанный нами ContentProcessor нужно добавить ссылку на CollisionContentProcessor для проекта ресурсов игры и на вкладке свойств для каждой 3Д-модели в проекте установить значение ContentProcessor в ModelCollisionProcessor.
Теперь, когда у нас есть данные о геометрии надо с ними что-то делать. Напишем класс для обработки столкновений. Я писал данный класс руководствуясь этой статьей на gamedev.ru.
Создадим новый класс GroundCamera и унаследуем его от Camera, принцип работы таков: по земле у нас катится шар для которого мы обрабатываем коллизии, а на некоторой дистанции от него сверху приделана наша камера.
Заменим Camera на GroundCamera в Game1.cs, в методе Update будем вызывать функцию обработки столкновений. Для большей наглядности добавим в сцену еще один объект — детскую площадку. Ну и напоследок напишем простенький skybox.
Что у нас получилось в итоге:
Скачать исходники к статье.
Что осталось за рамками этой статьи:
- Звук
- Шейдеры
- Анимации
- Мультитач
- ИИ
Если эта тема будет интересна хабрасообществу, обещаю написать еще парочку статей по XNA на Windows Phone.
Что почитать:
http://creators.xna.com
http://blogs.msdn.com/shawnhar/default.aspx
http://gamedev.ru
Статья участвует в Конкурсе
И пожалуйста, проголосуйте за статью Здесь