首页 > 其他分享 >【Unity】ScriptableObject的应用和3D物体跟随鼠标移动:鼠标放置物体在场景中

【Unity】ScriptableObject的应用和3D物体跟随鼠标移动:鼠标放置物体在场景中

时间:2024-11-18 09:22:30浏览次数:1  
标签:return 鼠标 物体 private ScriptableObject public Dir

需求说明

结合前篇,仓库管理获取鼠标点击的世界坐标位置 两篇内容,已经实现了:

  • 鼠标或键盘控制玩家移动;

  • 玩家触碰物体后,将物体放入仓库;

  • 鼠标点击仓库栏中的物体,任意放在空间中的功能。

接下来想要实现:

  • 鼠标点击仓库栏的物体之后,物体会跟随鼠标移动;

  • 键盘控制待放置的物体旋转;

  • 当鼠标点击空间中的位置时,物体被放置在该位置。

实现该部分的内容主要需要技术点

  • 使用ScriptableObject类,定义资源;

  • 鼠标跟随3D物体;

  • 利用层级,使不同层级的物体不发生触碰;

成果展示

Scene部分

利用层级,使不同层级的物体不发生触碰

避免鼠标跟随的待放置物体与玩家发生碰撞关系,进行如下设置:

新建层级(Layer), 11层 为 Ghost, 任意层位 Player

BuildingGhost物体的层级设为 Ghost,将 玩家的层级 设为 Player

然后在窗口 Edit -> project Setting -> Physics 将 层级 GhostPlayer 的关系取消勾选。

利用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");
            }

        }

    }
}

其他的,仓库类、物品类没有发生改变,修改的只有物品的种类。

标签:return,鼠标,物体,private,ScriptableObject,public,Dir
From: https://www.cnblogs.com/sitarblogs/p/18551687

相关文章

  • CSS3 超实用属性:pointer-events(可穿透图层的鼠标事件)
    1、是什么pointer-events 直译为指针事件,该属性指定在什么情况下某个DOM可以成为鼠标事件的target。简而言之,就是允许/禁止DOM的鼠标事件(click事件、hover事件、mouse事件等鼠标事件)2、具体属性分析用法分析:pointer-events:auto|none|visiblePainted|visibleFill|......
  • MDK 的编译过程及文件类型全解——好记性不如烂鼠标
    本文是学习野火的指南针开发板过程的学习笔记,可能有误,详细请看B站野火官方配套视频教程(这个教程真的讲的很详细,给官方三连吧)引言:     我们用MDK编写源代码,然后编译生成机器码,再把机器码下载到STM32芯片上运行,但是这个编译、下载的过程MDK究竟做了什么工作?它编译后......
  • 《Java核心技术 卷I》用户图形界面鼠标事件
    鼠标事件        如果只希望用户能够点击按钮或菜单,那么就不需要显式地处理鼠标事件,鼠标操作将由用户界面中的各种组件内部处理,不过,如果希望用户能使用鼠标画图,就需要捕获鼠标移动,点击和拖动事件。本节,我们将展示一个简单的图形编辑器应用,它允许用户在画布上放置、移......
  • 【Unity】鼠标点击获取世界坐标位置:物体移动至鼠标点击的位置
    需求说明鼠标点击3D场景时,可以获取其所在的世界坐标;鼠标点击3D物体时,可以获取该物体;鼠标点击3D物体时,可以让玩家移动至该物体;成果展示Scene部分关于仓库栏的设置,物体如何进入仓库内容【查看此篇】能够被获取的物体,必须需要是一个碰撞体,即绑定了碰撞体组件。Mouse3D需要......
  • 【Unity】仓库逻辑:拾取物体进仓库和扔掉物品
    需求说明目标:实现玩家移动过程中,拾取物体,物体被放入仓库;点击仓库中物体,重新扔回3D场景中逻辑。逻辑分析:需要玩家可以移动;需要检测玩家和物体的碰撞,并摧毁物体;需要识别物体的类别;新物体直接新建,已有的物体增加数量;需要记录仓库的物体详情列表,并响应列表的变化;需要仓库的UI......
  • 透视视角下利用depthTest和depthWrite控制物体前后遮挡关系,优化渲染层次感
    https://easyv.cloud/c/article/10206.html二、深度测试(depthTest)深度测试是一种机制,它决定了一个像素是否可见。当一个新的像素需要被绘制时,其深度值会与深度缓冲中的当前值进行比较。根据比较结果的不同,采取不同的行动:小于:如果新像素的深度值小于缓冲区中的值,说明新像素更靠......
  • 模拟鼠标真人移动轨迹算法-易语言
    一.简介鼠标轨迹算法是一种模拟人类鼠标操作的程序,它能够模拟出自然而真实的鼠标移动路径。鼠标轨迹算法的底层实现采用C/C++语言,原因在于C/C++提供了高性能的执行能力和直接访问操作系统底层资源的能力。鼠标轨迹算法具有以下优势:模拟人工轨迹:算法能够模拟出非贝塞尔曲线......
  • 如何在算家云搭建YOLOv5(物体检测)
    一、YOLOv5简介YOLOv5模型是一种以实时物体检测闻名的计算机视觉模型,由Ultralytics开发,并于2020年年中发布。它是YOLO系列的升级版,继承了YOLO系列以实时物体检测能力而著称的特点。二、模型搭建流程1.选择模型实例在应用社区中搜索或找到“YOLOv5”模型,或者在“其......
  • 用 Python 控制你的鼠标和键盘
    嗨,大家好!今天咱们来聊聊怎么用Python操控你的鼠标和键盘,轻松“接管”你的电脑。通过pynput这个库,咱们可以实现对键盘和鼠标的全面掌控,听起来是不是有点酷?而且,比起其他库如pygame或pyglet,pynput在操作上更为简单,适合像咱们这种想快速搞定任务的技术人群。好了,废话不多......
  • 基于KD树、包围盒与RayCast(射线投射)实现物体拾取的示例代码框架
    以下是一个基于KD树、包围盒与RayCast(射线投射)实现物体拾取的示例代码框架及相关解释。这个示例假设是在一个三维空间场景下进行操作,主要目的是通过从指定视点发出射线,利用KD树对场景中的物体包围盒进行组织和快速搜索,来判断射线与哪个物体相交,从而实现物体的拾取。#include<ios......