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

文章目录

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

前言

今天我们要实现一个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,至于如何升级我这里就不过多介绍了,毕竟之前已经说了很多次了,不懂的可以看看我之前的文章

开始

导入上面下载的开始项目,会带有基本的场景和一些预制体直接可以使用,节约大家时间
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第1张图片

1. 修改鼠标指针显示

第一步,我们将学习如何将鼠标位置转换为网格坐标系,这样我们就可以选择一个特定的单元格
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第2张图片
新建输入管理器脚本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
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第3张图片

新建一个球体3d对象作为临时的鼠标指针,修改它的缩放为0.2
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第4张图片
挂载脚本,并配置参数
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第5张图片

【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第6张图片
修改指针球体图层为Water,因为前面设置了射线检测图层为default层,这样我们的检测系统才不会探测到球体本身
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第7张图片
运行查看效果现在我们应该看到我们的球体跟随鼠标指针
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第8张图片

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

在BuildingSystem中新增两个空对象,分别命名为网格父物体和网格,并在网格上挂载Grid组件
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第9张图片
完善我们的放置脚本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轴适当调高
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第10张图片

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

挂载指示器和网格组件
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第12张图片
运行效果

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

安装shader graph,并导入demo样例,等会要用到
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第13张图片
创建一个无光照影响的shader graph
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第14张图片

首先创建一个grid节点
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第15张图片
如果你搜索没有找到grid这个节点,可能是前面你忘记了导入shader graph 样例,当然你也可以选择手动拖入grid
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第16张图片
因为我们要渲染有透明效果的物体,记得将surfece Type设置为Transparent
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第17张图片
配置shader graph节点,并保存
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第18张图片

按这个shader graph,生成材质
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第19张图片
在场景右键,新增一个3d plane物体,适当提高它的y轴高度,防止它们重合被草地覆盖
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第20张图片
将我们刚才创建的材质,拖入平面plane物体上
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第21张图片
可以看到,我们就实现了我们的网格可视化

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

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

平面大小,默认10x10
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第22张图片

颜色,默认白色
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第23张图片

单个网格大小,默认1x1
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第24张图片

网格的厚度,默认设置为滑块控制值大小
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第25张图片

完整的shader graph连线图
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第26张图片

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

4. 优化

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

5. 扩展说明

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

比如,我们把尺寸修改为2x2,网格会被切分成更细
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第28张图片
别忘了,记得同时修改网格组件的尺寸,为0.5x0.5,这样每一个小网格就为一个新区域
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第29张图片

5.2 缩放网格平面

如果我们直接缩放网格平面,可能出现一些问题,我们需要同步调整网格平面的xz的偏移即可
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第30张图片

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

开始项目已经准备好了很多预制体,需要注意的是,你会发现预制体都是外面包裹一个父级空对象组成的,这样做的好处是,可以让放置物品时,准确的按父级空对象的位置进行放置,且自定义调节物品在网格中的偏移量,留有空隙,放置出来的物品会更加美观
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第31张图片

新建脚本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,保存各个物品并配置参数
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第32张图片
完善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重命名)
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第33张图片
给UI按钮绑定点击事件,注意配置ID为前面对应OS的ID,一一对应
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第34张图片
新增图层Placement
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第35张图片
修改可视化网格图层和InputManager的检测图层
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第36张图片
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第37张图片

效果

默认进去不显示可视化网格,当点击物品时,才显示出网格,点击位置放置物品
点击esc按钮就会退出物品放置且隐藏可视化网格
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第38张图片

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即可
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第39张图片
同样按这个shader graph创建材质,放置一个物品,预览一下效果,可以看到修改材质的地方变为了半透明效果
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第40张图片
新建脚本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);// 更新位置
    }
}

绑定脚本
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第41张图片
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第42张图片

效果

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,这个现在已经没有用了
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第43张图片
挂载脚本
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第44张图片
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第45张图片
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第46张图片
删除按钮绑定点击事件
【unity实战】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)_第47张图片
效果

10. 使用DoTween添加动效

添加物品时,我们还可以添加动效,让我们的反馈更加明显,增强视觉效果

这里我直接选择使用DoTween插件,关于DoTween的使用,我之前也有做过相关的介绍,如果有不懂得也可以先去看看我之前的文章:DoTween动画插件的安装和使用整合

using DG.Tweening;

//使物体的缩放发生抖动
newObject.transform.DOShakeScale(0.5f, 0.2f, 10, 0.5f);

运行游戏,可以看到物品放置有了一个不错的果冻般效果

源码下载

https://download.csdn.net/download/qq_36303853/88050109

参考

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

完结

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

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

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

你可能感兴趣的:(#,制作100个游戏,unity,3d,游戏引擎,游戏)