И опять, привет хабравчанам!
Буквально несколько дней назад — начал цикл статей, о том, как можно создавать крутые игры с помощью XNA Framework, своей студии у меня нет, поэтому ограничимся только 2D играми.
На этот раз — мы более подробно рассмотрим Draw и напишем свою первую систему частиц.
Какие темы будут затронуты в этой статье:
Во второй части расскажу:
Как всегда, сначала теория, потом —пирожки код.
О том, как рисовать, мы рассматривали в прошлой статье. Давайте теперь чуть подробнее посмотрим на эти методы:
Вот так у нас начинается прорисовка чего-нибудь на экране, это последняя перегрузка метода, поэтому тут рассмотрим все.
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.
texture — сама текстура, которую мы будем рисовать.
position — позиция на экране (мире, если есть матрица трансформации, иначе говоря: «камера»).
sourceDest — прямоугольник из текстуры (какую часть текстуры будем рисовать, если всю, то new Rectangle(0, 0, width_texture, height_texture))
color — цвет объекта.
angle — угол поворота.
origin — так называемый offset или «центр масс» текстуры. Иначе говоря — смещает центр текстуры на NxM пикселей.
scale — размеры текстуры по X и Y
effects — различные эффекты отображения текстуры, например: можно нарисовать её зеркальное отражение.
layerDepth — глубина слоя.
Параметры основных функций, которые отвечают за прорисовку — разобрались.
Напишем простую систему частиц, в нашем случае это трейл (trail, шлейф, хвост), который будет оставаться от движения мышки.
Дальше будет код.
Создаем новый класс Particle, это будет наша единичная частичка (дым, искра,деньги), листинг с комментариями:
Теперь, нужно создать класс, который будет управлять всеми частичками в игре, назовем его ParticleController, листинг с комментариями:
А в главном классе прописываем LoadContent, Update, Draw в соответствующих местах, заодно добавим генерацию частичек каждый апдейт:
Запускаем, двигаем мышку, любуемся:

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

На этот раз — мы более подробно рассмотрим 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: вторая часть статьи.