Pull to refresh

Пишем игры для Windows Phone

Game development *

Не так давно публике была представлена платформа 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

Статья участвует в Конкурсе
И пожалуйста, проголосуйте за статью Здесь
Tags:
Hubs:
Total votes 131: ↑106 and ↓25 +81
Views 3.5K
Comments Comments 18