You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity开发儿童类画图应用:快速拖动卡顿线条锯齿问题求助

问题:快速拖动手指时绘制卡顿,线条呈直线且带锯齿

我正在Unity中开发一款面向儿童的画图应用,核心功能已经实现,但遇到了一个问题:当手指快速拖动时,应用会卡顿,绘制出的线条不仅是直线,还带有明显锯齿,希望能得到优化建议。

应用的基本逻辑:玩家可以选择颜色,在画笔和橡皮擦之间切换。拖动鼠标/手指划过屏幕时,代码会修改指定Image的像素。

public class PaintColor : MonoBehaviour, IPointerDownHandler, IDragHandler
{
    [SerializeField] ColorManager manager; //manages whether the player can draw and with what color

    Vector2 prevPos;
    Image sr;
    [SerializeField] int brushWidth = 10;
    [SerializeField] float maxDistance = 5; //when dragging, max possible distance between previous stored position and current

    void Start()
    {
        sr = GetComponent<Image>();
        ClearPanel();
    }

    /*get pixel coordinates from mouse position*/
    Vector2 PixelPos(Vector2 v)
    {
        var rt = GetComponent<RectTransform>();
        int px = Mathf.Clamp(0, (int)(((v.x - rt.rect.x) * sr.sprite.texture.width) / rt.rect.width), sr.sprite.texture.width);
        int py = Mathf.Clamp(0, (int)(((v.y - rt.rect.y) * sr.sprite.texture.height) / rt.rect.height), sr.sprite.texture.height);

        return new Vector2(px, py);
    }

    public void OnPointerDown(PointerEventData eventData)
    {
        if (manager.CanColor())
        {
            Vector2 pixelPos;
            if (RectTransformUtility.ScreenPointToLocalPointInRectangle(GetComponent<RectTransform>(), eventData.position, eventData.pressEventCamera, out pixelPos))
                Draw(PixelPos(pixelPos));
            prevPos = pixelPos;
        }
    }

    public void OnDrag(PointerEventData eventData)
    {
        if (manager.CanColor())
        {
            Vector2 pixelPos;
            if (RectTransformUtility.ScreenPointToLocalPointInRectangle(GetComponent<RectTransform>(), eventData.position, eventData.pressEventCamera, out pixelPos))
            {
                var distance = Mathf.Max(Vector2.Distance(prevPos, pixelPos), maxDistance);
                for (float lerp = 0; lerp <= 1; lerp += 1 / distance)    //color all pixels in between last recorded position and current
                {
                    Draw(PixelPos(Vector2.Lerp(prevPos, pixelPos, lerp)));
                }
            }
            prevPos = pixelPos;
        }
    }

    /*clear out drawing panel*/
    public void ClearPanel()
    {
        var clear = new Color[(int)sr.sprite.rect.width * (int)sr.sprite.rect.height];
        for (int x = 0; x < clear.Length; x++)
            clear[x] = Color.clear;
        sr.sprite.texture.SetPixels(clear);
        sr.sprite.texture.Apply();
    }

    void Draw(Vector2 pixelPos)
    {
        /*color pixels in area around pointer position*/
        for (int i = (int)pixelPos.x - brushWidth / 2; i < (int)pixelPos.x + brushWidth / 2; i++)
        {
            for (int j = (int)pixelPos.y - brushWidth / 2; j < (int)pixelPos.y + brushWidth / 2; j++)
            {
                sr.sprite.texture.SetPixel(i, j, manager.GetSelectedColor());
            }
        }
        sr.sprite.texture.Apply();
    }
}

优化建议

1. 减少Texture.Apply()调用频率

当前代码在每次Draw()中都调用Apply(),这是性能卡顿的核心原因——每次修改单个像素块就提交GPU更新,会引发大量同步开销。

  • 方案:移除Draw()内的Apply(),仅在OnDrag()循环结束后调用一次,或者在Update()中每帧统一提交一次修改。
  • 示例修改:
    public void OnDrag(PointerEventData eventData)
    {
        if (manager.CanColor())
        {
            Vector2 pixelPos;
            if (RectTransformUtility.ScreenPointToLocalPointInRectangle(GetComponent<RectTransform>(), eventData.position, eventData.pressEventCamera, out pixelPos))
            {
                Vector2 prevPixel = PixelPos(prevPos);
                Vector2 currPixel = PixelPos(pixelPos);
                float pixelDistance = Vector2.Distance(prevPixel, currPixel);
                int steps = Mathf.Max(Mathf.RoundToInt(pixelDistance), 1);
                for (int step = 0; step <= steps; step++)
                {
                    Vector2 lerpedPixel = Vector2.Lerp(prevPixel, currPixel, (float)step / steps);
                    Draw(lerpedPixel);
                }
                // 仅在完成所有点绘制后提交一次
                sr.sprite.texture.Apply();
            }
            prevPos = pixelPos;
        }
    }
    
    void Draw(Vector2 pixelPos)
    {
        // 移除原有的sr.sprite.texture.Apply();
        // ... 原有像素修改逻辑 ...
    }
    

2. 修复线条补全逻辑

当前用UI本地坐标计算距离,容易因为缩放导致采样点不足,快速拖动时线条断裂。改为基于像素坐标计算步长,确保足够的采样密度:

  • 用像素坐标计算两点距离,按像素数量拆分插值步骤,保证快速拖动时也能覆盖所有缺失的像素点
  • 避免用maxDistance强制限制步长,改为动态根据像素距离生成步骤数

3. 批量修改像素提升效率

原Draw()的双层循环逐个设置像素效率极低,改用SetPixels()批量修改:

  • 提前计算画笔覆盖的有效矩形范围,创建对应大小的颜色数组,批量赋值后一次性设置到纹理
  • 提前做边界 clamp,避免循环内重复判断
  • 示例修改:
    void Draw(Vector2 pixelPos)
    {
        int startX = Mathf.Clamp((int)pixelPos.x - brushWidth / 2, 0, sr.sprite.texture.width - 1);
        int endX = Mathf.Clamp((int)pixelPos.x + brushWidth / 2, 0, sr.sprite.texture.width - 1);
        int startY = Mathf.Clamp((int)pixelPos.y - brushWidth / 2, 0, sr.sprite.texture.height - 1);
        int endY = Mathf.Clamp((int)pixelPos.y + brushWidth / 2, 0, sr.sprite.texture.height - 1);
    
        int width = endX - startX + 1;
        int height = endY - startY + 1;
        Color[] brushPixels = new Color[width * height];
        Array.Fill(brushPixels, manager.GetSelectedColor());
    
        sr.sprite.texture.SetPixels(startX, startY, width, height, brushPixels);
    }
    

4. 纹理配置优化

  • 确保纹理的Read/Write Enabled已勾选(必须开启才能修改像素)
  • 选择RGBA32/ARGB32格式,避免使用压缩纹理(压缩纹理修改像素开销更大)
  • 移动端可适当降低纹理分辨率,或改用RenderTexture替代普通纹理,渲染纹理的更新效率更高

5. 其他细节优化

  • 缓存manager.GetSelectedColor()结果,避免每次Draw()都调用获取
  • 开启Unity的Multi Touch Support(Edit > Project Settings > Input System Package),避免移动端触摸事件丢失
  • 检查UI的Raycast Target,仅保留必要的UI元素开启该选项,减少事件检测开销

内容的提问来源于stack exchange,提问作者Filippo Biancone

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 07:54:57