【Unity】网格系统:物体使用网格坐标定位
需求分析
前面物体放置在地板上都是地板任意位置放置,本节开始对物体放置的位置做限制。
- 建立网格,网格可以设置起始世界坐标、单元格大小和规格;
- 单元格中包括内部物体的信息;
- 物体的位置通过网格的坐标确定;
- 单元格中已经存在物体,该位置不能再放入其他物体;
成果展示
Scene部分
场景中删除了手动拖入的预制体,改为通过代码将物体在指定网格单元格中。
每个预制体都增加脚本PlaceObject.cs
脚本部分
设计网格类GridXZ
属性:宽度高度、单元格大小、原点世界坐标位置、网格矩阵;
方法:
- 创建网格(构造函数),需要将每个单元格填充入对应类实例。
- 世界坐标和网格坐标相互转换,根据世界坐标位置获取网格坐标位置,根据网格坐标获取世界坐标位置。
- 设置或获取单元格中实例,定义单元格中实例发生变化的响应事件。
public class GridXZ<TGridObject>
{
public event EventHandler<OnGridObjectChangedEventArgs> OnGridObjectChanged;
public class OnGridObjectChangedEventArgs : EventArgs
{
public int x;
public int z;
}
private int width;
private int height;
private float cellSize;
private Vector3 originPosition;
private TGridObject[,] gridArray;
public GridXZ(int width, int height, float cellSize, Vector3 originPosition, Func<GridXZ<TGridObject>, int, int, TGridObject> createGridObject)
{
this.width = width;
this.height = height;
this.cellSize = cellSize;
this.originPosition = originPosition;
gridArray = new TGridObject[width, height];
for (int x = 0; x < gridArray.GetLength(0); x++)
{
for (int z = 0; z < gridArray.GetLength(1); z++)
{
gridArray[x, z] = createGridObject(this, x, z);
}
}
//绘制了调试线,帮助观察网格的状态
#region debugDrawLine
bool showDebug = true;
if (showDebug)
{
TextMesh[,] debugTextArray = new TextMesh[width, height];
for (int x = 0; x < gridArray.GetLength(0); x++)
{
for (int z = 0; z < gridArray.GetLength(1); z++)
{
debugTextArray[x, z] = UtilsClass.CreateWorldText("", null, GetWorldPosition(x, z) + new Vector3(cellSize, 0, cellSize) * .5f,8, Color.white, TextAnchor.MiddleCenter, TextAlignment.Center);
Debug.DrawLine(GetWorldPosition(x, z), GetWorldPosition(x, z + 1), Color.white, 100f);
Debug.DrawLine(GetWorldPosition(x, z), GetWorldPosition(x + 1, z), Color.white, 100f);
}
}
Debug.DrawLine(GetWorldPosition(0, height), GetWorldPosition(width, height), Color.white, 100f);
Debug.DrawLine(GetWorldPosition(width, 0), GetWorldPosition(width, height), Color.white, 100f);
OnGridObjectChanged += (object sender, OnGridObjectChangedEventArgs eventArgs) =>
{
debugTextArray[eventArgs.x, eventArgs.z].text = gridArray[eventArgs.x, eventArgs.z]?.ToString();
};
}
#endregion
}
public int GetWidth()
{
return width;
}
public int GetHeight()
{
return height;
}
public float GetCellSize()
{
return cellSize;
}
public Vector3 GetWorldPosition(int x, int z)
{
return new Vector3(x, 0, z) * cellSize + originPosition;
}
public void GetXZ(Vector3 worldPosition, out int x, out int z)
{
x = Mathf.FloorToInt((worldPosition - originPosition).x / cellSize);
z = Mathf.FloorToInt((worldPosition - originPosition).z / cellSize);
}
public void SetGridObject(int x, int z, TGridObject value)
{
if (x >= 0 && z >= 0 && x < width && z < height)
{
gridArray[x, z] = value;
TriggerGridObjectChanged(x, z);
}
}
public void TriggerGridObjectChanged(int x, int z)
{
OnGridObjectChanged?.Invoke(this, new OnGridObjectChangedEventArgs { x = x, z = z });
}
public void SetGridObject(Vector3 worldPosition, TGridObject value)
{
GetXZ(worldPosition, out int x, out int z);
SetGridObject(x, z, value);
}
public TGridObject GetGridObject(int x, int z)
{
if (x >= 0 && z >= 0 && x < width && z < height)
{
return gridArray[x, z];
}
else
{
return default(TGridObject);
}
}
public TGridObject GetGridObject(Vector3 worldPosition)
{
int x, z;
GetXZ(worldPosition, out x, out z);
return GetGridObject(x, z);
}
public Vector2Int ValidateGridPosition(Vector2Int gridPosition)
{
return new Vector2Int(
Mathf.Clamp(gridPosition.x, 0, width - 1),
Mathf.Clamp(gridPosition.y, 0, height - 1)
);
}
}
设计单元格内对象GridObject
属性:对应的网格实例、坐标、单元格中可以填充的内容。
针对单元格中可以填充的内容,不同的业务,设计的类可能不同。
本篇中,单元格中是放置处理过的物体预制体,为了统一每个预制体,所有可以放入单元格的预制体会绑定一个脚本PlaceObject。
因此设计时,会加入该属性,当单元格中放入物体时,该属性会被赋值。当物体被销毁时,该属性值会被null
。
放该属性发生变化时,会触发网格类中定义的事件。
public class GridObject
{
private GridXZ<GridObject> grid;
private int x;
private int z;
private PlaceObject placeObject;
public GridObject(GridXZ<GridObject> grid, int x, int z)
{
this.grid = grid;
this.x = x;
this.z = z;
}
public PlaceObject GetPlaceObject()
{
return placeObject;
}
public void SetPlaceObject(PlaceObject placeObject)
{
this.placeObject = placeObject;
grid.TriggerGridObjectChanged(x, z);
}
public void ClearPlaceObject()
{
placeObject = null;
grid.TriggerGridObjectChanged(x, z);
}
public bool CanBuild()
{
return placeObject == null;
}
//可以用来标记单元格中的内容
public override string ToString()
{
return x + "," + z;// + "\n" + placeObject?.goodsName;
}
}
单元格中可以填充的内容类PlaceObject
将前面章节中实例化预制体的部分写入该类中。
属性:PlacedObjectTypeSO实例、物体原点坐标、方向、父物体。
可以创建物体、销毁物体;
获取物体所占的所有网格坐标;
public class PlaceObject : MonoBehaviour
{
public static PlaceObject Create(
Vector3 worldPosition, Vector2Int origin,
PlacedObjectTypeSO.Dir dir, PlacedObjectTypeSO placedObjectTypeSO, Transform parent
)
{
Transform placeObjectTransform = Instantiate(
placedObjectTypeSO.prefab,
worldPosition,
Quaternion.Euler(0, placedObjectTypeSO.GetRotationAngle(dir), 0)
);
placeObjectTransform.SetParent(parent);
placeObjectTransform.gameObject.SetActive(true);
PlaceObject placeObject = placeObjectTransform.GetComponent<PlaceObject>();
placeObject.origin = origin;
placeObject.dir = dir;
placeObject.placedObjectTypeSO = placedObjectTypeSO;
return placeObject;
}
private PlacedObjectTypeSO placedObjectTypeSO;
private Vector2Int origin;
private PlacedObjectTypeSO.Dir dir;
public void DestorySelf()
{
Destroy(gameObject);
}
public List<Vector2Int> GetGridPositionList()
{
return placedObjectTypeSO.GetGridPositionList(origin, dir);
}
public GoodsName goodsName {
get {
return placedObjectTypeSO.goodsName;
}
}
}
使用网格
修改PlaceObjectBuilding中网格相关的部分
实例化网格类
//初始状态,每个单元格中都没有物体
grid = new GridXZ<GridObject>(16, 16, 1, new Vector3(0, 0, 0), (GridXZ<GridObject> g, int x, int z) => new GridObject(g, x, z));
放置物体
传入网格坐标、方向、PlacedObjectTypeSO实例;
检查传入坐标单元格及物体需要占据的全部单元格是否已经被占用;
依次给所占的所有单元格中内容赋值;
[! WARNING] 修正物体放置的位置
鼠标选择单元格时,获取的是单元格中的任意位置,而物体放置时位置是以其Anchor为原点的。因此需要将鼠标的点击位置修正为单元格的Anchor,使物体能够刚好放入单元格中。
修正结果需要分别显现在物体放置和物体跟随鼠标两个地方。
Vector3 clickedPosition = Mouse3D.GetMouseWorldPosition();
grid.GetXZ(clickedPosition, out int x, out int z);
Vector2Int rotationOffset = placedObjectTypeSO.GetRotationOffset(dir);
Vector3 placedObjectWorldPosition = grid.GetWorldPosition(x, z) + new Vector3(rotationOffset.x, 0, rotationOffset.y) * grid.GetCellSize();
放置物体时,可以通过代码直接放置、也可以点击单元格放置。
private void ExcutePlaceObjectOnGrid(Vector2Int gridPosition, PlacedObjectTypeSO placedObjectTypeSO, Dir dir)
{
Vector2Int rotationOffset = placedObjectTypeSO.GetRotationOffset(dir);
Vector3 placedObjectWorldPosition = grid.GetWorldPosition(gridPosition.x, gridPosition.y) +
new Vector3(rotationOffset.x, 0, rotationOffset.y) * grid.GetCellSize();
//需要判断当前位置是否能够放置,是否已经被占用
List<Vector2Int> locateGridPositions = placedObjectTypeSO.GetGridPositionList(gridPosition, dir);
bool canBuild = true;
foreach (var item in locateGridPositions)
{
if (!grid.GetGridObject(item.x, item.y).CanBuild())
{
canBuild = false;
break;
}
}
if (canBuild)
{
PlaceObject placeObject = PlaceObject.Create(placedObjectWorldPosition, gridPosition, dir, placedObjectTypeSO, transform.parent);
//需要标记对应网格被占用
locateGridPositions.ForEach(_ =>
{
grid.GetGridObject(_.x, _.y).SetPlaceObject(placeObject);
});
}
}
//直接代码放置物体
ExcutePlaceObjectOnGrid(new Vector2Int(3, 10), placedObjectTypeSOList[0], Dir.Down);
//点击单元格放置物体
private void Update()
{
if (selectedPlacedObjectTypeSO != null)
{
if (Input.GetMouseButtonDown(0))
{
Vector3 placePosition = Mouse3D.GetMouseWorldPosition();
grid.GetXZ(placePosition, out int x, out int z);
Vector2Int rotationOffset = selectedPlacedObjectTypeSO.GetRotationOffset(dir);
if (Mouse3D.GetClickedTransform().parent == transform.parent)
{
ExcutePlaceObjectOnGrid(new Vector2Int(x, z), selectedPlacedObjectTypeSO, dir);
DeselectObjectType();
}
}
}
}
删除物体
删除物体也同样需要清除物体所占用的所有单元格中的物体信息。
public void DestroyPlacedObject(PlaceObject placeObject)
{
if (placeObject != null)
{
AddGoods(placeObject.goodsName);
placeObject.DestorySelf();
List<Vector2Int> gridPositionList = placeObject.GetGridPositionList();
foreach (var gridPosition in gridPositionList)
{
grid.GetGridObject(gridPosition.x, gridPosition.y).ClearPlaceObject();
}
}
}
使用删除
业务中,是玩家触碰到物体时,物体被摧毁并放入仓库,因此在Player.cs脚本中修改相关的代码。
public class Player : MonoBehaviour
{
private void OnTriggerEnter(Collider c)
{
Transform cProfab = c.transform.parent.parent;
if (Enum.TryParse(cProfab.tag, true, out GoodsName goodsName))
{
PlaceObjectBuilding.Instance.DestroyPlacedObject(cProfab.GetComponent<PlaceObject>());
}
}
}
完整代码
public class PlaceObjectBuilding : MonoBehaviour
{
private void Awake()
{
Instance = this;
inventory = new Inventory(new List<Goods>(), (goods) =>
{
inventory.DeleteGoods(goods.GetGoodsName());
selectedPlacedObjectTypeSO = placedObjectTypeSOList.Find(_ => _.nameString == goods.GetGoodsName().ToString());
RefreshSelectedObjectType();
});
ui_inventory.Init(inventory);
grid = new GridXZ<GridObject>(16, 16, 1, new Vector3(0, 0, 0), (GridXZ<GridObject> g, int x, int z) => new GridObject(g, x, z));
inventory.AddGoods(placedObjectTypeSOList[1]);
inventory.AddGoods(placedObjectTypeSOList[1]);
inventory.AddGoods(placedObjectTypeSOList[2]);
inventory.AddGoods(placedObjectTypeSOList[3]);
inventory.AddGoods(placedObjectTypeSOList[4]);
ExcutePlaceObjectOnGrid(new Vector2Int(3, 10), placedObjectTypeSOList[0], Dir.Down);
ExcutePlaceObjectOnGrid(new Vector2Int(15, 6), placedObjectTypeSOList[0], Dir.Down);
ExcutePlaceObjectOnGrid(new Vector2Int(9, 1), placedObjectTypeSOList[2], Dir.Down);
}
private void ExcutePlaceObjectOnGrid(Vector2Int gridPosition, PlacedObjectTypeSO placedObjectTypeSO, Dir dir)
{
Vector2Int rotationOffset = placedObjectTypeSO.GetRotationOffset(dir);
Vector3 placedObjectWorldPosition = grid.GetWorldPosition(gridPosition.x, gridPosition.y) +
new Vector3(rotationOffset.x, 0, rotationOffset.y) * grid.GetCellSize();
//需要判断当前位置是否能够放置,是否已经被占用
List<Vector2Int> locateGridPositions = placedObjectTypeSO.GetGridPositionList(gridPosition, dir);
bool canBuild = true;
foreach (var item in locateGridPositions)
{
if (!grid.GetGridObject(item.x, item.y).CanBuild())
{
canBuild = false;
break;
}
}
if (canBuild)
{
PlaceObject placeObject = PlaceObject.Create(placedObjectWorldPosition, gridPosition, dir, placedObjectTypeSO, transform.parent);
//需要标记对应网格被占用
locateGridPositions.ForEach(_ =>
{
grid.GetGridObject(_.x, _.y).SetPlaceObject(placeObject);
});
}
}
private void Update()
{
if (selectedPlacedObjectTypeSO != null)
{
if (Input.GetMouseButtonDown(0))
{
Vector3 placePosition = Mouse3D.GetMouseWorldPosition();
grid.GetXZ(placePosition, out int x, out int z);
Vector2Int rotationOffset = selectedPlacedObjectTypeSO.GetRotationOffset(dir);
if (Mouse3D.GetClickedTransform().parent.parent == craftTable.parent)
{
PlaceObject.Create(placePosition + new Vector3(rotationOffset.x, 0, rotationOffset.y), Vector2Int.zero, dir, selectedPlacedObjectTypeSO, craftTable);
craftingRecipeSOList.ForEach(_ =>
{
PlacedObjectTypeSO outGoodsSo = _.GoodsOnTableChanged(selectedPlacedObjectTypeSO);
if (outGoodsSo != null)
{
for (int i = 0; i < craftTable.childCount; i++)
{
Destroy(craftTable.GetChild(i).gameObject);
}
craftingRecipeSOList.ForEach(recipeSo =>
{
recipeSo.Init();
});
inventory.AddGoods(outGoodsSo);
};
});
DeselectObjectType();
}
else if (Mouse3D.GetClickedTransform().parent == transform.parent)
{
ExcutePlaceObjectOnGrid(new Vector2Int(x, z), selectedPlacedObjectTypeSO, dir);
DeselectObjectType();
}
}
if (Input.GetKeyDown(KeyCode.Alpha0))
{
GoodsName goodsName = (GoodsName)Enum.Parse(typeof(GoodsName), selectedPlacedObjectTypeSO.nameString);
inventory.AddGoods(selectedPlacedObjectTypeSO);
DeselectObjectType();
}
if (Input.GetKeyDown(KeyCode.R))
{
dir = GetNextDir(dir);
}
}
}
public Vector3 GetMouseWorldSnappedPosition()
{
Vector3 mousePosition = Mouse3D.GetMouseWorldPosition();
if (grid == null) return mousePosition;
grid.GetXZ(mousePosition, out int x, out int z);
if (selectedPlacedObjectTypeSO != null)
{
Vector2Int rotationOffset = selectedPlacedObjectTypeSO.GetRotationOffset(dir);
Vector3 placedObjectWorldPosition = grid.GetWorldPosition(x, z) + new Vector3(rotationOffset.x, 0, rotationOffset.y) * grid.GetCellSize();
return placedObjectWorldPosition;
}
else
{
return mousePosition;
}
}
public void DestroyPlacedObject(PlaceObject placeObject)
{
if (placeObject != null)
{
AddGoods(placeObject.goodsName);
placeObject.DestorySelf();
List<Vector2Int> gridPositionList = placeObject.GetGridPositionList();
foreach (var gridPosition in gridPositionList)
{
grid.GetGridObject(gridPosition.x, gridPosition.y).ClearPlaceObject();
}
}
}
}