【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)

news2024/11/24 11:55:48

文章目录

  • 前言
  • 开始项目和素材
    • 1. 素材来源
    • 2. 开始项目包(两种选择一种下载导入即可)
  • 开始
    • 1. 修改鼠标指针显示
    • 2. 给鼠标对应的平面位置绑定对应的指示器
    • 3. 使用Shader Graph创建网格可视化
    • 3. 网格的大小缩放和颜色控制
    • 4. 优化
    • 5. 扩展说明
      • 5.1 我们就可以通过修改参数,实现不同的网格效果
      • 5.2 缩放网格平面
    • 6. 在地图上放置地砖和家具
    • 7. 检测放置物品不能重叠
    • 8. 实现放置物品实时预览效果
    • 9. 删除物体和添加音效功能
    • 10. 最终效果
  • 源码
  • 参考
  • 完结

前言

今天我们要实现一个unity的网格放置系统,及装修建造种植功能,我们可以在网格上放置对像,并可以将其移除

首先,我先放出最终效果,以决定你是否想要继续往下学习
请添加图片描述
源码见文章末尾

开始项目和素材

1. 素材来源

https://kenney.nl/

2. 开始项目包(两种选择一种下载导入即可)

  • unity资源包
    链接:https://pan.baidu.com/s/1pgAdMfmCIFrYY-b8x8dJUQ
    提取码:8yol
  • 项目资源压缩包
    链接:https://pan.baidu.com/s/1pdrZjzvVqeiR5NeO1KspCA
    提取码:0agm

注意:如果你选择新建项目,可以直接新建一个3d带URP的项目,也可以选择将普通项目升级到URP,至于如何升级我这里就不过多介绍了,毕竟之前已经说了很多次了,不懂的可以看看我之前的文章

开始

导入上面下载的开始项目,会带有基本的场景和一些预制体直接可以使用,节约大家时间
在这里插入图片描述

1. 修改鼠标指针显示

第一步,我们将学习如何将鼠标位置转换为网格坐标系,这样我们就可以选择一个特定的单元格
在这里插入图片描述
新建输入管理器脚本InputManager

using UnityEngine;

public class InputManager : MonoBehaviour
{
    [SerializeField]
    private Camera sceneCamera;

    private Vector3 lastPosition;

    [SerializeField]
    private LayerMask placementLayermask;

    // 获取选中的地图位置
    public Vector3 GetSelectedMapPosition()
    {
        // 获取鼠标位置
        Vector3 mousePos = Input.mousePosition;
        mousePos.z = sceneCamera.nearClipPlane;

        // 创建射线
        Ray ray = sceneCamera.ScreenPointToRay(mousePos);

        // 射线检测
        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, 100, placementLayermask))
        {
            // 更新最后位置
            lastPosition = hit.point;
        }

        // 返回最后位置
        return lastPosition;
    }
}

新建放置脚本PlacementSystem

using UnityEngine;

public class PlacementSystem : MonoBehaviour
{
    [SerializeField]
    private GameObject mouseIndicator;

    [SerializeField]
    private InputManager inputManager;


    private void Update()
    {
        // 获取鼠标位置
        Vector3 mousePosition = inputManager.GetSelectedMapPosition();
        // 更新鼠标指示器位置
        mouseIndicator.transform.position = mousePosition;
        
    }
}

在BuildingSystem中新增两个空对象,分别命名为InputManager和PlacementSystem
在这里插入图片描述

新建一个球体3d对象作为临时的鼠标指针,修改它的缩放为0.2
在这里插入图片描述
挂载脚本,并配置参数
在这里插入图片描述

在这里插入图片描述
修改指针球体图层为Water,因为前面设置了射线检测图层为default层,这样我们的检测系统才不会探测到球体本身
在这里插入图片描述
运行查看效果现在我们应该看到我们的球体跟随鼠标指针
在这里插入图片描述

2. 给鼠标对应的平面位置绑定对应的指示器

在BuildingSystem中新增两个空对象,分别命名为网格父物体和网格,并在网格上挂载Grid组件
在这里插入图片描述
完善我们的放置脚本PlacementSystem

using UnityEngine;

public class PlacementSystem : MonoBehaviour
{
    [SerializeField]
    private GameObject mouseIndicator, cellIndicator;

    [SerializeField]
    private InputManager inputManager;
    [SerializeField]
    private Grid grid;


    private void Update()
    {
        // 获取鼠标位置
        Vector3 mousePosition = inputManager.GetSelectedMapPosition();
        // 将鼠标位置转换为网格位置
        Vector3Int gridPosition = grid.WorldToCell(mousePosition);
        // 设置鼠标指示器的位置为鼠标位置
        mouseIndicator.transform.position = mousePosition;
        // 设置单元格指示器的位置为网格位置
        cellIndicator.transform.position = grid.CellToWorld(gridPosition);
    }
}

为了防止指示器(指示器在预制体里)被草地覆盖,可以把y轴适当调高
在这里插入图片描述

在这里插入图片描述

挂载指示器和网格组件
在这里插入图片描述
运行效果
在这里插入图片描述

3. 使用Shader Graph创建网格可视化

安装shader graph,并导入demo样例,等会要用到
在这里插入图片描述
创建一个无光照影响的shader graph
在这里插入图片描述

首先创建一个grid节点
在这里插入图片描述
如果你搜索没有找到grid这个节点,可能是前面你忘记了导入shader graph 样例,当然你也可以选择手动拖入grid
在这里插入图片描述
因为我们要渲染有透明效果的物体,记得将surfece Type设置为Transparent
在这里插入图片描述
配置shader graph节点,并保存
在这里插入图片描述

按这个shader graph,生成材质
在这里插入图片描述
在场景右键,新增一个3d plane物体,适当提高它的y轴高度,防止它们重合被草地覆盖
在这里插入图片描述
将我们刚才创建的材质,拖入平面plane物体上
在这里插入图片描述
可以看到,我们就实现了我们的网格可视化

3. 网格的大小缩放和颜色控制

为了我们能够自由的进行网格的大小缩放和颜色控制
我们需要继续完善我们的shader graph,我们新增几个变量控制网格

平面大小,默认10x10
在这里插入图片描述

颜色,默认白色
在这里插入图片描述

单个网格大小,默认1x1
在这里插入图片描述

网格的厚度,默认设置为滑块控制值大小
在这里插入图片描述

完整的shader graph连线图
在这里插入图片描述

效果
在这里插入图片描述
在这里插入图片描述

4. 优化

将plane移动到我们的网格同级,这样做的好处是,哪怕网格父体发生偏移,也不会影响我们网格的选择问题
在这里插入图片描述

5. 扩展说明

5.1 我们就可以通过修改参数,实现不同的网格效果

比如,我们把尺寸修改为2x2,网格会被切分成更细
在这里插入图片描述
别忘了,记得同时修改网格组件的尺寸,为0.5x0.5,这样每一个小网格就为一个新区域
在这里插入图片描述

5.2 缩放网格平面

如果我们直接缩放网格平面,可能出现一些问题,我们需要同步调整网格平面的xz的偏移即可
在这里插入图片描述

6. 在地图上放置地砖和家具

开始项目已经准备好了很多预制体,需要注意的是,你会发现预制体都是外面包裹一个父级空对象组成的,这样做的好处是,可以让放置物品时,准确的按父级空对象的位置进行放置,且自定义调节物品在网格中的偏移量,留有空隙,放置出来的物品会更加美观
在这里插入图片描述

新建脚本ObjectsDatabaseSO,我们创建ScriptableObject保存各个物品参数

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu]
public class ObjectsDatabaseSO : ScriptableObject
{
    // 对象数据列表
    public List<ObjectData> objectsData;
}

[Serializable]
public class ObjectData
{
    // 对象名称
    [field: SerializeField]
    public string Name { get; private set; }
    // 对象ID
    [field: SerializeField]
    public int ID { get; private set; }
    // 对象尺寸
    [field: SerializeField]
    public Vector2Int Size { get; private set; } = Vector2Int.one;
    // 对象预制体
    [field: SerializeField]
    public GameObject Prefab { get; private set; }
}

新建ScriptableObject,保存各个物品并配置参数
在这里插入图片描述
完善InputManager代码

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class InputManager : MonoBehaviour
{
    [SerializeField]
    private Camera sceneCamera;

    private Vector3 lastPosition;

    [SerializeField]
    private LayerMask placementLayermask;

    public event Action OnClicked, OnExit;

    private void Update()
    {
        // 检测鼠标左键点击事件
        if (Input.GetMouseButtonDown(0))
            OnClicked?.Invoke();

        // 检测按下ESC键事件
        if (Input.GetKeyDown(KeyCode.Escape))
            OnExit?.Invoke();
    }

    // 检测鼠标是否悬停在UI元素上
    public bool IsPointerOverUI()
        => EventSystem.current.IsPointerOverGameObject();

    // 获取选中的地图位置
    public Vector3 GetSelectedMapPosition()
    {
        Vector3 mousePos = Input.mousePosition;
        mousePos.z = sceneCamera.nearClipPlane;
        Ray ray = sceneCamera.ScreenPointToRay(mousePos);
        RaycastHit hit;
        // 发射射线检测碰撞
        if (Physics.Raycast(ray, out hit, 100, placementLayermask))
        {
            lastPosition = hit.point;
        }
        return lastPosition;
    }
}

完善PlacementSystem代码

using UnityEngine;

public class PlacementSystem : MonoBehaviour
{
    [SerializeField]
    private GameObject mouseIndicator, cellIndicator;

    [SerializeField]
    private InputManager inputManager;
    [SerializeField]
    private Grid grid;

    [SerializeField]
    private ObjectsDatabaseSO database;
    private int seletedObjectIndex = -1;
    [SerializeField]
    private GameObject gridVisualization;

    private void Start()
    {
        // 隐藏网格可视化和单元格指示器
        gridVisualization.SetActive(false);
    }

    // 开始放置物体
    public void StartPlacement(int ID)
    {
    	// 停止之前的放置
        StopPlacement();
        // 查找选中物体的索引
        seletedObjectIndex = database.objectsData.FindIndex(data => data.ID == ID);
        if (seletedObjectIndex < 0)
        {
            Debug.LogError("seletedObjectIndex没有");
            return;
        }
        // 激活网格可视化和单元格指示器
        gridVisualization.SetActive(true);
        cellIndicator.SetActive(true);
        // 添加放置物体的事件监听
        inputManager.OnClicked += PlaceStructure;
        inputManager.OnExit += StopPlacement;
    }

    // 放置物体
    private void PlaceStructure()
    {
        if (inputManager.IsPointerOverUI())
        {
            return;
        }
        // 获取鼠标位置和对应的网格位置
        Vector3 mousePosition = inputManager.GetSelectedMapPosition();
        Vector3Int gridPosition = grid.WorldToCell(mousePosition);
        // 实例化选中物体并设置位置
        GameObject newObject = Instantiate(database.objectsData[seletedObjectIndex].Prefab);
        newObject.transform.localPosition = grid.CellToWorld(gridPosition);

    }

    // 停止放置物体
    private void StopPlacement()
    {
        seletedObjectIndex = -1;
        // 隐藏网格可视化和单元格指示器
        gridVisualization.SetActive(false);
        cellIndicator.SetActive(false);
        // 移除放置物体的事件监听
        inputManager.OnClicked -= PlaceStructure;
        inputManager.OnExit -= StopPlacement;
    }

    private void Update()
    {
    	// 如果没有选中任何物体,直接返回
        if (seletedObjectIndex < 0)
            return;
        // 获取鼠标在地图上的位置
        Vector3 mousePosition = inputManager.GetSelectedMapPosition();
        // 将鼠标的世界坐标转换为网格坐标
        Vector3Int gridPosition = grid.WorldToCell(mousePosition);
		// 设置鼠标指示器的位置为鼠标的位置
        mouseIndicator.transform.position = mousePosition;
        // 设置单元格指示器的位置为当前网格的世界坐标
        cellIndicator.transform.position = grid.CellToWorld(gridPosition);

    }
}

绑定SO数据和可视化网格(前面的Plane重命名)
在这里插入图片描述
给UI按钮绑定点击事件,注意配置ID为前面对应OS的ID,一一对应
在这里插入图片描述
新增图层Placement
在这里插入图片描述
修改可视化网格图层和InputManager的检测图层
在这里插入图片描述
在这里插入图片描述

效果

默认进去不显示可视化网格,当点击物品时,才显示出网格,点击位置放置物品
点击esc按钮就会退出物品放置且隐藏可视化网格
在这里插入图片描述

7. 检测放置物品不能重叠

我们还需要进行放置有效性检查,及我们不能把家具放在其他物体的上面,但是可以放在地板上

大致逻辑就是放置保存物品的位置信息,放置时作比较,看位置是否已经存在物体,判断是否可放置

新建脚本GridData

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GridData
{
    // 存储放置物体的字典
    Dictionary<Vector3Int, PlacementData> placedObjects = new();

    // 在指定的网格位置添加物体
    public void AddObjectAt(Vector3Int gridPosition,
                            Vector2Int objectSize,
                            int ID,
                            int placedObjectIndex)
    {
        // 计算需要占据的位置
        List<Vector3Int> positionToOccupy = CalculatePositions(gridPosition, objectSize);
        // 创建放置数据对象
        PlacementData data = new PlacementData(positionToOccupy, ID, placedObjectIndex);
        // 遍历需要占据的位置,并将放置数据添加到字典中
        foreach (var pos in positionToOccupy)
        {
        	// 如果字典中已经包含此位置,抛出异常
            if (placedObjects.ContainsKey(pos))
                throw new Exception($"字典已经包含此位置 {pos}");
            // 将放置数据添加到字典中
            placedObjects[pos] = data;
        }
    }

    // 计算需要占据的位置
    private List<Vector3Int> CalculatePositions(Vector3Int gridPosition, Vector2Int objectSize)
    {
        List<Vector3Int> returnVal = new();
        for (int x = 0; x < objectSize.x; x++)
        {
            for (int y = 0; y < objectSize.y; y++)
            {
            	// 计算并添加需要占据的位置
                returnVal.Add(gridPosition + new Vector3Int(x, 0, y));
            }
        }
        return returnVal;
    }

    // 检查是否可以在指定的网格位置放置物体
    public bool CanPlaceObejctAt(Vector3Int gridPosition, Vector2Int objectSize)
    {
        // 计算需要占据的位置
        List<Vector3Int> positionToOccupy = CalculatePositions(gridPosition, objectSize);
        // 遍历需要占据的位置,如果有任何一个位置已经被占据,则返回false
        foreach (var pos in positionToOccupy)
        {
        	// 如果字典中已经包含此位置,返回false
            if (placedObjects.ContainsKey(pos))
                return false;
        }
        return true;
    }

    // 获取指定网格位置的放置物体索引
    internal int GetRepresentationIndex(Vector3Int gridPosition)
    {
        // 如果字典中不包含指定位置的放置数据,则返回-1
        if (placedObjects.ContainsKey(gridPosition) == false)
            return -1;
        // 返回指定位置的放置物体索引
        return placedObjects[gridPosition].PlacedObjectIndex;
    }

    // 移除指定网格位置的放置物体
    internal void RemoveObjectAt(Vector3Int gridPosition)
    {
        // 遍历放置数据中的所有位置,并从字典中移除
        foreach (var pos in placedObjects[gridPosition].occupiedPositions)
        {
            placedObjects.Remove(pos);
        }
    }
}

public class PlacementData
{
    // 占据的位置列表
    public List<Vector3Int> occupiedPositions;
    // 物体的ID
    public int ID { get; private set; }
    // 放置物体的索引
    public int PlacedObjectIndex { get; private set; }

    // 构造函数
    public PlacementData(List<Vector3Int> occupiedPositions, int iD, int placedObjectIndex)
    {
        this.occupiedPositions = occupiedPositions;
        ID = iD;
        PlacedObjectIndex = placedObjectIndex;
    }
}

修改PlacementSystem脚本代码

private GridData floorData, furnitureData;// 地板数据,家具数据
private Renderer previewRenderer;
private List<GameObject> placedGameObjects = new();//已放置物体列表

private void Start()
{
     gridVisualization.SetActive(false); // 隐藏网格可视化和单元格指示器
     floorData = new GridData(); // 创建地板数据对象
     furnitureData = new GridData(); // 创建家具数据对象
     previewRenderer = cellIndicator.GetComponentInChildren<Renderer>(); // 获取单元格指示器的渲染器组件
 }

// 放置物体
private void PlaceStructure()
{
	//。。。
	
	// 检查放置的有效性,如果无效则返回
    bool placementValidity = CheckPlacementValidity(gridPosition, seletedObjectIndex);
    if(placementValidity == false)
    	#TODO:这里可以播放禁止放置的音效
        return;
	// 将物体添加到已放置物体列表中
    placedGameObjects.Add(newObject);
	// 选择数据
    GridData selectedData = database.objectsData[seletedObjectIndex].ID == 0 ? floorData : furnitureData;
    // 在指定位置添加对象
    selectedData.AddObjectAt(gridPosition, database.objectsData[seletedObjectIndex].Size, database.objectsData[seletedObjectIndex].ID, placedGameObjects.Count - 1);
 }
 
private void Update()
{
	//。。。
	
	// 检查放置有效性
	bool placementValidity = CheckPlacementValidity(gridPosition, seletedObjectIndex);
	// 如果可以放置,预览物体的颜色为白色,否则为红色
	previewRenderer.material.color = placementValidity ? Color.white : Color.red;
}

// 检查在给定的网格位置是否可以放置指定的物体
private bool CheckPlacementValidity(Vector3Int gridPosition, int selectedObjectIndex)
 {
     // 如果选中的物体的ID为0,表示是地板,否则是家具
    GridData selectedData = database.objectsData[selectedObjectIndex].ID == 0 ? floorData : furnitureData;
	// 检查在给定的网格位置是否可以放置指定大小的物体
    return selectedData.CanPlaceObejctAt(gridPosition, database.objectsData[selectedObjectIndex].Size);
 }

效果
在这里插入图片描述

8. 实现放置物品实时预览效果

新建shader graph 实现物品透视变色效果,color默认设置为白色,透明度100即可
在这里插入图片描述
同样按这个shader graph创建材质,放置一个物品,预览一下效果,可以看到修改材质的地方变为了半透明效果
在这里插入图片描述
新建脚本PreviewSystem,控制物品预览

using UnityEngine;

public class PreviewSystem : MonoBehaviour
{
    //预览的Y轴偏移量,防止它被草地遮挡
    [SerializeField]
    private float previewYOffset = 0.06f;

    // 序列化字段,单元格指示器
    [SerializeField]
    private GameObject cellIndicator;
    // 预览对象
    private GameObject previewObject;

    // 序列化字段,预览材质预制体
    [SerializeField]
    private Material previewMaterialPrefab;
    // 预览材质实例
    private Material previewMaterialInstance;

    // 单元格指示器渲染器
    private Renderer cellIndicatorRenderer;

    // 开始方法
    private void Start()
    {
        // 初始化预览材质实例
        previewMaterialInstance = new Material(previewMaterialPrefab);
        // 设置单元格指示器为非活动状态
        cellIndicator.SetActive(false);
        // 获取单元格指示器的渲染器
        cellIndicatorRenderer = cellIndicator.GetComponentInChildren<Renderer>();
    }

    // 开始显示放置预览
    public void StartShowingPlacementPreview(GameObject prefab, Vector2Int size)
    {
        // 实例化预览对象
        previewObject = Instantiate(prefab);
        // 准备预览
        PreparePreview(previewObject);
        // 准备光标
        PrepareCursor(size);
        // 设置单元格指示器为活动状态
        cellIndicator.SetActive(true);
    }

    // 准备光标
    private void PrepareCursor(Vector2Int size)
    {
        // 如果尺寸大于0
        if(size.x > 0 || size.y > 0)
        {
            // 设置单元格指示器的缩放
            cellIndicator.transform.localScale = new Vector3(size.x, 1, size.y);
            // 设置单元格指示器材质的主纹理缩放
            cellIndicatorRenderer.material.mainTextureScale = size;
        }
    }

    // 准备预览
    private void PreparePreview(GameObject previewObject)
    {
        // 获取预览对象的所有渲染器
        Renderer[] renderers = previewObject.GetComponentsInChildren<Renderer>();
        // 遍历所有渲染器
        foreach(Renderer renderer in renderers)
        {
            // 获取渲染器的所有材质
            Material[] materials = renderer.materials;
            // 遍历所有材质
            for (int i = 0; i < materials.Length; i++)
            {
                // 设置材质为预览材质实例
                materials[i] = previewMaterialInstance;
            }
            // 设置渲染器的材质
            renderer.materials = materials;
        }
    }

    // 停止显示预览
    public void StopShowingPreview()
    {
        // 设置单元格指示器为非活动状态
        cellIndicator.SetActive(false );
        // 如果预览对象不为空,销毁预览对象
        if(previewObject!= null)
            Destroy(previewObject );
    }

    // 更新位置
    public void UpdatePosition(Vector3 position, bool validity)
    {
        // 如果预览对象不为空
        if(previewObject != null)
        {
            // 移动预览
            MovePreview(position);
            // 应用反馈到预览
            ApplyFeedbackToPreview(validity);

        }

        // 移动光标
        MoveCursor(position);
        // 应用反馈到光标
        ApplyFeedbackToCursor(validity);
    }

    // 应用反馈到预览
    private void ApplyFeedbackToPreview(bool validity)
    {
        // 如果有效,颜色为白色,否则为红色
        Color c = validity ? Color.white : Color.red;
        
        // 设置颜色的透明度为0.5
        c.a = 0.5f;
        // 设置预览材质实例的颜色
        previewMaterialInstance.color = c;
    }

    // 应用反馈到光标
    private void ApplyFeedbackToCursor(bool validity)
    {
        // 如果有效,颜色为白色,否则为红色
        Color c = validity ? Color.white : Color.red;

        // 设置颜色的透明度为0.5
        c.a = 0.5f;
        // 设置单元格指示器渲染器材质的颜色
        cellIndicatorRenderer.material.color = c;
    }

    // 移动光标
    private void MoveCursor(Vector3 position)
    {
        // 设置单元格指示器的位置
        cellIndicator.transform.position = position;
    }

    // 移动预览
    private void MovePreview(Vector3 position)
    {
        // 设置预览对象的位置
        previewObject.transform.position = new Vector3(
            position.x, 
            position.y + previewYOffset, 
            position.z);
    }

    // 开始显示移除预览
    internal void StartShowingRemovePreview()
    {
        // 设置单元格指示器为活动状态
        cellIndicator.SetActive(true);
        // 准备光标
        PrepareCursor(Vector2Int.one);
        // 应用反馈到光标
        ApplyFeedbackToCursor(false);
    }
}

同步修改PlacementSystem代码,这里我只放了修改部分的代码
删除原来的cellIndicator和previewRenderer相关数据,并进行修改

[SerializeField]
private PreviewSystem preview;
private Vector3Int lastDetectedPosition = Vector3Int.zero;// 最后检测到的位置

// 开始放置函数
public void StartPlacement(int ID)
{
	// cellIndicator.SetActive(true);
	// 开始显示放置预览
    preview.StartShowingPlacementPreview(database.objectsData[seletedObjectIndex].Prefab, database.objectsData[seletedObjectIndex].Size);
}
// 放置物体
private void PlaceStructure()
{
	//。。。
	
	// 更新位置
	preview.UpdatePosition(grid.CellToWorld(gridPosition), false);
}
// 停止放置物体
private void StopPlacement()
{
	// cellIndicator.SetActive(false);
    preview.StopShowingPreview();// 停止显示预览
    
    //。。。
    
    lastDetectedPosition = Vector3Int.zero; // 重置最后检测到的位置
}

private void Update()
{
    //。。。

    if (lastDetectedPosition != gridPosition)
    {
        // 检查放置有效性
        bool placementValidity = CheckPlacementValidity(gridPosition, seletedObjectIndex);
        // 如果可以放置,预览物体的颜色为白色,否则为红色
        // previewRenderer.material.color = placementValidity ? Color.white : Color.red;

        // 设置鼠标指示器的位置为鼠标的位置
        mouseIndicator.transform.position = mousePosition;
        // 设置单元格指示器的位置为当前网格的世界坐标
        // cellIndicator.transform.position = grid.CellToWorld(gridPosition);
        preview.UpdatePosition(grid.CellToWorld(gridPosition), placementValidity);// 更新位置
    }
}

绑定脚本
在这里插入图片描述
在这里插入图片描述

效果
在这里插入图片描述

9. 删除物体和添加音效功能

开始前,我想先重构一下我们的放置脚本PlacementSystem,将逻辑代码分离出来,目前所有的逻辑基本都写在这里,改动起来很麻烦,而且可读性不高

将放置物体和删除物体功能脱离出来

新建ObjectPlacer脚本

using System.Collections.Generic;
using UnityEngine;
public class ObjectPlacer : MonoBehaviour
{
    // 定义一个私有的GameObject类型的列表,用于存放已放置的游戏对象
    [SerializeField]
    private List<GameObject> placedGameObjects = new();

    // 定义一个公共方法,用于在指定位置放置游戏对象,并返回该对象在列表中的索引
    public int PlaceObject(GameObject prefab, Vector3 position)
    {
        // 实例化游戏对象
        GameObject newObject = Instantiate(prefab);
        // 设置游戏对象的位置
        newObject.transform.position = position;
        // 将游戏对象添加到列表中
        placedGameObjects.Add(newObject);
        // 返回游戏对象在列表中的索引
        return placedGameObjects.Count - 1;
    }

    // 定义一个内部方法,用于移除指定索引的游戏对象
    internal void RemoveObjectAt(int gameObjectIndex)
    {
        // 如果索引超出列表范围或者指定索引的游戏对象为空,则直接返回
        if (placedGameObjects.Count <= gameObjectIndex 
            || placedGameObjects[gameObjectIndex] == null)
            return;
        // 销毁指定索引的游戏对象
        Destroy(placedGameObjects[gameObjectIndex]);
        // 将列表中对应的游戏对象设置为null
        placedGameObjects[gameObjectIndex] = null;
    }
}

新建声音管理脚本SoundFeedback

using UnityEngine;

// 声音反馈类
public class SoundFeedback : MonoBehaviour
{
    // 定义私有音频剪辑:点击音、放置音、移除音、错误放置音
    [SerializeField]
    private AudioClip clickSound, placeSound, removeSound, wrongPlacementSound;

    // 定义私有音频源
    [SerializeField]
    private AudioSource audioSource;

    // 播放音效的方法
    public void PlaySound(SoundType soundType)
    {
        // 根据音效类型播放对应音效
        switch (soundType)
        {
            case SoundType.Click:
                audioSource.PlayOneShot(clickSound);  // 播放点击音
                break;
            case SoundType.Place:
                audioSource.PlayOneShot(placeSound);  // 播放放置音
                break;
            case SoundType.Remove:
                audioSource.PlayOneShot(removeSound);  // 播放移除音
                break;
            case SoundType.wrongPlacement:
                audioSource.PlayOneShot(wrongPlacementSound);  // 播放错误放置音
                break;
            default:
                break;
        }
    }
}

// 音效类型枚举
public enum SoundType
{
    Click,  // 点击
    Place,  // 放置
    Remove,  // 移除
    wrongPlacement  // 错误放置
}

新建脚本PlacementState

using UnityEngine;

public class PlacementState : IBuildingState
{
    // 选中的对象索引
    private int selectedObjectIndex = -1;
    int ID;
    Grid grid;
    PreviewSystem previewSystem;
    ObjectsDatabaseSO database;
    GridData floorData;
    GridData furnitureData;
    ObjectPlacer objectPlacer;
    SoundFeedback soundFeedback;

    // PlacementState 构造函数
    public PlacementState(int iD,
                          Grid grid,
                          PreviewSystem previewSystem,
                          ObjectsDatabaseSO database,
                          GridData floorData,
                          GridData furnitureData,
                          ObjectPlacer objectPlacer,
                          SoundFeedback soundFeedback)
    {
        // 初始化变量
        ID = iD;
        this.grid = grid;
        this.previewSystem = previewSystem;
        this.database = database;
        this.floorData = floorData;
        this.furnitureData = furnitureData;
        this.objectPlacer = objectPlacer;
        this.soundFeedback = soundFeedback;

        // 查找选定对象的索引
        selectedObjectIndex = database.objectsData.FindIndex(data => data.ID == ID);
        if (selectedObjectIndex > -1)
        {
            // 如果找到,开始显示预览
            previewSystem.StartShowingPlacementPreview(
                database.objectsData[selectedObjectIndex].Prefab,
                database.objectsData[selectedObjectIndex].Size);
        }
        else
            // 如果未找到,抛出异常
            throw new System.Exception($"No object with ID {iD}");

    }

    // 结束状态的方法
    public void EndState()
    {
        // 停止显示预览
        previewSystem.StopShowingPreview();
    }

    // 执行操作的方法
    public void OnAction(Vector3Int gridPosition)
    {
        // 检查放置的有效性
        bool placementValidity = CheckPlacementValidity(gridPosition, selectedObjectIndex);
        if (placementValidity == false)
        {
            // 如果无效,播放错误音效
            soundFeedback.PlaySound(SoundType.wrongPlacement);
            return;
        }
        // 如果有效,播放放置音效
        soundFeedback.PlaySound(SoundType.Place);
        int index = objectPlacer.PlaceObject(database.objectsData[selectedObjectIndex].Prefab,
            grid.CellToWorld(gridPosition));

        // 选择数据
        GridData selectedData = database.objectsData[selectedObjectIndex].ID == 0 ?
            floorData :
            furnitureData;
        // 在选定位置添加对象
        selectedData.AddObjectAt(gridPosition,
            database.objectsData[selectedObjectIndex].Size,
            database.objectsData[selectedObjectIndex].ID,
            index);

        // 更新预览位置
        previewSystem.UpdatePosition(grid.CellToWorld(gridPosition), false);
    }

    // 检查放置有效性的私有方法
    private bool CheckPlacementValidity(Vector3Int gridPosition, int selectedObjectIndex)
    {
        // 选择数据
        GridData selectedData = database.objectsData[selectedObjectIndex].ID == 0 ? floorData : furnitureData;

        // 检查是否可以在选定位置放置对象
        return selectedData.CanPlaceObejctAt(gridPosition, database.objectsData[selectedObjectIndex].Size);
    }

    // 更新状态的方法
    public void UpdateState(Vector3Int gridPosition)
    {
        // 检查放置的有效性
        bool placementValidity = CheckPlacementValidity(gridPosition, selectedObjectIndex);

        // 更新预览位置
        previewSystem.UpdatePosition(grid.CellToWorld(gridPosition), placementValidity);
    }
}

IBuildingState接口脚本

using UnityEngine;

public interface IBuildingState
{
    void EndState();
    void OnAction(Vector3Int gridPosition);
    void UpdateState(Vector3Int gridPosition);
}

RemovingState脚本

using UnityEngine;

public class RemovingState : IBuildingState
{
    private int gameObjectIndex = -1;
    Grid grid;
    PreviewSystem previewSystem;
    GridData floorData;
    GridData furnitureData;
    ObjectPlacer objectPlacer;
    SoundFeedback soundFeedback; 

    // RemovingState构造函数
    public RemovingState(Grid grid,
                         PreviewSystem previewSystem,
                         GridData floorData,
                         GridData furnitureData,
                         ObjectPlacer objectPlacer,
                         SoundFeedback soundFeedback)
    {
        // 初始化变量
        this.grid = grid;
        this.previewSystem = previewSystem;
        this.floorData = floorData;
        this.furnitureData = furnitureData;
        this.objectPlacer = objectPlacer;
        this.soundFeedback = soundFeedback;
        // 开始显示移除预览
        previewSystem.StartShowingRemovePreview();
    }

    // 结束状态方法
    public void EndState()
    {
        // 停止显示预览
        previewSystem.StopShowingPreview();
    }

    // 执行操作方法
    public void OnAction(Vector3Int gridPosition)
    {
        GridData selectedData = null;
        // 检查是否可以在指定位置放置家具
        if(furnitureData.CanPlaceObejctAt(gridPosition,Vector2Int.one) == false)
        {
            selectedData = furnitureData;
        }
        // 检查是否可以在指定位置放置地板
        else if(floorData.CanPlaceObejctAt(gridPosition, Vector2Int.one) == false)
        {
            selectedData = floorData;
        }

        // 如果无法放置,则播放错误音效
        if(selectedData == null)
        {
            soundFeedback.PlaySound(SoundType.wrongPlacement);
        }
        else
        {
            // 否则,播放移除音效,并移除对象
            soundFeedback.PlaySound(SoundType.Remove);
            gameObjectIndex = selectedData.GetRepresentationIndex(gridPosition);
            if (gameObjectIndex == -1)
                return;
            selectedData.RemoveObjectAt(gridPosition);
            objectPlacer.RemoveObjectAt(gameObjectIndex);
        }
        // 更新预览位置
        Vector3 cellPosition = grid.CellToWorld(gridPosition);
        previewSystem.UpdatePosition(cellPosition, CheckIfSelectionIsValid(gridPosition));
    }

    // 检查选择是否有效
    private bool CheckIfSelectionIsValid(Vector3Int gridPosition)
    {
        return !(furnitureData.CanPlaceObejctAt(gridPosition, Vector2Int.one) &&
            floorData.CanPlaceObejctAt(gridPosition, Vector2Int.one));
    }

    // 更新状态方法
    public void UpdateState(Vector3Int gridPosition)
    {
        bool validity = CheckIfSelectionIsValid(gridPosition);
        // 更新预览位置
        previewSystem.UpdatePosition(grid.CellToWorld(gridPosition), validity);
    }
}

同步修改PlacementSystem脚本代码

using UnityEngine;

public class PlacementSystem : MonoBehaviour
{
    [SerializeField]
    private InputManager inputManager; // 输入管理器
    [SerializeField]
    private Grid grid; // 网格

    [SerializeField]
    private ObjectsDatabaseSO database; // 数据库

    [SerializeField]
    private GameObject gridVisualization; // 网格可视化

    private GridData floorData, furnitureData; // 地板和家具数据

    [SerializeField]
    private PreviewSystem preview; // 预览系统

    private Vector3Int lastDetectedPosition = Vector3Int.zero; // 最后检测到的位置

    [SerializeField]
    private ObjectPlacer objectPlacer; // 对象放置器

    IBuildingState buildingState; // 建筑状态

    [SerializeField]
    private SoundFeedback soundFeedback; // 声音反馈

    // Start方法
    private void Start()
    {
        gridVisualization.SetActive(false); // 设置网格可视化为不活动
        floorData = new(); // 创建新的地板数据
        furnitureData = new(); // 创建新的家具数据
    }

    // 开始放置方法
    public void StartPlacement(int ID)
    {
        StopPlacement(); // 停止放置
        gridVisualization.SetActive(true); // 设置网格可视化为活动
        buildingState = new PlacementState(ID,
                                           grid,
                                           preview,
                                           database,
                                           floorData,
                                           furnitureData,
                                           objectPlacer,
                                           soundFeedback); // 创建新的放置状态
        inputManager.OnClicked += PlaceStructure; // 点击时放置结构
        inputManager.OnExit += StopPlacement; // 退出时停止放置
    }

    // 开始移除方法
    public void StartRemoving()
    {
        StopPlacement(); // 停止放置
        gridVisualization.SetActive(true); // 设置网格可视化为活动
        buildingState = new RemovingState(grid, preview, floorData, furnitureData, objectPlacer, soundFeedback); // 创建新的移除状态
        inputManager.OnClicked += PlaceStructure; // 点击时放置结构
        inputManager.OnExit += StopPlacement; // 退出时停止放置
    }

    // 放置结构方法
    private void PlaceStructure()
    {
        if(inputManager.IsPointerOverUI())
        {
            return; // 如果指针在UI上,返回
        }
        Vector3 mousePosition = inputManager.GetSelectedMapPosition(); // 获取鼠标位置
        Vector3Int gridPosition = grid.WorldToCell(mousePosition); // 获取网格位置

        buildingState.OnAction(gridPosition); // 执行动作

    }

    // 停止放置方法
    private void StopPlacement()
    {
        soundFeedback.PlaySound(SoundType.Click); // 播放点击音效
        if (buildingState == null)
            return; // 如果建筑状态为空,返回
        gridVisualization.SetActive(false); // 设置网格可视化为不活动
        buildingState.EndState(); // 结束状态
        inputManager.OnClicked -= PlaceStructure; // 移除点击时放置结构的事件
        inputManager.OnExit -= StopPlacement; // 移除退出时停止放置的事件
        lastDetectedPosition = Vector3Int.zero; // 设置最后检测到的位置为零
        buildingState = null; // 设置建筑状态为null
    }

    // Update方法
    private void Update()
    {
        if (buildingState == null)
            return; // 如果建筑状态为空,返回
        Vector3 mousePosition = inputManager.GetSelectedMapPosition(); // 获取鼠标位置
        Vector3Int gridPosition = grid.WorldToCell(mousePosition); // 获取网格位置
        if(lastDetectedPosition != gridPosition)
        {
            buildingState.UpdateState(gridPosition); // 更新状态
            lastDetectedPosition = gridPosition; // 更新最后检测到的位置
        }
        
    }
}

删除Sphere,这个现在已经没有用了
在这里插入图片描述
挂载脚本
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
删除按钮绑定点击事件
在这里插入图片描述

10. 最终效果

在这里插入图片描述

源码

私信我获取

参考

【视频】https://www.youtube.com/watch?v=l0emsAHIBjU

完结

赠人玫瑰,手有余香!如果文章内容对你有所帮助,希望你不要吝啬自己的点赞评论和关注,第一时间告诉我,你的每一次支持都是我不断创作的最大动力。当然如果你发现了文章中存在错误或者有更好的解决方法,也欢迎评论私信告诉我哦!

好了,我是向宇,https://xiangyu.blog.csdn.net/

一位在小公司默默奋斗的开发者,出于兴趣爱好,于是最近才开始自习unity。如果你有任何问题,欢迎你来评论私信告诉我, 虽然有些问题我可能也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/752516.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

总结:WEB流量劫持

一、背景 在整理WEB鉴权与登陆等技术的时候&#xff0c;会涉及到安全&#xff0c;而鉴权登陆不安全有个很大的可能就是流量被劫持了。 本篇文章主要是想弄明白黑客是怎么劫持的。 二、WEB流量劫持是如何做到的&#xff1f; WEB流量劫持是指黑客将受害者的网页请求重定向到攻…

python appium UI 自动化测试框架讨论

目录 前言&#xff1a; 框架共性总结 Auto_Analysis 权限弹窗识别 前言&#xff1a; Python Appium UI自动化测试框架是一种用于测试移动应用程序的工具&#xff0c;它结合了Python编程语言和Appium测试框架的功能。 框架共性总结 1 自动找设备 连接设备 2 自动启 appium …

Java设计模式之行为型-状态模式(UML类图+案例分析)

目录 一、基础概念 二、UML类图 三、角色设计 四、案例分析 五、总结 一、基础概念 状态模式允许一个对象在其内部状态改变时改变它的行为&#xff0c;对象看起来似乎修改了它的类&#xff0c;状态模式主要解决的是当控制一个对象状态转换的条件表达式过于复杂时的情况&a…

有必要买apple pencil吗?ipad pencil替代品

苹果的电容笔与一般的电容笔有什么不同&#xff1f;事实上&#xff0c;从外观上来看&#xff0c;两者并没有太大的区别。唯一不同的是&#xff0c;苹果电容笔的重量更大&#xff0c;笔尖内部有专门的重力感应器&#xff0c;可以感应到重力压感对线条的影响。因此苹果的这个产品…

MongoDB复制集

文章目录 一、介绍1、存在的意义和作用&#xff1f;2、需要实现啥功能&#xff1f;3、典型案例4、注意事项 二、搭建1、安装MongoDB&#xff0c;配置环境变量2、创建数据目录3、配置文件4、启动 MongoDB 进程5、配置复制集6、验证 三、写策略writeConcern1、w参数2、j参数2.1 介…

《如何使用思维导图进行知识结构的建立和扩展》

I.思维导图作为知识管理工具的重要性 思维导图是一款强大的效率工具&#xff0c;可以帮助有效的管理知识。”一图胜千言“它用图形的方式&#xff0c;将各个主题连接起来。我们可以清晰的了解到各主题之间的关系。 在知识管理中&#xff0c;通过创建一个知识主题的中心&#xf…

PVE虚拟化平台之安装Ubuntu-server系统

PVE虚拟化平台之安装Ubuntu-server系统 一、Ubuntu介绍1.1 Ubuntu简介1.2 Ubuntu版本1.3 ubuntu命名规则 二、上传镜像到PVE存储2.1 检查PVE环境2.2 上传镜像 三、创建虚拟机3.1 设置虚拟机名称3.2 操作系统设置3.3 系统设置3.4 磁盘设置3.5 cpu设置3.6 内存设置3.7 网络设置3.…

libvirt 热迁移流程及参数介绍

01 热迁移基本原理 1.1 热迁移概念 热迁移也叫在线迁移&#xff0c;是指虚拟机在开机状态下&#xff0c;且不影响虚拟机内部业务正常运行的情况下&#xff0c;从一台宿主机迁移到另外一台宿主机上的过程。 1.2 虚拟机数据传输预拷贝和后拷贝 预拷贝(pre-copy)&#xff1a; …

3、wampserver中查看各项当前版本及简单配置PHP

wampserver点击左键&#xff0c;即可查看Apache&#xff0c;PHP&#xff0c;MySQL&#xff0c;MariaDB的当前版本 在wampserver的安装目录中&#xff0c;在相应的D:\wamp64\bin\php\php8.0.26 php.ini文件中&#xff0c;short_open_tag On&#xff08;是否允许使用 PHP代码开…

在新建环境下配置低版本opencv

我这边是要解决 python报错&#xff1a;AttributeError: ‘module’ object has no attribute xfeatures2d’的问题&#xff0c; xfeatures2d在新版本已经被取消&#xff0c;但是需要使用老版本的一个函数 确定opencv与python的版本对应关系 一般来说可以对照这个表 具体来说…

Mac下安装python使用TensorFlow训练自己的模型

程序猿日常 Mac下安装python使用TensorFlow训练自己的模型目标 https://www.tensorflow.org/lite/models/modify/model_maker/image_classification?hlzh-cn 安装Python3.8版本 下载地址双击安装 安装pip curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py pyth…

ZKML:区块链世界的AI+隐私

1. 引言 本文主要参考&#xff1a; 2023年6月drCathieSo.eth 与 Ethereum Malaysia 视频 ZKML: Verifiable & Privacy-Preserving Compute in Blockchain2023年6月drCathieSo.eth 与 PSE 视频 Folding Circom circuits: a ZKML case study - Dr. Cathie So ZKML&#xf…

解决SpringMVC中@ResponseBody返回中文乱码

错误 解决方案一 Controller中的注解采用如下方式&#xff1a; GetMapping(value "/init" ,produces "application/json;charsetutf-8")这种方式仅对设置了的方法有效。 解决方案二 在applicationContext.xml中添加如下代码&#xff1a; <!-- 解…

广播与组播

目录 一、广播1. 什么是广播&#xff1f;2. 广播的实现 二、组播1. 分类的IP地址2. 多播IP地址3. 组播的实现 广播与组播和实现UDP通信的代码差不多 一、广播 1. 什么是广播&#xff1f; 数据包发送方式只有一个接受方&#xff0c;称为单播 如果同时发给局域网中的所有主机&…

解决vite+vue3打包部署到非根目录路径问题

修改vite.config.js文件&#xff0c;base为部署路径 base配置选项&#xff1a;

vue3 my-cron-vue3插件的使用

my-cron-vue3 这是一个cron表达式生成插件,基于vue3.0与element-plus实现。 npm i my-cron-vue3//前置配置 import { createApp } from vue import ElementPlus from element-plus; import element-plus/lib/theme-chalk/index.css; import App from ./App.vue //全局引入 imp…

探索华为、思科和瞻博网络的基本ACL和高级ACL配置方法

在网络安全中&#xff0c;访问控制列表&#xff08;Access Control List&#xff0c;简称ACL&#xff09;是一种重要的工具&#xff0c;用于控制数据包在网络中的流动。多家网络设备厂商提供了各自的ACL配置方法&#xff0c;其中华为、思科和瞻博网络是备受认可和使用广泛的品牌…

(中等)LeetCode 剑指OfferII 074. 合并区间

排序&#xff1a; 用数组merged存储最终的答案 首先&#xff0c;将列表中的区间按照左端点升序排序&#xff0c;将第一个区间加入merged数组中&#xff0c;并按顺序依次考虑之后的每个区间&#xff1a; 如果当前区间的左端点在数组merged中最后一个区间的右端点之后&#xf…

ChatGPT提问的万能公式,强烈建议收藏!泰裤辣!

在实际使用GPT的时候&#xff0c;并不是GPT不够强大&#xff0c;而是我们需要很多时间去调教AI&#xff0c;以便输出我们期望的答案&#xff0c;为了让输出无限的靠近你的期望&#xff0c;就需要下面这个万能的框架&#xff0c;如果大家记不住这个框架或者没有形成习惯&#xf…

nginx相关

1、nginx无默认配置文件 参考文章&#xff1a;nginx配置失败&#xff0c;卸载后重装无 nginx.conf文件_haojuntu的博客-CSDN博客 2、nginx更改服务器的端口号 参考文章&#xff1a;https://www.cnblogs.com/chaosfe/p/16123585.html#:~:text%E6%88%91%E4%BB%AC%E6%9F%A5%E7%…