『7x24小时有问必答』

前言

在 WinForms 开发中引入高性能 2D 图形能力,SkiaSharp 一直是首选方案。然而,随着 SkiaSharp 版本迭代,许多开发发现原有的  DrawText  方法被标记为过期,中文显示异常、内存泄漏、性能下降等问题也随之浮现。这些问题不仅影响开发体验,更可能成为产品稳定性的隐患。
本文将从实际痛点出发,系统性地讲解如何基于新版 SkiaSharp API 构建一个结构清晰、性能优良、支持中文的 2D 游戏精灵引擎,让 WinForms 应用在图形渲染上真正"跟上时代"。

一、老旧 API 带来的典型问题

在升级到新版 SkiaSharp 后,开发者常遇到以下几类问题:
1、API 过期警告SKCanvas.DrawText(string, float, float, SKPaint)  已被弃用,继续使用会触发编译警告,且未来版本可能彻底移除;
2、中文无法正常显示:默认字体(如 Arial)不包含中文字符集,导致乱码或方块;
3、性能瓶颈:在绘制循环中频繁创建  SKPaintSKFont  等对象,引发大量垃圾回收(GC),造成帧率波动;
4、资源泄漏风险:SkiaSharp 的底层对象基于非托管内存,若未显式调用  Dispose(),极易造成内存泄漏,长时间运行后程序崩溃。
其中最核心的变化是:新版 SkiaSharp 要求使用独立的  SKFont  对象来定义字体,而非通过  SKPaint  设置字体名称和大小。这一设计更符合现代图形 API 的分离原则,但也要求开发者调整编码习惯。
项目效果
1.png

二、现代化解决方案

1、新版文本绘制 API

旧写法(已过期):
canvas.DrawText("Hello World",  10,  30, paint);

新写法(推荐):
var  font =  new  SKFont(typeface,  16);

canvas.DrawText("Hello World",  10,  30, SKTextAlign.Left, font, paint);

关键变化在于:字体信息由  SKFont  封装,绘制时需显式传入对齐方式、字体和画笔。

2、中文兼容的字体管理

为确保在不同 Windows 系统上都能正确显示中文,采用多级字体回退策略:
private  void  InitializeFontsAndPaints()

{

       var  typeface = SKTypeface.FromFamilyName("Microsoft YaHei",  

            SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright)

            ?? SKTypeface.FromFamilyName("SimHei")

            ?? SKTypeface.FromFamilyName("Arial Unicode MS")

            ?? SKTypeface.Default;

      infoFont =  new  SKFont(typeface,  16);

      

      infoPaint =  new  SKPaint

      {

            Color = SKColors.White,

            IsAntialias =  true,

            FilterQuality = SKFilterQuality.High

      };

}

该方案优先使用"微软雅黑",其次"黑体"、"Arial Unicode MS",最后兜底为系统默认字体,极大提升跨环境兼容性。

3、资源复用与内存安全

所有  SKFontSKPaintSKBitmap  等对象应在初始化阶段创建并复用,避免在每帧绘制中重复分配。同时,在窗体关闭时必须显式释放资源:
protected  override  void  OnClosed(EventArgs e)

{

      gameTimer?.Stop();

      gameTimer?.Dispose();

      backgroundBitmap?.Dispose();

      infoFont?.Dispose();

      infoPaint?.Dispose();

       base.OnClosed(e);

}

三、游戏引擎核心实现

1、引擎架构

主窗体维护精灵列表、定时器、字体与画笔资源,并通过双缓冲机制实现流畅渲染:
public  partial  class  FrmMain  :  Form

{

       private  List<sprite> sprites;

       private  Timer gameTimer;

       private  SKFont infoFont;

       private  SKPaint infoPaint;

       private  float  currentFps;

       private  SKBitmap backgroundBitmap;

}

2、高性能 UI 绘制

信息面板采用半透明背景提升可读性,所有文本均使用预初始化的  infoFont  和  infoPaint
private  void  DrawInfo(SKCanvas canvas)

{

       if  (infoFont ==  null  || infoPaint ==  null)  return;

      

       var  backgroundPaint =  new  SKPaint

      {

            Color = SKColors.Black.WithAlpha(120),

            Style = SKPaintStyle.Fill

      };

      canvas.DrawRect(5,  5,  200,  120, backgroundPaint);

      backgroundPaint.Dispose();

      canvas.DrawText($"精灵数量:  {sprites.Count}",  10,  30, SKTextAlign.Left, infoFont, infoPaint);

      canvas.DrawText($"游戏状态:  {(isPlaying ?  "运行中"  :  "暂停")}",  10,  55, SKTextAlign.Left, infoFont, infoPaint);

      canvas.DrawText($"FPS:  {CalculateFPS():F1}",  10,  80, SKTextAlign.Left, infoFont, infoPaint);

      canvas.DrawText($"碰撞检测: 开启",  10,  105, SKTextAlign.Left, infoFont, infoPaint);

       var  canvasHeight = canvas.LocalClipBounds.Height;

      canvas.DrawText("操作提示:",  10, canvasHeight -  65, SKTextAlign.Left, infoFont, infoPaint);

      canvas.DrawText("• 左键点击: 添加白色精灵",  10, canvasHeight -  45, SKTextAlign.Left, infoFont, infoPaint);

      canvas.DrawText("• 右键点击: 删除最近精灵",  10, canvasHeight -  25, SKTextAlign.Left, infoFont, infoPaint);

}

3、精灵更新与碰撞检测

每个精灵独立更新位置,并在边界处反弹。碰撞检测采用 O(n²) 双重循环,适用于中小规模精灵数量:
private  void  UpdateSprites()

{

       foreach  (var  sprite  in  sprites.ToList())

      {

            sprite.Update();

             if  (sprite.X <=  0  || sprite.X >= skiaCanvas.Width)

            {

                  sprite.VelocityX = -sprite.VelocityX;

                  sprite.X = Math.Max(0, Math.Min(skiaCanvas.Width, sprite.X));

            }

             if  (sprite.Y <=  0  || sprite.Y >= skiaCanvas.Height)

            {

                  sprite.VelocityY = -sprite.VelocityY;

                  sprite.Y = Math.Max(0, Math.Min(skiaCanvas.Height, sprite.Y));

            }

      }

      CheckCollisions();

}

private  void  CheckCollisions()

{

       for  (int  i =  0; i < sprites.Count; i++)

      {

             for  (int  j = i +  1; j < sprites.Count; j++)

            {

                   if  (sprites.CollidesWith(sprites[j]))

                  {

                        sprites.OnCollision(sprites[j]);

                        sprites[j].OnCollision(sprites);

                  }

            }

      }

}

4、精灵类实现示例

以圆形精灵为例,其绘制包含主体、边框和光效三层,体现 SkiaSharp 的丰富表现力:
using  SkiaSharp;

using  System;

namespaceAppSpriteGameEngine

{

       publicclassCircleSprite  :  Sprite

      {

             publicfloat  Radius {  get;  privateset; }

            

             public  CircleSprite(float  x,  float  y,  float  velocityX,  float  velocityY, SKColor color,  float  radius)

                  :  base(x, y, velocityX, velocityY, color)

            {

                  Radius = radius;

                  Width = Height = radius *  2;

            }

             public  override  void  Draw(SKCanvas canvas)

            {

                   var  paint =  new  SKPaint

                  {

                        Color = Color,

                        IsAntialias =  true,

                        Style = SKPaintStyle.Fill

                  };

                  canvas.DrawCircle(X, Y, Radius, paint);

                  paint.Style = SKPaintStyle.Stroke;

                  paint.Color = SKColors.White;

                  paint.StrokeWidth =  2;

                  canvas.DrawCircle(X, Y, Radius, paint);

                  paint.Style = SKPaintStyle.Fill;

                  paint.Color = Color.WithAlpha(100);

                  canvas.DrawCircle(X - Radius /  3, Y - Radius /  3, Radius /  3, paint);

            }

      }

}

5、FPS 监控

通过计数器和时间差计算实时帧率,用于性能评估:
private  void  UpdateFPS()

{

      frameCount++;

       var  now = DateTime.Now;

       var  elapsed = (now - lastFpsUpdate).TotalSeconds;

      

       if  (elapsed >=  1.0)

      {

            currentFps = frameCount / (float)elapsed;

            frameCount =  0;

            lastFpsUpdate = now;

      }

}

总结

通过本次实践,我们不仅解决了 SkiaSharp 新版 API 的适配问题,更构建了一个结构合理、性能可控、支持中文的 2D 游戏引擎原型。整个过程验证了三个关键原则:
1、API 必须与时俱进:主动拥抱  SKFont  等新接口,避免依赖已弃用的方法;
2、国际化需提前规划:通过字体回退链保障中文等复杂文字的正确渲染;
3、资源管理不可忽视:所有 SkiaSharp 对象必须显式释放,防止非托管内存泄漏。
WinForms 虽然"年长",但借助 SkiaSharp 这样的现代图形库,依然能支撑高性能、高交互性的应用场景。关键在于——用工程化思维对待每一个细节,从字体到内存,从 API 到架构,稳扎稳打,方能焕发新生。

关键词

最后
如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。也可以加入微信公众号[DotNet技术匠]  社区,与其他热爱技术的同行一起交流心得,共同成长!
作者:技术老小子
出处:mp.weixin.qq.com/s/sYqntrZh_darYUSfh1UkfA
声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!

END

方便大家交流、资源共享和共同成长
纯技术交流群,需要加入的小伙伴请扫码,并备注加群

推荐阅读

觉得有收获?不妨分享让更多人受益
关注「DotNet技术匠」,共同提升技术实力

收藏
点赞
分享
在看
</sprite>

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

上一主题上一主题         下一主题下一主题
QQ手机版小黑屋粤ICP备17165530号

关于我们·投诉举报· 用户帮助· 联系我们 · 本站服务 · 版权声明· 隐私政策 · 投搞指南

法律保护:PLC技术网,plcjs.com,plcjs.net等字样
Copyright 2010-2030. All rights reserved. 


微信公众号二维码 抖音二维码 百家号二维码 今日头条二维码哔哩哔哩二维码