Pull to refresh

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

Reading time 6 min
Views 29K
И опять, привет хабравчанам!

Буквально несколько дней назад — начал цикл статей, о том, как можно создавать крутые игры с помощью 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: вторая часть статьи.
Tags:
Hubs:
+57
Comments 20
Comments Comments 20

Articles