🗄

Prefab Drawer 基本逻辑

扩展编辑器窗口部分中已经完成了一个简单的 Prefab 管理工具,可以对工程下的 Prefab 进行简单的操作。现在通过对 Scene View 的扩展,实现一个简单的 Prefab 绘制工具,主要包含以下功能:

实现之前,首先需要把与 Prefab Center 相关的资源、脚本等导入工程。然后新建脚本 Assets/Tools/PrefabDrawer/Editor/PrefabDrawerSceneView.cs

Prefab Drawer 的多种模式

首先通过工具栏实现多种模式的选择切换:

private void DrawToolbar () {
		_mode = (Mode)GUI.Toolbar (
				new Rect (5, 5, 300, 40),
				(int)_mode,
				new string [] { "View", "Paint", "Erase" }
		);
}

其中 Mode 为自定义的枚举类型,表示当前的模式:

public enum Mode {
		View = 0,
		Paint,
		Erase
}

后续的具体逻辑中,只需依据 _mode 的值分别进行处理即可。

Unity 默认工具的预设

Unity 内置有多种工具,最基本的工具栏位于 Unity 面板的顶部:

其中最左侧的工具栏对应 Scene 视图中的基本操作,例如视图工具、移动工具、放缩工具、旋转工具等。为避免 Unity 的默认工具和 Prefab Drawer 的操作冲突,我们需要通过代码预设默认工具:

private void HandleUnityTools () {
		switch (_mode) {
		case Mode.View:
				Tools.current = Tool.View;
				break;
		case Mode.Paint:
		case Mode.Erase:
				Tools.current = Tool.None;
				break;
		}
}

绘制模式

在绘制模式下,用户可以在 Scene 视图中通过单击鼠标快速添加物体。应该实现这样一个函数:将使用者鼠标点击的位置传入,然后便可以在场景的对应位置中生成一个确定 Prefab 的实例。需要实现的核心步骤如下:

首先在 Prefab Drawer 的 Inspector 视图中设置一个控件,用于设定要绘制的 Prefab。新建脚本 Assets/Scripts/PrefabDrawer/PrefabDrawer.cs 并添加代码:

using UnityEngine;

namespace SceneViewExtension.PrefabDrawer {
		public class PrefabDrawer : MonoBehaviour {
				[SerializeField]
				GameObject _selectedPrefab;
		
				public GameObject SelectedPrefab {
						get { return _selectedPrefab; }
						set { _selectedPrefab = value; }
				}
		}
}

则可以通过设置控件来设定想要绘制的 Prefab:

然后代码实现屏幕左边转换为 2D 场景坐标。由于这里的坐标转换参照的 Camera 并不是新建场景中的默认照相机,而是 Scene 视图对应的 SceneView.currentDrawingSceneView.camera ,所以在具体转换中需要进行一些额外的计算:

private Vector3 ScreenCoordsToSceneViewCoords (Vector3 screen) {
		Camera camera = SceneView.currentDrawingSceneView.camera;
		Vector3 newPos = camera.ScreenToWorldPoint (new Vector3(screen.x * 2, (camera.pixelHeight / 2 - screen.y) * 2, 0));
	
		return new Vector3 (newPos.x, newPos.y, 0);
}

最后将选定好的 SelectedPrefab 实例化,并移动到转换后的坐标位置上:

private void Paint (Vector3 pos) {
		if (_target.SelectedPrefab) {
				GameObject newObj = (GameObject)PrefabUtility.InstantiatePrefab (_target.SelectedPrefab);
				newObj.transform.position = ScreenCoordsToSceneViewCoords (pos);
				newObj.AddComponent<PrefabDrawerItem> ();
				_objs.Add (newObj);
		} else {
				Debug.LogError ("No selected prefab.");
		}
}

擦除模式

在擦除模式下,用户可以在 Scene 视图中通过单击或者拖动鼠标删除之前绘制的 Prefab 实例。一个不考虑性能的简单实现方式为:在单击或拖动鼠标时,将屏幕坐标转换为场景坐标,然后判断这个坐标是否被某个物体的包围盒所包含,如果是则将这个物体删除。

首先需要知道哪些物体是之前由 Prefab Drawer 绘制的物体。为了找到这些物体,现通过代码在绘制时给新建的物体挂一个脚本 PrefabDrawerItem,并把这些物体都收集到一个列表中,从而之后便于寻找:

private void Paint (Vector3 pos) {
		if (_target.SelectedPrefab) {
				// ...

				newObj.AddComponent<PrefabDrawerItem> ();
				_objs.Add (newObj);
		} else {
				// ...
		}
}

然后只需简单的坐标转换和遍历判断即可:

private void Erase (Vector3 pos) {
		Camera camera = SceneView.currentDrawingSceneView.camera;
		Vector3 worldCoord = ScreenCoordsToSceneViewCoords (pos);
	
		foreach (GameObject obj in _objs) {
				if (obj.GetComponent<Collider2D>().bounds.Contains(worldCoord)) {
						_objs.Remove (obj);
						DestroyImmediate (obj);
						break;
				}
		}
}