iOS与Unity通用绘制工具:数据模型与归一化坐标设计
2026/9/4 20:41:14 网站建设 项目流程

很多开发者容易把“绘制工具”想成一个纯 UI 问题:画布上放张图,手指划过去能留下笔迹就够了。但一旦业务同时落在 iOS 原生端、Unity 引擎端,事情就变得不那么简单。iOS 侧要处理 UITouch 与 Core Graphics,Unity 侧要处理 UGUI、Texture2D、甚至不同 Canvas 缩放规则,两边的触摸坐标体系和渲染 API 完全不同。如果一开始没有抽出统一的数据模型,大概率会出现这样的结局:产品在 iOS 上已经稳定跑了一个版本,Unity 那边的同款白板功能又从头写了一遍。

这篇文章会展开一个更务实的认知:“iOS-Unity 通用绘制工具”不是指同一份代码在两端直接跑,而是把绘制需求拆成“数据模型、坐标约定、交互层、渲染层”,让前两层跨端统一,后两层各自实现。绘制工具真正值得复用的,是笔迹数据结构、归一化坐标语义和回放/存储协议;不能复用的,是触控采集 API 和渲染 API。基于这个判断,文中会提供一套可以直接落地的笔迹数据格式、一个 iOS 原生绘制示例、一个 Unity 侧 Texture2D 画布示例,以及坐标转换、性能优化、常见踩坑记录。

用一个最小原型把两端跑通,远比一开始就想做“跨端万能画板”更符合实际工程节奏。下面按顺序展开。

1. 为什么需要一套 iOS 与 Unity 通用的绘制能力

先看一个真实场景。假设你在一款教育类 App 里负责手写批注功能,iOS 原生端用 UIKit 实现了标记工具,可以在 PDF 或截图上画出红笔、荧光笔效果。后来公司决定把同一块能力切入 Unity 社区的互动教学场景,于是 Unity 端也要做“画线标注”,比如老师在 3D 场景里画辅助线、标注重点、手写解题过程。

最省事的方式,是在 Unity 里把 iOS 的逻辑重写一遍:重新定义点集、重新写触摸坐标转换、重新做撤销重做、重新设计导出的 JSON。结果两端数据格式不一致,业务方要求“iOS 画的笔记在 Unity 里能够回放”,又要加一层互相转换。显然,这份重复成本不是画板本身带来的,而是没有把业务无关的绘制描述先独立出来

从这个角度看,通用绘制工具的出发点不是炫技,而是阻止同一份逻辑在每次新增平台时重新发明。它需要解决四件事:

  • 笔迹数据能被任意端解析,结构稳定,不绑定 UIKit 或 UnityEngine 类型;
  • 坐标语义清晰,不随屏幕分辨率、Canvas 缩放和 Safe Area 变化而漂移;
  • 采集层可以在 iOS 用 UITouch、在 Unity 用 IPointerDownHandler / IDragHandler,但产出的是同一种笔迹;
  • 渲染层可以各自使用 Core Graphics、Texture2D、LineRenderer 甚至 Metal,但上层业务感知不到底层差异。

文内所有代码,都以“先抽出公共笔迹协议,再做平台侧薄适配”为核心。这样设计出来的工具,才经得起后续业务扩展。

2. 先拆清楚:绘制工具里哪些能通用,哪些不能通用

“通用”这个词必须说得保守一点。iOS 和 Unity 的运行时模型、UI 体系、渲染管线完全不同,现实里不会也不应该强制共用一份二进制。真正可通用的是设计层面的协议和抽象。

绘制能力可以按职责拆成四层:

层次是否能跨端通用内容说明
数据模型可以通用笔迹 ID、点集、颜色、线宽、工具类型、时间戳
坐标语义可以通用推荐使用 0~1 归一化坐标描述点在画布中的相对位置
触控与手势采集不能通用iOS 是 UITouch / UIEvent,Unity 是 EventSystem 的 Pointer 事件
渲染实现不能通用iOS 常用 Core Graphics / PencilKit,Unity 常用 Texture2D / LineRenderer / Mesh

最容易误解的地方,是认为“通用”意味着渲染也必须一致。实际上,绘制工具最大的跨端成本在于两点:一是保存下来的笔迹数据能不能被另一端识别;二是同一点在两端画出来会不会错位。这两点都属于数据模型和坐标语义,应该优先抽象。

为了避免后续踩进设备像素泥潭,坐标建议统一采用“归一化坐标”。比如触摸点在画布中间,不论 iOS 的 view 是 375x667 还是 Unity 的 Canvas 是 1920x1080,保存下来的坐标都写(0.5, 0.5)。渲染时再根据目标画布尺寸换算回像素或者 UI 局部坐标。这样做意味着笔迹的原始描述不依赖任何屏幕参数,导出、同步、回放才能保持稳定。

把“什么是数据、什么是渲染”从代码里分开,是本文最重要的架构建议。数据层稳定,渲染层哪怕换成 SwiftUI、Metal、UGUI 或者将来的新框架,都不会影响历史铅笔迹的兼容性。

3. 统一坐标与笔迹数据模型

要设计两端通用的绘制工具,第一步不是写 UI,而是定义一套稳定的跨端笔迹协议。这里要注意:协议里的字段最好用平台无关语言描述,不要直接使用 iOS 的CGPoint或 Unity 的Vector2序列化结果。Swift 和 C# 虽然都有相关类型,但序列化成文件或走网络时,字段命名、浮点精度、自定义属性差异会带来不必要的麻烦。

先约定一个通用笔迹描述。

{ "payloadVersion": 1, "canvas": { "width": 1920, "height": 1080 }, "strokes": [ { "id": "stroke-0001", "startTimeMs": 1720000000000, "brush": { "type": "pen", "color": "#101010", "width": 8 }, "points": [ { "x": 0.10, "y": 0.82, "t": 0, "pressure": 1.0 }, { "x": 0.12, "y": 0.81, "t": 16, "pressure": 1.0 }, { "x": 0.15, "y": 0.79, "t": 32, "pressure": 0.9 } ] } ] }

这段 JSON 里最关键的设计有几点:

  • xy是 0~1 归一化坐标,不存设备像素坐标,避免不同分辨率之间的错位问题;
  • t是本笔画从开始到当前点的相对毫秒,用于笔迹重放和压感轨迹还原;
  • pressure是可选项,没有压感设备时可以统一置为 1.0;
  • startTimeMs建议使用 UTC 毫秒作为全局时间基准,这样多端回放时可以按时间轴排序。

iOS 端对应的数据模型可以这样写:

// 文件路径:CanvasModels.swift import Foundation struct PenPointPayload: Codable { let x: Double let y: Double let t: Double let pressure: Double } struct BrushPayload: Codable { let type: String let color: String let width: Double } struct StrokePayload: Codable { let id: String let startTimeMs: Int64 let brush: BrushPayload let points: [PenPointPayload] } struct DrawingPayload: Codable { let version: Int let canvas: CanvasSizePayload let strokes: [StrokePayload] } struct CanvasSizePayload: Codable { let width: Double let height: Double }

Unity 端因为默认使用 C#,可以定义对应的 DTO 类:

// 文件路径:Assets/Scripts/DrawingPayload.cs using System; using System.Collections.Generic; [Serializable] public class PenPointPayload { public float x; public float y; public float t; public float pressure; } [Serializable] public class BrushPayload { public string type; public string color; public float width; } [Serializable] public class StrokePayload { public string id; public long startTimeMs; public BrushPayload brush; public List<PenPointPayload> points = new List<PenPointPayload>(); } [Serializable] public class DrawingPayload { public int version = 1; public float canvasWidth; public float canvasHeight; public List<StrokePayload> strokes = new List<StrokePayload>(); }

这里没有直接往磁盘序列化 iOS 的CGPoint或 Unity 的Vector2,而是使用扁平的xytpressure结构。后续只要保持这套字段命名不变,两端就能安全地互相解析同一份文件。

真正统一之后,会发现一个额外好处:笔迹数据不依赖任何 UI 框架,可以进入普通的版本管理流程。回归测试时,输入同一份 JSON,两端画出的点集在逻辑坐标上完全一致,唯一差异只发生在渲染层的像素取整上。

4. iOS 原生侧实现:一个最小可运行的白板画布

接下来进入平台侧代码。先从 iOS 原生侧实现一个最简自定义白板。这里不引入 PencilKit,因为重点是展示 UITouch 到归一化坐标再到 Core Graphics 渲染的过程。引入系统高封装组件反而会掩盖核心逻辑。

在 iOS 中,绘制最基本的机制是继承UIView,重写touchesBegantouchesMovedtouchesEnded来收集触摸点,再用draw(_:)方法把笔迹画到上下文中。

// 文件路径:DrawingCanvasView.swift import UIKit private struct Stroke { var color: UIColor var lineWidth: CGFloat var normalizedPoints: [CGPoint] } final class DrawingCanvasView: UIView { private var completedStrokes: [Stroke] = [] private var activeStroke: Stroke? // MARK: - Touch Events override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { guard let touch = touches.first else { return } let point = normalizedPoint(touch.location(in: self)) activeStroke = Stroke( color: .systemBlue, lineWidth: 4.0, normalizedPoints: [point] ) setNeedsDisplay() } override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) { guard let touch = touches.first else { return } let point = normalizedPoint(touch.location(in: self)) if var stroke = activeStroke { stroke.normalizedPoints.append(point) activeStroke = stroke } setNeedsDisplay() } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { if let stroke = activeStroke { completedStrokes.append(stroke) } activeStroke = nil setNeedsDisplay() } override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) { activeStroke = nil } // MARK: - Drawing override func draw(_ rect: CGRect) { guard let context = UIGraphicsGetCurrentContext() else { return } let allStrokes = completedStrokes + (activeStroke.map { [$0] } ?? []) for stroke in allStrokes { guard let first = stroke.normalizedPoints.first else { continue } context.setStrokeColor(stroke.color.cgColor) context.setLineWidth(stroke.lineWidth) context.setLineCap(.round) context.setLineJoin(.round

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询