1. 项目背景与核心价值
去年公司年会筹备期间,行政部突然找到我,说今年想搞点不一样的抽奖环节。传统的大屏幕滚动名单实在太老套,想要一个能调动全场气氛的3D抽奖效果。市面上现成的抽奖软件要么太简陋,要么价格高得离谱,于是这个任务就落到了我这个"技术宅"头上。
经过一周的折腾,最终用C#+WPF开发出了这款支持3D圆球动画的抽奖软件。最让我自豪的是,它不仅实现了基础抽奖功能,还加入了粒子特效、背景音乐控制和多轮次抽奖记录等实用特性。年会上效果炸裂,抽奖环节成了整场晚会的亮点,后来好几个兄弟公司都来要源码。
2. 技术方案选型
2.1 为什么选择WPF而不是WinForm
刚开始考虑过用WinForm快速实现,但测试发现两个致命问题:
- GDI+的图形渲染性能在动画场景下帧率不足
- 实现3D效果需要引入第三方库,增加复杂度
WPF的优势在于:
- 原生支持硬件加速渲染
- 内置3D变换功能(Viewport3D)
- 数据绑定机制让抽奖逻辑与UI解耦
- 矢量图形自动适配不同分辨率
2.2 3D效果的实现路径
测试过三种方案:
- 纯XAML 3D建模:用MeshGeometry3D构建球体,但动态更新顶点数据性能较差
- HelixToolkit:功能强大的开源3D库,但依赖项太多
- 3D变换+2D投影:最终采用的方案,用PerspectiveCamera模拟3D视角,实际操作的是2D元素
xml复制<Viewport3D>
<Viewport3D.Camera>
<PerspectiveCamera Position="0,0,800" LookDirection="0,0,-1"/>
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<DirectionalLight Direction="-1,-1,-3"/>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
3. 核心功能实现细节
3.1 3D圆球构造
采用"贴图法"而非真实3D建模:
- 准备带透明通道的圆形PNG素材
- 用ImageBrush作为球面材质
- 通过RotateTransform3D实现旋转动画
csharp复制// 创建材质
DiffuseMaterial material = new DiffuseMaterial(
new ImageBrush(new BitmapImage(new Uri("ball_texture.png")))
);
// 设置3D变换
RotateTransform3D rotation = new RotateTransform3D(
new AxisAngleRotation3D(new Vector3D(0,1,0), 0)
);
ballModel.Transform = rotation;
3.2 抽奖逻辑设计
关键设计点:
- 使用ObservableCollection存储候选人名单
- 抽奖时随机打乱集合顺序
- 通过数据绑定自动更新UI
csharp复制private void StartLottery()
{
// 打乱顺序算法
var rng = new Random();
Winners = new ObservableCollection<Person>(
Candidates.OrderBy(x => rng.Next()).Take(WinnerCount)
);
// 启动旋转动画
BeginAnimation();
}
3.3 粒子特效实现
抽奖时的爆发效果使用两种方案:
- 内置粒子系统:通过DispatcherTimer逐帧更新粒子位置
- 第三方库(ParticleEffectLibrary):最终采用的方案,性能更好
配置示例:
xml复制<pe:ParticleEmitter
ParticleImage="Assets/star.png"
BirthRate="50"
LifeTime="1.5"
Speed="300"
Spread="45"/>
4. 关键问题解决方案
4.1 动画卡顿优化
初期版本在低配电脑上会出现明显卡顿,通过以下措施优化:
- 启用WPF的硬件加速渲染
xml复制<Window ...
AllowsTransparency="True"
WindowStyle="None"
Background="Transparent">
- 降低粒子数量(动态调整BirthRate)
- 使用CompositionTarget.Rendering替代DispatcherTimer
4.2 多显示器适配
年会现场使用投影仪时遇到两个问题:
- 分辨率自动适应
- 全屏显示时鼠标位置错位
解决方案:
csharp复制// 获取主显示器尺寸
var screen = System.Windows.Forms.Screen.PrimaryScreen;
Width = screen.Bounds.Width;
Height = screen.Bounds.Height;
// 鼠标位置校正
Point GetCorrectedMousePos()
{
var wpfPos = Mouse.GetPosition(this);
return new Point(
wpfPos.X * (ActualWidth / RenderSize.Width),
wpfPos.Y * (ActualHeight / RenderSize.Height)
);
}
5. 进阶功能实现
5.1 背景音乐控制
通过NAudio库实现:
- 抽奖开始自动播放激昂音乐
- 中奖时刻触发音效
- 支持音量渐变控制
csharp复制private void PlaySound(string filePath)
{
using (var audioFile = new AudioFileReader(filePath))
using (var outputDevice = new WaveOutEvent())
{
outputDevice.Init(audioFile);
outputDevice.Play();
}
}
5.2 多轮次抽奖记录
设计要点:
- 使用JSON序列化保存历史记录
- 防止同一人中奖多次
- 支持导出Excel报表
csharp复制void SaveHistory()
{
var history = new {
DateTime = DateTime.Now,
Winners = Winners.ToList()
};
File.AppendAllText("history.json",
JsonConvert.SerializeObject(history) + Environment.NewLine);
}
6. 部署与使用指南
6.1 环境要求
- .NET Framework 4.7.2+
- 显卡支持DirectX 9+
- 建议分辨率1920x1080
6.2 操作流程
- 导入人员名单(支持Excel/CSV)
- 设置奖项数量
- 点击开始按钮
- 按空格键停止抽奖
6.3 自定义配置
修改Config.json可调整:
json复制{
"AnimationSpeed": 1.5,
"BallColor": "#FF5722",
"MaxParticles": 200,
"SoundVolume": 0.7
}
7. 实际踩坑经验
- DPI缩放问题:在高DPI显示器上元素错位,需添加app.manifest配置
xml复制<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
- 内存泄漏:持续动画导致内存增长,需要手动释放资源
csharp复制protected override void OnClosed(EventArgs e)
{
compositionTarget.Rendering -= OnRendering;
particleEmitter.Dispose();
base.OnClosed(e);
}
- 字体渲染模糊:WPF默认ClearType在旋转时模糊,改用以下设置
xaml复制<TextBlock
Text="中奖名单"
TextOptions.TextFormattingMode="Display"
TextOptions.TextRenderingMode="Auto"/>
这个项目让我深刻体会到,好的技术方案不仅要考虑功能实现,更要关注用户体验。比如抽奖时的音效与动画节奏配合,停顿时刻的紧张感营造,这些细节往往比技术本身更能打动使用者。如果重做一次,我会考虑加入人脸识别自动签到功能,让整个流程更加智能化。