|
前言
搜了一下unity地编相关,河流、道路工具倒是有人说,或者用宇宙无敌的【RamSpline】插件,但是其他的东西好像寥寥无几。
也许是我对地编的工作流理解有问题吧,也可能是没人在Unity里面做地编,别人都是用DCC程序化生成呢。
权当是闲得无聊,分享一个没什么技术含量的玩具。
模型资源来自:RPG Poly Pack - Lite | 3D Landscapes | Unity Asset Store
一、 工具效果
模型生成
一个玩具而已,可以在SceneView点击鼠标生成线段,然后在线段上生成想要的模型。
在构建某些重复性场景时可能对地编友好(大概)。
这个工具长这样:
平平无奇小工具
二、 完整代码
没什么说头,直接贴代码,感觉也没什么值得学习的东西。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
namespace MyUnityTool
{
public class BuildWallsWindow : EditorWindow
{
private enum InstanceType
{
Clone,
Prefab
}
private enum AlignAxis
{
Right,
Left,
//Forward,
//Back,
//Up,
//Down
}
private class GenerationRecord
{
public List<GameObject> instances = new List<GameObject>();
public Vector3 lastPos;
public void Clear()
{
instances.Clear();
}
public void AddRange(IEnumerable<GameObject> instances)
{
this.instances.AddRange(instances);
}
}
private Transform instanceParent;
private List<GameObject> stamps;
private Stack<GenerationRecord> records;
private GameObject targetPrefab;
private GUIStyle textStyle;
private KeyCode startKey = KeyCode.A, endKey = KeyCode.S;
private KeyCode cancelKey = KeyCode.D;
private AlignAxis alignAxis = AlignAxis.Right;
private InstanceType instanceType = InstanceType.Prefab;
private Vector3[] recordPoints;
private Vector3 lastPoint;
private RaycastHit hit;
private LayerMask layerMask = 0;
private int m_layerMask => 1 << layerMask;
private float interval = 5;
private float intervalAccuracy = 0.1f;
private float genrationScale = 1;
private int oldCount = 0, desireCount = 0;
private bool startStamp;
private bool onXZplane = true;
private bool lockX, lockY, lockZ;
[MenuItem(&#34;MyWindows/WallsBuilder&#34;)]
public static void OpenWindow()
{
EditorWindow.GetWindow<BuildWallsWindow>().Show();
}
private void OnEnable()
{
SceneView.duringSceneGui += OnSceneView;
recordPoints = new Vector3[2];
stamps = new List<GameObject>();
records = new Stack<GenerationRecord>();
textStyle = new GUIStyle();
textStyle.normal.textColor = Color.black;
textStyle.fontStyle = FontStyle.Bold;
textStyle.fontSize = 20;
}
private void OnDisable()
{
SceneView.duringSceneGui -= OnSceneView;
ResetStamps();
records.Clear();
}
private void OnSceneView(SceneView sceneView)
{
SceneView.RepaintAll();
if (targetPrefab != null)
{
var mousPos = Event.current.mousePosition;
var ray = HandleUtility.GUIPointToWorldRay(mousPos);
if (Event.current.type == EventType.KeyDown && Event.current.keyCode == startKey)
{
if (Physics.Raycast(ray, out hit, float.MaxValue, m_layerMask))
{
recordPoints[0] = hit.point;
startStamp = true;
}
}
if (Event.current.type == EventType.KeyDown && Event.current.keyCode == endKey)
{
ResetStamps();
startStamp = false;
}
if (Event.current.type == EventType.KeyDown && Event.current.keyCode == cancelKey)
{
UndoInstance();
ResetStamps();
}
if (Event.current.type == EventType.ScrollWheel)
{
if (Event.current.alt)
{
interval += Event.current.delta.y > 0 ? intervalAccuracy : -intervalAccuracy;
Event.current.Use();
}
}
if (startStamp)
{
if (Physics.Raycast(ray, out hit, float.MaxValue, m_layerMask))
{
recordPoints[1] = hit.point;
var forward = recordPoints[1] - recordPoints[0];
var rotationAxis = forward;
if (onXZplane)
{
rotationAxis.y = 0;//钳制xz面进行旋转
}
var length = forward.magnitude;
var quaternion = Quaternion.FromToRotation(GetAlignAxis(alignAxis), rotationAxis);
if (!onXZplane)
{
quaternion = LockRotation(quaternion);
}
desireCount = (int)(length / interval);
while (oldCount < desireCount)
{
GameObject stamp;
if (instanceType == InstanceType.Prefab)
{
stamp = (GameObject)PrefabUtility.InstantiatePrefab(targetPrefab);
}
else
{
stamp = (GameObject)GameObject.Instantiate(targetPrefab);
}
stamp.transform.localScale = Vector3.one * genrationScale;
stamp.hideFlags = HideFlags.HideAndDontSave;
stamps.Add(stamp);
oldCount++;
if (oldCount == desireCount)
{
break;
}
}
while (oldCount > desireCount)
{
var stamp = stamps[stamps.Count - 1];
stamps.Remove(stamp);
GameObject.DestroyImmediate(stamp);
oldCount--;
if (oldCount == desireCount)
{
break;
}
}
for (int i = 0; i < stamps.Count; i++)
{
var stamp = stamps;
stamp.transform.rotation = quaternion;
stamp.transform.position = recordPoints[0] + forward.normalized * interval * (i + 0.5f);
}
lastPoint = recordPoints[0] + forward.normalized * interval * (stamps.Count);
}
if (Event.current.type == EventType.MouseDown && Event.current.button == 0)
{
GenInstances(recordPoints[0]);
recordPoints[0] = lastPoint;
}
DrawHandles();
}
}
}
private void DrawHandles()
{
Handles.color = Color.red;
Handles.DrawWireCube(recordPoints[0], Vector3.one);
Handles.DrawWireCube(recordPoints[1], Vector3.one);
Handles.color = Color.green;
Handles.DrawLine(recordPoints[0], recordPoints[1]);
Handles.DrawWireDisc(lastPoint, Vector3.up, interval);
Handles.Label(recordPoints[1], $&#34;间隔: {interval}m&#34;, textStyle);
Handles.color = Color.blue;
Handles.DrawLine(lastPoint, recordPoints[1]);
}
private Quaternion LockRotation(Quaternion quaternion)
{
if (lockX)
{
quaternion.x = 0;
}
if (lockY)
{
quaternion.y = 0;
}
if (lockZ)
{
quaternion.z = 0;
}
return quaternion.normalized;
}
private void GenInstances(Vector3 lastPos)
{
foreach (var stamp in stamps)
{
stamp.hideFlags = HideFlags.None;
stamp.transform.parent = instanceParent;
}
var record = new GenerationRecord();
record.AddRange(stamps);
record.lastPos = lastPos;
records.Push(record);
ResetStamps(false);
}
private void OnGUI()
{
EditorGUILayout.LabelField(&#34;围墙生成器&#34;, EditorStyles.boldLabel);
layerMask = EditorGUILayout.LayerField(&#34;目标检测Layer&#34;, layerMask);
targetPrefab = EditorGUILayout.ObjectField(&#34;围墙预制体&#34;, targetPrefab, typeof(GameObject), false) as GameObject;
instanceParent = EditorGUILayout.ObjectField(&#34;实例生成父对象&#34;, instanceParent, typeof(Transform), true) as Transform;
if (targetPrefab == null)
{
EditorGUILayout.HelpBox(&#34;请选择目标预制体&#34;, MessageType.Error);
}
else
{
instanceType = (InstanceType)EditorGUILayout.EnumPopup(&#34;实例化类型&#34;, instanceType);
alignAxis = (AlignAxis)EditorGUILayout.EnumPopup(&#34;对齐方向轴&#34;, alignAxis);
onXZplane = EditorGUILayout.Toggle(&#34;XZ面对齐&#34;, onXZplane);
EditorGUILayout.HelpBox(&#34;暂时仅支持Right轴的xz面对齐方式&#34;, MessageType.Warning);
if (!onXZplane)
{
using (new EditorGUILayout.HorizontalScope())
{
lockX = EditorGUILayout.Toggle(&#34;锁定X轴旋转&#34;, lockX);
lockY = EditorGUILayout.Toggle(&#34;锁定Y轴旋转&#34;, lockY);
lockZ = EditorGUILayout.Toggle(&#34;锁定Z轴旋转&#34;, lockZ);
}
}
genrationScale = EditorGUILayout.Slider(&#34;生成缩放&#34;, genrationScale, 0, 10);
if (GUILayout.Button(&#34;自动调整间隔大小&#34;))
{
var meshFilter = targetPrefab.GetComponent<MeshFilter>();
if (meshFilter == null)
{
meshFilter = targetPrefab.GetComponentInChildren<MeshFilter>();
}
if (meshFilter != null)
{
var mesh = meshFilter.sharedMesh;
var size = mesh.bounds.size;
interval = size.x * meshFilter.transform.lossyScale.x * genrationScale;
}
else
{
Debug.LogError(&#34;未找到模型MeshFilter,自动修正间隔失败...&#34;);
}
}
interval = EditorGUILayout.Slider(&#34;实例间隔&#34;, interval, 0.01f, 100);
intervalAccuracy = EditorGUILayout.FloatField(&#34;实例间隔修改精度&#34;, intervalAccuracy);
EditorGUILayout.HelpBox(&#34;开始:A;结束:S;撤销:D;Alt + 鼠标滚轮可以微调生成间距&#34;, MessageType.Info);
}
}
private void UndoInstance()
{
if (records.Count > 0)
{
var record = records.Pop();
if (record != null)
{
foreach (var ins in record.instances)
{
DestroyImmediate(ins);
}
recordPoints[0] = record.lastPos;
recordPoints[1] = record.lastPos;
record.Clear();
}
}
}
private void ResetStamps(bool destroyStamps = true)
{
if (destroyStamps)
{
foreach (var stamp in stamps)
{
DestroyImmediate(stamp);
}
}
oldCount = desireCount = 0;
stamps.Clear();
}
private Vector3 GetAlignAxis(AlignAxis alignAxis)
{
switch (alignAxis)
{
case AlignAxis.Right:
return Vector3.right;
case AlignAxis.Left:
return Vector3.left;
//case AlignAxis.Forward:
// return Vector3.forward;
//case AlignAxis.Back:
// return Vector3.back;
//case AlignAxis.Up:
// return Vector3.up;
//case AlignAxis.Down:
// return Vector3.down;
}
return Vector3.zero;
}
}
}
三、 算法细节
1. 撤销功能
只能说只要是个工具,就必须支持一定的容错操作,那么撤销功能必不可少。为了实现这个功能,我们额外定义了一个“操作记录”类与“历史栈”:
操作记录
记录操作的栈
每次撤销时,就是从栈里面出栈一个历史记录,然后重置线段起点、终点,销毁已经实例化的游戏对象罢了。
历史记录出栈,还原已记录信息
2. 沿轴分布
如何把模型沿着确定的线段轴对齐分布?我们得确定他的旋转与位置坐标。
旋转的处理,核心是Quaternion.FromToRotation方法。
一般的,假设我们的线段方向是(1,0,1),想把模型的x正轴对齐线段,那么这个旋转量就是:
var quaternion = Quaternion.FromToRotation(Vector3.right, new Vector3(1, 0, 1));
为什么使用【Vector3.right】进行旋转量计算而非具体的某个【GameObject.transform.right】?
因为我们可以认为模型的沿轴旋转是在【模型空间】而非【世界空间】,自然是使用【Vector3.right】常量就行。
为什么要钳制在xz面进行旋转?
因为我们的默认是在xz面进行建筑生成,那么物体的旋转轴自然是y轴才符合常识。如果想进行多维度扩展,可以自行修改旋转量。 然后把他赋值给transform.rotation就行啦。
stamp就是某个具体的GameObject
为了支持对齐模型的不同轴向,我们使用GetAlignAxis来确定具体的轴, 他长这样:
暂时只实现了x正负轴的对齐,实际上其他轴的对齐方式也是同理
而位置信息的计算,是这样一句话:
stamp.transform.position = recordPoints[0] + forward.normalized * interval * (i + 0.5f);
其中,【recordPoints[0]】是线段起点,【forward】是沿轴方向,【interval】是两个模型之间的间隔,【i】是模型数组的下标。
由于默认模型网格的原点是在网格的中心位置,所以有一个0.5f的偏移。
至此,旋转+位置的计算实现了模型沿轴分布。
感觉还蛮好玩的
3. 间隔计算
如何确定模型之间的间隔?以对齐x轴为例的话,只要知道模型的x轴向的宽度就可以了。有没有什么办法自动计算呢?自然是有的。
自动计算间隔代码
注意这一句话:
interval = size.x * meshFilter.transform.lossyScale.x * genrationScale;
我们是读取了模型的【MeshFilter】上的【Mesh】信息,获取 【mesh.bounds.size】。
由于我们是沿x轴对齐,自然是取size.x,再考虑到模型可能存在缩放什么的,把缩放系数乘上去,就是最终的interval啦。
注意Mesh信息的“Bounds Size”
然而,这样计算出来的【interval】过于“精确”,会导致模型紧挨着一起。为了灵活调整,我们支持【interval】的“缩放”。
根据鼠标滚轮微调interval,一般的,intervalAccuracy = 0.1
微调interval
4. 预览对象与实例对象
有些时候,我们想实例化一个“预制体”,而不是“克隆体”,实现的API是这两个:
两种API
克隆对象与保持预制体连接的对象
在我们拉线时,实际上已经生成了游戏对象,只是修改【HideFlags】隐藏了他们的某些信息:
stamp.hideFlags = HideFlags.HideAndDontSave;
在确定生成时,只是进行了还原。
stamp.hideFlags = HideFlags.None;
5. 其他细节
每帧刷新SceneView:SceneView.RepaintAll();
屏幕坐标转世界射线:
var mousPos = Event.current.mousePosition;
var ray = HandleUtility.GUIPointToWorldRay(mousPos);
注册/注销SceneView更新:
SceneView.duringSceneGui += OnSceneView;
SceneView.duringSceneGui -= OnSceneView;
什么?你说你不会写UnityEditor扩展? 请:
Unity3d Editor 编辑器扩展功能详解(1) 目录索引
小结
实际上,对于某些“沙盒建造”类的GamePlay,功能逻辑和本文的逻辑别无二致,这个玩具有点好玩(雾)。 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|