- Делаем эмиттер полностью настраиваемым
- Делаем генерацию частиц равномерной
- Управляем эмиттером
- Управляем специальными точками
- Привязываем гравитон к мышке
- Как понять, что частица попала в область действия гравитона
- Правим мини баг
- Как текст рисовать
Делаем эмиттер полностью настраиваемым
Сейчас, когда у нас эмиттер создает частицы у нас нет возможности контролировать этот процесс.
Например, мы не можем зафиксировать конкретное направление генерации частиц или, например, поменять цвет частиц
Давайте вынесем эти значения в поля
public class Emitter
{
/* ... */
public int X; // координата X центра эмиттера, будем ее использовать вместо MousePositionX
public int Y; // соответствующая координата Y
public int Direction = 0; // вектор направления в градусах куда сыпет эмиттер
public int Spreading = 360; // разброс частиц относительно Direction
public int SpeedMin = 1; // начальная минимальная скорость движения частицы
public int SpeedMax = 10; // начальная максимальная скорость движения частицы
public int RadiusMin = 2; // минимальный радиус частицы
public int RadiusMax = 10; // максимальный радиус частицы
public int LifeMin = 20; // минимальное время жизни частицы
public int LifeMax = 100; // максимальное время жизни частицы
public Color ColorFrom = Color.White; // начальный цвет частицы
public Color ColorTo = Color.FromArgb(0, Color.Black); // конечный цвет частиц
/* ... */
}
теперь подключим эти параметры в метод сброса частицы
public virtual void ResetParticle(Particle particle)
{
particle.Life = Particle.rand.Next(LifeMin, LifeMax);
particle.X = X;
particle.Y = Y;
var direction = Direction
+ (double)Particle.rand.Next(Spreading)
- Spreading / 2;
var speed = Particle.rand.Next(SpeedMin, SpeedMax);
particle.SpeedX = (float)(Math.Cos(direction / 180 * Math.PI) * speed);
particle.SpeedY = -(float)(Math.Sin(direction / 180 * Math.PI) * speed);
particle.Radius = Particle.rand.Next(RadiusMin, RadiusMax);
}
добавим метод для генерации частицы, на случай если захочется его переопределить
/* добавил метод */
public virtual Particle CreateParticle()
{
var particle = new ParticleColorful();
particle.FromColor = ColorFrom;
particle.ToColor = ColorTo;
return particle;
}
/* и подключим его в UpdateState */
public void UpdateState()
{
foreach (var particle in particles)
{
/* ... */
}
for (var i = 0; i < 10; ++i)
{
if (particles.Count < ParticlesCount)
{
var particle = CreateParticle(); // и собственно теперь тут его вызываем
ResetParticle(particle);
particles.Add(particle);
}
else
{
break;
}
}
}
Делаем генерацию частиц равномерной
Сейчас мы строго задаем количество частиц, которые можно сгенерировать, но можно настроить систему так чтобы она выдавала фиксированное количество частиц каждый такт,
Добавим новое поле, в котором можно будет указать количество частиц в такт:
public class Emitter
{
/* ... */
public int LifeMin = 20;
public int LifeMax = 100;
public int ParticlesPerTick = 1; // добавил новое поле
/* ... */
}
а теперь подкрутим метод UpdateState так, чтобы он учитывал это поле, тут немного хитрые алгоритм, так что если вы его не поняли с первого раза, то ничего страшного.
public void UpdateState()
{
int particlesToCreate = ParticlesPerTick; // фиксируем счетчик сколько частиц нам создавать за тик
foreach (var particle in particles)
{
if (particle.Life <= 0) // если частицы умерла
{
/*
* то проверяем надо ли создать частицу
*/
if (particlesToCreate > 0)
{
/* у нас как сброс частицы равносилен созданию частицы */
particlesToCreate -= 1; // поэтому уменьшаем счётчик созданных частиц на 1
ResetParticle(particle);
}
}
else
{
/* тут ничего не меняется */
}
}
// второй цикл меняем на while,
// этот новый цикл также будет срабатывать только в самом начале работы эмиттера
// собственно пока не накопится критическая масса частиц
while (particlesToCreate >= 1)
{
particlesToCreate -= 1;
var particle = CreateParticle();
ResetParticle(particle);
particles.Add(particle);
}
}
Управляем эмиттером
Теперь, когда эмиттер у нас полностью настраиваемый, у нас есть возможность управлять его поведением используя контроллеры на форме.
И так, давайте поставим один эмиттер на форму.
Тут очень важный момент, что если мы хотим им управлять нам надо эмиттер хранить в поле класса.
Если мы так сделаем, то и из других методов класса можно будет с ним чего-нибудь делать.
Добавляем
public partial class Form1 : Form
{
List<Emitter> emitters = new List<Emitter>();
Emitter emitter; // добавим поле для эмиттера
public Form1()
{
InitializeComponent();
picDisplay.Image = new Bitmap(picDisplay.Width, picDisplay.Height);
this.emitter = new Emitter // создаю эмиттер и привязываю его к полю emitter
{
Direction = 0,
Spreading = 10,
SpeedMin = 10,
SpeedMax = 10,
ColorFrom = Color.Gold,
ColorTo = Color.FromArgb(0, Color.Red),
ParticlesPerTick = 10,
X = picDisplay.Width / 2,
Y = picDisplay.Height / 2,
};
emitters.Add(this.emitter); // все равно добавляю в список emitters, чтобы он рендерился и обновлялся
}
/* ... */
}
получится вот так:
тут пока все просто.
Теперь чтобы изменять значение, например, направления добавим компоненту TrackBar, которая представляет собой ползунок, и очень удобна для управления числовыми параметрами, изменяющимися в заданном промежутке значений.
Вот так:
Теперь заставим наш эмиттер реагировать на изменение ползунка. Для этого щелкнем на него два раза, и в появившемся обработчике напишем:
public Form1()
{
/* ... */
private void tbDirection_Scroll(object sender, EventArgs e)
{
emitter.Direction = tbDirection.Value; // направлению эмиттера присваиваем значение ползунка
}
}
проверяем:
для наглядности лучше конечно добавить информацию что мы меняем и текущее значение
и в обработчике
public Form1()
{
/* ... */
private void tbDirection_Scroll(object sender, EventArgs e)
{
emitter.Direction = tbDirection.Value;
lblDirection.Text = $"{tbDirection.Value}°"; // добавил вывод значения
}
}
получится так
При желании естественно можно добавлять сколько угодно обработчиков и сколько угодно trackbar`ов, например, для изменения разброса
Управляем специальными точками
Добавим пару специальных точек. Например, два гравитона:
public Form1()
{
InitializeComponent();
picDisplay.Image = new Bitmap(picDisplay.Width, picDisplay.Height);
this.emitter = new Emitter
{
/* ... */
};
emitters.Add(this.emitter);
// добавил гравитон
emitter.impactPoints.Add(new GravityPoint
{
X = picDisplay.Width / 2 + 100,
Y = picDisplay.Height / 2,
});
// добавил второй гравитон
emitter.impactPoints.Add(new GravityPoint
{
X = picDisplay.Width / 2 - 100,
Y = picDisplay.Height / 2,
});
/* ... */
}
}
частицы сразу начнёт интересно закручивать
В принципе мы можем и этим процессом вполне управлять. Для этого у точек есть поле Power, значение которого можно менять с помощью трэкбара.
И так, добавляем ползунок
кликнем на него два раза, получим обработчик
private void tbGraviton_Scroll(object sender, EventArgs e)
{
}
чего в него добавлять?
Можно сначала пойти глобальным путем, и менять силу притяжения всех точек, привязанных к эмиттеру одновременно:
private void tbGraviton_Scroll(object sender, EventArgs e)
{
foreach(var p in emitter.impactPoints)
{
if (p is GravityPoint) // так как impactPoints не обязательно содержит поле Power, надо проверить на тип
{
// если гравитон то меняем силу
(p as GravityPoint).Power = tbGraviton.Value;
}
}
}
получится так:
Что бы было понятнее что у нас тут происходит, давайте как-нибудь визуализируем отображение специальных точек.
Для начала пойдём в IImpactPoint и сделаем метод Render виртуальным
public abstract class IImpactPoint
{
public float X;
public float Y;
public abstract void ImpactParticle(Particle particle);
public virtual void Render(Graphics g) // добавил слово virtual, ну чтобы override потом можно было юзать
{
/* ... */
}
}
а теперь доскроллим до GravityPoint и переопределим этот метод
public class GravityPoint : IImpactPoint
{
/* ... */
public override void Render(Graphics g)
{
// буду рисовать окружность с диаметром равным Power
g.DrawEllipse(
new Pen(Color.Red),
X - Power / 2,
Y - Power / 2,
Power,
Power
);
}
}
запускаем, и от такие очёчки получатся:
Ну и встает вопрос: “А как изменять каждую точку отдельно?”
Да очень просто! Достаточно вынести точки в поля класса и изменять параметры поля:
public partial class Form1 : Form
{
List<Emitter> emitters = new List<Emitter>();
Emitter emitter;
GravityPoint point1; // добавил поле под первую точку
GravityPoint point2; // добавил поле под вторую точку
public Form1()
{
// от сюда НЕ ТРОГАЕМ
InitializeComponent();
picDisplay.Image = new Bitmap(picDisplay.Width, picDisplay.Height);
this.emitter = new Emitter
{
/* ... */
};
emitters.Add(this.emitter);
// до сюда НЕ ТРОГАЕМ
// привязываем гравитоны к полям
point1 = new GravityPoint
{
X = picDisplay.Width / 2 + 100,
Y = picDisplay.Height / 2,
};
point2 = new GravityPoint
{
X = picDisplay.Width / 2 - 100,
Y = picDisplay.Height / 2,
};
// привязываем поля к эмиттеру
emitter.impactPoints.Add(point1);
emitter.impactPoints.Add(point2);
}
/* ... */
}
и теперь добавляем второй трекбар, прописываем обработчики
private void tbGraviton_Scroll(object sender, EventArgs e)
{
point1.Power = tbGraviton1.Value;
}
private void tbGraviton2_Scroll(object sender, EventArgs e)
{
point2.Power = tbGraviton2.Value;
}
и вуаля, каждый теперь управляет конкретной точкой
Привязываем гравитон к мышке
Да-да, еще можем заставить точку за мышкой следовать. Тут совсем просто, идем в обработчик MouseMove и добавляем:
private void picDisplay_MouseMove(object sender, MouseEventArgs e)
{
// это не трогаем
foreach (var emitter in emitters)
{
emitter.MousePositionX = e.X;
emitter.MousePositionY = e.Y;
}
// а тут передаем положение мыши, в положение гравитона
point2.X = e.X;
point2.Y = e.Y;
}
такая красота получается:
Как понять, что частица попала в область действия гравитона
Этот пункт пригодится всем, кто будет делать желтые и красные задачки.
Доработаем наш гравитон так чтобы он притягиваю только те частицы которые попали в красную окружность.
Идем в GravityPoint и правим метод ImpactParticle
public override void ImpactParticle(Particle particle)
{
float gX = X - particle.X;
float gY = Y - particle.Y;
double r = Math.Sqrt(gX * gX + gY * gY); // считаем расстояние от центра точки до центра частицы
if (r + particle.Radius < Power / 2) // если частица оказалось внутри окружности
{
// то притягиваем ее
float r2 = (float)Math.Max(100, gX * gX + gY * gY);
particle.SpeedX += gX * Power / r2;
particle.SpeedY += gY * Power / r2;
}
}
уиииииии!!! =)
Правим мини баг
Еще, у меня был небольшой косяк который не мешался при работе гравитона, но может начать мешаться если вы будете делать свои задачки.
Заключался он в том, что я меняю положение частицы в эмиттере после применения методов ImpactParticle, а лучше делать это до. И так идем в метод UpdateState
public void UpdateState()
{
int particlesToCreate = ParticlesPerTick;
foreach (var particle in particles)
{
if (particle.Life <= 0)
{
/* ... */
}
else
{
/* теперь двигаю вначале */
particle.X += particle.SpeedX;
particle.Y += particle.SpeedY;
particle.Life -= 1;
foreach (var point in impactPoints)
{
point.ImpactParticle(particle);
}
particle.SpeedX += GravitationX;
particle.SpeedY += GravitationY;
/* это уехало вверх
particle.X += particle.SpeedX;
particle.Y += particle.SpeedY; */
}
}
while (particlesToCreate >= 1)
{
/* ... */
}
}
лучше всего это видно на задачке с радаром, вот как выглядит детекция, если двигать частицу до:
а вот как после
то есть детектируются частицы которые визуально не находятся в области
Как текст рисовать
Чтобы рисовать текст, надо использовать метод DrawString.
Например, хочу я добавить текст к гравитону и делаю так
public class GravityPoint : IImpactPoint
{
/* ... */
public override void ImpactParticle(Particle particle)
{
/* ... */
}
public override void Render(Graphics g)
{
g.DrawEllipse(/* ... */);
g.DrawString(
$"Я гравитон\nc силой {Power}", // надпись, можно перенос строки вставлять (если вы Катя, то может не работать и надо использовать \r\n)
new Font("Verdana", 10), // шрифт и его размер
new SolidBrush(Color.White), // цвет шрифта
X, // расположение в пространстве
Y
);
}
}
если хочется рисовать по центру, то надо использовать специальный класс StringFormat
public class GravityPoint : IImpactPoint
{
/* ... */
public override void ImpactParticle(Particle particle)
{
/* ... */
}
public override void Render(Graphics g)
{
g.DrawEllipse(/* ... */);
var stringFormat = new StringFormat(); // создаем экземпляр класса
stringFormat.Alignment = StringAlignment.Center; // выравнивание по горизонтали
stringFormat.LineAlignment = StringAlignment.Center; // выравнивание по вертикали
g.DrawString(
$"Я гравитон\nc силой {Power}",
new Font("Verdana", 10),
new SolidBrush(Color.White),
X,
Y,
stringFormat // передаем инфу о выравнивании
);
}
}
иногда хочется нарисовать фон для текста, чтобы буквы четко читались, для этого надо сначала померить размеры тексты.
В этих целях используется функция MeasureString
public class GravityPoint : IImpactPoint
{
/* ... */
public override void ImpactParticle(Particle particle)
{
/* ... */
}
public override void Render(Graphics g)
{
g.DrawEllipse(/* ... */);
var stringFormat = new StringFormat();
stringFormat.Alignment = StringAlignment.Center;
stringFormat.LineAlignment = StringAlignment.Center;
// обязательно выносим текст и шрифт в переменные
var text = $"Я гравитон\nc силой {Power}";
var font = new Font("Verdana", 10);
// вызываем MeasureString, чтобы померить размеры текста
var size = g.MeasureString(text, font);
// рисуем подложнку под текст
g.FillRectangle(
new SolidBrush(Color.Red),
X - size.Width / 2, // так как я выравнивал текст по центру то подложка должна быть центрирована относительно X,Y
Y - size.Height / 2,
size.Width,
size.Height
);
// ну и текст рисую уже на базе переменных
g.DrawString(
text,
font,
new SolidBrush(Color.White),
X,
Y,
stringFormat
);
}
}
вот такая штука получается:
Ну собственно и все, по идее этого должно хватить чтобы сделать любую задачку.
Удачи! =)