【Unity】ScriptableObject的应用和3D物体跟随鼠标移动:鼠标放置物体在场景中
需求说明
结合前篇,仓库管理 和 获取鼠标点击的世界坐标位置 两篇内容,已经实现了:
-
鼠标或键盘控制玩家移动;
-
玩家触碰物体后,将物体放入仓库;
-
鼠标点击仓库栏中的物体,任意放在空间中的功能。
接下来想要实现:
-
鼠标点击仓库栏的物体之后,物体会跟随鼠标移动;
-
键盘控制待放置的物体旋转;
-
当鼠标点击空间中的位置时,物体被放置在该位置。
实现该部分的内容主要需要技术点
-
使用
ScriptableObject
类,定义资源; -
鼠标跟随3D物体;
-
利用层级,使不同层级的物体不发生触碰;
成果展示
Scene部分
利用层级,使不同层级的物体不发生触碰
避免鼠标跟随的待放置物体与玩家发生碰撞关系,进行如下设置:
新建层级(Layer
), 11层 为 Ghost
, 任意层位 Player
将BuildingGhost
物体的层级设为 Ghost
,将 玩家的层级 设为 Player
然后在窗口 Edit -> project Setting -> Physics
将 层级 Ghost
和Player
的关系取消勾选。
利用Raycast,确定鼠标点击的世界坐标位置
由于该方法是获取鼠标点击3D物体位置,如果想要让仓库栏的物体放在指定位置,则需要创建Plane地板物体,用于定位鼠标点击的世界坐标位置,然后将物体放在地板上。
利用鼠标的世界坐标位置,让物体跟随鼠标移动
创建空物体 BuildingGhost
, 并绑定脚本 BuildingGhost.cs
新建物体实例,然后不断获取鼠标位置,让物体跟随鼠标移动。
跟随鼠标移动的物体层级都设为 11层 Ghost
。
处理预制体,并拖拽若干预制体到场景中
准备了三种可以放置的物体。
为了让物体能够放置在地板上,而非悬空或穿模,需要让物体的预制体中心Y轴与地板表面重合。
同时下一章节将设计网格系统,帮助更多物体的放置,并且使所有物体的状态都数据化。因此需要模板化处理可放置物体的预制体,这里由于网格系统尚未开发,默认单元格宽度为1*1。
物体所占有网格的面积;
物体的边角位置,将该边角作为预制体的坐标中心;
边角Anchor
与面积Area
部分在物体准备放置时显示帮助定位,物体放置之后隐藏;
模型相关的内容单独放在一个空物体下。
ScriptableObject配置可放置物体类型
设计ScriptableObject
类,用于配置可放置的物体,包含其名称、预制体和单元格大小等;
给予可放置物体可旋转的属性;
其中,物体旋转之后 物体的中心坐标和世界坐标会发生偏差,为了物体跟随鼠标的视觉效果保持始终鼠标在物体的左下方,物体的位置在旋转后需要一定的偏移。
为所有可放置的物体新建为ScriptableObject
类实例,并进行配置。
这里说明:配置ScriptableObject
类实例,并非一定同本文一样在Asset中进行配置,也可以直接在脚本中 New。
存放所有可放置物体类型,并管理物体的选择、旋转、放置和取消
创建空物体 PlaceObjectBuilding
, 并绑定脚本 PlaceObjectBuilding.cs
将所有配置好的ScriptableObject
实例放入脚本中,用于后期的选择、旋转、放置和取消。
脚本部分
ScriptableObject类设计
[CreateAssetMenu()]
public class PlacedObjectTypeSO : ScriptableObject
{
//改变方向
public static Dir GetNextDir(Dir dir)
{
switch (dir)
{
default:
case Dir.Down: return Dir.Left;
case Dir.Left: return Dir.Up;
case Dir.Up: return Dir.Right;
case Dir.Right: return Dir.Down;
}
}
public enum Dir
{
Down,
Left,
Up,
Right,
}
public string nameString;
public Transform prefab;
public int width;
public int height;
public int GetRotationAngle(Dir dir)
{
switch (dir)
{
default:
case Dir.Down: return 0;
case Dir.Left: return 90;
case Dir.Up: return 180;
case Dir.Right: return 270;
}
}
public Vector2Int GetRotationOffset(Dir dir)
{
switch (dir)
{
default:
case Dir.Down: return new Vector2Int(0, 0);
case Dir.Left: return new Vector2Int(0, width);
case Dir.Up: return new Vector2Int(width, height);
case Dir.Right: return new Vector2Int(height, 0);
}
}
}
管理放置物体类的设计
这里同时也会用于管理仓库。
public class PlaceObjectBuilding : MonoBehaviour
{
public static PlaceObjectBuilding Instance;
Inventory inventory;
[SerializeField] UI_Inventory ui_inventory;
[SerializeField] List<PlacedObjectTypeSO> placedObjectTypeSOList;
PlacedObjectTypeSO selectedPlacedObjectTypeSO;
public event EventHandler OnSelectedChanged;
private Dir dir = Dir.Down;
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);
}
private void Update()
{
if (selectedPlacedObjectTypeSO != null)
{
//鼠标左击 放置物体
if (Input.GetMouseButtonDown(0))
{
Vector3 placePosition = Mouse3D.GetMouseWorldPosition();
placePosition.y = 0;
Transform placeObjectTransform = Instantiate(
selectedPlacedObjectTypeSO.prefab,
placePosition,
Quaternion.Euler(0, selectedPlacedObjectTypeSO.GetRotationAngle(dir), 0)
);
placeObjectTransform.SetParent(transform.parent);
DeselectObjectType();
}
//键盘0 取消选择的物体,并放回仓库
if (Input.GetKeyDown(KeyCode.Alpha0))
{
GoodsName goodsName = (GoodsName)Enum.Parse(typeof(GoodsName), selectedPlacedObjectTypeSO.nameString);
inventory.AddGoods(goodsName);
DeselectObjectType();
}
//键盘R 选择待放置的物体
if (Input.GetKeyDown(KeyCode.R))
{
dir = GetNextDir(dir);
}
}
}
public void AddGoods(GoodsName goodsName) {
inventory.AddGoods(goodsName);
}
//取消选择物体
private void DeselectObjectType()
{
selectedPlacedObjectTypeSO = null;
RefreshSelectedObjectType();
}
//选中的物体发生了变化,会触发鼠标跟随的物体也发生改变
private void RefreshSelectedObjectType()
{
OnSelectedChanged?.Invoke(this, EventArgs.Empty);
}
public PlacedObjectTypeSO GetPlacedObjectTypeSO()
{
return selectedPlacedObjectTypeSO;
}
//获取鼠标的位置
public Vector3 GetMouseWorldSnappedPosition()
{
Vector3 mousePosition = Mouse3D.GetMouseWorldPosition();
if (selectedPlacedObjectTypeSO != null)
{
//旋转后,物体的位置会发生偏移
Vector2Int rotationOffset = selectedPlacedObjectTypeSO.GetRotationOffset(dir);
Vector3 placedObjectWorldPosition = mousePosition + new Vector3(rotationOffset.x, 0, rotationOffset.y);
return placedObjectWorldPosition;
}
else
{
return mousePosition;
}
}
//物体旋转了,放置的物体也需要旋转
public Quaternion GetPlacedObjectRotation()
{
if (selectedPlacedObjectTypeSO != null)
{
return Quaternion.Euler(0, selectedPlacedObjectTypeSO.GetRotationAngle(dir), 0);
}
else
{
return Quaternion.identity;
}
}
}
物体跟随鼠标移动
public class BuildingGhost : MonoBehaviour
{
private Transform profab;
private PlacedObjectTypeSO placedObjectTypeSO;
private void Start()
{
RefreshVisual();
PlaceObjectBuilding.Instance.OnSelectedChanged += Instance_OnSelectedChanged;
}
private void Instance_OnSelectedChanged(object sender, System.EventArgs e)
{
RefreshVisual();
}
//不断更新位置
private void LateUpdate()
{
if (placedObjectTypeSO == null) return;
Vector3 targetPosition = PlaceObjectBuilding.Instance.GetMouseWorldSnappedPosition();
targetPosition.y = 0.3f;
transform.position = Vector3.Lerp(transform.position, targetPosition, Time.deltaTime * 15f);
transform.rotation = Quaternion.Lerp(transform.rotation, PlaceObjectBuilding.Instance.GetPlacedObjectRotation(), Time.deltaTime * 15f);
}
//更新跟随的物体实例
private void RefreshVisual()
{
if (profab != null)
{
Destroy(profab.gameObject);
profab = null;
}
placedObjectTypeSO = PlaceObjectBuilding.Instance.GetPlacedObjectTypeSO();
if (placedObjectTypeSO != null)
{
profab = Instantiate(placedObjectTypeSO.prefab, Vector3.zero, Quaternion.identity);
DisplayAreaAnchor(profab.Find("locate"), true);
profab.parent = transform;
profab.localPosition = Vector3.zero;
profab.localEulerAngles = Vector3.zero;
SetLayerRecursive(profab.gameObject, 11);
}
}
//隐藏定位辅助部分
private void DisplayAreaAnchor(Transform locate, bool isVisible)
{
locate.gameObject.SetActive(isVisible);
}
//设置层级
private void SetLayerRecursive(GameObject targetGameObject, int layer)
{
targetGameObject.layer = layer;
foreach (Transform child in targetGameObject.transform)
{
SetLayerRecursive(child.gameObject, layer);
}
}
}
玩家的控制
大多数内容与前篇相同,但是将仓库类inventory操作的部分放到了搭建脚本PlaceObjectBuilding中。
public class Player : MonoBehaviour
{
[SerializeField] private float moveSpeed = 20f;
private void OnTriggerEnter(Collider c)
{
Transform cProfab = c.transform.parent.parent;
if (Enum.TryParse(cProfab.tag, true, out GoodsName goodsName))
{
//PlaceObjectBuilding单例中操作仓库
PlaceObjectBuilding.Instance.AddGoods(goodsName);
Destroy(cProfab.gameObject);
}
}
private void Update()
{
Vector3 inputDir = new Vector3(0, 0, 0);
if (Input.GetKey(KeyCode.UpArrow)) inputDir.z += +1f;
if (Input.GetKey(KeyCode.DownArrow)) inputDir.z += -1f;
if (Input.GetKey(KeyCode.LeftArrow)) inputDir.x += -1f;
if (Input.GetKey(KeyCode.RightArrow)) inputDir.x += +1f;
Vector3 moveDir = transform.forward * inputDir.z + transform.right * inputDir.x;
transform.position += moveDir * moveSpeed * Time.deltaTime;
if (Input.GetMouseButtonUp(1))
{
Debug.Log(Mouse3D.GetMouseWorldPosition());
Transform t = Mouse3D.GetClickedTransform();
if (t != null)
{
Vector3 targetPosition = t.position;
targetPosition.y = transform.position.y;
transform.DOMove(targetPosition, 2);
}
else
{
Debug.Log("null");
}
}
}
}
其他的,仓库类、物品类没有发生改变,修改的只有物品的种类。