Привет дорогой друг.
Опять прошло много времени и я не радовал вас интересной информацией по
поводу разработки игр. Сегодня эту статью я хочу посветить разработке игр под WP7, используя
замечательный фреймворк XNA, о котором я писал здесь, здесь и здесь и тут. А конкретно, я вам расскажу, как можно сделать красивые эффекты без шейдеров. В этой статье рассмотрим эффект искажения. Остальное под катом.
Последние две статьи я писал про шейдеры и о том, как можно улучшить визуальное восприятие в вашей игре. Но если посмотреть на сравнение Reach и HiDef профилей XNA, то можно с ужасом
увидеть, что Reach поддерживает Shader Modele 2.0, а WP7 её не поддерживает вообще. И от этого хочетсявзять и ударить придумать, как это все можно сделать без шейдеров.
Конечно, речь не идет о крутом освещении с normal mapping (хотя, извратиться можно), а просто о том, как можно погнуть пиксели с помощью BasicEffect. Такой метод прост до безумия, но крайне эффективен.
Итак, если вспомнить,кто я такой и о чем я писал, то можно вспомнить алгоритм, который мы
использовали в шейдерах: есть карта искажений и цветная карта. По карте искажений — гнем цветную карту. Просто? Забудьте. Такой метод крайне сложен в реализации под WP7 без вмешательства GPU (доступа к которому у нас, к сожалению, нет).
— Как же быть, парень?
Вспомним, по каком принципу у нас рисуется что-либо в 3D, например обычный плоский квадрат? Он рисуется с помощью двух треугольников. В чем это может нам помочь? Все просто, создаем много треугольников, а потом, с помощью координат текстуры — будем двигать «какбэ» сам треугольник, отчего создается эффект искажения.
На деле — есть картинка — 480x800, мы создаем сетку размером 48x80 (поверьте, для красивого эффекта — в самый раз). Сетка — одномерный массив, состоящий из 3840 элементов. Просчитывается это все на WP7 примерно 3-4 ms, при более низком качестве сетки — 1-2 ms. Но если сетка будет слишком маленькая, то при искажении будет заметно, что треугольники все-таки существуют. А вот когда сетка меньше в 10 раз, это мало заметно, для сравнения — шаг на экране в 3 мм = 10 пикселям. Ну да ладно, что-то я разговорился.
— Эй, чувак, хватит теории, переходи к практике.
Чтобы рисовать что-либо на экране из примитивов, нужен BasicEffect. Например, spriteBatch — огромный класс, который скрывает от наших глаз всякие BasicEffect, но в конечном счете — все сводится к рисованию примитивов, накладывание текстур на них. Постараюсь более подробно объяснить об использовании BasicEffect в нашем случае.
Собираемся в путь, ищем материал.
Для начала нам нужна текстура, которую мы будем гнуть, встречайте нашего любимого друга:

И как-то странно, но нам еще нужен пустой проект, создаем его.
Сразу скажу, что одна из особенностей XNA под WP7, что по дефолту там 30 FPS (взамен, родных 60 FPS). Но что-то мне подсказывает, что можно и больше; С другой стороны — кому нужен батарея-киллер, а не таймкиллер? Поэтому, мы будем использовать 30 FPS.
В пустом проекте вы найдете:
Строчка, отвечающая за FPS — догадайтесь сами.
Следующий момент, это отсутствие клавиатуры, все действия выполняются с помощью мультитача.
Единственную кнопку, какую можно перехватить, это кнопка Back (назад), по дефолту — она выходит из приложения:
Поэтому, трогать в пустом проекте мы ничего не будем, приступаем к програмированию и собственно, практике. Для начала — наполним Game1 смысломи любовью.
Создаем переменные:
background — наша текстура, ну или RenderTarget какой-то.
BasicEffect — наш герой, нужный для отрисовки примитивов.
Грузим контент:
Чуть не забыл, выставляем в конструкторе:
Дабы была одна ориентация и не было проблем с позиционированием.
Инициализируем BasicEffect (в Initialize):
Projection — матрица-проекция трехмерного объекта на двухмерную плоскость (экран).
View — матрица вида, камеры, если хотите.
World — мировая матрица: вращение, размер, позиция.
Зададим View и World — единичные матрицы.
А Projection зададим ортогональную проекцию, т.е. у нас будет примитив проецироваться на экран полностью. Концы примитива с концами экрана, если объяснить проще.
Так, пока с Game1 все, создадим новый класс GridVertexPositionColorTexture, и вот его полный листинг (прошу прощения за полный, но он с комментариями):
Все хорошо, класс, отвечающий за прорисовку примитивов и за саму сетку — создан. Теперь нужно придумать контроллер к этой сетке, который будет её гнуть. В этой статье — расскажу вам про два контроллера сетки: SimpleGrid, ElasticGrid.
Первый у нас будет сбрасывать сетку, применять к ней текущие искажения.
Второй превратит нашу сетку в желе, которая будет колебаться, пока не придет к дефолтному состоянию.
Напишем первый контроллер, создадим новый класс SimpleGrid и его листинг:
Контроллер написан, теперь вернемся к Game1, две новых переменных:
Их инициализация:
Сам Update:
Ну и наконец прорисовка:
Запускаем, касаемся экрана и видим искажения или эффект линзы.
Но повеселимся еще, сделаем желе из текстуры, класс ElasticGrid, наследуемый от SimpleGrid:
Меняем контроллер сетки в Game1 и любуемся искажениями.
Вот такой простой и интересный подход. Контроллеров может быть бесконечное кол-во, например, реалистичная вода в реалтайме без всяких шейдеров с волнами и шлюхами.
В другой раз я попробую описать другие методы придания вашей игре красоты.
Так же из серии по XNA планируется написание статей на тематику 3D.
Ну и как бонус, следующая статья возможно будет о том, как можно написать игру на WP7, разместить её в маркете бесплатно, без рекламы и сидеть в нищете.
Исходники скачать можно тут.
Экспериментируйте, творите; до новых встреч :-)
Опять прошло много времени и я не радовал вас интересной информацией по
поводу разработки игр. Сегодня эту статью я хочу посветить разработке игр под WP7, используя
замечательный фреймворк XNA, о котором я писал здесь, здесь и здесь и тут. А конкретно, я вам расскажу, как можно сделать красивые эффекты без шейдеров. В этой статье рассмотрим эффект искажения. Остальное под катом.
Теория
Последние две статьи я писал про шейдеры и о том, как можно улучшить визуальное восприятие в вашей игре. Но если посмотреть на сравнение Reach и HiDef профилей XNA, то можно с ужасом
увидеть, что Reach поддерживает Shader Modele 2.0, а WP7 её не поддерживает вообще. И от этого хочется
Конечно, речь не идет о крутом освещении с normal mapping (хотя, извратиться можно), а просто о том, как можно погнуть пиксели с помощью BasicEffect. Такой метод прост до безумия, но крайне эффективен.
Итак, если вспомнить,
использовали в шейдерах: есть карта искажений и цветная карта. По карте искажений — гнем цветную карту. Просто? Забудьте. Такой метод крайне сложен в реализации под WP7 без вмешательства GPU (доступа к которому у нас, к сожалению, нет).
— Как же быть, парень?
Вспомним, по каком принципу у нас рисуется что-либо в 3D, например обычный плоский квадрат? Он рисуется с помощью двух треугольников. В чем это может нам помочь? Все просто, создаем много треугольников, а потом, с помощью координат текстуры — будем двигать «какбэ» сам треугольник, отчего создается эффект искажения.
На деле — есть картинка — 480x800, мы создаем сетку размером 48x80 (поверьте, для красивого эффекта — в самый раз). Сетка — одномерный массив, состоящий из 3840 элементов. Просчитывается это все на WP7 примерно 3-4 ms, при более низком качестве сетки — 1-2 ms. Но если сетка будет слишком маленькая, то при искажении будет заметно, что треугольники все-таки существуют. А вот когда сетка меньше в 10 раз, это мало заметно, для сравнения — шаг на экране в 3 мм = 10 пикселям. Ну да ладно, что-то я разговорился.
— Эй, чувак, хватит теории, переходи к практике.
Практика
Чтобы рисовать что-либо на экране из примитивов, нужен BasicEffect. Например, spriteBatch — огромный класс, который скрывает от наших глаз всякие BasicEffect, но в конечном счете — все сводится к рисованию примитивов, накладывание текстур на них. Постараюсь более подробно объяснить об использовании BasicEffect в нашем случае.
Собираемся в путь, ищем материал.
Для начала нам нужна текстура, которую мы будем гнуть, встречайте нашего любимого друга:

И как-то странно, но нам еще нужен пустой проект, создаем его.
Сразу скажу, что одна из особенностей XNA под WP7, что по дефолту там 30 FPS (взамен, родных 60 FPS). Но что-то мне подсказывает, что можно и больше; С другой стороны — кому нужен батарея-киллер, а не таймкиллер? Поэтому, мы будем использовать 30 FPS.
В пустом проекте вы найдете:
// Frame rate is 30 fps by default for Windows Phone. TargetElapsedTime = TimeSpan.FromTicks(333333); // Extend battery life under lock. InactiveSleepTime = TimeSpan.FromSeconds(1);
Строчка, отвечающая за FPS — догадайтесь сами.
Следующий момент, это отсутствие клавиатуры, все действия выполняются с помощью мультитача.
Единственную кнопку, какую можно перехватить, это кнопка Back (назад), по дефолту — она выходит из приложения:
if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed) this.Exit();
Поэтому, трогать в пустом проекте мы ничего не будем, приступаем к програмированию и собственно, практике. Для начала — наполним Game1 смыслом
Создаем переменные:
Texture2D background; BasicEffect basicEffect;
background — наша текстура, ну или RenderTarget какой-то.
BasicEffect — наш герой, нужный для отрисовки примитивов.
Грузим контент:
background = Content.Load<Texture2D>("distortion");
Чуть не забыл, выставляем в конструкторе:
graphics.PreferredBackBufferWidth = 480; graphics.PreferredBackBufferHeight = 800; graphics.IsFullScreen = true;
Дабы была одна ориентация и не было проблем с позиционированием.
Инициализируем BasicEffect (в Initialize):
basicEffect = new BasicEffect(GraphicsDevice); basicEffect.TextureEnabled = true; // включаем накладывание текстур на примитивы basicEffect.Projection = Matrix.CreateOrthographicOffCenter(0, 480, 800, 0, 0f, 10f); basicEffect.View = Matrix.Identity; basicEffect.World = Matrix.Identity;
Projection — матрица-проекция трехмерного объекта на двухмерную плоскость (экран).
View — матрица вида, камеры, если хотите.
World — мировая матрица: вращение, размер, позиция.
Зададим View и World — единичные матрицы.
А Projection зададим ортогональную проекцию, т.е. у нас будет примитив проецироваться на экран полностью. Концы примитива с концами экрана, если объяснить проще.
Так, пока с Game1 все, создадим новый класс GridVertexPositionColorTexture, и вот его полный листинг (прошу прощения за полный, но он с комментариями):
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Xna.Framework.Graphics; using Microsoft.Xna.Framework; namespace GridDistortion { public class GridVertexPositionColorTexture { public VertexPositionColorTexture[] Vertices; // Массив из вертексов, которые несут в себе позицию вертекса, цвет вертекса и UV координаты текстуры public short[] Indices; // Индексы, о них я расскажу в другой статье, где будет 3D тематика, заодно и расскажу, как строить примитивы public int Width; // размер сетки по X public int Height; // размер сетки по Y public Vector2 CellSize; // шаг между точками в сетке по X и Y соответственно public void BuildGeometry(int colums, int rows, Vector2 cellSize) // строим геометрию нашего примитива { Width = colums; Height = rows; CellSize = cellSize; Vertices = new VertexPositionColorTexture[(Width + 1) * (Height + 1)]; // инициализация массива вертексов Indices = new short[Width * Height * 6]; // тоже самое и про индексы /* заполнение массива вертексов */ for (int i = 0; i < Width + 1; i++) { for (int j = 0; j < Height + 1; j++) { int index = j * (Width + 1) + i; VertexPositionColorTexture vertex = new VertexPositionColorTexture() { Position = new Vector3(new Vector2(i, j) * CellSize, 0f), // позиция вертекса Color = Color.White, TextureCoordinate = GetDefaultUV(index) // текстурная координата }; Vertices[index] = vertex; } } /* заполнение массива индексов */ int indexPos = 0; for (int i = 0; i < Width; i++) { for (int j = 0; j < Height; j++) { int v0 = j * (Width + 1) + i; int v1 = j * (Width + 1) + i + 1; int v2 = (j + 1) * (Width + 1) + i; int v3 = (j + 1) * (Width + 1) + i + 1; Indices[indexPos] = (short)v0; Indices[indexPos + 1] = (short)v1; Indices[indexPos + 2] = (short)v2; Indices[indexPos + 3] = (short)v2; Indices[indexPos + 4] = (short)v1; Indices[indexPos + 5] = (short)v3; indexPos += 6; } } } public void Draw(GraphicsDevice graphicsDevice) // отрисовка массива из наших VertexPositionColorTexture { graphicsDevice.DrawUserIndexedPrimitives<VertexPositionColorTexture>(PrimitiveType.TriangleList, Vertices, 0, Vertices.Length, Indices, 0, Indices.Length / 3); } public void ResetUVs() // сброс UV сетки { for (int i = 0; i < Vertices.Length; i++) { VertexPositionColorTexture v = Vertices[i]; v.TextureCoordinate = GetDefaultUV(i); Vertices[i] = v; } } public Vector2 GetUV0(int index) // получить значение сетки { return Vertices[index].TextureCoordinate; } public void SetUV0(int index, Vector2 value) // задать значение сетки { Vertices[index].TextureCoordinate = value; } public Vector2 GetDefaultUV(int index) // получить значение для сетки по дефолту { int i = index % (Width + 1); int j = index / (Width + 1); return new Vector2((float)i / Width, (float)j / Height); } } }
Все хорошо, класс, отвечающий за прорисовку примитивов и за саму сетку — создан. Теперь нужно придумать контроллер к этой сетке, который будет её гнуть. В этой статье — расскажу вам про два контроллера сетки: SimpleGrid, ElasticGrid.
Первый у нас будет сбрасывать сетку, применять к ней текущие искажения.
Второй превратит нашу сетку в желе, которая будет колебаться, пока не придет к дефолтному состоянию.
Напишем первый контроллер, создадим новый класс SimpleGrid и его листинг:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Xna.Framework; namespace GridDistortion { public class SimpleGrid { publicGridVertexPositionColorTexture grid; // наша подконтрольная сетка public SimpleGrid(GridVertexPositionColorTexture grid) { this.grid = grid; } public void Update() { swap(); } public virutal void swap() // просто сбрасываем сетку на стандартные значения { grid.ResetUVs(); } public void Rebellion(Vector2 pos_rebellion, float radius) // делаем искажение на сетке с позицией и радиусом { Vector2 gridSize = new Vector2(grid.Width, grid.Height) * grid.CellSize; // размер нашего примитива for (int i = 0; i < grid.Vertices.Length; i++) { Vector2 pos = grid.GetUV0(i) * gridSize; // получаем реальную позицию вертекса из сетки Vector2 newPos = pos; Vector2 center = pos_rebellion; // где произошло искажение float distance = Distance(pos, center); // получаем дистанцию от центра искажение до текущей точки if (distance < radius) // если дистанция больше радиуса, то не трогаем пиксель, экономим ресурсы { Vector2 dir = pos - center; // получаем вектор направления float length = dir.Length(); float minDisplacement = -length; if (dir.Length() != 0) { dir.Normalize(); // нормализуем вектор } Vector2 displacement = dir * Math.Max(-100f, minDisplacement); // задаем вектор искажения, где -100f — его сила, положительная величина — не увеличит изображение, а сожмет newPos += displacement * (1f - distance / radius) * 0.25f; grid.SetUV0(i, newPos / gridSize); // задаем новую позицию } } } public static float Distance(Vector2 vector1, Vector2 vector2) { double value = ((vector2.X - vector1.X) * (vector2.X - vector1.X)) + ((vector2.Y - vector1.Y) * (vector2.Y - vector1.Y)); return (float)Math.Sqrt(value); } } }
Контроллер написан, теперь вернемся к Game1, две новых переменных:
GridVertexPositionColorTexture grid; SimpleGrid simpleGrid;
Их инициализация:
grid = new GridVertexPositionColorTexture(); grid.BuildGeometry(48, 80, new Vector2(10, 10)); simpleGrid = new SimpleGrid(grid);
Сам Update:
simpleGrid.Update(); // обновляем контроллер сетки TouchCollection collection = TouchPanel.GetState(); // получаем все прикосновения foreach (TouchLocation point in collection) { if (point.State == TouchLocationState.Moved) { simpleGrid.Rebellion(point.Position, 100f); // создаем искажение в точки point.Position с радиусом 100f } }
Ну и наконец прорисовка:
GraphicsDevice.SamplerStates[0] = SamplerState.LinearClamp; // уставливаем Clamp, т.к. с Wrap-ом будут проблемы в Reach профиле, чьи размеры не степень двойки basicEffect.Texture = background; // задаем текстуру для примитива basicEffect.CurrentTechnique.Passes[0].Apply(); // применяем basicEffect grid.Draw(GraphicsDevice); // рисуем примитив
Запускаем, касаемся экрана и видим искажения или эффект линзы.
Но повеселимся еще, сделаем желе из текстуры, класс ElasticGrid, наследуемый от SimpleGrid:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Xna.Framework; namespace GridDistortion { public class ElasticGrid : SimpleGrid { private Vector2[] Velocity; // скорости вертексов public ElasticGrid(GridVertexPositionColorTexture grid) : base(grid) { this.Velocity = new Vector2[(grid.Width + 1) * (grid.Height + 1)]; } public override void swap() { Vector2 gridSize = new Vector2(grid.Width, grid.Height) * grid.CellSize; for (int i = 0; i < grid.Vertices.Length; i++) { //Get the position in pixels Vector2 pos = grid.GetUV0(i) * gridSize; Vector2 pos_default = grid.GetDefaultUV(i) * gridSize; Vector2 dir = (pos_default - pos) / 1.1f; // получаем вектор скорости и делим его каждый раз на 1.1f, //Set the new Texture Coordinates grid.SetUV0(i, (pos + Velocity[i]) / gridSize); // задаем позицию + вектор скорости Velocity[i] = dir; // пишем в массив скорость } } } }
Меняем контроллер сетки в Game1 и любуемся искажениями.
Вот такой простой и интересный подход. Контроллеров может быть бесконечное кол-во, например, реалистичная вода в реалтайме без всяких шейдеров с волнами
В другой раз я попробую описать другие методы придания вашей игре красоты.
Так же из серии по XNA планируется написание статей на тематику 3D.
Ну и как бонус, следующая статья возможно будет о том, как можно написать игру на WP7, разместить её в маркете бесплатно, без рекламы и сидеть в нищете.
Исходники скачать можно тут.
Экспериментируйте, творите; до новых встреч :-)
