На днях увидел этот пост со списком материалов по разработке под Windows Phone, и, к сожалению, не встретил там ни одной статьи по разработке приложений с использованием как Silverlight, так и XNA одновременно. Эта замечательная возможность для разработчиков появилась с приходом Mango.
Мне захотелось заполнить этот пробел и рассказать вам о следующем:
Пример из этой статьи может быть знаком посетителям первого потока вечерней школы Стаса Павлова.
Давайте сначала посмотрим, что нам даёт использование Silverlight и XNA в одном приложении.
В первую очередь, у разработчиков игр появились следующие возможности:
Причем стоит заметить, что не все страницы должны совмещать в себе и Silverlight и XNA. Так же хочу обратить внимание на то, что эта возможность пригодится не только разработчикам игр. Например, XNA имеет замечательную встроенную поддержку жестов и очень удобные методы для работы с текстурами. Пример замечательного неигрового приложения, использующего Silverlight и XNA – Holiday Photo.
В качестве примера я выбрал очень известную математическую игру Жизнь, о которой неоднократно писали на Хабре (1, 2, 3 и многие другие). Если вкратце, то игра является «zero-player game», это означает, что игрок устанавливает только начальное состояние. Далее состояние каждой клетки в каждом поколении рассчитывается исходя из состояний восьми окружающих её клеток по довольно простым правилам.
Сразу хочу оговориться, что я использую самый простой алгоритм, использующий 2 массива, но это ведь и не пост об алгоритмах и структурах данных. Так что реализация оптимального и быстрого алгоритма будет одним из ваших домашних заданий.
При создании нового проекта необходимо выбрать «Windows Phone Silverlight and XNA Application». Создастся новый проект с 2мя страницами MainPage и GamePage.
MainPage — обычная страница Silverlight-приложения, на которой располагается кнопка «Change to game page», при нажатии на которую, соответственно, открывается страница GamePage.
GamePage — как раз наша страница с XNA. Если посмотреть на содержание её xaml файла, то вместо разметки страницы там будет всего одна строчка:
Таким образом, когда мы захотим добавить контролы на эту страницу нам нужно будет заменить этот комментарий нашим кодом, давайте это и сделаем, разместив кнопку Start.
Давайте теперь запустим наш проект, нажмем кнопку "Change to game page" и увидим лишь голубой экран, нашей кнопки нигде нет. Но в этом нет ничего страшного, сейчас мы это исправим.
Откроем файл GamePage.xaml.cs, в котором и находится вся логика нашего XNA приложения. Для отображения Silverlight элементов нам необходимо создать специальный объект UIElementRenderer:
Теперь в цикле рисования нам необходимо рендерить и отрисовывать интерфейс:
Как вы могли заметить, мы создали объект uiRenderer, но нигде не инициализировали его. Есть несколько точек зрения о том, как это лучше делать. Я предпочитаю отлавливать обновления нашего интерфейса и проверять наличие рендерера, и при его отсутствии или несоответствии – создавать заново:
GamePage.xaml
GamePage.xaml.cs
Теперь снова запустим наш проект. Как вы видите, наша кнопка теперь появилась на нужном месте. Давайте теперь нарисуем игровое поле.
Давайте договоримся, что наше поле будет размером 100x100 клеток, каждая клетка 30x30 пикселей. Итого наше поле будет 3000x3000 пикселей.
Создадим класс Area:
В XNA нет возможности нарисовать линию, зато можно нарисовать прямоугольник, залитый любым цветом или текстурой. Здесь мы создаем 100 горизонтальных и 100 вертикальных линий, залитых текстурой point, содержащей черную точку размером 1x1.
Теперь давайте создадим и инициализируем наше поле в GamePage.xaml.cs:
При совместном использовании XNA и Silverlight нет необходимости в использовании отдельного метода для загрузки контента, как это было в чистом XNA приложении (LoadContent). В общем, вы можете загружать игровой контент когда хотите, однако вы должны убедиться, что во время загрузки графического контента приложение находится в режиме XNA рендеринга (вызывая метод SetSharingMode). Если вы попытаетесь загрузить графический контент во время Silverlight-рендеринга, то это вызовет исключение. Не графический контент вы можете загружать в любое время, когда у вас создан ContentManager. Я обычно загружаю контент в методе OnNavigatedTo:
Теперь отрисуем наше поле в методе OnDraw:
Как вы можете видеть, я отрисовываю поле в отдельном spriteBatch. Почему это именно так – я расскажу немного позже.
Запустим наше приложение: теперь мы можем полюбоваться нашим полем с кнопкой.
Сделаем небольшие косметические изменения: добавим полупрозрачную подложку под кнопку и сделаем поле белым.
GamePage.xaml:
GamePage.xaml.cs:
XNA Framework поддерживает 10 различных жестов, рассказ о них заслуживает отдельной статьи (которую, если желаете, напишу). В нашем же приложении мы будем использовать 2 вида жестов: Tap (нажатие) и FreeDrag (свободное перемещение).
Давайте начнем с добавления точек на наше поле. Я не хочу приводить полный код класса Dots здесь, вы можете скачать пример внизу статьи и посмотреть его самостоятельно. Нас же сейчас будет интересовать только метод AddDot, который используется для добавления точек:
В этот метод мы передаем координаты точки касания, и на её основании рассчитываем нужную ячейку в матрице. Всё просто.
Для начала нам необходимо включить поддержку жестов в методе OnNavigatedTo():
Теперь можно их отслеживать в цикле OnUpdate:
Не забудьте вывести наши живые клетки на экран в методе OnDraw():
Если вы сейчас запустите приложение и попробуете добавить точку, то она мгновенно исчезнет. Причина этого – скорость обновления. Необходимо реализовать некоторую задержку для вызова dots.Update(). У меня в примере самая простейшая реализация этой задержки:
Где IsGameStarted – флаг, который меняет значение при нажатии на кнопку Start. Второе домашнее задание – реализовать задержку, используя класс GameTime.
Теперь у нас есть уже вполне работоспособная игра, но мы видим лишь небольшую часть поля. Давайте это исправим, добавив возможность перемещаться по полю.
В простейшем приближении камера – некоторая матрица, с помощью которой мы проецируем наш игровой мир на экран в виде изображения. Класс Matrix имеет множество соответствующих методов, например CreateScale(), CreateTranslation(), CreateRotationX(). Как раз CreateTranslation() нам и понадобится. Сначала узнаем, на сколько нам необходимо сдвинуть наш игровой мир и создадим матрицу:
Затем необходимо отрисовать новую проекцию. Для этого необходимо передать нашу матрицу в spriteBatch:
Именно поэтому нам понадобился второй spriteBatch: если бы мы всё отрисовывали в одном – наша кнопка тоже двигалась бы с полем, что нам не нужно. Домашнее задание – посмотреть что будет, если разместить отрисовку игры и интерфейса в одном spriteBatch.
Осталась одна проблема – мы не учитываем сдвиг нашей камеры при добавлении точек. Это решается просто, в метод AddDot() необходимо передавать ещё totalShift:
Теперь точки будут добавляться в нужных нам местах. Домашнее задание: при попытке добавить живую клетку за пределами поля будет всё равно неправильная реакция: либо добавление в зеркальной точке, либо выход за границы массива. Сделайте так, чтобы нельзя было выйти за границы поля.
Чеширский кот
Собственно, на этом всё. У вас есть вполне работоспособная игра Жизнь для Windows Phone и домашнее задание, над которым можно поломать голову пару-тройку часов. Так же можно сделать различные плюшки, типа счетчика поколений, рандомного заполнения и многое другое. Мою финальную версию вы можете найти в Marketplace под названием SilverLife.
Скачать исходники приложения.
Всем хорошо провести новогодние каникулы!
Мне захотелось заполнить этот пробел и рассказать вам о следующем:
- Использование Silverlight и XNA на одной странице
- Простейшая обработка жестов в XNA
- Основы работы с камерой в XNA
Пример из этой статьи может быть знаком посетителям первого потока вечерней школы Стаса Павлова.
Давайте сначала посмотрим, что нам даёт использование Silverlight и XNA в одном приложении.
Что это даёт разработчику?
В первую очередь, у разработчиков игр появились следующие возможности:
- Быстрое создание UI с использованием контролов из Silverlight
- Удобная навигация между страницами, используя Navigation Service
- Использование WebClient для интеграции различных социальных сервисов
- Создание интерфейса в Expression Blend
Причем стоит заметить, что не все страницы должны совмещать в себе и Silverlight и XNA. Так же хочу обратить внимание на то, что эта возможность пригодится не только разработчикам игр. Например, XNA имеет замечательную встроенную поддержку жестов и очень удобные методы для работы с текстурами. Пример замечательного неигрового приложения, использующего Silverlight и XNA – Holiday Photo.
Howto: разработка первого приложения на Silverlight+XNA
В качестве примера я выбрал очень известную математическую игру Жизнь, о которой неоднократно писали на Хабре (1, 2, 3 и многие другие). Если вкратце, то игра является «zero-player game», это означает, что игрок устанавливает только начальное состояние. Далее состояние каждой клетки в каждом поколении рассчитывается исходя из состояний восьми окружающих её клеток по довольно простым правилам.
Сразу хочу оговориться, что я использую самый простой алгоритм, использующий 2 массива, но это ведь и не пост об алгоритмах и структурах данных. Так что реализация оптимального и быстрого алгоритма будет одним из ваших домашних заданий.
Шаг 1. Создание проекта и рендеринг интерфейса
При создании нового проекта необходимо выбрать «Windows Phone Silverlight and XNA Application». Создастся новый проект с 2мя страницами MainPage и GamePage.
MainPage — обычная страница Silverlight-приложения, на которой располагается кнопка «Change to game page», при нажатии на которую, соответственно, открывается страница GamePage.
GamePage — как раз наша страница с XNA. Если посмотреть на содержание её xaml файла, то вместо разметки страницы там будет всего одна строчка:
<!--No XAML content is required as the page is rendered entirely with the XNA Framework-->
Таким образом, когда мы захотим добавить контролы на эту страницу нам нужно будет заменить этот комментарий нашим кодом, давайте это и сделаем, разместив кнопку Start.
<Grid Name="Layout" LayoutUpdated="Layout_LayoutUpdated">
<Button Content="Start" Height="71" Name="button1" Width="160" Margin="25,717,295,12" Click="button1_Click" />
</Grid>
Давайте теперь запустим наш проект, нажмем кнопку "Change to game page" и увидим лишь голубой экран, нашей кнопки нигде нет. Но в этом нет ничего страшного, сейчас мы это исправим.
Откроем файл GamePage.xaml.cs, в котором и находится вся логика нашего XNA приложения. Для отображения Silverlight элементов нам необходимо создать специальный объект UIElementRenderer:
UIElementRenderer uiRenderer;
Теперь в цикле рисования нам необходимо рендерить и отрисовывать интерфейс:
private void OnDraw(object sender, GameTimerEventArgs e)
{
SharedGraphicsDeviceManager.Current.GraphicsDevice.Clear(Color.White);
uiRenderer.Render();
spriteBatch.Begin();
spriteBatch.Draw(uiRenderer.Texture, Vector2.Zero, Color.White);
spriteBatch.End();
// TODO: Add your drawing code here
}
Как вы могли заметить, мы создали объект uiRenderer, но нигде не инициализировали его. Есть несколько точек зрения о том, как это лучше делать. Я предпочитаю отлавливать обновления нашего интерфейса и проверять наличие рендерера, и при его отсутствии или несоответствии – создавать заново:
GamePage.xaml
<Grid Name="Layout" LayoutUpdated="Layout_LayoutUpdated">
GamePage.xaml.cs
private void Layout_LayoutUpdated(object sender, EventArgs e)
{
int width = (int)ActualWidth;
int height = (int)ActualHeight;
// Ensure the page size is valid
if (width <= 0 || height <= 0)
return;
// Do we already have a UIElementRenderer of the correct size?
if (uiRenderer != null &&
uiRenderer.Texture != null &&
uiRenderer.Texture.Width == width &&
uiRenderer.Texture.Height == height)
{
return;
}
// Before constructing a new UIElementRenderer, be sure to Dispose the old one
if (uiRenderer != null)
uiRenderer.Dispose();
// Create the renderer
uiRenderer = new UIElementRenderer(this, width, height);
}
Теперь снова запустим наш проект. Как вы видите, наша кнопка теперь появилась на нужном месте. Давайте теперь нарисуем игровое поле.
2. Вывод XNA
Давайте договоримся, что наше поле будет размером 100x100 клеток, каждая клетка 30x30 пикселей. Итого наше поле будет 3000x3000 пикселей.
Создадим класс Area:
public class Area
{
Texture2D point;
Rectangle line;
public Area(Texture2D point)
{
this.point = point;
}
public void Draw(SpriteBatch spriteBatch)
{
for (int i = 0; i < 100; i++)
{
line = new Rectangle(i*30, 0, 1, 3000);
spriteBatch.Draw(point, line, Color.White);
line = new Rectangle( 0, i*30, 3000, 1);
spriteBatch.Draw(point, line, Color.White);
}
}
}
В XNA нет возможности нарисовать линию, зато можно нарисовать прямоугольник, залитый любым цветом или текстурой. Здесь мы создаем 100 горизонтальных и 100 вертикальных линий, залитых текстурой point, содержащей черную точку размером 1x1.
Теперь давайте создадим и инициализируем наше поле в GamePage.xaml.cs:
Texture2D point;
Area area;
При совместном использовании XNA и Silverlight нет необходимости в использовании отдельного метода для загрузки контента, как это было в чистом XNA приложении (LoadContent). В общем, вы можете загружать игровой контент когда хотите, однако вы должны убедиться, что во время загрузки графического контента приложение находится в режиме XNA рендеринга (вызывая метод SetSharingMode). Если вы попытаетесь загрузить графический контент во время Silverlight-рендеринга, то это вызовет исключение. Не графический контент вы можете загружать в любое время, когда у вас создан ContentManager. Я обычно загружаю контент в методе OnNavigatedTo:
protected override void OnNavigatedTo(NavigationEventArgs e)
{
// Set the sharing mode of the graphics device to turn on XNA rendering
SharedGraphicsDeviceManager.Current.GraphicsDevice.SetSharingMode(true);
// Create a new SpriteBatch, which can be used to draw textures.
spriteBatch = new SpriteBatch(SharedGraphicsDeviceManager.Current.GraphicsDevice);
// TODO: use this.content to load your game content here
point = contentManager.Load<Texture2D>("point");
area = new Area(point);
// Start the timer
timer.Start();
base.OnNavigatedTo(e);
}
Теперь отрисуем наше поле в методе OnDraw:
private void OnDraw(object sender, GameTimerEventArgs e)
{
SharedGraphicsDeviceManager.Current.GraphicsDevice.Clear(Color.CornflowerBlue);
uiRenderer.Render();
spriteBatch.Begin();
area.Draw(spriteBatch)
spriteBatch.End();
spriteBatch.Begin();
spriteBatch.Draw(uiRenderer.Texture, Vector2.Zero, Color.White);
spriteBatch.End();
// TODO: Add your drawing code here
}
Как вы можете видеть, я отрисовываю поле в отдельном spriteBatch. Почему это именно так – я расскажу немного позже.
Запустим наше приложение: теперь мы можем полюбоваться нашим полем с кнопкой.
Сделаем небольшие косметические изменения: добавим полупрозрачную подложку под кнопку и сделаем поле белым.
GamePage.xaml:
<Grid Name="Layout" LayoutUpdated="Layout_LayoutUpdated">
<Rectangle Height="100" HorizontalAlignment="Left" Margin="0,700,0,0" Name="rectangle1" Stroke="Black" StrokeThickness="1" VerticalAlignment="Top" Width="480" Fill="#B1000000" />
<Button Content="Start" Height="71" Name="button1" Width="160" Margin="25,717,295,12" Click="button1_Click" />
<Button Content="1 gen" Height="72" HorizontalAlignment="Left" Margin="254,716,0,0" Name="button2" VerticalAlignment="Top" Width="160" Click="button2_Click" />
</Grid>
GamePage.xaml.cs:
private void OnDraw(object sender, GameTimerEventArgs e)
{
SharedGraphicsDeviceManager.Current.GraphicsDevice.Clear(Color.White);
uiRenderer.Render();
...
3. Работа с жестами
XNA Framework поддерживает 10 различных жестов, рассказ о них заслуживает отдельной статьи (которую, если желаете, напишу). В нашем же приложении мы будем использовать 2 вида жестов: Tap (нажатие) и FreeDrag (свободное перемещение).
Давайте начнем с добавления точек на наше поле. Я не хочу приводить полный код класса Dots здесь, вы можете скачать пример внизу статьи и посмотреть его самостоятельно. Нас же сейчас будет интересовать только метод AddDot, который используется для добавления точек:
public void AddDot(int x, int y, Vector2 shift)
{
DotX = (int)(x / DotSize );
DotY = (int)(y / DotSize );
DotsNow[DotX, DotY] = !DotsNow[DotX, DotY];
}
В этот метод мы передаем координаты точки касания, и на её основании рассчитываем нужную ячейку в матрице. Всё просто.
Для начала нам необходимо включить поддержку жестов в методе OnNavigatedTo():
TouchPanel.EnabledGestures = GestureType.Tap | GestureType.FreeDrag;
Теперь можно их отслеживать в цикле OnUpdate:
private void OnUpdate(object sender, GameTimerEventArgs e)
{
while (TouchPanel.IsGestureAvailable)
{
GestureSample gesture = TouchPanel.ReadGesture();
if (gesture.GestureType == GestureType.Tap)
{
dots.AddDot((int)gesture.Position.X, (int)gesture.Position.Y);
}
}
}
Не забудьте вывести наши живые клетки на экран в методе OnDraw():
dots.Draw(spriteBatch);
Если вы сейчас запустите приложение и попробуете добавить точку, то она мгновенно исчезнет. Причина этого – скорость обновления. Необходимо реализовать некоторую задержку для вызова dots.Update(). У меня в примере самая простейшая реализация этой задержки:
i++;
if (IsGameStarted && Math.IEEERemainder(i, 15) == 0)
{
i = 0;
dots.Update();
}
Где IsGameStarted – флаг, который меняет значение при нажатии на кнопку Start. Второе домашнее задание – реализовать задержку, используя класс GameTime.
Теперь у нас есть уже вполне работоспособная игра, но мы видим лишь небольшую часть поля. Давайте это исправим, добавив возможность перемещаться по полю.
4. Работа с камерой
В простейшем приближении камера – некоторая матрица, с помощью которой мы проецируем наш игровой мир на экран в виде изображения. Класс Matrix имеет множество соответствующих методов, например CreateScale(), CreateTranslation(), CreateRotationX(). Как раз CreateTranslation() нам и понадобится. Сначала узнаем, на сколько нам необходимо сдвинуть наш игровой мир и создадим матрицу:
while (TouchPanel.IsGestureAvailable)
{
GestureSample gesture = TouchPanel.ReadGesture();
if (gesture.GestureType == GestureType.Tap)
{
dots.AddDot((int)gesture.Position.X, (int)gesture.Position.Y, totalShift);
}
if (gesture.GestureType == GestureType.FreeDrag)
{
shift = gesture.Delta;
totalShift += shift;
}
}
matrix *= Matrix.CreateTranslation(totalShift.X, totalShift.Y, 0);
Затем необходимо отрисовать новую проекцию. Для этого необходимо передать нашу матрицу в spriteBatch:
spriteBatch.Begin(SpriteSortMode.Deferred, null, null, null, null, null, matrix);
area.Draw(spriteBatch);
dots.Draw(spriteBatch);
spriteBatch.End();
Именно поэтому нам понадобился второй spriteBatch: если бы мы всё отрисовывали в одном – наша кнопка тоже двигалась бы с полем, что нам не нужно. Домашнее задание – посмотреть что будет, если разместить отрисовку игры и интерфейса в одном spriteBatch.
Осталась одна проблема – мы не учитываем сдвиг нашей камеры при добавлении точек. Это решается просто, в метод AddDot() необходимо передавать ещё totalShift:
public void AddDot(int x, int y, Vector2 shift)
{
DotX = (int)(Math.Abs(x-shift.X) / DotSize );
DotY = (int)(Math.Abs(y-shift.Y) / DotSize );
DotsNow[DotX, DotY] = !DotsNow[DotX, DotY];
}
Теперь точки будут добавляться в нужных нам местах. Домашнее задание: при попытке добавить живую клетку за пределами поля будет всё равно неправильная реакция: либо добавление в зеркальной точке, либо выход за границы массива. Сделайте так, чтобы нельзя было выйти за границы поля.
Чеширский кот
Заключение
Собственно, на этом всё. У вас есть вполне работоспособная игра Жизнь для Windows Phone и домашнее задание, над которым можно поломать голову пару-тройку часов. Так же можно сделать различные плюшки, типа счетчика поколений, рандомного заполнения и многое другое. Мою финальную версию вы можете найти в Marketplace под названием SilverLife.
Скачать исходники приложения.
Всем хорошо провести новогодние каникулы!