Программирование в XNA Game Studio 4.0 на Visual C# приложений и игр для Windows-PC, Xbox 360 и мобильных устройств с операционной системой Windows Phone

 

Типичные двухмерные игры. Часть 1. Игра в крестики-нолики для двух игроков

 

Общие сведения

Опишем методику программирования на Visual C#, например, для мобильных устройств с операционной системой Windows Phone типичной и широко распространённой игры в крестики-нолики. Известно много вариантов этой игры как в ручном, так и в компьютерном варианте. В ручном варианте смысл игры состоит в том, что, например, в тетради в клеточку или на земле расчерчиваются две горизонтальные и две вертикальные линии. После чего получается поле из квадратов, например, 3 x 3 квадрата (цифры соответствуют количеству квадратов по осям “x” и “y”). Далее два игрока по очереди выделяют квадраты крестиками и ноликами, стараясь первым выделить горизонтальную, вертикальную или диагональную линию из определённого количества, например, из 3-х крестиков или ноликов. Если никому из игроков этого не удалось (а все квадраты заполнены), то – ничья.

Известны два варианта компьютерной игры: Игрок 1 – Игрок 2 и Игрок – Компьютер. Данную игру мы будем разрабатывать по первому варианту, следуя проекту TicTacToe (автор Nick Ohm  с сайта mobile.tutsplus.com от 7.10.10), но с нашими усовершенствованиями. В игре (Игрока 1 с Игроком 2) будут использоваться графические файлы. Игроки будут воздействовать на квадраты пальцем (на реальном сенсорном экране смартфона) или указателем мыши (на эмуляторе смартфона в разработанном ниже проекте).

 Правила игры

1. После запуска игры на форме появляется поле игры, состоящее из 3 x 3 квадратов (рис. 1, все рисунки даны в журнале). По умолчанию установлен режим, когда сразу же можно щёлкать по квадратам указателем мыши (на эмуляторе смартфона) или нажимать пальцем на сенсорный экран реального смартфона.

          Рис. 1. Перед игрой.             Рис. 2. Игрок 1 поставил ‘О’.      Рис. 3. Игрок 2 поставил ‘X’.                Рис. 4. Ничья.

2. Игрокам нужно сначала договориться (например, подбрасывая монетку), кто будет Игроком 1 и ходить первым.

За Игроком 1 закреплён символ типа нолика ‘0’, а за Игроком 2 – типа крестика ‘X’, о чём перед каждым ходом напоминает надпись в верхней части экрана смартфона.

3. Игрок 1 нажимает пальцем или щёлкает мышью по выбранному им квадрату, на этом квадрате появляется нолик ‘0’ (рис. 2).

4. Теперь Игрок 2 нажимает пальцем или щёлкает мышью по выбранному им квадрату, на этом квадрате появляется крестик ‘X’ (рис. 3).

5. Так по очереди Игрок 1 и Игрок 2 записывают нолики и крестики, стараясь первым выделить горизонтальную, вертикальную или диагональную линию из ноликов или крестиков. Но если Игрок 1 или Игрок 2 видят, что следующим ходом его соперник соберёт линию из 3-х символов, он должен (чтобы не проиграть) на этот квадрат поместить свой символ (даже в ущерб своей тактике игры). 

6. Если никому из игроков не удалось собрать линию из 3-х своих символов (а все квадраты заполнены), то – ничья (рис. 4).

7. Как только Игрок 1 или Игрок 2 первым соберёт линию из 3-х своих символов,  в верхней части формы появляется сообщение, кто из игроков победил (рис. 5 и 6).

Рис. 5. Игрок ‘О’ победил. Рис. 6. Игрок ‘X’ победил.  Рис. 7. Панель SE.                              Рис. 8. Панель Properties. 

8. Перед новой игрой следует щёлкнуть появившуюся кнопку Reset.

Появляется уже показанный выше интерфейс игры.

9. Теперь первый ход (щёлкая кнопку) делает тот игрок, который в предыдущей игре начинал вторым.

Если в игре участвует несколько человек, то можно условиться, что победитель в предыдущей игре начинает первым.

10. Для закрытия игры следует на форме выбрать значок Close.

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

Создание проекта

Считаем, что с сайта разработчика Microsoft.com мы бесплатно загрузили и установили на наш компьютер последние версии пакетов Visual Studio Express for Windows Phone и Windows Phone SDK.

В VS щёлкаем значок New Project (или File, New Project), в панели New Project в окне Project types выбираем тип проекта Visual C#, XNA Game Studio, в окне Templates выделяем шаблон Windows Phone Game, в окне Name записываем имя проекта (точнее, решения из 2-х проектов) TicTacToe, снимаем флажок Create solution (Создать каталог для решения) и щёлкаем OK; в появившейся панельке выбираем (или оставляем по умолчанию) целевую версию ОС Windows Phone (в зависимости от цели нашего приложения) и щёлкаем OK. VS создаёт проект и выводит файл Game1.cs с шаблоном кода. Проверяем работоспособность проекта на нашем компьютере: F5; если появилась панель с сообщением об ошибке, то, как правило, ошибка связана с нехваткой оперативной памяти для вывода эмулятора смартфона или с недостаточной мощностью видеокарты; если на мониторе появился эмулятор смартфона с голубым экраном, то можно программировать дальше.

В панели Solution Explorer (рис. 7) выполняем правый щелчок по имени второго проекта Content, в контекстном меню выбираем Add, New Folder, в поле появившегося значка папки записываем Textures и щёлкаем рядом с этой записью (или нажимаем клавишу Enter). Добавляем в эту папку Textures файлы рисунков: выполняем правый щелчок по имени этой папки, в контекстном меню выбираем Add, Existing Item, в панели Add Existing Item в окне “Files of type” выбираем “All Files”, в центральном окне находим (на прилагаемом диске в папке с именем проекта) и с нажатой клавишей Shift или Ctrl выделяем имена всех подлежащих добавлению файлов, и щёлкаем кнопку Add (или дважды щёлкаем по имени каждого файла).

Эти добавленные рисунки в виде графических файлов формата *.png можно построить самостоятельно в любом графическом редакторе, например, в Paint по следующей инструкции:

файл TicTacToe_Draw.png – это горизонтальный прямоугольник размером 168 x 51 пикселей красного цвета;

файл TicTacToe_Grid.png – это вертикальный прямоугольник размером 480 x 800 пикселей белого цвета экрана;

файл TicTacToe_0.png – это квадрат размером 116 x 116 пикселей красного цвета;

файл TicTacToe_0_Turn.png – это горизонтальный прямоугольник размером 289 x 52 пикселей красного цвета;

файл TicTacToe_0_Winner.png – это горизонтальный прямоугольник размером 245 x 51 красного цвета;

файл TicTacToe_0_Reset.png – это горизонтальный прямоугольник размером 450 x 80 пикселей белого цвета экрана, внутри которого построен горизонтальный прямоугольник красного цвета для надписи Reset;

файл TicTacToe_X.png – это квадрат размером 116 x 116 пикселей красного цвета;

файл TicTacToe_X_Turn.png – это горизонтальный прямоугольник  размером 290 x 51 пикселей красного цвета;

файл TicTacToe_X_Winner.png – это горизонтальный прямоугольник размером 245 x 51 пикселей красного цвета.

Если в панели Solution Explorer выделить какой-либо файл, то ниже в панели Properties в свойстве Asset Name (Имя актива) мы увидим имя (рис. 8), под которым этот файл загружается при выполнении программы. Мы можем здесь изменить имя файла, но тогда далее в программе следует записать новое имя этого файла.

Код и выполнение программы

В шаблон файла Game1.cs записываем (или копируем с диска из папки с именем проекта и вставляем) наши блоки кода, каждый из которых заключён между двумя строками (//Мы начинаем добавлять код: … //Мы закончили добавлять код.) на следующем листинге этого файла.

Листинг 1. Файл Game1.cs с нашими добавлениями.

namespace TicTacToe

{

    //Мы начинаем добавлять код:

    public enum TicTacToePlayer { None, PlayerO, PlayerX }

    //Мы закончили добавлять код.

    public class Game1 : Microsoft.Xna.Framework.Game

    {

        GraphicsDeviceManager graphics;

        SpriteBatch spriteBatch;

        //Мы начинаем добавлять код:

        Texture2D gridTexture;

        Rectangle gridRectangle;

        Texture2D resetButton;

        Rectangle resetButtonPosition;

        Texture2D oPiece;

        Texture2D xPiece;

        Texture2D oWinner;

        Texture2D xWinner;

        Texture2D noWinner;

        Texture2D oTurn;

        Texture2D xTurn;

        bool winnable;

        TicTacToePlayer winner;

        TicTacToePlayer current;

        TicTacToePlayer[,] grid;

        bool touching;

        void Reset()

        {

            current = TicTacToePlayer.PlayerO;

            winnable = true;

            winner = TicTacToePlayer.None;

            grid = new TicTacToePlayer[3, 3];

            for (int i = 0; i < 3; i++)

            {

                for (int j = 0; j < 3; j++)

                {

                    grid[i, j] = TicTacToePlayer.None;

                }

            }

        }

        void HandleBoardTouch(TouchLocation touch)

        {

            if (winner == TicTacToePlayer.None)

            {

                for (int i = 0; i < 3; i++)

                {

                    for (int j = 0; j < 3; j++)

                    {

                        Rectangle box = GetGridSpace(i, j, 150, 150);

                        if (grid[i, j] == TicTacToePlayer.None &&

                            box.Contains((int)touch.Position.X, (int)touch.Position.Y))

                        {

                            grid[i, j] = current;

                            CheckForWin(current);

                            CheckForWinnable();

                            current = current == TicTacToePlayer.PlayerO ?

                                TicTacToePlayer.PlayerX : TicTacToePlayer.PlayerO;

                        }

                    }

                }

            }

        }

        void CheckForWin(TicTacToePlayer player)

        {

            Func<TicTacToePlayer, bool> checkWinner = b => b == player;

            if (

                grid.Row(0).All(checkWinner) || grid.Row(1).All(checkWinner) ||

                grid.Row(2).All(checkWinner) || grid.Column(0).All(checkWinner) ||

                grid.Column(1).All(checkWinner) || grid.Column(2).All(checkWinner) ||

                grid.Diagonal(MultiDimensionalArrayExtensions.DiagonalDirection.DownRight).

                All(checkWinner) || grid.Diagonal(MultiDimensionalArrayExtensions.

                DiagonalDirection.DownLeft).All(checkWinner)

                )

            {

                winner = player;

            }

        }

        void CheckForWinnable()

        {

            if (winner == TicTacToePlayer.None)

            {

                Func<TicTacToePlayer, bool> checkNone = b => b == TicTacToePlayer.None;

                if (!grid.All().Any(checkNone))

                {

                    winnable = false;

                }

            }

        }

        void HandleResetTouch(TouchLocation touch)

        {

            if ((winner != TicTacToePlayer.None || !winnable)

                && resetButtonPosition.Contains((int)touch.Position.X, (int)touch.Position.Y))

            {

                Reset();

            }

        }

 

        void DrawGrid()

        {

            spriteBatch.Draw(gridTexture, gridRectangle, Color.White);

        }

        void DrawPieces()

        {

            for (int i = 0; i < 3; i++)

            {

                for (int j = 0; j < 3; j++)

                {

                    if (grid[i, j] != TicTacToePlayer.None)

                    {

                        Texture2D texture = grid[i, j] ==

                            TicTacToePlayer.PlayerO ? oPiece : xPiece;

                        Rectangle position = GetGridSpace(i, j, texture.Width, texture.Height);

                        spriteBatch.Draw(texture, position, Color.White);

                    }

                }

            }

        }

        void DrawStatus()

        {

            Texture2D texture;

            if (winner != TicTacToePlayer.None)

            {

                texture = winner == TicTacToePlayer.PlayerO ? oWinner : xWinner;

            }

            else if (!winnable)

            {

                texture = noWinner;

            }

            else

            {

                texture = current == TicTacToePlayer.PlayerO ? oTurn : xTurn;

            }

            Rectangle position = new Rectangle(spriteBatch.GraphicsDevice.Viewport.Width / 2 -

                (texture.Width / 2), 15, texture.Width, texture.Height);

            spriteBatch.Draw(texture, position, Color.White);

        }

        void DrawResetButton()

        {

            if (winner != TicTacToePlayer.None || !winnable)

            {

                spriteBatch.Draw(resetButton, resetButtonPosition, Color.White);

            }

        }

        Rectangle GetGridSpace(int column, int row, int width, int height)

        {

            int centerX = spriteBatch.GraphicsDevice.Viewport.Width / 2;

            int centerY = spriteBatch.GraphicsDevice.Viewport.Height / 2;

            int x = centerX + ((column - 1) * 150) - (width / 2);

            int y = centerY + ((row - 1) * 150) - (height / 2);

            return new Rectangle(x, y, width, height);

        }

        //Мы закончили добавлять код.

        public Game1()

        {

            graphics = new GraphicsDeviceManager(this);

            Content.RootDirectory = "Content";

            TargetElapsedTime = TimeSpan.FromTicks(333333);

            InactiveSleepTime = TimeSpan.FromSeconds(1);

            //Мы начинаем добавлять код:

            graphics.PreferredBackBufferWidth = 480;

            graphics.PreferredBackBufferHeight = 800;

            //Мы закончили добавлять код.

        }

        protected override void Initialize()

        {

            //Мы начинаем добавлять код:

            Reset();

            //Мы закончили добавлять код.

            base.Initialize();

        }

        protected override void LoadContent()

        {

            spriteBatch = new SpriteBatch(GraphicsDevice);

            //Мы начинаем добавлять код:

            gridTexture = Content.Load<Texture2D>("Textures/TicTacToe_Grid");

            gridRectangle = new Rectangle(0, 0, spriteBatch.GraphicsDevice.Viewport.Width,

                spriteBatch.GraphicsDevice.Viewport.Height);

            oPiece = Content.Load<Texture2D>("Textures/TicTacToe_O");

            xPiece = Content.Load<Texture2D>("Textures/TicTacToe_X");

            resetButton = Content.Load<Texture2D>("Textures/TicTacToe_Reset");

            resetButtonPosition = new Rectangle(spriteBatch.GraphicsDevice.Viewport.Width / 2 -

                (resetButton.Width / 2), spriteBatch.GraphicsDevice.Viewport.Height - 95,

                resetButton.Width, resetButton.Height);

            oWinner = Content.Load<Texture2D>("Textures/TicTacToe_O_Winner");

            xWinner = Content.Load<Texture2D>("Textures/TicTacToe_X_Winner");

            noWinner = Content.Load<Texture2D>("Textures/TicTacToe_Draw");

            oTurn = Content.Load<Texture2D>("Textures/TicTacToe_O_Turn");

            xTurn = Content.Load<Texture2D>("Textures/TicTacToe_X_Turn");

            //Мы закончили добавлять код.

        }

        protected override void UnloadContent() {}

        protected override void Update(GameTime gameTime)

        {

            if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed)

                this.Exit();

            //Мы начинаем добавлять код:

            TouchCollection touches = TouchPanel.GetState();

            if (!touching && touches.Count > 0)

            {

                touching = true;

                TouchLocation touch = touches.First();

                HandleBoardTouch(touch);

                HandleResetTouch(touch);

            }

            else if (touches.Count == 0)

            {

                touching = false;

            }

            //Мы закончили добавлять код.

            base.Update(gameTime);

        }

        protected override void Draw(GameTime gameTime)

        {

            //Мы комментируем эту строку кода:

            //GraphicsDevice.Clear(Color.CornflowerBlue);

            //Мы начинаем добавлять код:

            GraphicsDevice.Clear(Color.Black);

            spriteBatch.Begin();

            DrawGrid();

            DrawPieces();

            DrawStatus();

            DrawResetButton();

            spriteBatch.End();

            //Мы закончили добавлять код.

            base.Draw(gameTime);

        }

    }

}

В панели Solution Explorer выполняем правый щелчок по имени первого проекта, в контекстном меню выбираем Add, New Item, в панели Add New Item выделяем шаблон Code File, в окне Name записываем имя MultiDimensionalArrayExtensions.cs и щёлкаем кнопку Add. В появившееся пустое окно редактирования кода записываем код со следующего листинга.

        Листинг 2.  Добавляемый файл формата *.cs.

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

namespace TicTacToe

{

    public static class MultiDimensionalArrayExtensions

    {

        public static IEnumerable<T> Row<T>(this T[,] array, int row)

        {

            var columnLower = array.GetLowerBound(1);

            var columnUpper = array.GetUpperBound(1);

            for (int i = columnLower; i <= columnUpper; i++)

            {

                yield return array[row, i];

            }

        }

        public static IEnumerable<T> Column<T>(this T[,] array, int column)

        {

            var rowLower = array.GetLowerBound(0);

            var rowUpper = array.GetUpperBound(0);

            for (int i = rowLower; i <= rowUpper; i++)

            {

                yield return array[i, column];

            }

        }

        public static IEnumerable<T> Diagonal<T>(this T[,] array,

                                                 DiagonalDirection direction)

        {

            var rowLower = array.GetLowerBound(0);

            var rowUpper = array.GetUpperBound(0);

            var columnLower = array.GetLowerBound(1);

            var columnUpper = array.GetUpperBound(1);

            for (int row = rowLower, column = columnLower;

                 row <= rowUpper && column <= columnUpper;

                 row++, column++)

            {

                int realColumn = column;

                if (direction == DiagonalDirection.DownLeft)

                    realColumn = columnUpper - columnLower - column;

                yield return array[row, realColumn];

            }

        }

        public enum DiagonalDirection

        {

            DownRight,

            DownLeft

        }

        public static IEnumerable<T> All<T>(this T[,] array)

        {

            var rowLower = array.GetLowerBound(0);

            var rowUpper = array.GetUpperBound(0);

            var columnLower = array.GetLowerBound(1);

            var columnUpper = array.GetUpperBound(1);

            for (int row = rowLower; row <= rowUpper; row++)

            {

                for (int column = columnLower; column <= columnUpper; column++)

                {

                    yield return array[row, column];

                }

            }

        }

    }

}

Схема записи и вывода справочной информации, например, с правилами игры после выбора кнопки Справка будет дана далее. Строим и запускаем программу: Build, Build Solution (или F6); Debug, Start Debugging (или F5). В ответ Visual Studio выводит эмулятор смартфона с показанным выше полем игры из квадратов. Далее Игрок 1 играет с Игроком 2 в крестики-нолики согласно приведённым выше правилам.

По данной методике можно разрабатывать самые разнообразные игры в крестики-нолики и другие подобные игры и приложения.