Статьи

Разработка игр для Windows Phone 7: создание Tic-Tac-Toe с XNA

XNA — это инфраструктура создания игр высокого уровня для устройств Microsoft, включая ПК с Windows, Xbox 360 и совершенно новую операционную систему Windows Phone 7. В предыдущем уроке мы рассмотрели основы инфраструктуры XNA, включая обработку ввода и отображение спрайтов. В этой статье вы узнаете, как объединить эти навыки с собственной игровой идеей, чтобы создать что-то интересное и легкое в освоении.

В этом уроке вы создадите простую игру в крестики-нолики, в которую можно играть с друзьями. Крестики-нолики — это простая игра, в которой два игрока поочередно размещают персонажей на сетке три на три. Первый игрок использует O, а второй игрок использует X. Чтобы выиграть игру, игрок должен расположить трех своих персонажей в ряд, столбец или диагональ.

Хотя эта игра проста, для ее создания требуется несколько различных навыков. Во-первых, вы будете использовать XNA и его графические возможности для создания своей игры. Таким образом, вы должны быть знакомы с отображением спрайтов. Во-вторых, вам нужно знать, как обрабатывать прикосновения к сенсорной панели телефона. К счастью, XNA предоставляет высокоуровневый API для доступа к этим штрихам. Наконец, вам нужно применить немного знаний программирования и логики, чтобы определить победителя. В этом уроке многое из этого будет вам дано. При создании игр в будущем вам придется придумывать свои собственные идеи и логику.

Для начала убедитесь, что вы установили новейшие средства разработки для Windows Phone 7. Если вы не обновляли свои инструменты со времени последнего учебника WP7 по MobileTuts, вам следует посетить Центр загрузки Microsoft и получить версию RTW . Эта последняя версия поставляется с финальным эмулятором, который демонстрирует, как ваши приложения будут работать на аппаратном обеспечении в день выпуска.

Убедившись, что ваши инструменты обновлены, откройте Visual Studio 2010 и нажмите ссылку «Новый проект…» на левой боковой панели. В появившемся диалоговом окне выберите «XNA Game Studio 4» в левом столбце и убедитесь, что шаблон «Windows Phone Game (4.0)» выбран справа. Дайте вашему проекту соответствующее имя, например «TicTacToe», и убедитесь, что установлен флажок «Создать каталог для решения». Если вы все сделали правильно, ваше диалоговое окно должно соответствовать следующему изображению:

Диалог создания игрового проекта Windows Phone 7

Нажмите «ОК», чтобы создать новый проект. Visual Studio 2010 сгенерирует необходимые файлы в указанном вами каталоге и откроет Game1.cs для редактирования.

Поскольку вы будете использовать спрайты для всей игровой графики в этом проекте, вам необходимо импортировать необходимые элементы в ваш проект. В загрузке, сопровождающей это руководство, вы найдете каталог Media содержащий множество изображений. В обозревателе решений в правой части экрана найдите проект Content (называемый TicTacToeContent) и щелкните его правой кнопкой мыши. В контекстном меню выберите «Добавить> Существующий элемент…». Когда откроется диалоговое окно, перейдите в папку « Media », разархивированную из загружаемой учебной программы, и выберите все содержащиеся в ней изображения. Вы должны быть в состоянии сказать по именам изображений, что именно содержит каждый элемент.

После импорта мультимедиа обозреватель решений должен выглядеть следующим образом:

Обозреватель решений Visual Studio 2010 после импорта всех игровых файлов

Теперь, когда ваши медиафайлы были импортированы в проект Content, прикрепленный к вашему решению, вам нужно загрузить каждое изображение в виде отдельной текстуры. Поскольку вы будете использовать эти текстуры на протяжении всей игры, вы будете хранить их в полях внутри своего игрового класса. Откройте файл Game1.cs и добавьте следующие поля в начало вашего объявления класса:

Вы видите, что каждое импортированное вами изображение имеет поле для его резервного копирования. Кроме того, основная игровая сетка и текстура кнопки сброса имеют поля типа Rectangle которые будут использоваться для позиционирования этих элементов. Они хранятся в виде полей, потому что они не будут меняться в течение игры.

Теперь, когда у вас есть соответствующие поля, пришло время создать экземпляры объектов Texture2D и Rectangle , которые назначены полям. Прокрутите файл Game1.cs вниз, пока не дойдете до метода LoadContent . Внутри этого метода вставьте следующий код после строки, которая читает spriteBatch = new SpriteBatch(GraphicsDevice); :

После загрузки спрайтов, которые будут использоваться игрой, вам нужно придумать рабочий процесс, на котором будет запускаться игра. Для целей Tic-Tac-Toe это довольно просто и выглядит примерно так:

  1. Нарисуйте игровую сетку
  2. Нарисуйте пьесы, сыгранные в данный момент
  3. Нарисуйте текущий статус игры (чей ход или если есть победитель)
  4. Если игра выиграна или больше не выигрывается, нарисуйте кнопку «Сброс»
  5. Если никто не выиграл, а игра все еще выигрышная, обработайте касания по сетке и проверьте победителя, если сделано касание
  6. Если игра выиграна или больше не выигрывается, нажмите кнопку сброса
    • Если нажать кнопку сброса, перезагрузите игру

Как вы, вероятно, можете сказать, эти шаги попадают в одну из двух основных категорий, Draw или Update. Сканируя файл Game1.cs вы увидите, что у вас есть два метода, Draw и Update которые являются идеальными контейнерами для кода, необходимого для описания рабочего процесса.

Глядя на рабочий процесс, вы можете заметить, что есть четыре различных элемента, которые нужно отслеживать для управления состоянием игры. Во-первых, вы должны отслеживать состояние каждой точки сетки на игровом поле. Вы сделаете это, используя пользовательское перечисление и многомерный массив. Далее, вы должны следить за тем, была ли игра выиграна и кем. В-третьих, вы должны отслеживать, чья очередь. Наконец, вы должны отслеживать, является ли игра выигрышной. Этот предмет инициализируется как true и пересчитывается после каждого хода игрока. Когда все точки сетки заполнены, это становится false .

Вы начнете с определения пользовательского перечисления, описывающего игрока для вашей игры. Вверху вашего файла класса, над объявлением класса, добавьте следующий код:

Это перечисление описывает трех игроков (None, PlayerO и PlayerX) и будет использоваться для отслеживания как состояния сетки, так и победителя игры. Теперь добавьте переменные экземпляра, которые помогут вам отслеживать состояние игры. Эти элементы должны быть добавлены в начале вашего объявления класса:

Вы храните четыре вещи здесь. Сначала вы определяете, можно ли выиграть в игре. Во-вторых, вы объявляете победителя. Если победителем является None, игра продолжается. После этого вы сохраняете текущий плеер. Наконец, вы сохраняете состояние сетки. На этом этапе вам все еще нужно инициализировать каждую из этих переменных. Забегая наперед, вы знаете, что вам нужно будет повторно инициализировать их всякий раз, когда кто-то нажимает кнопку «Сброс», и поэтому мы можем создать новый метод, который будет выполнять эту инициализацию. Добавьте новый метод в ваш класс под методом Initialize следующим образом:

Теперь вызовите этот новый метод из метода Initialize , изменив его следующим образом:

На данный момент вы храните все необходимые данные, поэтому начать рисование интерфейса должно быть просто.

Прежде чем вы начнете что-либо рисовать, вам нужно установить предпочитаемую ширину и высоту для вашего устройства. Вы будете делать это внутри конструктора Game1 . Измените его следующим образом:

Вы добавили операторы, заявляющие, что вы хотите, чтобы ширина заднего буфера была 480 пикселей, а высота — 800 пикселей. Это значительно упрощает прорисовку остальных компонентов игры.

Для простоты вы будете выполнять каждый шаг рисования внутри отдельного метода. Эти методы затем будут вызываться из базового метода Draw который уже существует. Давайте начнем с обдумывания некоторых хороших имен для каждого из этапов рабочего процесса игры, создания методов с таким именем и добавления вызовов к этим методам из Draw . На мой взгляд, следующие имена методов адекватно описывают то, что они будут делать, и описанные шаги рабочего процесса:

  • DrawGrid — охватывает первый шаг рабочего процесса и рисует игровую сетку.
  • DrawPieces — охватывает второй шаг рабочего процесса и рисует фигуры, где бы они ни были сыграны
  • DrawStatus — охватывает третий шаг рабочего процесса и отображает текущее состояние игры («Ход О», «Ход Х», «Побед О!», «Побед Х!» Или «Рисовать!»)
  • DrawResetButton — охватывает рабочий процесс четвертого шага и рисует кнопку сброса

Создайте эти методы сейчас, вставив следующий код под ваш метод Draw :

Вскоре вы напишите код для этих методов, но сейчас вам нужно добавить их в ваш метод Draw , изменив его следующим образом:

Вы заметите, что окружили вызовы своих вспомогательных методов вызовами spriteBatch Begin и End объекта spriteBatch . Вы делаете это, потому что все ваши вспомогательные методы будут рисовать спрайты, и гораздо эффективнее вызывать пару Begin и End один раз внутри Draw а не внутри каждого вспомогательного метода.

Теперь давайте поработаем над тем, чтобы показать игровую сетку. Сетка воспроизведения представляет собой изображение шириной 480 пикселей и высотой 800 пикселей с прозрачным фоном и белой сеткой из квадратов шириной 150 пикселей, расположенной в центре. Вы импортировали это ранее. Рисование на экране телефона не может быть проще. Вы берете загруженную и сохраненную gridTexture переменную gridTexture и рисуете ее, позиционируя ее с помощью предварительно gridRectangle переменной gridRectangle , передавая оба элемента в метод spriteBatch объекта spriteBatch . Измените метод DrawGrid следующим образом:

Сохраните файл, в котором вы работаете, и нажмите F5, чтобы скомпилировать и запустить ваш проект. Эмулятор Windows Phone 7 должен отображаться и отображать сетку крестики-нолики на черном фоне, как показано на следующем рисунке:

Сетка отображается на экране телефона.

Теперь, когда сетка на месте, давайте нарисуем игровые фигуры. На этом этапе мы можем применить некоторую простую логику, чтобы показать кусочки, но ничего не будет отображаться, пока мы не добавим код, чтобы фактически обрабатывать касания и играть в игру. Измените ваш метод DrawPieces следующим образом:

Если у вас острый взгляд (или, что более вероятно, Visual Studio показывает красные волнистые линии), вы увидите, что метод GetGridSpace отсутствует. GetGridSpace — это удобный метод, который помогает сохранить часть кода в методе DrawPieces а также пригодится при дальнейшей обработке касаний. Добавьте его в конец вашего объявления класса следующим образом:

Теперь давайте посмотрим на остальные DrawPieces . Этот метод немного сложнее, чем DrawGrid но все же он должен быть довольно простым для понимания. Вы перебираете каждую строку и столбец игрового поля, сохраненные в переменной grid , и проверяете, чтобы увидеть состояние этого пространства сетки. Если пространство сетки содержит игрока, отличного от «Нет», тогда рисуем соответствующую текстуру. Вы используете троичный оператор, чтобы получить правильную текстуру на основе состояния пространства сетки, а затем нарисовать ее, используя прямоугольник, полученный из GetGridSpace .

Следующая проблема, с которой нужно разобраться — это рисование текущего статуса. Статус показывает, чья это очередь, кто выиграл игру или является ли игра ничьей. Заполните метод следующим образом:

Этот метод не сильно отличается от других методов рисования, которые вы создали до сих пор. У вас есть текстура и позиция, и вам нужно нарисовать текстуру на экране. Интересной частью этого метода является определение текстуры для рисования. Сначала вы проверяете, есть ли победитель. Если есть, вы выбираете правильную текстуру победителя и рисуете это. Затем вы проверяете, все ли ячейки сетки заняты, и игра больше не выигрывает. Если это так, то вы выбираете текстуру без победителя и рисуете ее. Если ни одно из этих условий не выполняется, вы проверяете, какой ход игрока, выбираете правильную текстуру хода и рисуете. Если вы скомпилируете и запустите свой проект в этот момент (нажав F5), вы увидите, что интерфейс показывает, что настала очередь О:

Ход игрока

Наконец, вы собираетесь создать код, который рисует кнопку сброса. Это довольно просто. Если есть победитель или игра больше не выигрывает, вы рисуете текстуру кнопки сброса. Измените метод DrawResetButton так, чтобы он DrawResetButton следующим образом:

На данный момент вы создали весь код, необходимый для рисования вашего интерфейса и всех его компонентов. Теперь вам просто нужно справиться с обновлением игры на основе касаний игроков.

Если вы следовали последнему учебнику по XNA , большая часть следующего кода будет выглядеть знакомо. Обработка прикосновений на экране — это что-то довольно стандартное, и только в коде обработки прикосновений у вас будет логика игры. Для начала давайте поместим базовый код обработки касаний в метод Update . Найдите Update в своем классе Game1 и измените его следующим образом:

В этом методе есть несколько вещей, на которые следует обратить внимание. Во-первых, вы заметите новую touching переменную, которая не существует. Эта переменная хранит информацию о том, касался ли игрок доску во время предыдущего вызова на Update и предотвращает многократное воспроизведение затянувшегося пальца, не отпуская его с экрана. Добавьте touching в качестве переменной экземпляра в верхней части объявления вашего класса.

Вы также заметите, что вы делаете два вызова метода внутри метода Update для методов, которые еще не существуют. Добавьте эти методы в объявление класса прямо под методом Update :

Теперь, пройдя по методу Update вы увидите, что проверяете наличие текущих касаний на экране. Если есть прикосновения, и игрок ранее не касался экрана, тогда вы берете первое касание и передаете его методам, которые обрабатывают касания доски и сбрасывают касания. Если игрок не касается экрана и они были ранее, тогда вы touching переменную touching на false.

На данный момент, давайте заполнить методы HandleBoardTouch и HandleResetTouch . Они соответствуют шагам 5 и 6 рабочего процесса соответственно.

Когда пользователь касается экрана, игра должна сделать несколько вещей:

  • Проверьте, находится ли касание в игровой зоне
  • Если он находится в игровой зоне, проверьте, занято ли уже затронутое место
  • Если место не занято, поместите фигуру туда и проверьте, выиграл ли текущий игрок
  • Если сыгранная часть была победителем, обновите статус игры как таковой. Если сыгранная фигура заняла последнее доступное место, отметьте игру как не выигранную
  • Наконец, поменяйте местами текущего игрока

Обновите HandleBoardTouch следующим образом, HandleBoardTouch все вышеперечисленные шаги:

Как вы можете видеть, этот метод следует основной схеме выше. Если нет победителя, он выполняет итерацию по каждому пространству сетки, проверяет, находится ли касание в этом пространстве, проверяет, открыто ли пространство, и затем помещает туда часть. Проверка на победителя и уверенность в том, что игра все еще можно выиграть, обрабатываются другими методами. Давайте заглушим эти методы сейчас. Под методом HandleBoardTouch добавьте следующий код:

Пока оставьте эти методы пустыми, скомпилируйте и запустите вашу игру. Попробуйте прикоснуться к доске, щелкнув по эмулятору, и просмотрите сообщение об изменении состояния и отображение играющих фигур. Вы уже на пути к полноценной игре!

Фигуры, отображаемые на доске впервые

В этот момент вы попадаете в настоящую суть игры, выигрышную логику. Как обсуждалось ранее, выигрышное состояние возникает, когда фигуры одного игрока занимают один из рядов, один из столбцов или одну из двух диагоналей. Игра становится невозможной, если не объявлен победитель и не осталось свободных мест. В этой игре мы используем многомерный массив для хранения состояния сетки, и C # не предоставляет методов для проверки строк, столбцов или диагоналей. К счастью, язык поддерживает замечательную функцию, называемую методами расширения, которые вы будете использовать здесь, чтобы сделать свой код чище.

Чтобы создать методы расширения, вы должны сначала создать новый класс. Нажмите на название вашего проекта в обозревателе решений и нажмите «Добавить> Класс…». Назовите свой класс MultiDimensionalArrayExtensions и нажмите «Добавить». Когда ваш новый файл откроется для редактирования, измените его так, чтобы объявление вашего класса выглядело следующим образом:

Вы увидите, что вы добавили модификаторы public и static в объявление класса. Это необходимо для создания методов расширения. Теперь мы собираемся создать пару методов, которые нам понадобятся, каждый из которых возвращает IEnumerable для удобного запроса:

  • Строка — возвращает все элементы в определенной строке
  • Столбец — возвращает все элементы в определенном столбце.
  • Диагональ — возвращает все элементы по диагонали
  • Все — Возвращает все элементы в многомерном массиве

Снова измените ваш класс, добавив методы следующим образом:

Вы можете видеть, что в этих методах нет ничего особенного. Для каждого из них определенная часть многомерного массива повторяется, и эта часть массива возвращается как часть IEnumerable . Единственной действительно сложной задачей может быть использование ключевого слова yield . Объяснение поведения этого ключевого слова выходит за рамки данной статьи, но ссылка C # на MSDN для ключевого слова yield может помочь, если вы хотите узнать больше. Как примечание, большая часть работы над этими методами расширения взята из пользовательского вклада в StackOverflow, который вы можете найти здесь

Теперь, когда необходимые методы расширения реализованы, должно быть довольно легко реализовать логику выигрыша. Вернемся к методу CheckForWin и реализуем его. Обновите метод следующим образом:

Учитывая то, что вы уже знаете по созданию методов расширения, это должно быть довольно просто расшифровать. Сначала вы создаете "Func":http://msdn.microsoft.com/en-us/library/bb549151.aspx объект, который действует делегатом и позволяет вам использовать лямбда-оператор (который b => b == player часть b => b == player ) запросить IEnumerable (например, возвращенный из метода расширения) и вернуть bool . Затем вы применяете этот объект Func каждой строке, столбцу и диагонали, используя метод IEnumerable.All . Если какой-либо из этих случаев верен, то вы назначаете переменную экземпляра winner параметру player . Если ни один из этих случаев не соответствует действительности, то ничего не происходит.

Теперь измените ваш метод CheckForWinnable :

Этот метод очень похож на CheckForWin . Сначала вы проверяете, есть ли в игре победитель. Если это не так, вы создаете объект Func , который проверяет, равен ли элемент TicTacToePlayer None . Затем вы применяете этот Func ко всем пробелам в сетке, проверяя, свободны ли какие-либо пробелы. Если их нет, игра больше не выигрывает, и вы переключаете переменную экземпляра winnable .

На данный момент ваша игра готова к работе. Вы можете скомпилировать и запустить свой проект, нажав F5 и начать играть (либо самостоятельно, либо с партнером). По очереди размещайте фигуры на доске, следите за изменением сообщения о статусе и смотрите, что произойдет, когда вы выиграете. После того, как вы выиграете или сыграете вничью, нажмите кнопку сброса и посмотрите, как игра вернется в исходное состояние.

Игрок Х выиграл игру

На данный момент есть множество разных вещей, которые вы могли бы сделать. Вы можете реализовать подсчет выигрышей, который показывает, сколько раз выиграл каждый игрок. Вы можете изменить способ отображения фигур или добавить классную анимацию, когда объявлен победитель. Вы могли бы сделать игру более интересной, возможно, противопоставив Альянсу повстанцев Галактическую Империю?

Крестики-нолики с использованием иконок Star Wars

На данный момент все зависит от вас, чтобы расширять и развивать то, что вы хотите. Я надеюсь, что вам понравилось читать этот урок так же, как и мне, и я с нетерпением жду ваших комментариев.