XNA Draw или пишем систему частиц. Часть I

  • Tutorial
И опять, привет хабравчанам!

Буквально несколько дней назад — начал цикл статей, о том, как можно создавать крутые игры с помощью XNA Framework, своей студии у меня нет, поэтому ограничимся только 2D играми.

На этот раз — мы более подробно рассмотрим Draw и напишем свою первую систему частиц.

Какие темы будут затронуты в этой статье:
  • Методы spriteBatch.Begin() и spriteBatch.Draw()
  • Реализация системы частиц


Во второй части расскажу:
  • Что такое пиксельный шейдер
  • Что такое post-processing
  • Что такое RenderTarget2D и с чем его едят заправляют
  • Искажающий шейдер с Displacemenet-map


Как всегда, сначала теория, потом — пирожки код.

Метод spriteBatch.Begin()



О том, как рисовать, мы рассматривали в прошлой статье. Давайте теперь чуть подробнее посмотрим на эти методы:

spriteBatch.Begin()


spriteBatch.Begin(SpriteSortMode, BlendState, SamplerState, DepthStencilState, RasterizerState, Effect, Matrix);


Вот так у нас начинается прорисовка чего-нибудь на экране, это последняя перегрузка метода, поэтому тут рассмотрим все.

SpriteSortMode — способ сортировки спрайтов. Ничего интересного.

BlendState включает в себя:

Additive — Настройка для «additive blend». Смешивает один спрайт с другим используя альфа-канал спрайтов.


AlphaBlend — Настройка для «alpha blend». Накладывает один спрайт на другой, используя альфа-канал спрайтов.


NonPremultiplied — Настройка для «blending with non-premultipled alpha», Накладывает один спрайт на другой, используя альфу цвета Draw'а.


Opaque — Настройка для «opaque blend», Накладывает один спрайт на другой как бы «перезаписывая» его.


SamplerState включает в себя:
AnisotropicClamp — Содержит состояние по умолчанию для анизотропного фильтрования и TextureUV — Clamp
AnisotropicWrap — Содержит состояние по умолчанию для анизотропного фильтрования и TextureUV — Wrap


Грубо говоря, Clamp — растягивает текстуру, а Wrap её тайлит (повторяет).

Используем текстуру 55x20 и растягиваем (Clamp) её в пять раз, различия Anisotropic/Linear, Point:

Anisotropic/Linear:


Point:


DepthStencilState — опять сортировка, нам не нужна.
RasterizerState — для 2D нам не очень надо.
Effect — шейдер (эффект), который будет обрабатывать нарисованный объект.
Matrix — матрица трансформации объекта (например, с помощью её можно реализовать 2D камеру)

Рассмотрим метод, который включен между Begin и End.

Метод spriteBatch.Draw()



spriteBatch.Draw(Texture2D texture, Vector2D position, Rectangle sourceDest, Color color, float angle, Vector2D origin, Vector2D scale, SpriteEffects effects, float layerDepth);


texture — сама текстура, которую мы будем рисовать.

position — позиция на экране (мире, если есть матрица трансформации, иначе говоря: «камера»).

sourceDest — прямоугольник из текстуры (какую часть текстуры будем рисовать, если всю, то new Rectangle(0, 0, width_texture, height_texture))

color — цвет объекта.

angle — угол поворота.

origin — так называемый offset или «центр масс» текстуры. Иначе говоря — смещает центр текстуры на NxM пикселей.

scale — размеры текстуры по X и Y

effects — различные эффекты отображения текстуры, например: можно нарисовать её зеркальное отражение.

layerDepth — глубина слоя.

Параметры основных функций, которые отвечают за прорисовку — разобрались.

Система частиц



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

Дальше будет код.

Создаем новый класс Particle, это будет наша единичная частичка (дым, искра, деньги), листинг с комментариями:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using System.Diagnostics;

namespace ParticleSystem
{
    public class Particle
    {

        public Texture2D Texture { get; set; }        // Текстура нашей частички
        public Vector2 Position { get; set; }        // Позиция частички
        public Vector2 Velocity { get; set; }        // Скорость частички
        public float Angle { get; set; }            // Угол поворота частички
        public float AngularVelocity { get; set; }    // Угловая скорость
        public Vector4 Color { get; set; }            // Цвет частички
        public float Size { get; set; }                // Размеры
        public float SizeVel { get; set; }		// Скорость уменьшения размера
        public float AlphaVel { get; set; }		// Скорость уменьшения альфы
        public int TTL { get; set; }                // Время жизни частички

        public Particle(Texture2D texture, Vector2 position, Vector2 velocity,
            float angle, float angularVelocity, Vector4 color, float size, int ttl, float sizeVel, float alphaVel) // конструктор
        {
            Texture = texture;
            Position = position;
            Velocity = velocity;
            Angle = angle;
            Color = color;
            AngularVelocity = angularVelocity;
            Size = size;
            SizeVel = sizeVel;
            AlphaVel = alphaVel;
            TTL = ttl;
        }

        public void Update() // цикл обновления
        {
            TTL--; // уменьшаем время жизни

            // Меняем параметры в соответствии с скоростями
            Position += Velocity;
            Angle += AngularVelocity;
            Size += SizeVel;

            Color = new Vector4(Color.X, Color.Y, Color.Z, Color.W - AlphaVel); // убавляем цвет. Кстати, цвет записан в Vector4, а не в Color, потому что: Color.R/G/B имеет тип Byte (от 0x00 до 0xFF), чтобы не проделывать лишней трансформации, используем float и Vector4

        }


        public void Draw(SpriteBatch spriteBatch)
        {
            
                Rectangle sourceRectangle = new Rectangle(0, 0, Texture.Width, Texture.Height); // область из текстуры: вся
                Vector2 origin = new Vector2(Texture.Width / 2, Texture.Height / 2); // центр

                spriteBatch.Draw(Texture, Position, sourceRectangle, new Color(Color),
                    Angle, origin, Size, SpriteEffects.None, 0); // акт прорисовки

        }

    }
}


Теперь, нужно создать класс, который будет управлять всеми частичками в игре, назовем его ParticleController, листинг с комментариями:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;

using System.Diagnostics;

namespace ParticleSystem
{
    class ParticleController
    {

        public List<Particle> particles;

        private Texture2D dot; // текстура точки
        private Texture2D smoke; // текстура дыма

        private Random random;

        public ParticleController()
        {
            this.particles = new List<Particle>();
            random = new Random();
        }

        public void LoadContent(ContentManager Manager)
        {
            dot = Manager.Load<Texture2D>("spark");
            smoke = Manager.Load<Texture2D>("smoke");
           
        }

        public void EngineRocket(Vector2 position) // функция, которая будет генерировать частицы
        {
            for (int a = 0; a < 2; a++) // создаем 2 частицы дыма для трейла
            {
                Vector2 velocity = AngleToV2((float)(Math.PI * 2d * random.NextDouble()), 0.6f);
                float angle = 0;
                float angleVel = 0;
                Vector4 color = new Vector4(1f, 1f, 1f, 1f);
                float size = 1f;
                int ttl = 40;
                float sizeVel = 0;
                float alphaVel = 0;


                GenerateNewParticle(smoke, position, velocity, angle, angleVel, color, size, ttl, sizeVel, alphaVel);
            }

            for (int a = 0; a < 1; a++) // создаем 1 искру для трейла
            {
                Vector2 velocity = AngleToV2((float)(Math.PI * 2d * random.NextDouble()), .2f);
                float angle = 0;
                float angleVel = 0;
                Vector4 color = new Vector4(1.0f, 0.5f, 0.5f, 0.5f);
                float size = 1f;
                int ttl = 80;
                float sizeVel = 0;
                float alphaVel = .01f;


                GenerateNewParticle(dot, position, velocity, angle, angleVel, color, size, ttl, sizeVel, alphaVel);
            }

            for (int a = 0; a < 10; a++) // создаем 10 дыма, но на практике — реактивная струя для трейла
            {
                Vector2 velocity = Vector2.Zero;
                float angle = 0;
                float angleVel = 0;
                Vector4 color = new Vector4(1.0f, 0.5f, 0.5f, 1f);
                float size = 0.1f + 1.8f * (float)random.NextDouble();
                int ttl = 10;
                float sizeVel = -.05f;
                float alphaVel = .01f;


                GenerateNewParticle(smoke, position, velocity, angle, angleVel, color, size, ttl, sizeVel, alphaVel);
            }
        }
      
        private Particle GenerateNewParticle(Texture2D texture, Vector2 position, Vector2 velocity,
            float angle, float angularVelocity, Vector4 color, float size, int ttl, float sizeVel, float alphaVel) // генерация новой частички
        {
            Particle particle = new Particle(texture, position, velocity, angle, angularVelocity, color, size, ttl, sizeVel, alphaVel);
            particles.Add(particle);
            return particle;
        }

        public void Update(GameTime gameTime)
        {

            for (int particle = 0; particle < particles.Count; particle++) 
            {
                particles[particle].Update();
                if (particles[particle].Size <= 0 || particles[particle].TTL <= 0) // если время жизни частички или её размеры равны нулю, удаляем её
                {
                    particles.RemoveAt(particle);
                    particle--;
                }
            }

        }

        public void Draw(SpriteBatch spriteBatch)
        {
            spriteBatch.Begin(SpriteSortMode.Deferred, BlendState.Additive); // ставим режим смешивания Addictive

            for (int index = 0; index < particles.Count; index++) // рисуем все частицы
            {
                particles[index].Draw(spriteBatch); 
            }

            spriteBatch.End();
        }

        public Vector2 AngleToV2(float angle, float length)
        {
            Vector2 direction = Vector2.Zero;
            direction.X = (float)Math.Cos(angle) * length;
            direction.Y = -(float)Math.Sin(angle) * length;
            return direction;
        }
    }
}


А в главном классе прописываем LoadContent, Update, Draw в соответствующих местах, заодно добавим генерацию частичек каждый апдейт:

particleController.EngineRocket(new Vector2(Mouse.GetState().X, Mouse.GetState().Y));


Запускаем, двигаем мышку, любуемся:


Как вы понимаете, такую систему можно сделать еще красивее: добавить шейдеры. Но пусть объем статьи останется адекватный. О том, как можно использовать шейдеры в своих целях — расскажу во второй части статьи.

Прикладываю исходники и демо.

До новый встреч ;)

UPD: вторая часть статьи.
Share post

Comments 20

    +2
    Спасибо за статью! Ждём следующих частей.
      +2
      Класс! Спасибо вам за уроки.
        0
        Жду следующую часть с нетерпением!
          0
          Очень ценю такие обзоры, написано с любовью прям.
            0
            Win 7 x64 демка не запускается :(
              +1
              Хотя постойте, у меня не стоял XNA Framework! Приношу свои извинения.
              ForhaxeD, это волшебно!
              +1
              XNA установлен, но у меня видяха встроенная и демо не запускается, при запуске пишет: No suitable graphics card found.
                0
                Это означает, что у вас нет видеокарты с аппаратным DirectX9 на борту/нет драйверов нормальных.
                  0
                  Ну какие-то другие вещи свободно запускаются, например, Terraria 1.0.6, которая использует XNA 4-ой версии.
                    +2
                    Это означает, что ты используешь HiDef версию игры, которой требуется DX10. Нужно перейти на Reach в настройках проекта.
                    blogs.msdn.com/b/shawnhar/archive/2010/03/12/reach-vs-hidef.aspx
                      0
                      Это я понял, но ты быстрее написал ;)
                  +2
                  Не самая удачная система частиц с точки зрения производительности. класс Particle лучше сделать структурой. Еще можно избавится от необходимости добавления и удаления частиц из списка во время отрисовки. Но в целом для демонстрации принципов работы с XNA вполне подойдет.
                    +1
                    Надо бы пул частиц использовать, да
                      –1
                      Если интересно, то можно посмотреть вот эту систему частиц mpe,
                      0
                      Точно! О структуре я как-то не подумал, буду использовать структуры, но статьи закончу в таком виде. Спасибо.
                        0
                        Я бы сделал так: делаем большой массив структур, поля которой описывают частицу, причем чтобы этот массив еще и разрастался по необходимости. В этой структуре также было бы поле Active, которое указывало бы, что частица активна. Это в принципе и основной регулятор: нужна частица — ставим Active в true, не нужна — в false. Еще будет массив свободных блоков. Каждый элемент этого массива — это индекс начала свободного блока, где частицы неактивны. Вначале туда помещается один индекс — нуль. При создании частицы берется первый такой свободной блок и в его начало помещается новая частица, а начало смещается на единицу. Если следующая частица также активна, то блок удаляется из списка блоков. При удалении частицы если слева от нее частица неактивна, то в список свободных блоков добавляем индекс текущей частицы. Если добавляем частицу, а список свободных блоков пуст, то значит нужно нарастить массив структур.

                        Пул — это такая структура данных, в которой удаленные объекты используются повторно.
                      +3
                      Как-то ты, Леонид, смело предположил, что сортировка спрайтов не интересна. Во-первых, DepthStencilState никак не относится к сортировке напрямую; этот параметр указывает состояние буфера глубины, который повсеместно используется в трехмерных играх, но и в двумерных можно получить выгоду. Для чего он нужен? При отрисовке каждый пиксель после всех трансформаций имеет три координаты: координаты на экране и глубину. Эта самая глубина сравнивается с тем, что уже записано в буфер определенной операцией сравнения, которая задается состоянием (DepthStencilState). Если пиксель проходит тест, то он передается в пиксельный шейдер и записывается в буфер глубины. Для чего это нужно? Для регулирования порядка отображения спрайтов. Сейчас это делается напрямую: отрисовал один спрайт, потом второй, и вот второй находится над первым. С буфером глубины можно не заботится о порядке отрисовки спрайтов, но при этом если рисовать их в порядке от меньшей глубины к большей, то экономятся пиксели в пиксельном шейдере при перекрывающихся спрайтах, т.к. пиксели спрайта, который находится под пикселями другого спрайта не пройдут тест на буфер глубины.

                      Тут в дело вступает SpriteSortMode и layerDepth. Точно не знаю, как устроен spriteBatch (это нужно еще проверить), но я надеюсь, что layerDepth регулирует как раз ту самую глубину. Я писал свой собственный OutputBatch, который поддерживает вывод помимо спрайтов еще и примитивы, и там depth я сделал одним из главных параметров. Очень важная штука: SpriteSortMode.Texture — сортировка по текстурам. При смене текстуры необходимо менять состояние устройства, а это накладно, поэтому лучше спрайты с одинаковой текстурой группировать. В случае с системой частиц это не нужно. Также это не нужно, когда кол-во разных текстур не далеко ушло от количества спрайтов. В этом случае лучше сделать одну большую текстуру и воспользоваться sourceRectangle при использовании Draw. Это сильно повышает производительность.
                      0
                      Перевыложите пожалуйста исходники и демо, а то у меня ничего не скачалось — ERROR 404 — Not Found!

                      Only users with full accounts can post comments. Log in, please.