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

Unity编辑模式下Camera.ScreenToWorldPoint()方法异常问题

Unity编辑器窗口桌游制作:Camera.ScreenToWorldPoint坐标匹配异常问题

我正在Unity中开发编辑器窗口用于快速制作桌游,目前遇到的核心问题是Camera.ScreenToWorldPoint()方法导致脚本识别的瓦片位置与实际位置完全不符。

已尝试的解决思路均无效:

  • 使用主相机时,脚本会错误判定瓦片位于屏幕中心
  • 将相机聚焦到上一个瓦片会增加后续分支路径系统的实现难度
  • 同时转换瓦片位置与鼠标位置后对比,问题仍未解决
  • 测试OnSceneGUI()中不同相机组合,包括ChatGPT提供的代码,均无效果

推测问题根源:相机视角与瓦片所在世界坐标系不匹配,拖动场景视图会改变sceneCam视角下的世界视图"中心",但始终处于世界坐标中心的首个瓦片位置并未改变,导致向量无法正确匹配。


StencilScript(问题疑似所在脚本)

using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using UnityEngine;
using UnityEditor;
[ExecuteInEditMode]
public class StencilScript : MonoBehaviour
{
    private static EdgeCollider2D tileDist;
    private static Vector3 mousePos;
    private static Vector3 worldMousePos;
    private static Vector3 worldTilePos;
    private static Vector3 relativePos;
    private static Transform body;
    void OnEnable()
    {
        body = gameObject.transform;
        tileDist = gameObject.AddComponent<EdgeCollider2D>();
        tileDist.points = new Vector2[] {
            new Vector2(-1, -1),
            new Vector2(-1, -0.5f),
            new Vector2(-1, 0),
            new Vector2(-1, 0.5f),
            new Vector2(-1, 1),
            new Vector2(-0.5f, 1),
            new Vector2(0, 1),
            new Vector2(0.5f, 1),
            new Vector2(1, 1),
            new Vector2(1, 0.5f),
            new Vector2(1, 0),
            new Vector2(1, -0.5f),
            new Vector2(1, -1),
            new Vector2(0.5f, -1),
            new Vector2(0, -1),
            new Vector2(-0.5f, -1),
            new Vector2(-1, -1) };
        SceneView.duringSceneGui += OnSceneGUI;
    }
    private static void OnSceneGUI(SceneView sceneView)
    {
        Event e = Event.current;
        if (e.isMouse)
        {
            Camera cam = Camera.main;
            Camera sceneCam = sceneView.camera;
            mousePos = e.mousePosition;
            mousePos.y = cam.pixelHeight - mousePos.y;
            worldMousePos = sceneCam.ScreenToWorldPoint(new Vector3(mousePos.x, mousePos.y, sceneCam.nearClipPlane));
        }
    }
    private static Vector2 GetDrawLocation()
    {
        Vector2 exactPos = tileDist.ClosestPoint(worldMousePos);
        Vector2[] targets = tileDist.points;
        Vector2 drawPos = targets[0];
        for (int i = 1; i < targets.Length; i++)
        {
            float distanceA = Vector2.Distance(exactPos, targets[i]);
            float distanceB = Vector2.Distance(exactPos, drawPos);
            if (distanceA < distanceB)
            {
                drawPos = targets[i];
            }
        }
        return drawPos;
    }
    public void UpdateDrawLocation(GameObject tile)
    {
        if (tile != null)
        {
            Vector2 drawLocation = GetDrawLocation();
            tile.transform.position = new Vector3(drawLocation.x, drawLocation.y, tile.transform.position.z);
        }
    }
}

BoardBuilder Script

using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
public class BoardBuilder : EditorWindow
{
    StencilScript _stencilScript;
    string boardName = "";
    GameObject active;
    GameObject stencil;
    GameObject board;
    GameObject nextToPlace;
    List<Transform> tiles = new List<Transform>();
    int tileCount;
    int nextSuitID;
    bool buildButton;
    string prefabFolder = "Assets/Prefabs/Tiles";
    private GameObject[] prefabs;
    private string[] tileNames;
    GameObject[] suitSpaces = new GameObject[4];
    int selected;
    static Vector2 mousePos;

    [MenuItem("Tools/Board Builder")]
    public static void ShowWindow() {
        GetWindow(typeof(BoardBuilder)); // GetWindow is from the EditorWindow Class
    }
    private void OnEnable() {
        LoadTiles();
        board = GameObject.FindGameObjectWithTag("Board");
        if (board != null)
        {
            tiles.Add(board.transform);
            for (int i = 0; i < board.transform.childCount; i++)
            {
                tiles.Add(board.transform.GetChild(i));
            }
            tileCount = board.transform.childCount + 1;
            active = tiles[tiles.Count - 1].gameObject;
        }
    }
    private void OnGUI()
    {
        boardName = EditorGUILayout.TextField("BoardName", boardName);
        if (GUILayout.Button("Create Board") && GameObject.FindGameObjectWithTag("Board") == null)
        {
            board = Instantiate(prefabs[0].gameObject);
            board.tag = "Board";
            board.name = boardName + " (Home)";
            board.transform.position = new Vector3(0, 0, 0);
            tileCount = 1;
        }
        tileCount = EditorGUILayout.IntField("Tile Count", tileCount);
        buildButton = GUILayout.Toggle(buildButton, "Build Mode");
        selected = GUILayout.SelectionGrid(selected, tileNames, 2);
        if (GUILayout.Button(new GUIContent("Refresh Tile Selection", "Checks for new tiles in the 'Tile' folder")))
        {
            LoadTiles();
        }
        GUILayout.Label("--------------\nMap?");

    }

    private void LoadTiles()
    {
        string[] guids = AssetDatabase.FindAssets("t:Prefab", new[] { prefabFolder });
        prefabs = new GameObject[guids.Length];
        tileNames = new string[guids.Length];
        for (int i = 0; i < guids.Length; i++)
        {
            string path = AssetDatabase.GUIDToAssetPath(guids[i]);
            prefabs[i] = AssetDatabase.LoadAssetAtPath<GameObject>(path);
            tileNames[i] = prefabs[i].name;
        }
    }
    [ExecuteInEditMode]
    void Update()
    {
        if(board != null)
        {
            if (!buildButton)
            {
                if(active!= null && active.GetComponent<StencilScript>() != null)
                {
                    DestroyImmediate(active.GetComponent<EdgeCollider2D>());
                    DestroyImmediate(active.GetComponent<StencilScript>());
                }
                if (nextToPlace != null)
                {
                    ChangeTileInUse(-1);
                }
            }
            else
            {
                active = Selection.activeGameObject;
                if(active != null)
                {
                    if (active.GetComponent<StencilScript>() == null)
                    {
                        _stencilScript = active.AddComponent<StencilScript>();
                    }
                    if (nextToPlace == null || nextToPlace.GetComponent<Tile>().tileType != selected)
                    {
                        ChangeTileInUse(selected);
                    }
                    _stencilScript.UpdateDrawLocation(nextToPlace);
                    if (Input.GetMouseButtonDown(0))
                    {
                        PlaceTile();
                    }
                } 
            }
        }        
    }
    void ChangeTileInUse(int tileID)
    {
        DestroyImmediate(nextToPlace);
        if (tileID == -1)
        {
            nextToPlace = null;
        }
        else
        {
            nextToPlace = Instantiate(prefabs[tileID], board.transform);
            nextToPlace.GetComponent<Tile>().tileType = selected;
        }
    }
    private void PlaceTile()
    {
        UnityEngine.Debug.Log("place");
        DestroyImmediate(active.GetComponent<StencilScript>());
        DestroyImmediate(active.GetComponent<BoxCollider2D>());
        tiles[tileCount] = active.transform;
        Selection.activeGameObject = nextToPlace;
        active = nextToPlace;
        active.AddComponent<StencilScript>();
        nextToPlace = Instantiate(prefabs[selected].gameObject, board.transform);
        tileCount++;
    }
}

核心问题修复方案

1. 鼠标坐标转换错误修正

OnSceneGUI中混用了主相机和SceneView相机的像素高度,导致Y轴坐标转换错误。同时ScreenToWorldPoint的深度参数错误,应该使用相机到瓦片所在平面的距离:

private static void OnSceneGUI(SceneView sceneView)
{
    Event e = Event.current;
    if (e.isMouse)
    {
        Camera sceneCam = sceneView.camera;
        mousePos = e.mousePosition;
        // 使用SceneView相机的像素高度反转Y轴
        mousePos.y = sceneCam.pixelHeight - mousePos.y;
        // 假设瓦片在Z=0平面,计算相机到该平面的深度
        float depth = Mathf.Abs(sceneCam.transform.position.z);
        worldMousePos = sceneCam.ScreenToWorldPoint(new Vector3(mousePos.x, mousePos.y, depth));
    }
}

2. EdgeCollider2D局部坐标转世界坐标

EdgeCollider2D的points是局部坐标,直接与世界坐标的鼠标位置对比会导致偏差,需要转换后再计算:

private static Vector2 GetDrawLocation()
{
    Vector2 exactPos = tileDist.ClosestPoint(worldMousePos);
    Vector2[] targets = tileDist.points;
    Vector2 drawPos = targets[0];
    Vector2 drawPosWorld = (Vector2)body.TransformPoint(drawPos);
    
    for (int i = 1; i < targets.Length; i++)
    {
        Vector2 targetWorld = (Vector2)body.TransformPoint(targets[i]);
        float distanceA = Vector2.Distance(exactPos, targetWorld);
        float distanceB = Vector2.Distance(exactPos, drawPosWorld);
        if (distanceA < distanceB)
        {
            drawPos = targets[i];
            drawPosWorld = targetWorld;
        }
    }
    // 返回世界坐标用于设置瓦片位置
    return drawPosWorld;
}

3. 移除全局静态变量避免冲突

StencilScript中大量静态变量会导致多瓦片挂载时数据冲突,改为实例变量:

private EdgeCollider2D tileDist;
private Vector3 mousePos;
private Vector3 worldMousePos;
private Transform body;

void OnEnable()
{
    body = gameObject.transform;
    tileDist = gameObject.AddComponent<EdgeCollider2D>();
    // 其余初始化代码不变
    SceneView.duringSceneGui += OnSceneGUI;
}

private void OnSceneGUI(SceneView sceneView)
{
    // 内部使用实例变量,逻辑同修正后的代码
}

private Vector2 GetDrawLocation()
{
    // 内部使用实例变量body和tileDist,逻辑同修正后的代码
}

4. BoardBuilder额外优化

  • PlaceTile中tiles[tileCount] = active.transform易导致索引越界,改为tiles.Add(active.transform)
  • 用OnSelectionChange替代Update中频繁获取选中对象,提升性能:
private void OnSelectionChange()
{
    if (buildButton && board != null)
    {
        active = Selection.activeGameObject;
        if (active != null && active.GetComponent<StencilScript>() == null)
        {
            _stencilScript = active.AddComponent<StencilScript>();
        }
    }
    Repaint();
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 08:32:05