今天我们要实现一个unity的
网格放置系统
,及装修建造种植功能,我们可以在网格上放置对像,并可以将其移除
首先,我先放出最终效果,以决定你是否想要继续往下学习
源码见文章末尾
https://kenney.nl/
注意
:如果你选择新建项目,可以直接新建一个3d带URP的项目,也可以选择将普通项目升级到URP,至于如何升级我这里就不过多介绍了,毕竟之前已经说了很多次了,不懂的可以看看我之前的文章
第一步,我们将学习如何将鼠标位置转换为网格坐标系,这样我们就可以选择一个特定的单元格
新建输入管理器脚本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层,这样我们的检测系统才不会探测到球体本身
运行查看效果现在我们应该看到我们的球体跟随鼠标指针
在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轴适当调高
安装shader graph,并导入demo样例
,等会要用到
创建一个无光照影响的shader graph
首先创建一个grid节点
如果你搜索没有找到grid这个节点,可能是前面你忘记了导入shader graph 样例,当然你也可以选择手动拖入grid
因为我们要渲染有透明效果的物体,记得将surfece Type设置为Transparent
配置shader graph节点,并保存
按这个shader graph,生成材质
在场景右键,新增一个3d plane物体,适当提高它的y轴高度,防止它们重合被草地覆盖
将我们刚才创建的材质,拖入平面plane物体上
可以看到,我们就实现了我们的网格可视化
为了我们能够自由的进行网格的大小缩放和颜色控制
我们需要继续完善我们的shader graph,我们新增几个变量控制网格
将plane移动到我们的网格同级,这样做的好处是,哪怕网格父体发生偏移,也不会影响我们网格的选择问题
比如,我们把尺寸修改为2x2,网格会被切分成更细
别忘了,记得同时修改网格组件的尺寸,为0.5x0.5,这样每一个小网格就为一个新区域
如果我们直接缩放网格平面,可能出现一些问题,我们需要同步调整网格平面的xz的偏移即可
开始项目已经准备好了很多预制体,需要注意的是,你会发现预制体都是外面包裹一个父级空对象组成的,这样做的好处是,可以让放置物品时,准确的按父级空对象的位置进行放置,且自定义调节物品在网格中的偏移量,留有空隙,放置出来的物品会更加美观
新建脚本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
按钮就会退出物品放置且隐藏可视化网格
我们还需要进行放置有效性检查,及我们不能把家具放在其他物体的上面,但是可以放在地板上
大致逻辑就是放置保存物品的位置信息,放置时作比较,看位置是否已经存在物体,判断是否可放置
新建脚本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);
}
效果
新建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);// 更新位置
}
}
效果
开始前,我想先重构一下我们的放置脚本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,这个现在已经没有用了
挂载脚本
删除按钮绑定点击事件
效果
添加物品时,我们还可以添加动效,让我们的反馈更加明显,增强视觉效果
这里我直接选择使用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。如果你有任何问题,欢迎你来评论私信告诉我, 虽然有些问题我可能也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~