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

Unity中如何判定用户绘制线条与目标线条的相似度?

嘿,我来帮你搞定这个线条相似度判断的问题!你已经有了基础的绘制逻辑,现在只需要加上特征标准化和相似度计算的逻辑,就能区分出像绿色那样的正确线条和红色那样的错误线条了。

下面是一步步的具体实现方案,直接可以结合你的现有代码来用:

核心思路

要判断两条线条相似,不能直接对比每个点的位置(用户画的点数量、速度都不一样),得先把两条线的位置、大小、采样密度标准化,再用合适的算法计算相似度,最后和阈值对比来判定是否正确。


第一步:准备目标线条数据

首先你需要把目标线条(蓝色那条)的点数据存下来,方便后续对比。在你的DrawLine类里加几个变量:

public float similarityThreshold = 0.1f; // 可调整的判定阈值,越小要求越严格
public LineRenderer targetLineRenderer; // 在Inspector里拖入目标线条的LineRenderer组件
private List<Vector2> normalizedTargetPoints; // 标准化后的目标线条点

然后在Start方法里初始化目标线条的标准化数据:

void Start()
{
    if (targetLineRenderer != null)
    {
        // 从目标LineRenderer中提取点,并做标准化
        normalizedTargetPoints = NormalizeLine(GetLinePointsFromRenderer(targetLineRenderer));
    }
}

// 从LineRenderer中提取点的工具方法
private List<Vector2> GetLinePointsFromRenderer(LineRenderer lr)
{
    List<Vector2> points = new List<Vector2>();
    for (int i = 0; i < lr.positionCount; i++)
    {
        // 注意:要和你绘制时的坐标空间一致,这里用世界坐标转屏幕坐标,和你的绘制逻辑匹配
        points.Add(Camera.main.WorldToScreenPoint(lr.GetPosition(i)));
    }
    return points;
}

第二步:标准化线条数据

这一步是关键,用来消除两条线的位置、大小、采样数量差异:

private List<Vector2> NormalizeLine(List<Vector2> points)
{
    if (points.Count < 2) return new List<Vector2>();
    
    // 1. 平移到原点:把线条的起点移到(0,0),消除位置差异
    Vector2 offset = points[0];
    List<Vector2> translatedPoints = points.Select(p => p - offset).ToList();
    
    // 2. 缩放至单位长度:把线条总长度缩为1,消除大小差异
    float totalLength = 0;
    for (int i = 0; i < translatedPoints.Count - 1; i++)
    {
        totalLength += Vector2.Distance(translatedPoints[i], translatedPoints[i+1]);
    }
    if (totalLength == 0) return translatedPoints;
    List<Vector2> scaledPoints = translatedPoints.Select(p => p / totalLength).ToList();
    
    // 3. 重采样固定数量的点:把线条统一采样为20个点,方便后续对比
    List<Vector2> resampledPoints = new List<Vector2>();
    float step = totalLength / 19; // 20个点需要19步
    float currentDistance = 0;
    resampledPoints.Add(scaledPoints[0]);

    for (int i = 1; i < scaledPoints.Count; i++)
    {
        float segmentLength = Vector2.Distance(scaledPoints[i-1], scaledPoints[i]);
        while (currentDistance + segmentLength >= step)
        {
            float ratio = (step - currentDistance) / segmentLength;
            Vector2 interpolatedPoint = Vector2.Lerp(scaledPoints[i-1], scaledPoints[i], ratio);
            resampledPoints.Add(interpolatedPoint);
            currentDistance += segmentLength;
            step += totalLength / 19;
        }
        currentDistance += segmentLength;
    }
    // 确保最后一个点被加入
    if (resampledPoints.Count < 20)
    {
        resampledPoints.Add(scaledPoints.Last());
    }

    return resampledPoints;
}

第三步:相似度计算(推荐用DTW算法)

这里推荐用动态时间规整(DTW),它能很好地应对用户绘制速度不同导致的点数量差异,比简单的逐点对比更准确:

private float CalculateDTWSimilarity(List<Vector2> lineA, List<Vector2> lineB)
{
    int n = lineA.Count;
    int m = lineB.Count;
    float[,] costMatrix = new float[n, m];

    // 初始化矩阵的第一行和第一列
    costMatrix[0, 0] = Vector2.Distance(lineA[0], lineB[0]);
    for (int i = 1; i < n; i++)
    {
        costMatrix[i, 0] = costMatrix[i-1, 0] + Vector2.Distance(lineA[i], lineB[0]);
    }
    for (int j = 1; j < m; j++)
    {
        costMatrix[0, j] = costMatrix[0, j-1] + Vector2.Distance(lineA[0], lineB[j]);
    }

    // 填充成本矩阵
    for (int i = 1; i < n; i++)
    {
        for (int j = 1; j < m; j++)
        {
            float pointDistance = Vector2.Distance(lineA[i], lineB[j]);
            costMatrix[i, j] = pointDistance + Mathf.Min(
                Mathf.Min(costMatrix[i-1, j], costMatrix[i, j-1]),
                costMatrix[i-1, j-1]
            );
        }
    }

    // 返回归一化后的相似度分数(值越小越相似)
    return costMatrix[n-1, m-1] / (n + m);
}

如果你想要更简单的方案,也可以用逐点平均距离,但精度会差一些:

private float CalculateAverageDistance(List<Vector2> lineA, List<Vector2> lineB)
{
    if (lineA.Count != lineB.Count) return float.MaxValue;
    float totalDistance = 0;
    for (int i = 0; i < lineA.Count; i++)
    {
        totalDistance += Vector2.Distance(lineA[i], lineB[i]);
    }
    return totalDistance / lineA.Count;
}

第四步:集成到你的绘制逻辑中

修改Update方法里的鼠标抬起事件,当用户完成绘制时触发相似度判断:

if (Input.GetMouseButtonUp(0))
{
    mouseButtonState++;
    // 当用户完成一次绘制后(根据你的mouseButtonState逻辑,这里假设状态为2时完成)
    if (mouseButtonState == 2)
    {
        List<Vector2> normalizedUserPoints = NormalizeLine(fingerPositions);
        if (normalizedTargetPoints.Count > 0 && normalizedUserPoints.Count > 0)
        {
            float similarityScore = CalculateDTWSimilarity(normalizedTargetPoints, normalizedUserPoints);
            // 或者用CalculateAverageDistance(normalizedTargetPoints, normalizedUserPoints)
            
            if (similarityScore < similarityThreshold)
            {
                Debug.Log("绘制正确!(像绿色线条那样)");
                // 这里添加正确后的逻辑:比如加分、显示成功提示、保留线条
            }
            else
            {
                Debug.Log("绘制不正确!(像红色线条那样)");
                // 这里添加错误后的逻辑:比如销毁当前线条、允许重新绘制
                Destroy(currentLine);
                mouseButtonState = 0; // 重置状态,让用户可以重新画
            }
        }
    }
}

一些优化建议

  1. 过滤抖动噪声:用户绘制时可能有手抖,你可以在UpdateLine方法里加入平滑逻辑,比如只当新点和上一个点的距离超过0.2f才添加,或者用移动平均处理点数据。
  2. 方向无关判定:如果不需要考虑绘制方向(比如从左到右和从右到左算一样),可以把用户的线条反转后再计算一次相似度,取最小的分数。
  3. 可视化反馈:判定后给用户直观反馈,比如把正确的线条改成绿色,错误的改成红色,或者弹出提示文本。
  4. 阈值调试:similarityThreshold需要根据你的实际场景调整,多测试几次找到最合适的数值。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 06:29:23