主题
Canvas 绘制系统
Canvas 控件提供即时模式 2D 绘制 API,用于自定义图形、动画、小地图等。仅客户端可用(#if CLIENT)。
定义位置:GameUI.Control.Primitive.Canvas
Canvas 是对底层 NanoVG 绘制能力的 C# 封装:通过当前画笔、变换状态、路径和裁剪区域进行即时绘制。熟悉 NanoVG / SVG / HTML5 Canvas 的开发者可以复用这些图形概念,但业务代码应调用 WasiCore Canvas API,不应直接使用 nvg* 底层函数。
基本用法
csharp
var canvas = new Canvas();
canvas.FullScreen();
canvas.OnRender += (s, e) =>
{
canvas.FillPaint = Color.Red;
canvas.FillRectangle(10, 10, 100, 50);
};
canvas.AddToVisualTree();绘制在 OnRender 事件中进行。订阅 OnRender 启用渲染循环,取消订阅停止渲染。
生命周期与资源释放
Canvas 是普通 GameUI 控件,生命周期规则与 Panel、Button、Label 一致:
| 操作 | 含义 | 适用场景 |
|---|---|---|
RemoveFromParent() / RemoveFromVisualTree() | 只从 UI 树摘除,不释放控件和底层资源 | 临时隐藏、换父节点、对象池复用 |
ClearChildren() | 只移除容器的直接子控件,不调用子控件 Destroy() | 清空容器后复用子控件或由业务另行销毁 |
Destroy() | 释放控件,清事件,递归销毁子控件并销毁 native control | 控件永久不用 |
因此,临时 Canvas 可以移出 UI 树等待复用;但浮字、战斗 HUD、弹窗、过期特效画布等不再使用时,应调用 Destroy()。不要把 RemoveFromParent() 或 ClearChildren() 当成销毁。
csharp
// ✅ 关闭后不再复用
canvas.OnRender -= DrawHud;
canvas.Destroy();
// ✅ CanvasAnimated 同理
animatedCanvas.OnAnimatedRender -= DrawAnimation;
animatedCanvas.Destroy();Canvas / CanvasAnimated 可能持有底层渲染资源和回调。长期运行中反复创建后只 RemoveFromParent() 或由父容器 ClearChildren() 摘掉,尤其在移动端,可能累积资源压力并最终表现为 FBO 或纹理创建失败。
NanoVG 概念对照
| NanoVG / 通用概念 | WasiCore Canvas API | 注意 |
|---|---|---|
| Fill / Stroke paint | FillPaint / StrokePaint | Color 可隐式转换为 SolidPaint |
| Stroke width | StrokeWidth | 形状和路径描边宽度 |
| Path commands | PathF.MoveTo / LineTo / CurveTo | 用 FillPath 或 DrawPath 输出 |
| Save / Restore state | SaveState() / RestoreState() | 控制样式、变换、裁剪作用域 |
| Rotate | Rotate(degrees) / RotateRadians(radians) | Rotate() 使用度数 |
| Scissor / clip | ClipRect(...) | 裁剪后续绘制 |
| Text | CreateFont / FontFaceId / DrawText | Canvas 字体独立于普通 GameUI 字体 family |
| Image drawing | Image + DrawImage(...) | 资源路径从项目 ui/ 下一级开始写 |
构造
csharp
Canvas(); // 使用默认模板
Canvas(IGameLink<GameDataControlCanvas> link); // 使用指定模板样式属性
csharp
Paint FillPaint { get; set; } // 填充画笔(Color 可隐式转换为 Paint)
Paint StrokePaint { get; set; } // 描边画笔
float StrokeWidth { get; set; } // 描边宽度
LineCap LineCap { get; set; } // 线端样式
LineJoin LineJoin { get; set; } // 连接样式
float MiterLimit { get; set; } // 斜接限制
float Alpha { get; set; } // 全局透明度
BlendMode BlendMode { get; set; } // 混合模式
WindingMode DefaultPathWinding { get; set; } // 路径默认填充规则
void SetColors(Color fillColor, Color strokeColor); // 同时设置填充和描边绘制方法
线段
csharp
void DrawLine(float x1, float y1, float x2, float y2);矩形
csharp
void FillRectangle(float x, float y, float width, float height);
void StrokeRectangle(float x, float y, float width, float height);
void FillRoundedRectangle(float x, float y, float width, float height, float cornerRadius);
void StrokeRoundedRectangle(float x, float y, float width, float height, float cornerRadius);
void FillSquare(float x, float y, float size);
void StrokeSquare(float x, float y, float size);圆形/椭圆
csharp
void FillCircle(float centerX, float centerY, float radius);
void StrokeCircle(float centerX, float centerY, float radius);
void StrokeCircle(PointF center, float radius);
void FillEllipse(float centerX, float centerY, float radiusX, float radiusY);
void StrokeEllipse(float centerX, float centerY, float radiusX, float radiusY);
void FillEllipse(PointF center, float radiusX, float radiusY);
void StrokeEllipse(PointF center, float radiusX, float radiusY);三角形
csharp
void FillTriangle(float x1, float y1, float x2, float y2, float x3, float y3);
void StrokeTriangle(float x1, float y1, float x2, float y2, float x3, float y3);路径
csharp
void DrawPath(PathF path);
void FillPath(PathF path, WindingMode? windingMode = null);图片
csharp
void DrawImage(Image image, float x, float y);
void DrawImage(Image image, float x, float y, float width, float height);
void DrawImage(Image image, float sourceX, float sourceY, float sourceWidth, float sourceHeight,
float x, float y, float width, float height);
void DrawImage(Image image, RectangleF sourceRect, RectangleF destRect);图片文件应放在项目 ui/ 目录下,代码路径从 ui/ 下一级开始写。例如 ui/image/abc.png 的读取路径是 image/abc.png:
csharp
var image = new Image("image/abc.png");
canvas.DrawImage(image, 100, 100, 256, 128);不要写成 new Image("ui/image/abc.png"),也不要把 Canvas 图片放到 user_files。Image 是引擎资源路径,不是 .NET 文件路径。完整资源路径规则见 项目资源路径指南。
文本
csharp
float DrawText(float x, float y, string text);
void DrawTextBox(float x, float y, float breakRowWidth, string text);
(float advance, RectangleF bounds) MeasureText(float x, float y, string text);
RectangleF MeasureTextBox(float x, float y, float breakRowWidth, string text);
TextMetrics? GetTextMetrics();MeasureText 和 MeasureTextBox 必须在 OnRender 内部调用才能获得正确结果。
文本样式
csharp
float FontSize { get; set; }
float FontBlur { get; set; }
float TextLetterSpacing { get; set; }
float TextLineHeight { get; set; }
TextAlign TextAlign { get; set; }
void FontFace(string fontName);
void FontFaceId(int fontId);
static int CreateFont(string name, string filename);
static int FindFont(string name);流式扩展:
csharp
canvas.FontSize(24f).LetterSpacing(1f).TextAlign(TextAlign.Center);动画图片
csharp
void DrawAnimatedImage(IAnimatedImageSource animatedImageSource, float x, float y);DrawAnimatedImage 面向 AnimatedImageSource 这类脚本层动画数据源,适合在 Canvas 游戏循环中绘制角色、敌人、飞行物或带图层/命中框的复杂动画。若只是按钮光效、加载动画、头像框等简单 UI 序列帧,优先使用 GameUI 原生 Sprites 控件。选型细节见 GameUI 序列帧选型指南。
变换
csharp
void SaveState(); // 保存当前变换和样式
void RestoreState(); // 恢复上次保存的状态
void ResetState(); // 重置所有状态
void Translate(float deltaX, float deltaY);
void Rotate(float degrees);
void RotateDegrees(float degrees);
void RotateRadians(float radians);
void Scale(float scaleX, float scaleY);
void Transform(float a, float b, float c, float d, float e, float f);
void ConcatenateTransform(Matrix3x2 transform);
void ResetTransform();SaveState/RestoreState 成对使用,避免变换泄漏:
csharp
canvas.SaveState();
canvas.Translate(100, 100);
canvas.Rotate(45);
canvas.FillRectangle(-25, -25, 50, 50); // 在 (100,100) 处绘制旋转 45 度的矩形
canvas.RestoreState();裁剪
csharp
void ClipRect(float x, float y, float width, float height);
void SubtractFromClip(float x, float y, float width, float height);PathF
路径构建器,所有路径方法返回 this 支持链式调用。
构造
csharp
PathF();
PathF(float x, float y);
PathF(PointF point);
PathF(PathF path); // 复制构建
csharp
PathF MoveTo(float x, float y);
PathF LineTo(float x, float y);
PathF QuadTo(float cx, float cy, float x, float y); // 二次贝塞尔
PathF CurveTo(float c1X, float c1Y, float c2X, float c2Y, float x, float y); // 三次贝塞尔
PathF ArcTo(PointF controlPoint1, PointF controlPoint2, float radius);
PathF AddCircleArc(PointF center, float radius, float startAngle, float endAngle, bool clockwise);
PathF AddEllipseArc(PointF center, float radiusX, float radiusY, float rotation,
float startAngle, float endAngle, bool clockwise);
void Close(); // 闭合路径
void Open(); // 开始新子路径便捷形状
csharp
PathF AddRectangle(float x, float y, float w, float h);
PathF AddRoundedRectangle(float x, float y, float w, float h, float cornerRadius);
PathF AddCircle(PointF center, float radius);
PathF AddEllipse(PointF center, float radiusX, float radiusY);变换和查询
csharp
PathF Rotate(float angleAsDegrees, PointF pivot);
void Transform(Matrix3x2 transform);
void Move(float x, float y);
RectangleF Bounds { get; }
PathF Reverse();
List<PathF> Separate();示例:星形
csharp
var star = new PathF();
for (int i = 0; i < 5; i++)
{
float outerAngle = i * 72 - 90;
float innerAngle = outerAngle + 36;
float outerX = center.X + outerRadius * MathF.Cos(outerAngle * MathF.PI / 180);
float outerY = center.Y + outerRadius * MathF.Sin(outerAngle * MathF.PI / 180);
float innerX = center.X + innerRadius * MathF.Cos(innerAngle * MathF.PI / 180);
float innerY = center.Y + innerRadius * MathF.Sin(innerAngle * MathF.PI / 180);
if (i == 0) star.MoveTo(outerX, outerY);
else star.LineTo(outerX, outerY);
star.LineTo(innerX, innerY);
}
star.Close();
canvas.FillPath(star);Paint 类型
| 类型 | 说明 |
|---|---|
SolidPaint(Color color) | 纯色 |
LinearGradientPaint(PointF start, PointF end, Color startColor, Color endColor) | 线性渐变 |
RadialGradientPaint(PointF center, float startRadius, float endRadius, Color startColor, Color endColor) | 径向渐变 |
BoxGradientPaint(RectangleF rect, float cornerRadius, float feather, Color startColor, Color endColor) | 矩形渐变 |
Color 可隐式转换为 SolidPaint,因此可直接赋值:
csharp
canvas.FillPaint = Color.Red;
canvas.FillPaint = new LinearGradientPaint(
new PointF(0, 0), new PointF(100, 0),
Color.Red, Color.Blue);坐标和分辨率
csharp
SizeF Resolution { get; set; } // 画布逻辑分辨率
bool AutoUpdateResolutionOnResize { get; set; } // 自动跟随控件尺寸
CanvasCoordinateMode CoordinateMode { get; set; } // 坐标模式
int TotalElapsedTimeInMilliseconds { get; } // 累计时间(用于动画)CanvasAnimated
CanvasAnimated 继承 Canvas,为动画和游戏开发提供帧间隔时间。定义位置:GameUI.Control.Primitive.CanvasAnimated
csharp
var canvas = new CanvasAnimated();
canvas.FullScreen();
canvas.StartTiming(); // 必须调用,否则 OnAnimatedRender 不触发
canvas.OnAnimatedRender += (sender, e) =>
{
float dt = e.DeltaTimeInSeconds; // 帧间隔(秒)
float total = e.TotalElapsedTimeInSeconds; // 总计时(秒)
canvas.ResetState();
// 使用 dt 实现帧率无关的动画
};
canvas.AddToVisualTree();CanvasAnimatedEventArgs
| 属性 | 类型 | 说明 |
|---|---|---|
DeltaTimeInMilliseconds | int | 距上次渲染的时间差(毫秒) |
DeltaTimeInSeconds | float | 距上次渲染的时间差(秒) |
TotalElapsedTimeInMilliseconds | int | 从 StartTiming() 开始的总时间(毫秒) |
TotalElapsedTimeInSeconds | float | 从 StartTiming() 开始的总时间(秒) |
附加方法
| 方法 | 说明 |
|---|---|
StartTiming() | 设置计时起点为当前时刻(开始触发 OnAnimatedRender) |
StartTimingDelayed(int ms) | 延迟 ms 毫秒后开始计时 |
> CanvasAnimated 拥有 Canvas 的全部绘图 API 和事件(含 OnRender)。动态游戏推荐使用 OnAnimatedRender 以获取时间信息。
相关文档
- GameUI 序列帧选型指南 —
SpritesvsAnimatedImageSource+CanvasAnimated - 动画图片源 — 精灵帧动画
- 指针捕获 — Canvas 上的指针输入处理
- 坐标系统指南 — 坐标系说明
- Canvas / CanvasAnimated 游戏开发指南 — Physics2D + Canvas 架构
- 移动端 Canvas 性能调优 — 降低 Canvas 底层绘制分辨率以缓解手机帧耗时和发热