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

文章目录

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

前言

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

首先,我先放出最终效果,以决定你是否想要继续往下学习

源码见文章末尾

开始项目和素材

1. 素材来源

https://kenney.nl/

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

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

开始

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

1. 修改鼠标指针显示

第一步,我们将学习如何将鼠标位置转换为网格坐标系,这样我们就可以选择一个特定的单元格

新建输入管理器脚本InputManager

csharp 复制代码
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

csharp 复制代码
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

csharp 复制代码
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保存各个物品参数

csharp 复制代码
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代码

csharp 复制代码
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代码

csharp 复制代码
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

csharp 复制代码
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脚本代码

csharp 复制代码
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,控制物品预览

csharp 复制代码
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相关数据,并进行修改

csharp 复制代码
[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脚本

csharp 复制代码
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

csharp 复制代码
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

csharp 复制代码
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接口脚本

csharp 复制代码
using UnityEngine;

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

RemovingState脚本

csharp 复制代码
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脚本代码

csharp 复制代码
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. 使用DoTween添加动效

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

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

csharp 复制代码
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。如果你有任何问题,欢迎你来评论私信告诉我, 虽然有些问题我可能也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~

相关推荐
逐·風4 小时前
unity关于自定义渲染、内存管理、性能调优、复杂物理模拟、并行计算以及插件开发
前端·unity·c#
_oP_i5 小时前
Unity Addressables 系统处理 WebGL 打包本地资源的一种高效方式
unity·游戏引擎·webgl
PandaQue6 小时前
《怪物猎人:荒野》游戏可以键鼠直连吗
游戏
代码盗圣9 小时前
GODOT 4 不用scons编译cpp扩展的方法
游戏引擎·godot
starsongda10 小时前
VR科技展厅重塑科技展示新风貌,引领未来展示潮流
科技·3d·vr
白狐欧莱雅10 小时前
使用python中的pygame简单实现飞机大战游戏
经验分享·python·游戏·pygame
兔老大的胡萝卜11 小时前
threejs 数字孪生,制作3d炫酷网页
前端·3d
豆本-豆豆奶12 小时前
用 Python 写了一个天天酷跑(附源码)
开发语言·python·游戏·pygame·零基础教程
CV-X.WANG14 小时前
【详细 工程向】基于Smart3D的五镜头相机三维重建
数码相机·3d
Leoysq14 小时前
【UGUI】实现点击注册按钮跳转游戏场景
游戏·unity·游戏引擎·ugui