Unity 实现一个可拓展的简单物品交互系统

发布于:2025-03-11 ⋅ 阅读:(23) ⋅ 点赞:(0)

         脚本由本人亲手所写,部分内容用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 之间,默认为 3
  • useConfig:是否通过配置表设置文字信息
  • info:用于显示对象信息的 TextMeshProUGUI 组件
  • player:玩家的 Transform 组件
方法
  • Start():在对象开始时调用,初始化 info 和 player
  • ShowInfo():显示对象信息,将信息文本框朝向玩家,并设置文本内容和透明度
  • HideInfo():隐藏对象信息,将信息文本框的透明度设置为 0
  • GetInfoResouce():根据 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 originalParenthandPos 和 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():在每一帧调用,根据检测频率更新交互检测和 UI
  • UpdateInteraction():更新交互检测,使用球形射线检测可交互对象
  • CheckDistanceValid():检查距离是否有效,根据 useSqrDistance 的值选择不同的计算方法
  • UpdateUI():更新 UI,处理交互按键事件,显示或隐藏可交互对象的信息,并设置光标的高亮状态