🎨 Графическое программирование на C#

← Вернуться к модулю 8

Урок 8.1: Системы частиц и эффекты

Модуль 8 ⏱️ ~1.5 часа

📌 Что вы изучите:

  • Принцип работы эмиттера частиц
  • Параметры частиц: скорость, размер, жизнь
  • Гравитация и плавное исчезновение
  • Отрисовка полупрозрачных частиц
  • Оптимизация большого количества элементов

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

Система частиц состоит из эмиттера, набора объектов частиц и цикла обновления. Каждая частица хранит позицию, скорость, размер, цвет и время жизни.

private void CreateParticle(float x, float y)
{
    particles.Add(new Particle
    {
        Position = new PointF(x, y),
        Velocity = new PointF((float)(rnd.NextDouble() - 0.5) * 6, (float)(rnd.NextDouble() - 1) * 6),
        Size = (float)(rnd.NextDouble() * 4 + 2),
        Lifetime = rnd.Next(40, 120),
        Color = Color.FromArgb(rnd.Next(150, 256), rnd.Next(150, 256), rnd.Next(150, 256))
    });
}

Плавное исчезновение

Для эффекта затухания используйте альфа-канал, основанный на оставшейся жизни частицы.

📝 Пример задания — система частиц

Эмиттер следует за курсором мыши, создаёт до 500 частиц с гравитацией и затуханием по альфа-каналу:

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

namespace CSharpGraphicsApp
{
    public class Task21_ParticleAnimation : Form
    {
        private class Particle
        {
            public PointF Position { get; set; }
            public PointF Velocity { get; set; }
            public Color Color { get; set; }
            public float Size { get; set; }
            public float Lifetime { get; set; }
            public float MaxLifetime { get; set; }
            public float Gravity { get; set; }

            public float Alpha => (Lifetime / MaxLifetime) * 255;

            public void Update()
            {
                var pos = Position;
                pos.X += Velocity.X;
                pos.Y += Velocity.Y;
                Position = pos;

                var vel = Velocity;
                vel.Y += Gravity;
                Velocity = vel;
                Lifetime -= 1;
            }

            public bool IsAlive => Lifetime > 0;
        }

        private List<Particle> particles = new List<Particle>();
        private Random random = new Random();
        private readonly Timer timer = new Timer();
        private int emitterX, emitterY;

        public Task21_ParticleAnimation()
        {
            Text = "Задание 21 — Система частиц (Particle Animation)";
            Size = new Size(800, 600);
            BackColor = Color.Black;
            DoubleBuffered = true;
            Paint += Form_Paint;
            MouseMove += (s, e) => { emitterX = e.X; emitterY = e.Y; };

            timer.Interval = 16;
            timer.Tick += Timer_Tick;
            timer.Start();
            FormClosed += (s, e) => timer.Stop();
        }

        private void Timer_Tick(object sender, EventArgs e)
        {
            if (particles.Count < 500)
            {
                for (int i = 0; i < 3; i++)
                {
                    particles.Add(new Particle
                    {
                        Position = new PointF(emitterX, emitterY),
                        Velocity = new PointF(
                            (float)(random.NextDouble() - 0.5) * 6,
                            (float)(random.NextDouble() - 1) * 6),
                        Color = Color.FromArgb(
                            random.Next(100, 256),
                            random.Next(100, 256),
                            random.Next(100, 256)),
                        Size = (float)(random.NextDouble() * 4 + 2),
                        MaxLifetime = random.Next(40, 120),
                        Lifetime = random.Next(40, 120),
                        Gravity = 0.15f
                    });
                }
            }

            for (int i = particles.Count - 1; i >= 0; i--)
            {
                particles[i].Update();
                if (!particles[i].IsAlive)
                    particles.RemoveAt(i);
            }
            Invalidate();
        }

        private void Form_Paint(object sender, PaintEventArgs e)
        {
            e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
            foreach (var p in particles)
            {
                byte alpha = (byte)Math.Min(255, Math.Max(0, p.Alpha));
                using (var brush = new SolidBrush(Color.FromArgb(alpha, p.Color)))
                    e.Graphics.FillEllipse(brush,
                        p.Position.X - p.Size / 2,
                        p.Position.Y - p.Size / 2,
                        p.Size, p.Size);
            }
            e.Graphics.FillEllipse(Brushes.White, emitterX - 5, emitterY - 5, 10, 10);
            e.Graphics.DrawString($"Частиц: {particles.Count} | двигайте мышью",
                new Font("Segoe UI", 11, FontStyle.Bold), Brushes.White, 10, 10);
        }
    }
}

Практическое задание

  • Реализуйте эмиттер, который создаёт новые частицы каждый тик.
  • Добавьте гравитацию и уменьшение скорости.
  • Рисуйте частицы с помощью FillEllipse и полупрозрачного кисти.
  • Очищайте устаревшие частицы.

Задание по аналогии: Система частиц

По аналогии с Task21_ParticleAnimation реализуйте эмиттер частиц с физикой.

  1. Создайте внутренний класс Particle с полями: Position, Velocity, Color, Size, Lifetime, MaxLifetime, Gravity. Добавьте вычисляемое свойство Alpha = (Lifetime / MaxLifetime) * 255.
  2. В методе Particle.Update() обновляйте позицию по скорости (pos.X += vel.X), добавляйте гравитацию (vel.Y += Gravity) и уменьшайте Lifetime на 1.
  3. В Timer_Tick создавайте 3 новые частицы за тик (если < 500), затем обновляйте все частицы и удаляйте мёртвые через цикл с конца списка.
  4. В Form_Paint для каждой частицы создавайте SolidBrush с Color.FromArgb(alpha, p.Color) и рисуйте FillEllipse размером p.Size.
  5. Дополнительно: добавьте несколько режимов через ComboBox — фейерверк, фонтан, дым — изменяя начальную скорость и гравитацию.

Пример результата выполнения задания:

Результат Task21

Скриншот программы (Task21)

💡 Совет: Всегда перебирайте список с конца (for (int i = Count - 1; i >= 0; i--)) при удалении элементов — иначе индексы сдвигаются и вы пропустите некоторые частицы.

Вопросы для самопроверки

Почему важно ограничивать число частиц? Как зависит альфа-канал от времени жизни?