脚本由本人亲手所写,部分内容用ai的辅助 比如剔除无用组件,所以可以很容易看出来这个所谓的交互系统的稚嫩与不足
不过经我验证,可以做到只需继承基类就可以写出新的交互逻辑
比如:
取走和放置物品
钥匙门,普通门和互锁门的实现
以及最最基本的信息显示
视频演示:
Unity花了一个多小时做了一个可拓展的简单物品交互系统
因为过于简单 思路也比较顺畅所以我就提供一下放置和捡起物品的实现思路 :
下面是参数说明,我用ai来解释吧 对于这种事情 还是ai做的比较好
1. InteractableObjectBase
类
这个类是一个基类,用于定义可交互对象的基本属性和行为
using TMPro;
using UnityEngine;
using UnityEngine.UI;
[System.Serializable]
public class InteractableObjectBase : MonoBehaviour
{
[Header("交互设置")]
[Tooltip("显示名称")]
public string objectInfo;
[Tooltip("可交互状态")]
public bool isInteractable = true;
[Tooltip("有效交互距离")]
[Range(0, 5)] public float checkDistance = 3f;
[Tooltip("是否通过配置表设置文字")]
public bool useConfig = false;
public TextMeshProUGUI info;
public Transform player;
protected virtual void Start() {
info = transform.Find("Canvas").GetComponentInChildren<TextMeshProUGUI>();
player = FindFirstObjectByType<InteractableSystem>().transform;
}
public virtual void ShowInfo()
{
info.rectTransform.LookAt(player);
info.text = objectInfo;
info.alpha = 1;
}
public virtual void HideInfo()
{
info.alpha = 0;
}
public void GetInfoResouce() {
if (useConfig == false)
{
objectInfo = this.gameObject.name;
}
else {
//请在这里填写内容
}
}
public virtual void Interact() { }
#if UNITY_EDITOR
#endif
}
属性
objectInfo
:可交互对象的显示名称isInteractable
:表示该对象是否可交互,默认为true
checkDistance
:有效交互距离,范围在 0 到 5 之间,默认为 3useConfig
:是否通过配置表设置文字信息info
:用于显示对象信息的TextMeshProUGUI
组件player
:玩家的Transform
组件
方法
Start()
:在对象开始时调用,初始化info
和player
ShowInfo()
:显示对象信息,将信息文本框朝向玩家,并设置文本内容和透明度HideInfo()
:隐藏对象信息,将信息文本框的透明度设置为 0GetInfoResouce()
:根据useConfig
的值设置objectInfo
Interact()
:虚方法,用于定义交互行为,具体实现由子类完成
2. InteractPickAndSet
类
这个类继承自 InteractableObjectBase
,用于实现物品的捡起和放置功能
using TMPro;
using UnityEngine;
public class InteractPickAndSet : InteractableObjectBase
{
[Header("是否可以捡起")]
public bool canPickup;
[Header("手持设置")]
[SerializeField] private Vector3 holdScale = new Vector3(0.3f, 0.3f, 0.3f);
[Header("拿起后的位置和缩放大小")]
[SerializeField] private Transform handPos;
[SerializeField] private Vector3 scale = new Vector3(0.3f, 0.3f, 0.3f);
public bool isHolding;
[Header("放置设置")]
[SerializeField] private LayerMask placementMask; // 可放置表面层级(在Inspector中设置)
[SerializeField] private Material previewMaterial; // 半透明预览材质
[SerializeField] private float maxPlaceDistance = 3f;
private GameObject go;
private Vector3 originalScale;
private Transform originalParent;
private Collider objCollider;
private float originalY;
protected override void Start()
{
base.Start();
originalScale = transform.localScale;
originalParent = transform.parent;
if (handPos == null)
handPos = player.transform.Find("Hand");
objCollider = GetComponentInChildren<Collider>();
}
public override void Interact()
{
if (!isHolding)
PickUp();
else
TryPlace();
}
private void PickUp()
{
if (!canPickup || handPos.childCount > 0) return;
// 保存原始状态
originalParent = transform.parent;
originalScale = transform.localScale;
originalY = transform.position.y; // 保存原始的y坐标
// 移动到手上
transform.SetParent(handPos);
transform.localPosition = Vector3.zero;
transform.localRotation = Quaternion.identity;
transform.localScale = holdScale;
isHolding = true;
objCollider.enabled = false;
}
private void TryPlace()
{
if (go == null) return;
// 执行放置
transform.SetParent(originalParent);
Vector3 newPosition = go.transform.position;
newPosition.y = originalY;
transform.position = newPosition;
transform.rotation = go.transform.rotation;
transform.localScale = originalScale;
objCollider.enabled = true;
Destroy(go);
isHolding = false;
}
void Update()
{
if (!isHolding) return;
UpdateSetPreview();
if (Input.GetKeyDown(KeyCode.Q))
{
CancelHold();
}
}
private void UpdateSetPreview()
{
Ray ray = Camera.main.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0));
if (Physics.Raycast(ray, out RaycastHit hit, maxPlaceDistance, placementMask))
{
if (go == null)
{
CreatePreview();
}
go.transform.position = hit.point;
go.transform.rotation = Quaternion.FromToRotation(Vector3.up, hit.normal);
}
else
{
DestroyHoldObj();
}
}
private void CreatePreview()
{
go = Instantiate(gameObject);
go.transform.SetParent(null);
// 恢复原始缩放
go.transform.localScale = originalScale;
// 禁用所有不需要的组件
foreach (var comp in go.GetComponents<Component>())
{
if (comp is Transform || comp is MeshFilter || comp is MeshRenderer)
continue;
Destroy(comp);
}
// 设置半透明材质
var renderer = go.GetComponentInChildren<Renderer>();
renderer.material = previewMaterial;
objCollider.enabled = false;
}
private void DestroyHoldObj()
{
if (go != null)
{
Destroy(go);
go = null;
}
}
private void CancelHold()
{
transform.SetParent(originalParent);
Vector3 newPosition = Vector3.zero;
newPosition.y = originalY;
transform.localPosition = newPosition;
transform.localScale = originalScale;
objCollider.enabled = true;
DestroyHoldObj();
isHolding = false;
}
public override void ShowInfo()
{
if (!isHolding) base.ShowInfo();
}
void OnDestroy()
{
DestroyHoldObj();
}
}
属性
canPickup
:表示该物品是否可以被捡起holdScale
:物品被拿起后的缩放大小handPos
:物品被拿起后放置的位置scale
:物品的缩放大小isHolding
:表示物品是否正在被持有placementMask
:可放置表面的层级previewMaterial
:半透明预览材质maxPlaceDistance
:最大放置距离go
:预览对象。originalScale
:物品的原始缩放大小originalParent
:物品的原始父对象objCollider
:物品的碰撞器originalY
:物品的原始 Y 坐标
方法
Start()
:在对象开始时调用,初始化originalScale
originalParent
handPos
和objCollider
Interact()
:重写父类的Interact
方法,根据isHolding
的值决定是捡起还是放置物品。PickUp()
:捡起物品,将物品移动到手上,并保存原始状态TryPlace()
:尝试放置物品,将物品放置到预览对象的位置,并恢复原始状态Update()
:在每一帧调用,更新放置预览,并处理取消持有操作UpdateSetPreview()
:更新放置预览,根据射线检测结果创建或销毁预览对象CreatePreview()
:创建预览对象,设置其缩放大小和材质DestroyHoldObj()
:销毁预览对象CancelHold()
:取消持有物品,将物品放回原始位置,并恢复原始状态ShowInfo()
:重写父类的ShowInfo
方法,只有在物品未被持有时才显示信息OnDestroy()
:在对象销毁时调用,销毁预览对象
3. InteractableSystem
类
这个类用于管理可交互对象的检测和交互
using UnityEngine;
using TMPro;
public class InteractableSystem : MonoBehaviour
{
#region Debug查看
// 当前持有的可交互物体
[SerializeField] private InteractableObjectBase currentObj;
// 上一个持有的可交互物体
[SerializeField] private InteractableObjectBase lastObj;
[SerializeField] private InteractableObjectBase heldObject;
#endregion
[SerializeField] private UICursor cursor;
[SerializeField] private Camera playerCamera;
[Header("检测设置")]
[Tooltip("检测距离(米)")]
public float playerCheckDistance = 5f;
[Tooltip("射线半径")]
[Range(0, 0.5f)] public float sphereRadius = 0.1f;
[Header("性能优化")]
[Tooltip("检测频率(秒)")]
public float checkInterval = 0.1f;
[Tooltip("启用检测距离优化")]
public bool useSqrDistance = true;
private float lastCheckTime;
[Header("交互层级")]
private LayerMask interactableLayer;
public void Start()
{
interactableLayer = LayerMask.GetMask("Interactable");
}
void Update()
{
if (Time.time - lastCheckTime >= checkInterval)
{
UpdateInteraction();
lastCheckTime = Time.time;
}
UpdateUI();
}
/// <summary>
/// 更新检测 本质就是球形检测
/// </summary>
private void UpdateInteraction()
{
currentObj = null;
Ray ray = playerCamera.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0));
if (Physics.SphereCast(ray, sphereRadius, out RaycastHit hit, playerCheckDistance, interactableLayer))
{
// 从碰撞器所在物体的父级获取 InteractableObject 组件
InteractableObjectBase obj = hit.collider.GetComponentInParent<InteractableObjectBase>();
if (obj != null && obj.isInteractable)
{
if (CheckDistanceValid(hit.point, obj.checkDistance))
{
currentObj = obj;
}
}
}
}
/// <summary>
/// 优化距离
/// </summary>
/// <param name="targetPos"></param>
/// <param name="checkDistance"></param>
/// <returns></returns>
private bool CheckDistanceValid(Vector3 targetPos, float checkDistance)
{
if (useSqrDistance)
{
float sqrDist = (transform.position - targetPos).sqrMagnitude;
return sqrDist <= checkDistance * checkDistance;
}
return Vector3.Distance(transform.position, targetPos) <= checkDistance;
}
private void UpdateUI()
{
#region 这里添加检测交互按键
if (Input.GetKeyDown(KeyCode.E))
{
InteractableObjectBase targetObj = heldObject != null ? heldObject : currentObj;
if (targetObj != null)
{
targetObj.Interact(); // 触发交互行为
if (targetObj is InteractPickAndSet pickAndSet)
{
if (pickAndSet.isHolding)
{
heldObject = targetObj; // 拿起物品
}
else
{
heldObject = null; // 放置物品
}
}
}
}
#endregion
if (currentObj != null)
{
Vector3 screenPos = playerCamera.WorldToScreenPoint(currentObj.transform.position);
if (screenPos.z > 0)//不重叠显示
{
currentObj.ShowInfo();
cursor.SetHighlight(true);
// 如果当前物体和上一个物体不同,隐藏上一个物体的信息
if (lastObj != null && lastObj != currentObj)
{
lastObj.HideInfo();
}
lastObj = currentObj;
return;
}
}
// 如果当前没有检测到可交互物体,隐藏上一个物体的信息
if (lastObj != null)
{
lastObj.HideInfo();
lastObj = null;
}
cursor.SetHighlight(false);
}
}
属性
currentObj
:当前检测到的可交互对象lastObj
:上一个检测到的可交互对象heldObject
:当前持有的可交互对象cursor
:UI 光标playerCamera
:玩家的相机playerCheckDistance
:检测距离sphereRadius
:射线半径checkInterval
:检测频率useSqrDistance
:是否启用检测距离优化lastCheckTime
:上次检测的时间interactableLayer
:可交互对象的层级
方法
Start()
:在对象开始时调用,初始化interactableLayer
Update()
:在每一帧调用,根据检测频率更新交互检测和 UIUpdateInteraction()
:更新交互检测,使用球形射线检测可交互对象CheckDistanceValid()
:检查距离是否有效,根据useSqrDistance
的值选择不同的计算方法UpdateUI()
:更新 UI,处理交互按键事件,显示或隐藏可交互对象的信息,并设置光标的高亮状态