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

Unity:如何实现TextMeshPro UI单个单词可拖拽?求解决方案

实现TextMeshPro单个单词可拖拽的解决方案

核心思路

不拆分单个单词为独立UI对象,通过文本解析+碰撞检测+富文本动态偏移实现单单词交互,完全适配不同屏幕尺寸。

具体实现步骤

1. 解析文本,定位单个单词的屏幕区域

利用TextMeshPro内置API,获取每个单词的字符范围与对应的屏幕矩形区域,存储为字典供后续交互判断:

using TMPro;
using UnityEngine;
using System.Collections.Generic;

public class DraggableWordHandler : MonoBehaviour
{
    private TMP_Text _tmpText;
    private Dictionary<TMP_WordInfo, Rect> _wordRectMap = new Dictionary<TMP_WordInfo, Rect>();

    void Start()
    {
        _tmpText = GetComponent<TMP_Text>();
        RefreshWordRects();
    }

    // 刷新单词区域(屏幕尺寸变化/文本更新时调用)
    public void RefreshWordRects()
    {
        _wordRectMap.Clear();
        TMP_TextInfo textInfo = _tmpText.textInfo;
        
        for (int i = 0; i < textInfo.wordCount; i++)
        {
            TMP_WordInfo wordInfo = textInfo.wordInfo[i];
            // 获取单词首尾字符的屏幕坐标,计算包围矩形
            Vector3 bottomLeft = _tmpText.transform.TransformPoint(textInfo.characterInfo[wordInfo.firstCharacterIndex].bottomLeft);
            Vector3 topRight = _tmpText.transform.TransformPoint(textInfo.characterInfo[wordInfo.lastCharacterIndex].topRight);
            
            Rect wordRect = new Rect(bottomLeft.x, bottomLeft.y, topRight.x - bottomLeft.x, topRight.y - bottomLeft.y);
            _wordRectMap[wordInfo] = wordRect;
        }
    }
}

2. 处理点击/触摸事件,识别选中单词

在Update中检测输入,判断点击位置落在哪个单词区域内,记录选中状态与拖拽偏移:

private TMP_WordInfo _selectedWord;
private Vector2 _dragOffset;
private Vector2 _originalWordLocalPos;

void Update()
{
    if (Input.GetMouseButtonDown(0))
    {
        Vector2 clickScreenPos = Input.mousePosition;
        _selectedWord = GetWordAtScreenPos(clickScreenPos);
        
        if (_selectedWord != null)
        {
            // 计算点击点与单词中心的偏移,避免拖拽跳变
            Rect wordRect = _wordRectMap[_selectedWord];
            _dragOffset = clickScreenPos - new Vector2(wordRect.center.x, wordRect.center.y);
            // 记录单词原始本地位置,用于复位
            Vector3 originalWorldPos = _tmpText.transform.TransformPoint(_tmpText.textInfo.characterInfo[_selectedWord.firstCharacterIndex].bottomLeft);
            _originalWordLocalPos = _tmpText.transform.InverseTransformPoint(originalWorldPos);
        }
    }

    // 执行拖拽逻辑
    if (Input.GetMouseButton(0) && _selectedWord != null)
    {
        Vector2 currentScreenPos = Input.mousePosition;
        Vector2 targetCenter = currentScreenPos - _dragOffset;
        // 转换为文本本地坐标,计算偏移量
        Vector3 targetWorldPos = new Vector3(targetCenter.x, targetCenter.y, 0);
        Vector2 targetLocalPos = _tmpText.transform.InverseTransformPoint(targetWorldPos);
        Vector2 offset = targetLocalPos - _originalWordLocalPos;
        
        // 更新文本显示
        UpdateDraggedWordOffset(offset);
    }

    // 释放拖拽,判断是否通关
    if (Input.GetMouseButtonUp(0))
    {
        if (_selectedWord != null)
        {
            if (IsWordInTargetArea())
            {
                // 触发通关逻辑
                Debug.Log("关卡通关!");
                // 可移除该单词或标记为已完成
                RemoveWordFromText(_selectedWord.GetWord());
            }
            else
            {
                // 单词复位
                ResetDraggedWord();
            }
            _selectedWord = null;
        }
    }
}

// 根据屏幕坐标获取对应的单词
private TMP_WordInfo GetWordAtScreenPos(Vector2 screenPos)
{
    foreach (var pair in _wordRectMap)
    {
        if (pair.Value.Contains(screenPos))
        {
            return pair.Key;
        }
    }
    return null;
}

// 判断单词是否进入目标区域
private bool IsWordInTargetArea()
{
    Rect targetScreenRect = GameObject.Find("TargetArea").GetComponent<RectTransform>().GetScreenRect();
    return targetScreenRect.Overlaps(_wordRectMap[_selectedWord]);
}

3. 用富文本实现单词拖拽视觉效果

通过TMP的<pos>富文本标签,动态修改选中单词的位置偏移,无需拆分UI:

private string _originalText;

void Awake()
{
    _originalText = _tmpText.text;
}

// 更新拖拽单词的位置偏移
private void UpdateDraggedWordOffset(Vector2 offset)
{
    string word = _selectedWord.GetWord();
    // 替换原单词为带位置偏移的富文本格式
    string formattedWord = $"<pos=({offset.x},{offset.y})>{word}</pos>";
    // 先清除之前的富文本标签,再替换
    string cleanText = RemoveAllPosTags(_originalText);
    _tmpText.text = cleanText.Replace(word, formattedWord);
}

// 复位拖拽单词
private void ResetDraggedWord()
{
    _tmpText.text = RemoveAllPosTags(_originalText);
}

// 移除所有<pos>标签,还原原始文本
private string RemoveAllPosTags(string text)
{
    return System.Text.RegularExpressions.Regex.Replace(text, @"<pos=\([\d\.-]+,[\d\.-]+\)>|<\/pos>", "");
}

// 移除已完成的单词
private void RemoveWordFromText(string word)
{
    _originalText = _originalText.Replace(word, "");
    _tmpText.text = _originalText;
    RefreshWordRects();
}

4. 屏幕适配优化

  • 监听Screen.resize事件,在屏幕尺寸变化时调用RefreshWordRects()重新计算单词区域。
  • 使用RectTransformUtility.ScreenPointToLocalPointInRectangle处理UI坐标转换,避免分辨率差异导致的定位偏差:
// 替代直接坐标转换,更适配UI系统
RectTransform rt = _tmpText.GetComponent<RectTransform>();
Vector2 localPoint;
RectTransformUtility.ScreenPointToLocalPointInRectangle(rt, Input.mousePosition, Camera.main, out localPoint);

额外提示

  • 可添加<color>或<material>富文本标签,给选中的单词添加高亮、阴影等视觉反馈。
  • 针对重复单词场景,可通过字符索引而非单词内容来定位,避免替换错误。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 04:05:32