还在羡慕游戏中那些绚丽的火焰、爆炸、雪花特效吗?作为C#开发者,你完全可以用SkiaSharp轻松实现这些视觉盛宴!
很多同学问我:"C# WinForm还能做出这么炫的效果?"答案是肯定的!今天我们就用SkiaSharp从零开始打造一个完整的粒子系统,让你的桌面应用瞬间提升逼格。无论你是游戏开发新手,还是想为企业软件添加视觉特效,这篇文章都能给你满满的干货。
本文将手把手教你构建一个支持火焰、烟花、雨雪等多种特效的粒子系统,代码完整可运行,包含性能优化和最佳实践。
相信做过WinForm开发的同学都有这样的经历:
c#// 1. 粒子实体 - 最小渲染单位
public class Particle
{
public SKPoint Position { get; set; }
public SKPoint Velocity { get; set; }
public SKColor Color { get; set; }
public float Life { get; set; }
public float Size { get; set; }
public bool IsAlive => Life > 0;
}
// 2. 粒子引擎 - 核心控制器
public class ParticleEngine
{
private List<Particle> particles;
public int EmissionRate { get; set; } = 50;
public float ParticleLifespan { get; set; } = 3.0f;
public void Update(float deltaTime) { /* 更新逻辑 */ }
public void Draw(SKCanvas canvas) { /* 渲染逻辑 */ }
}
// 3. 渲染控制器 - UI交互层
public partial class FrmParticleSystem : Form
{
private ParticleEngine particleEngine;
private Timer animationTimer;
// UI控件和事件处理
}
c#using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AppParticleSystem
{
public class Particle
{
public SKPoint Position { get; set; }
public SKPoint Velocity { get; set; }
public SKColor Color { get; set; }
public float Life { get; set; }
public float MaxLife { get; set; }
public float Size { get; set; }
public float Alpha { get; set; }
public bool IsAlive => Life > 0;
public Particle(SKPoint position, SKPoint velocity, SKColor color, float lifespan, float size)
{
Position = position;
Velocity = velocity;
Color = color;
Life = lifespan;
MaxLife = lifespan;
Size = size;
Alpha = 1.0f;
}
public void Update(float deltaTime, bool useGravity = true)
{
if (!IsAlive) return;
// 更新位置
Position = new SKPoint(
Position.X + Velocity.X * deltaTime,
Position.Y + Velocity.Y * deltaTime
);
// 应用重力
if (useGravity)
{
Velocity = new SKPoint(
Velocity.X * 0.99f, // 空气阻力
Velocity.Y + 98f * deltaTime // 重力加速度
);
}
// 更新生命周期
Life -= deltaTime;
Alpha = Math.Max(0, Life / MaxLife);
// 粒子老化效果
var ageRatio = 1 - (Life / MaxLife);
Size = Size * (1 - ageRatio * 0.3f);
}
public void Draw(SKCanvas canvas, SKPaint paint, bool useTrails = false)
{
if (!IsAlive) return;
// 设置颜色和透明度
var drawColor = Color.WithAlpha((byte)(255 * Alpha));
paint.Color = drawColor;
if (useTrails)
{
// 绘制拖尾效果
paint.Style = SKPaintStyle.Fill;
var trailLength = Math.Min(Velocity.Length * 0.1f, 20f);
for (int i = 0; i < 5; i++)
{
var trailAlpha = Alpha * (1 - i * 0.2f);
var trailColor = Color.WithAlpha((byte)(255 * trailAlpha));
paint.Color = trailColor;
var trailPos = new SKPoint(
Position.X - Velocity.X * i * 0.01f,
Position.Y - Velocity.Y * i * 0.01f
);
canvas.DrawCircle(trailPos, Size * (1 - i * 0.1f), paint);
}
}
else
{
// 普通粒子绘制
paint.Style = SKPaintStyle.Fill;
canvas.DrawCircle(Position, Size, paint);
}
}
}
}
c#using SkiaSharp;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AppParticleSystem
{
public enum ParticlePreset
{
Fire,
Fireworks,
Rain,
Snow,
Stars
}
public class ParticleEngine
{
private List<Particle> particles;
private Random random;
private SKPoint emitterPosition;
private bool isEmitting;
private float timeSinceLastEmission;
// 可调参数
public int EmissionRate { get; set; } = 50;
public float ParticleLifespan { get; set; } = 3.0f;
public int InitialSpeed { get; set; } = 100;
public int ParticleSize { get; set; } = 5;
public bool UseGravity { get; set; } = true;
public bool UseTrails { get; set; } = false;
// 当前预设
private ParticlePreset currentPreset = ParticlePreset.Fire;
public int ParticleCount => particles.Count(p => p.IsAlive);
public ParticleEngine()
{
particles = new List<Particle>();
random = new Random();
emitterPosition = new SKPoint(400, 300);
isEmitting = false;
timeSinceLastEmission = 0;
}
public void SetEmitterPosition(float x, float y)
{
emitterPosition = new SKPoint(x, y);
}
public void StartEmission()
{
isEmitting = true;
}
public void StopEmission()
{
isEmitting = false;
}
public void Update(float deltaTime)
{
// 发射新粒子
if (isEmitting)
{
timeSinceLastEmission += deltaTime;
var emissionInterval = 1.0f / EmissionRate;
while (timeSinceLastEmission >= emissionInterval)
{
EmitParticle();
timeSinceLastEmission -= emissionInterval;
}
}
// 更新现有粒子
foreach (var particle in particles)
{
particle.Update(deltaTime, UseGravity);
}
// 移除死亡的粒子
particles.RemoveAll(p => !p.IsAlive);
}
private void EmitParticle()
{
var particle = CreateParticleByPreset(currentPreset);
particles.Add(particle);
}
private Particle CreateParticleByPreset(ParticlePreset preset)
{
switch (preset)
{
case ParticlePreset.Fire:
return CreateFireParticle();
case ParticlePreset.Fireworks:
return CreateFireworkParticle();
case ParticlePreset.Rain:
return CreateRainParticle();
case ParticlePreset.Snow:
return CreateSnowParticle();
case ParticlePreset.Stars:
return CreateStarParticle();
default:
return CreateFireParticle();
}
}
private Particle CreateFireParticle()
{
var angle = (random.NextSingle() - 0.5f) * 1.5f; // 火焰向上扩散
var speed = InitialSpeed + random.Next(-20, 20);
var velocity = new SKPoint(
(float)Math.Sin(angle) * speed,
(float)Math.Cos(angle) * speed - 50 // 向上偏移
);
var colors = new[] {
SKColors.Red, SKColors.Orange, SKColors.Yellow, SKColors.OrangeRed
};
var color = colors[random.Next(colors.Length)];
var size = ParticleSize + random.Next(-2, 3);
var lifespan = ParticleLifespan + random.NextSingle() - 0.5f;
return new Particle(emitterPosition, velocity, color, lifespan, size);
}
private Particle CreateFireworkParticle()
{
var angle = random.NextSingle() * (float)(2 * Math.PI);
var speed = InitialSpeed + random.Next(-30, 50);
var velocity = new SKPoint(
(float)Math.Cos(angle) * speed,
(float)Math.Sin(angle) * speed
);
var colors = new[] {
SKColors.Red, SKColors.Blue, SKColors.Green, SKColors.Purple,
SKColors.Yellow, SKColors.Cyan, SKColors.Magenta
};
var color = colors[random.Next(colors.Length)];
var size = ParticleSize + random.Next(-1, 2);
var lifespan = ParticleLifespan + random.NextSingle();
return new Particle(emitterPosition, velocity, color, lifespan, size);
}
private Particle CreateRainParticle()
{
var xOffset = (random.NextSingle() - 0.5f) * 50;
var position = new SKPoint(emitterPosition.X + xOffset, emitterPosition.Y - 100);
var velocity = new SKPoint(
random.Next(-10, 10),
InitialSpeed + random.Next(0, 50)
);
var color = SKColor.FromHsv(200, 80, 90); // 蓝色调
var size = Math.Max(1, ParticleSize - 2);
var lifespan = ParticleLifespan + random.NextSingle();
return new Particle(position, velocity, color, lifespan, size);
}
private Particle CreateSnowParticle()
{
var xOffset = (random.NextSingle() - 0.5f) * 100;
var position = new SKPoint(emitterPosition.X + xOffset, emitterPosition.Y - 50);
var velocity = new SKPoint(
(random.NextSingle() - 0.5f) * 20,
InitialSpeed * 0.3f + random.Next(0, 20)
);
var color = SKColors.White;
var size = ParticleSize + random.Next(-2, 2);
var lifespan = ParticleLifespan * 2;
return new Particle(position, velocity, color, lifespan, size);
}
private Particle CreateStarParticle()
{
var angle = random.NextSingle() * (float)(2 * Math.PI);
var speed = InitialSpeed * 0.5f + random.Next(-10, 20);
var velocity = new SKPoint(
(float)Math.Cos(angle) * speed,
(float)Math.Sin(angle) * speed
);
var brightness = random.Next(150, 255);
var color = SKColor.FromHsv(45, 20, brightness); // 金黄色调
var size = ParticleSize + random.Next(-1, 3);
var lifespan = ParticleLifespan * 1.5f;
return new Particle(emitterPosition, velocity, color, lifespan, size);
}
public void SetPreset(ParticlePreset preset)
{
currentPreset = preset;
// 根据预设调整参数
switch (preset)
{
case ParticlePreset.Fire:
UseGravity = true;
UseTrails = false;
break;
case ParticlePreset.Fireworks:
UseGravity = true;
UseTrails = true;
break;
case ParticlePreset.Rain:
UseGravity = false;
UseTrails = true;
break;
case ParticlePreset.Snow:
UseGravity = false;
UseTrails = false;
break;
case ParticlePreset.Stars:
UseGravity = false;
UseTrails = false;
break;
}
}
public void Draw(SKCanvas canvas, int width, int height)
{
using (var paint = new SKPaint())
{
paint.IsAntialias = true;
paint.Style = SKPaintStyle.Fill;
// 绘制背景渐变
if (currentPreset == ParticlePreset.Stars)
{
using (var shader = SKShader.CreateRadialGradient(
new SKPoint(width / 2, height / 2),
Math.Max(width, height),
new[] { SKColor.FromHsv(240, 100, 10), SKColors.Black },
SKShaderTileMode.Clamp))
{
paint.Shader = shader;
canvas.DrawRect(0, 0, width, height, paint);
paint.Shader = null;
}
}
// 绘制粒子
foreach (var particle in particles)
{
if (particle.IsAlive)
{
particle.Draw(canvas, paint, UseTrails);
}
}
// 绘制发射器位置指示器
if (isEmitting)
{
paint.Color = SKColors.White;
paint.Style = SKPaintStyle.Stroke;
paint.StrokeWidth = 2;
canvas.DrawCircle(emitterPosition, 10, paint);
// 绘制十字标记
canvas.DrawLine(
emitterPosition.X - 5, emitterPosition.Y,
emitterPosition.X + 5, emitterPosition.Y, paint);
canvas.DrawLine(
emitterPosition.X, emitterPosition.Y - 5,
emitterPosition.X, emitterPosition.Y + 5, paint);
}
}
}
public void Reset()
{
particles.Clear();
isEmitting = false;
timeSinceLastEmission = 0;
}
}
}
c#// 🎯 重点:使用标准控件命名规范
private System.Windows.Forms.Panel pnlControls; // 控制面板
private System.Windows.Forms.TrackBar trkEmissionRate; // 发射速率滑块
private System.Windows.Forms.Label lblFPS; // FPS显示标签
private System.Windows.Forms.Button btnReset; // 重置按钮
private System.Windows.Forms.ComboBox cmbPresets; // 预设选择
private SkiaSharp.Views.Desktop.SKControl skControl; // 渲染控件
// 🎨 现代化UI配色方案
this.BackColor = System.Drawing.Color.FromArgb(30, 30, 30); // 深色背景
this.pnlControls.BackColor = System.Drawing.Color.FromArgb(45, 45, 48); // 控制面板
this.btnReset.BackColor = System.Drawing.Color.FromArgb(204, 64, 0); // 橙色按钮
c#private void AnimationTimer_Tick(object sender, EventArgs e)
{
var currentTime = DateTime.Now;
var deltaTime = (float)(currentTime - lastTime).TotalSeconds;
lastTime = currentTime;
// 🎯 关键优化:限制帧率避免过度渲染
particleEngine.Update(deltaTime);
skControl.Invalidate(); // 只在需要时重绘
// 📊 性能监控
frameCount++;
if ((currentTime - fpsLastTime).TotalSeconds >= 1.0)
{
currentFps = frameCount / (float)(currentTime - fpsLastTime).TotalSeconds;
lblFPS.Text = $"FPS: {currentFps:F1}";
frameCount = 0;
fpsLastTime = currentTime;
}
}
// 🚀 渲染优化:使用SKCanvas高效绘制
private void skControl_PaintSurface(object sender, SKPaintSurfaceEventArgs e)
{
var canvas = e.Surface.Canvas;
canvas.Clear(SKColors.Black);
// ⚡ 重要:复用Paint对象避免频繁创建
using (var paint = new SKPaint())
{
paint.IsAntialias = true;
particleEngine.Draw(canvas, e.Info.Width, e.Info.Height);
}
}



c#// 火焰特效核心算法
private Particle CreateFireParticle()
{
var angle = (random.NextSingle() - 0.5f) * 1.5f; // 扇形发射
var colors = new[] { SKColors.Red, SKColors.Orange, SKColors.Yellow };
// 关键:向上偏移 + 随机扩散
}
c#// 烟花爆炸算法
private Particle CreateFireworkParticle()
{
var angle = random.NextSingle() * (float)(2 * Math.PI); // 360度发射
var colors = new[] { SKColors.Red, SKColors.Blue, SKColors.Green, SKColors.Purple };
// 关键:径向爆炸 + 多彩颜色
}
c#// ❌ 错误:频繁创建Paint对象
foreach(var particle in particles)
{
using(var paint = new SKPaint()) // 每次都创建新对象
{
particle.Draw(canvas, paint);
}
}
// ✅ 正确:复用Paint对象
using(var paint = new SKPaint())
{
foreach(var particle in particles)
{
particle.Draw(canvas, paint);
}
}
c#// ✅ 及时清理死亡粒子,避免内存泄漏
particles.RemoveAll(p => !p.IsAlive);
// ✅ 限制粒子数量上限
if(particles.Count > MAX_PARTICLES)
{
particles.RemoveRange(0, particles.Count - MAX_PARTICLES);
}
想让你的粒子系统更进一步?这里给出几个升级方向:
通过本文的完整实战,我们成功实现了一个功能丰富的粒子系统。让我们回顾三个关键要点:
⚡ 高性能架构:合理的组件划分 + 对象复用 + 及时内存清理,确保流畅运行不卡顿。记住这个黄金法则:「能复用的绝不重创建,能批量的绝不逐个处理」。
🎨 灵活的特效系统:通过预设模式 + 参数化配置,轻松实现火焰、烟花、雨雪等多种视觉效果。这种设计让你的代码既易维护又易扩展,一套框架满足多种需求。
💡 实用的开发技巧:从控件命名规范到UI配色方案,从性能监控到错误处理,这些细节决定了项目的专业程度。好的代码不仅能跑,更要跑得优雅。
现在你已经掌握了用C# + SkiaSharp打造炫酷特效的完整技能包!无论是为企业项目增添视觉亮点,还是开发独立游戏作品,这套粒子系统都能成为你的得力助手。
💭 互动话题:你最想用这个粒子系统实现什么特效?在实际项目中遇到过哪些有趣的视觉需求?
🚀 实战挑战:试着基于本文代码实现一个「下雪+篝火」的组合场景,看看能否做出冰火两重天的效果!
觉得这篇技术干货对你有帮助?请转发给更多C#同行,让我们一起提升.NET生态的视觉表现力!关注我,获取更多通过网盘分享的文件:AppParticleSystem.zip 链接: https://pan.baidu.com/s/1MKTaiPc8XOEHO-S9RNZ6DQ?pwd=a17b 提取码: a17b --来自百度网盘超级会员v9的分享 :::
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!