本文主要是介绍【用unity实现100个游戏之4】手搓一个网格放置功能,及装修建造种植功能(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,至于如何升级我这里就不过多介绍了,毕竟之前已经说了很多次了,不懂的可以看看我之前的文章
开始
导入上面下载的开始项目,会带有基本的场景和一些预制体直接可以使用,节约大家时间
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);// 遍历需要占据的位置,如果有任何一个位置已经被占据,则返回falseforeach (var pos in positionToOccupy){// 如果字典中已经包含此位置,返回falseif (placedObjects.ContainsKey(pos))return false;}return true;}// 获取指定网格位置的放置物体索引internal int GetRepresentationIndex(Vector3Int gridPosition){// 如果字典中不包含指定位置的放置数据,则返回-1if (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;// 物体的IDpublic 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){// 如果尺寸大于0if(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.5c.a = 0.5f;// 设置预览材质实例的颜色previewMaterialInstance.color = c;}// 应用反馈到光标private void ApplyFeedbackToCursor(bool validity){// 如果有效,颜色为白色,否则为红色Color c = validity ? Color.white : Color.red;// 设置颜色的透明度为0.5c.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]);// 将列表中对应的游戏对象设置为nullplacedGameObjects[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. 使用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。如果你有任何问题,欢迎你来评论私信告诉我, 虽然有些问题我可能也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~
这篇关于【用unity实现100个游戏之4】手搓一个网格放置功能,及装修建造种植功能(2d3d通用,附源码)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!