一、需求
常见虚拟搭建的步骤:
- 1、在模型库中点击对应的模型图片
- 2、光标变成扳手
- 3、光标第一次进入地面后,对应3D物体出现在地面上,并跟随光标在地面上移动
- 4、鼠标单击物体的时候,物体停止移动并固定下来
- 5、完成搭建
二、关键点
- 1、光标变成扳手状
- 2、光标拖动3D物体在某3D物体平面上移动
三、光标变成扳手状——扳手图标在屏幕上跟随光标移动
光标变成扳手状,代表即将有建筑物需要安装
1、代码
using UnityEngine;
using System.Linq;
using static txlib;
/// <summary>
/// 图片在屏幕上的鼠标跟随
/// </summary>
public class DragTools : MonoBehaviour
{
/// <summary>
/// 扳手图片:需要鼠标跟随
/// 多处有图片,所以用数组
/// </summary>
[Header("扳手图片")]
[SerializeField]
public GameObject[] images;
/// <summary>
/// 图片跟随鼠标——开关
/// true:跟随
/// false:不跟随
/// </summary>
public static bool dragOn;
// Update is called once per frame
void Update()
{
if (dragOn)
{
images.ToList().ForEach(img => Drag2dObjectViaMouse(img));
}
}
#if UNITY_EDITOR
[ContextMenu("设置拖动开关的值")]
#endif
void Test5()
{
dragOn = !dragOn;
}
}
2、脚本配置
四、光标拖动3D物体在某3D物体平面上移动——虚拟搭建
1、常见虚拟搭建的步骤
(1)在模型库中点击对应的模型图片
(2)光标变成扳手
(3)光标第一次进入地面后,对应3D物体出现,并跟随光标在地面上移动
(4)鼠标单击物体的时候,物体停止移动并固定下来
(5)完成搭建
2、如何把【光标】在【地面上】的当前位置获取
从当前相机发射一条射线,穿过鼠标位置,与场景中的物体交汇,获取交汇的点,这是一个世界坐标,然后把物体摆放到该点。
核心代码:
Ray camRay = mainCamera.ScreenPointToRay(Input.mousePosition);
RaycastHit hitInfo;
bool isHit = Physics.Raycast(camRay, out hitInfo);
if (isHit)
{
Debug.Log($"交汇的物体:{hitInfo.transform.gameObject.name}");
Debug.Log($"交汇的坐标点:{hitInfo.point}");
}
示例代码:
// Update is called once per frame
void Update()
{
if ((isOn == true) && (obj != null))
{
Ray camRay = mainCamera.ScreenPointToRay(Input.mousePosition);
RaycastHit hitInfo;
bool isHit = Physics.Raycast(camRay, out hitInfo);
if (isHit)
{
colliders = placeType switch
{
1=> defaultColliders, //默认地面搭建
2 => m_grounds, //指定平台搭建
_ => throw new Exception("placeType:错误的参数类型"),
};
if (colliders.Contains(hitInfo.transform.gameObject.name))
{
Debug.Log($"交汇的物体:{hitInfo.transform.gameObject.name}");
Debug.Log($"交汇的坐标点:{hitInfo.point}");
obj.transform.position = new Vector3(hitInfo.point.x, obj.transform.position.y, hitInfo.point.z);//在xz平面上移动
obj.SetActive(true);
obj.GetComponentsInChildren<Transform>(true).ToList().ForEach(x => x.gameObject.SetActive(true)); //子物体也显示
hadIntersected = true;
}
}
}
}
3、一次搭建的逻辑
一次虚拟搭建的逻辑:
【1】打开[3D物体随光标在地面移动的开关],参数初始化(拖拽的物体设置,update打开...)
【2】光标移动
【3】等待光标第一次碰到地面的时候,建筑物出现,然后跟随光标在地面移动
【4】等待鼠标左键单击时,建筑安装到位,停止不动 TODO:单击指定地点才安装,比如【桥】要装在【水面】上,【房子】要装在【地面】上
【5】关闭脚本,参数更新
定义一个供外部调用的委托变量
/// <summary>
/// 放置物体【鼠标巡游放置】
/// 参数:
/// 1、要放置的物体
/// 2、要显示的物体(物体自己和物体的root)
/// 3、搭建在那个平台上(桥在【水面】,商铺在【铺装地面】)
/// 外部调用的入口,在Start()里面初始化
/// </summary>
public static Func<GameObject, GameObject[],string[], UniTask> PutObjAsync2;
委托的绑定——虚拟搭建的异步实现
//【随意位置安装】事件绑定
PutObjAsync2 = async (myObj, objs,grounds) =>
{
//【1】打开脚本
obj = myObj;
isOn = true;
placeType = 2;
hadIntersected = false;
m_grounds = grounds.ToList();
//【2】鼠标腾挪物体...ing
await UniTask.WaitUntil(() => hadIntersected == true);
objs.ToList().ForEach(x => x.SetActive(true));
//【3】等待鼠标左键单击事件
await UniTask.WaitUntil(() => Input.GetMouseButtonDown(0));//鼠标左键按下,代表摆放该物体。TODO:合理的逻辑应该是,鼠标点击地面才能摆放建筑
//【4】关闭脚本
isOn = false;
obj = null;
hadIntersected = false;
//Debug.Log($" {myObj.name} PutObjAsync()安装完成!");
};
4、虚拟搭建的代码
using Cysharp.Threading.Tasks;
using System;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using HighlightingSystem;
using UnityEngine.EventSystems;
using static txlib;
/// <summary>
/// 功能:获取鼠标与【物体】相交会的位置,然后在该位置摆放一些模型。
/// 【物体】说明——物体特指能用来摆放其它模型的地方,比如建筑物的地面广场,工厂的工作台......
/// 原理:从当前相机发射一条射线,穿过鼠标位置,与场景中的物体交汇,获取交汇的点,这是一个世界坐标,然后把物体摆放到该点。
/// 备注:摆放的时候,Y值保持不变,只在xz平面上移动
/// TODO:如果物体与【指定的物体】发生碰撞,则不能摆放,不碰撞时,才能够摆放。与【指定的物体】发生碰撞的意思——比如已经有一个房子在那里了,你却又摆了另一个房子上去,视为不合适的行为。
/// </summary>
public class MouseIntersectObject : MonoBehaviour
{
/********************************************
常见虚拟搭建的步骤:
1、在模型库中点击对应的模型图片
2、光标变成扳手
3、光标第一次进入地面后,对应3D物体出现,并跟随光标在地面上移动
4、鼠标单击物体的时候,物体停止移动并固定下来
5、完成搭建
关键点:
1、光标变成扳手
2、光标拖动3D物体在某3D物体平面上移动
一次虚拟搭建的逻辑:
【1】打开[3D物体随光标在地面移动的开关],参数初始化(拖拽的物体设置,update打开...)
【2】光标移动
【3】等待光标第一次碰到地面的时候,建筑物出现,然后跟随光标在地面移动
【4】等待鼠标左键单击时,建筑安装到位,停止不动 TODO:单击指定地点才安装,比如【桥】要装在【水面】上,【房子】要装在【地面】上
【5】关闭脚本,参数更新
********************************************/
/// <summary>
/// 要移动的物体
/// </summary>
[Header("要移动的物体")]
public GameObject obj;
/// <summary>
/// 主相机[射线发射的相机]
/// </summary>
[Header("主相机[射线发射的相机]")]
public Camera mainCamera;
/// <summary>
/// 脚本的功能开关,控制Update的逻辑,防止空刷
/// </summary>
public bool isOn;
/// <summary>
/// 光标是否与可摆放的位置发生碰撞
/// 建筑摆放的时候,不能让它提前显示出来,而是应该等到光标和摆放平台第一次Intersected时,才把建筑显示出来
/// </summary>
public static bool hadIntersected;
/// <summary>
/// 放置物体【鼠标巡游放置】
/// 参数:
/// 1、要放置的物体
/// 2、要显示的物体(物体自己和物体的root)
/// 外部调用的入口,在Start()里面初始化
/// </summary>
public static Func<GameObject,GameObject[], UniTask> PutObjAsync;
/// <summary>
/// 放置物体【鼠标巡游放置】
/// 参数:
/// 1、要放置的物体
/// 2、要显示的物体(物体自己和物体的root)
/// 3、搭建在那个平台上(桥在【水面】,商铺在【铺装地面】)
/// 外部调用的入口,在Start()里面初始化
/// </summary>
public static Func<GameObject, GameObject[],string[], UniTask> PutObjAsync2;
/// <summary>
/// 放置物体【不需要巡游】
/// 参数:
/// 1、要放置的物体
/// 2、要显示的物体(物体自己和物体的root)
/// 外部调用的入口,在Start()里面初始化
/// </summary>
public static Func<GameObject, GameObject[], UniTask> PutObjWithOutMouseNavegateAsync;
/// <summary>
/// 默认的搭建平台
/// </summary>
[Header("默认的搭建平台")]
public List<string> defaultColliders = new List<string>();
/// <summary>
/// 哪些是地面的碰撞体
/// </summary>
private List<string> colliders = new List<string>();
/// <summary>
/// 单例脚本,本脚本只能挂一处
/// </summary>
private static bool attached;
/// <summary>
/// 被点击的物体
/// </summary>
private static string clickedObj;
/// <summary>
/// 可以摆放建筑物的平台
/// </summary>
private List<string> m_grounds;
/// <summary>
/// 搭建的种类:对应1 = PutObjAsync(),2 = PutObjAsync2()
/// </summary>
private int placeType;
private void Awake()
{
attached = true;
if (attached)
{
Debug.Log($"脚本MouseIntersectObject只能挂载一次!! from {this.gameObject.name}");
}
}
private void Start()
{
#region ============事件绑定============begin
//【随意位置安装】事件绑定
PutObjAsync = async (myObj,objs) =>
{
//【1】打开脚本
obj = myObj;
isOn = true;
placeType = 1;
hadIntersected = false;
//【2】鼠标腾挪物体...ing
await UniTask.WaitUntil(() => hadIntersected == true);
objs.ToList().ForEach(x=>x.SetActive(true));
//【3】等待鼠标左键单击事件
await UniTask.WaitUntil(()=>Input.GetMouseButtonDown(0));//鼠标左键按下,代表摆放该物体。TODO:合理的逻辑应该是,鼠标点击地面才能摆放建筑
//【4】关闭脚本
isOn = false;
obj = null;
hadIntersected = false;
//Debug.Log($" {myObj.name} PutObjAsync()安装完成!");
};
//【随意位置安装】事件绑定
PutObjAsync2 = async (myObj, objs,grounds) =>
{
//【1】打开脚本
obj = myObj;
isOn = true;
placeType = 2;
hadIntersected = false;
m_grounds = grounds.ToList();
//【2】鼠标腾挪物体...ing
await UniTask.WaitUntil(() => hadIntersected == true);
objs.ToList().ForEach(x => x.SetActive(true));
//【3】等待鼠标左键单击事件
await UniTask.WaitUntil(() => Input.GetMouseButtonDown(0));//鼠标左键按下,代表摆放该物体。TODO:合理的逻辑应该是,鼠标点击地面才能摆放建筑
//【4】关闭脚本
isOn = false;
obj = null;
hadIntersected = false;
//Debug.Log($" {myObj.name} PutObjAsync()安装完成!");
};
//【固定位置安装】事件绑定
PutObjWithOutMouseNavegateAsync = async (myObj, objs) =>
{
await UniTask.DelayFrame(1);
//物体显示
objs.ToList().ForEach(x =>
{
x.SetActive(true);
});
//物体高亮开
objs.ToList().ForEach(async (x) =>
{
if (x.GetComponent<Highlighter>() == null)
{
x.AddComponent<Highlighter>();
//await UniTask.DelayFrame(1); //等待脚本初始化
}
Debug.Log($"点亮物体{x.name}");
x.GetComponent<Highlighter>().tween = true;
x.GetComponent<Highlighter>().overlay = true;
});
//等待click该物体
var clicked = false;
if (myObj.GetComponent<EventTrigger>() == null) //组件添加——防止为空
{
myObj.AddComponent<EventTrigger>();
//await UniTask.DelayFrame(1); //等待脚本初始化
}
myObj.GetComponent<EventTrigger>().AddListener(EventTriggerType.PointerDown, (PointerEventData eventData) =>
{
clicked = true;
});
await UniTask.WaitUntil(() => clicked == true);
//物体高亮关
objs.ToList().ForEach(x =>
{
x.GetComponent<Highlighter>().tween = false;
});
Debug.Log($"物体 {myObj.name} 摆放结束");
};
#endregion ============事件绑定============end
}
// Update is called once per frame
void Update()
{
if ((isOn == true) && (obj != null))
{
Ray camRay = mainCamera.ScreenPointToRay(Input.mousePosition);
RaycastHit hitInfo;
bool isHit = Physics.Raycast(camRay, out hitInfo);
if (isHit)
{
colliders = placeType switch
{
1=> defaultColliders, //默认地面搭建
2 => m_grounds, //指定平台搭建
_ => throw new Exception("placeType:错误的参数类型"),
};
if (colliders.Contains(hitInfo.transform.gameObject.name))
{
Debug.Log($"交汇的物体:{hitInfo.transform.gameObject.name}");
Debug.Log($"交汇的坐标点:{hitInfo.point}");
obj.transform.position = new Vector3(hitInfo.point.x, obj.transform.position.y, hitInfo.point.z);//在xz平面上移动
obj.SetActive(true);
obj.GetComponentsInChildren<Transform>(true).ToList().ForEach(x => x.gameObject.SetActive(true)); //子物体也显示
hadIntersected = true;
}
}
}
}
private void OnDestroy()
{
/*
* 场景销毁时(e.g. 切换场景),置false
*/
attached = false;
}
}