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
相关产品推荐
相关产品推荐

