【用unity实现100个游戏之14】Unity2d做一个建造与防御类rts游戏

前言

欢迎来到本次教程,我将为您讲解如何使用 Unity 引擎来开发一个建造与防御类 RTS(即实时战略)游戏。

在本教程中,我们将学习如何创建 2D 场景、设计 2D 精灵、制作 2D 动画、响应用户输入、管理游戏数据、以及其他有关游戏开发的重要话题。我们还将使用 C# 编程语言来实现游戏逻辑,并且会介绍一些常用的游戏编程模式和工具。

作为一个项目实战教程,我们不仅将讲解理论,还将创建一个完整的建造与防御类 RTS 游戏,并且在整个过程中,您将深入了解游戏开发流程、工作流程和实现细节。我们将从创建游戏场景开始,逐步添加游戏元素、实现游戏逻辑、处理用户输入、创建用户界面等等。这样,您将有足够的机会学习如何将理论知识应用到实践中。

在完成本教程后,您将有能力设计、创建和发布自己的 2D RTS 游戏,并且可以运用所学知识进行更深入的游戏开发工作。让我们开始吧!

最终效果,项目还在完善当中,目前做到一半,后续内容还会不断更新迭代,尽情期待。
在这里插入图片描述

素材

链接:https://pan.baidu.com/s/1CFEWC2o5xUtp-bGJD3-cig
提取码:7omd

新建项目

新建一个URP2d项目,并导入素材
在这里插入图片描述

放置物品

实现了一个建筑管理器,当玩家按下鼠标左键时,在鼠标点击的位置创建一个木材采集机的实例。其中,pfWoodHarvester是木材采集机的预制体,mainCamera是主摄像机的引用。

using UnityEngine;public class BuildingManager : MonoBehaviour
{[SerializeField] private Transform pfWoodHarvester; // 木材采集机预制体private Camera mainCamera;private void Start(){mainCamera = Camera.main; // 获取主摄像机对象}private void Update(){if (Input.GetMouseButtonDown(0)){// 在鼠标点击位置创建一个木材采集机实例Instantiate(pfWoodHarvester, GetMouseWorldPosition(), Quaternion.identity);}}// 获取鼠标点击位置对应的世界坐标private Vector3 GetMouseWorldPosition(){Vector3 mouseWorldPosition = mainCamera.ScreenToWorldPoint(Input.mousePosition);mouseWorldPosition.z = 0f; // 将Z轴坐标设为0,以保证在二维平面上创建实例return mouseWorldPosition;}
}

效果
在这里插入图片描述

放置不同物品类型

定义一个继承自ScriptableObject的建筑类型类。通过在Unity编辑器的菜单中创建ScriptableObject的选项,可以方便地创建建筑类型的实例,并在实例中设置名称和预制体。

using UnityEngine;[CreateAssetMenu(menuName = "ScriptableObjects/建筑类型")]
public class BuildingType : ScriptableObject
{public string nameString; // 建筑类型的名称字符串public Transform prefab; // 建筑类型对应的预制体
}

新增几种建筑类型类
在这里插入图片描述

定义一个包含一个名为buildingTypeList的List成员变量,用于存储建筑类型的列表。

using System.Collections.Generic;
using UnityEngine;[CreateAssetMenu(menuName = "ScriptableObjects/建筑类型列表")]
public class BuildingTypeList : ScriptableObject
{public List<BuildingType> buildingTypeList; // 建筑类型列表
}

建筑类型列表数据
在这里插入图片描述

修改BuildingManager ,其中,buildingTypeList是一个ScriptableObject,包含了多个建筑类型,buildingType表示当前选中的建筑类型。

public class BuildingManager : MonoBehaviour
{private BuildingTypeList buildingTypeList; // 建筑类型列表对象private BuildingType buildingType; // 当前选中的建筑类型对象private Camera mainCamera;private void Start(){mainCamera = Camera.main; // 获取主摄像机对象buildingTypeList = Resources.Load<BuildingTypeList>("ScriptableObject/建筑类型列表"); // 加载建筑类型列表buildingType = buildingTypeList.buildingTypeList[0]; // 初始化为列表中的第一个建筑类型}private void Update(){if (Input.GetMouseButtonDown(0)){// 在鼠标点击位置创建一个木材采集机实例Instantiate(buildingType.prefab, GetMouseWorldPosition(), Quaternion.identity);}if (Input.GetKeyDown(KeyCode.T)){buildingType = buildingTypeList.buildingTypeList[0]; // 切换为列表中的第一个建筑类型}else if (Input.GetKeyDown(KeyCode.Y)){buildingType = buildingTypeList.buildingTypeList[1]; // 切换为列表中的第二个建筑类型}}// 获取鼠标点击位置对应的世界坐标private Vector3 GetMouseWorldPosition(){//。。。}
}

效果
在这里插入图片描述

资源管理

定义一个继承自ScriptableObject的资源类型类

using UnityEngine;[CreateAssetMenu(menuName = "ScriptableObjects/资源类型")]
public class ResourceType : ScriptableObject
{public string nameString; // 资源类型的名称
}

在这里插入图片描述
资源类型列表

using System.Collections.Generic;
using UnityEngine;[CreateAssetMenu(menuName = "ScriptableObjects/资源类型列表")]
public class ResourceTypeList : ScriptableObject
{public List<ResourceTypeSo> list; // 资源类型的列表
}

在这里插入图片描述
新建资源管理器,生成资源测试

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class ResourceManager : MonoBehaviour
{private Dictionary<ResourceType, int> resourceAmountDictionary; // 资源类型与数量的字典private void Awake(){resourceAmountDictionary = new Dictionary<ResourceType, int>(); // 初始化资源字典// 加载资源类型列表ResourceTypeList resourceTypeList = Resources.Load<ResourceTypeList>("ScriptableObject/资源类型/资源类型列表");// 遍历资源类型列表,将每个资源类型添加到资源字典并初始化数量为0foreach (ResourceType resourceType in resourceTypeList.list){resourceAmountDictionary[resourceType] = 0;}TestLogResourceAmountDictionary(); // 测试输出资源字典}private void Update(){if (Input.GetKeyDown(KeyCode.T)){// 加载资源类型列表ResourceTypeList resourceTypeList = Resources.Load<ResourceTypeList>("ScriptableObject/资源类型/资源类型列表");// 将列表中第二个资源类型的数量增加2resourceAmountDictionary[resourceTypeList.list[1]] += 2;TestLogResourceAmountDictionary(); // 测试输出资源字典}}private void TestLogResourceAmountDictionary(){// 遍历资源字典,输出每个资源类型及其对应的数量foreach (ResourceType resourceType in resourceAmountDictionary.Keys){Debug.Log(resourceType.nameString + ": " + resourceAmountDictionary[resourceType]);}}
}

运行测试
在这里插入图片描述

管理和配置生成资源的信息

定义了一个名为 ResourceGeneratorData 的类,作为数据存储单元,用于管理和配置生成资源的信息。并添加了 [System.Serializable] 属性,使其可以在Unity编辑器中进行序列化和显示。

[System.Serializable]
public class ResourceGeneratorData
{public float timerMax; // 生成资源的时间间隔public ResourceType resourceType; // 资源类型
}

修改BuildingType

using UnityEngine;[CreateAssetMenu(menuName = "ScriptableObjects/建筑类型")]
public class BuildingType : ScriptableObject
{public string nameString; // 建筑类型的名称字符串public Transform prefab; // 建筑类型对应的预制体public ResourceGeneratorData resourceGeneratorData; // 资源生成器的数据
}

配置对应参数
在这里插入图片描述
BuildingTypeHolder 脚本,配置建筑类型

using UnityEngine;public class BuildingTypeHolder : MonoBehaviour
{public BuildingType buildingType; // 建筑类型对象
}

修改ResourceManager

public static ResourceManager Instance { get; private set;}private void Awake()
{Instance = this;
}public void AddResource(ResourceTypeso resourceType, int amount){resourceAmountDictionary[resourceType] += amount; // 增加资源数量TestLogResourceAmountDictionary(); // 调用测试方法,输出资源数量
}

新增ResourceGenerator脚本,资源生成者,控制资源生成

using UnityEngine;public class ResourceGenerator : MonoBehaviour
{private BuildingType buildingType; // 建筑类型对象private float timer; // 计时器private float timerMax; // 计时器最大值private void Awake(){buildingType = GetComponent<BuildingTypeHolder>().buildingType; // 获取建筑类型timerMax = buildingType.resourceGeneratorData.timerMax; // 获取计时器最大值}private void Update(){timer -= Time.deltaTime; // 更新计时器if (timer <= 0f) // 检查计时器是否到达或超过最大值{timer += timerMax; // 重置计时器// 调用 ResourceManager 的 AddResource 方法,增加资源ResourceManager.Instance.AddResource(buildingType.resourceGeneratorData.resourceType, 1);}}
}

配置不同建筑预制体数据
在这里插入图片描述
效果

在这里插入图片描述

绘制资源UI

绘制UI
在这里插入图片描述

using UnityEngine;
using UnityEngine.UI;
using TMPro;
using System.Collections.Generic;public class ResourcesUI : MonoBehaviour
{private ResourceTypeList resourceTypeList; // 资源类型列表对象private Dictionary<ResourceType, Transform> resourceTypeTransformDictionary; // 资源类型与UI Transform的映射字典[SerializeField] private Transform resourceTemplate; // 资源UI模板private void Awake(){resourceTypeList = Resources.Load<ResourceTypeList>("ScriptableObject/资源类型/资源类型列表"); // 加载资源类型列表对象resourceTypeTransformDictionary = new Dictionary<ResourceType, Transform>(); // 创建资源类型与UI Transform的映射字典resourceTemplate.gameObject.SetActive(false); // 禁用资源UI模板int index = 0; // 索引计数器foreach (ResourceType resourceType in resourceTypeList.list) // 遍历资源类型列表{Transform resourceTransform = Instantiate(resourceTemplate, transform); // 实例化资源UIresourceTransform.gameObject.SetActive(true); // 启用资源UIresourceTransform.Find("image").GetComponent<Image>().sprite = resourceType.sprite; // 设置资源UI的图片resourceTypeTransformDictionary[resourceType] = resourceTransform; // 将资源类型与UI Transform进行映射index++;}}private void Start(){UpdateResourceAmount(); // 更新资源数量}private void UpdateResourceAmount(){foreach (ResourceType resourceType in resourceTypeList.list) // 遍历资源类型列表{Transform resourceTransform = resourceTypeTransformDictionary[resourceType]; // 获取对应资源类型的UI Transformint resourceAmount = ResourceManager.Instance.GetResourceAmount(resourceType); // 获取资源数量resourceTransform.Find("text").GetComponent<TextMeshProUGUI>().SetText(resourceAmount.ToString()); // 设置资源UI的文本}}
}

修改ResourceType ,新增资源的图标变量

public Sprite sprite; // 资源的图标

修改ResourceManager,获取资源数量方法

// 获取资源数量
public int GetResourceAmount(ResourceType resourceType){return resourceAmountDictionary[resourceType];
}

效果
在这里插入图片描述

同步资源生成

在 ResourceManager 类中进行修改,添加了一个 OnResourceAmountChanged 事件。这个事件用于在资源数量发生变化时通知其他对象。

在 AddResource 方法中,每次增加资源数量后,会触发 OnResourceAmountChanged 事件,通知其他对象资源数量已发生改变。

using System;public event EventHandler OnResourceAmountChanged;public void AddResource(ResourceType resourceType, int amount){resourceAmountDictionary[resourceType] += amount; // 增加资源数量//使用了 ?.Invoke 运算符来避免空引用异常OnResourceAmountChanged?.Invoke(this, EventArgs.Empty);TestLogResourceAmountDictionary(); // 调用测试方法,输出资源数量
}

修改ResourcesUI,在 ResourcesUI 类中的 Start 方法中,订阅了 ResourceManager.Instance.OnResourceAmountChanged 事件,并指定了一个回调方法 ResourceManager_OnResourceAmountChanged

在 ResourceManager_OnResourceAmountChanged 方法中,调用了 UpdateResourceAmount 方法,实现资源数量发生变化时更新资源UI的功能。

private void Start()
{ResourceManager.Instance.OnResourceAmountChanged += ResourceManager_OnResourceAmountChanged;UpdateResourceAmount(); // 更新资源数量
}private void ResourceManager_OnResourceAmountChanged(object sender, System.EventArgs e){UpdateResourceAmount();
}

效果
在这里插入图片描述

绘制地图,优化场景

这里我加了一个背景
在这里插入图片描述

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

控制虚拟相机

添加虚拟相机
在这里插入图片描述
新建一个物体,作为虚拟相机Follow物体
在这里插入图片描述
新增CameraHandler脚本,控制虚拟相机的移动和缩放

using UnityEngine;
using Cinemachine;public class CameraHandler : MonoBehaviour
{[SerializeField] private CinemachineVirtualCamera cinemachinevirtualCamera;private float orthographicSize;private float targetOrthographicSize;// 获取初始的正交大小private void Start(){orthographicSize = cinemachinevirtualCamera.m_Lens.OrthographicSize;targetOrthographicSize = orthographicSize;}private void Update(){HandleMovement();HandleZoom();}// 处理摄像机移动private void HandleMovement(){float x = Input.GetAxisRaw("Horizontal");float y = Input.GetAxisRaw("Vertical");Vector3 moveDir = new Vector3(x, y).normalized;float moveSpeed = 60f;transform.position += moveDir * moveSpeed * Time.deltaTime;}// 处理缩放private void HandleZoom(){float zoomAmount = 2f;targetOrthographicSize += Input.mouseScrollDelta.y * zoomAmount;float minOrthographicSize = 10;float maxOrthographicSize = 30;targetOrthographicSize = Mathf.Clamp(targetOrthographicSize, minOrthographicSize, maxOrthographicSize);float zoomSpeed = 5f;orthographicSize = Mathf.Lerp(orthographicSize, targetOrthographicSize, Time.deltaTime * zoomSpeed);// 设置摄像机的正交大小cinemachinevirtualCamera.m_Lens.OrthographicSize = orthographicSize;}
}

效果
在这里插入图片描述

添加建筑物按钮UI

给图片添加外边框组件
在这里插入图片描述
在这里插入图片描述
效果
在这里插入图片描述

修改BuildingType,新增建筑的图标变量

public Sprite sprite; //建筑的图标

新增BuildingTypeSelectUI脚本控制建筑按钮的显示

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;public class BuildingTypeSelectUI : MonoBehaviour
{// 建筑按钮模板public Transform btnTemplate;private void Awake(){// 加载建筑类型列表资源BuildingTypeList buildingTypeList = Resources.Load<BuildingTypeList>("ScriptableObject/建筑类型/建筑类型列表");int index = 0;// 遍历建筑类型列表,创建对应的按钮foreach (BuildingType buildingType in buildingTypeList.buildingTypeList){Transform btnTransform = Instantiate(btnTemplate, transform);// 设置图片btnTransform.Find("image").GetComponent<Image>().sprite = buildingType.sprite;index++;}}
}

效果
在这里插入图片描述

UI上放置建筑问题修复

正常我们是不希望在UI上放置物品的
在这里插入图片描述
修改BuildingManager

using UnityEngine.EventSystems;private void Update()
{if (Input.GetMouseButtonDown(0) && !EventSystem.current.IsPointerOverGameObject()){// 在鼠标点击位置创建一个木材采集机实例Instantiate(buildingType.prefab, GetMouseWorldPosition(), Quaternion.identity);}
}

ps:EventSystem.current.IsPointerOverGameObject()是一个用于判断鼠标指针是否位于UI元素上的方法。

效果
在这里插入图片描述

添加点击事件

修改BuildingManager为单例,并添加修改当前选中的建筑类型对象方法

public static BuildingManager Instance {get; private set;}private BuildingType activeBuildingType; // 当前选中的建筑类型对象void Awake(){Instance = this;//。。。
}// 修改当前选中的建筑类型对象
public void SetActiveBuildingType(BuildingType buildingType){activeBuildingType = buildingType;
}

修改BuildingTypeSelectUI绑定点击事件

// 遍历建筑类型列表,创建对应的按钮
foreach (BuildingType buildingType in buildingTypeList.buildingTypeList)
{//。。。//绑定点击事件btnTransform.GetComponent<Button>().onClick.AddListener(()=>{BuildingManager.Instance.SetActiveBuildingType(buildingType);});
}

效果
在这里插入图片描述

选中效果

新增选中select底图
在这里插入图片描述
修改BuildingTypeSelectUI

private Dictionary<BuildingType, Transform> btnTransformDictionary;private void Awake(){btnTransformDictionary = new Dictionary<BuildingType, Transform>();//。。。// 遍历建筑类型列表,创建对应的按钮foreach (BuildingType buildingType in buildingTypeList.buildingTypeList){//。。。btnTransformDictionary[buildingType] = btnTransform;}
}private void Update(){UpdateActiveBuildingTypeButton();
}// 更新当前选中建筑类型按钮的样式
private void UpdateActiveBuildingTypeButton(){//默认关闭选中图像foreach (BuildingType buildingType in btnTransformDictionary.Keys){Transform btnTransform = btnTransformDictionary[buildingType];btnTransform.Find("selected").gameObject.SetActive(false);}//开启选中图像BuildingType activeBuildingType = BuildingManager.Instance.GetActiveBuildingType();btnTransformDictionary[activeBuildingType].Find("selected").gameObject.SetActive(true);}

BuildingManager新增方法,获取选中的建筑类型

//获取选中的建筑类型
public BuildingType GetActiveBuildingType(){return activeBuildingType;
}

效果
在这里插入图片描述

箭头空物体效果

新增鼠标建筑类型
在这里插入图片描述
建筑类型列表新增鼠标类型
在这里插入图片描述

修改BuildingManager

private void Update()
{if (Input.GetMouseButtonDown(0) && !EventSystem.current.IsPointerOverGameObject()){if(activeBuildingType.prefab != null){// 在鼠标点击位置创建一个建筑实例Instantiate(activeBuildingType.prefab, GetMouseWorldPosition(), Quaternion.identity);}}
}

效果
在这里插入图片描述

建造跟随鼠标显示

创建跟随模板
在这里插入图片描述
新增脚本,返回鼠标在世界坐标系中的位置

using UnityEngine;public static class Utilsclass
{private static Camera mainCamera;// 获取鼠标在世界坐标系中的位置public static Vector3 GetMouseWorldPosition(){// 如果主摄像机对象为空,则获取主摄像机对象if (mainCamera == null)mainCamera = Camera.main;// 将鼠标当前位置从屏幕坐标系转换为世界坐标系Vector3 mouseWorldPosition = mainCamera.ScreenToWorldPoint(Input.mousePosition);// 将鼠标世界位置的z坐标设置为零mouseWorldPosition.z = 0f;// 返回鼠标在世界坐标系中的位置return mouseWorldPosition;}
}

修改BuildingManager,通过事件通知其他对象

using System;public event EventHandler<OnActiveBuildingTypeChangedEventArgs> OnActiveBuildingTypeChanged;
public class OnActiveBuildingTypeChangedEventArgs : EventArgs{public BuildingType activeBuildingType;
}// 修改当前选中的建筑类型对象
public void SetActiveBuildingType(BuildingType buildingType){activeBuildingType = buildingType;OnActiveBuildingTypeChanged?.Invoke(this,new OnActiveBuildingTypeChangedEventArgs {activeBuildingType = activeBuildingType});}

新增BuildingGhost脚本,控制鼠标建筑物显示隐藏

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class BuildingGhost : MonoBehaviour
{private GameObject spriteGameobject;// 初始时隐藏建筑物private void Awake(){spriteGameobject = transform.Find("sprite").gameObject;Hide();}// 监听BuildingManager中的事件private void Start(){BuildingManager.Instance.OnActiveBuildingTypeChanged += BuildingManager_OnActiveBuildingTypeChanged;}// 处理BuildingManager中的事件private void BuildingManager_OnActiveBuildingTypeChanged(object sender, BuildingManager.OnActiveBuildingTypeChangedEventArgs e){if (e.activeBuildingType.prefab == null){Hide();}else{Show(e.activeBuildingType.sprite);}}// 每帧更新建筑物的位置private void Update(){transform.position = Utilsclass.GetMouseWorldPosition();}// 显示建筑物private void Show(Sprite ghostSprite){spriteGameobject.SetActive(true);spriteGameobject.GetComponent<SpriteRenderer>().sprite = ghostSprite;}// 隐藏建筑物private void Hide(){spriteGameobject.SetActive(false);}
}

修改BuildingTypeSelectUI,优化代码,使用事件更新当前选中建筑类型按钮的样式

// private void Update(){
//     UpdateActiveBuildingTypeButton();
// }private void Start(){BuildingManager.Instance.OnActiveBuildingTypeChanged += BuildingManager_OnActiveBuildingTypeChanged;UpdateActiveBuildingTypeButton();
}private void BuildingManager_OnActiveBuildingTypeChanged(object sender, BuildingManager.OnActiveBuildingTypeChangedEventArgs e){UpdateActiveBuildingTypeButton();
}

效果
在这里插入图片描述

添加资源物体

如果我们直接添加一些资源物体,会发现排序变得很乱
在这里插入图片描述
我们可以通过脚本来控制资源的排序,大致逻辑就是按物体的y轴来控制排序

using UnityEngine;public class SpritePositionSortingOrder : MonoBehaviour
{[SerializeField] private bool runOnce; // 是否只运行一次[SerializeField] private float positionOffsetY; // Y轴位置偏移量private SpriteRenderer spriteRenderer;private void Awake(){spriteRenderer = GetComponent<SpriteRenderer>(); // 获取当前对象的SpriteRenderer组件}private void LateUpdate(){float precisionMultiplier = 5f; // 精度乘数,可以根据需要调整// 根据当前对象的位置和Y轴偏移量计算出sortingOrder值,并将其赋给SpriteRenderer组件的sortingOrder属性spriteRenderer.sortingOrder = (int)(-(transform.position.y + positionOffsetY) * precisionMultiplier);if (runOnce){Destroy(this); // 如果设置了只运行一次,就在完成一次排序后销毁脚本组件}}
}

运行效果
在这里插入图片描述
添加树叶,设定好每个树叶的偏移值
在这里插入图片描述
效果
在这里插入图片描述

实现树叶的随风摇摆

新建shader graphs
在这里插入图片描述
新建材质
在这里插入图片描述
将材质挂载在树叶身上,效果

在这里插入图片描述

按附近资源数控制资源生成速度

新增脚本,挂载在建筑物上

using UnityEngine;public class ResourceNode : MonoBehaviour
{public ResourceType resourceType;
}

在这里插入图片描述

修改ResourceGeneratorData资源生成器数据类

public float resourecDetectionRadius; //资源检测半径
public int maxResourceAmount;   //最大资源数量

修改配置
在这里插入图片描述

修改ResourceGenerator

using UnityEngine;public class ResourceGenerator : MonoBehaviour
{private ResourceGeneratorData resourceGeneratorData;// private BuildingType buildingType; // 建筑类型对象private float timer; // 计时器private float timerMax; // 计时器最大值private void Awake(){resourceGeneratorData = GetComponent<BuildingTypeHolder>().buildingType.resourceGeneratorData; // 获取建筑类型timerMax = resourceGeneratorData.timerMax; // 获取计时器最大值}private void Start(){// 获取附近的资源节点数量Collider2D[] collider2DArray = Physics2D.OverlapCircleAll(transform.position, resourceGeneratorData.resourecDetectionRadius);int nearbyResourceAmount = 0;foreach (Collider2D collider2D in collider2DArray){ResourceNode resourceNode = collider2D.GetComponent<ResourceNode>();if (resourceNode != null){// 如果资源节点的资源类型与此资源生成器的资源类型匹配,则增加附近资源节点的数量if (resourceNode.resourceType == resourceGeneratorData.resourceType){nearbyResourceAmount++;}}}// 将附近的资源节点数量限制在最大值范围内,并禁用此资源生成器的 Update 方法nearbyResourceAmount = Mathf.Clamp(nearbyResourceAmount, 0, resourceGeneratorData.maxResourceAmount);if (nearbyResourceAmount == 0 ){enabled = false;}else{//按附近的资源数控制资源的增加速度timerMax = (resourceGeneratorData.timerMax / 2f)+resourceGeneratorData.timerMax*(1 -(float)nearbyResourceAmount / resourceGeneratorData.maxResourceAmount);}// 输出附近资源节点数量,用于调试Debug.Log("附近资源量:" + nearbyResourceAmount+";计时器最大值:" + timerMax);}private void Update(){timer -= Time.deltaTime; // 更新计时器if (timer <= 0f) // 检查计时器是否到达或超过最大值{timer += timerMax; // 重置计时器// 调用 ResourceManager 的 AddResource 方法,增加资源ResourceManager.Instance.AddResource(resourceGeneratorData.resourceType, 1);}}
}

效果
在这里插入图片描述

建筑物放置不可重叠

修改BuildingType,新增变量控制施工半径

public float minConstructionRadius; //最小施工半径

修改BuildingManager

private void Update()
{if (Input.GetMouseButtonDown(0) && !EventSystem.current.IsPointerOverGameObject()){//测试打印Debug.Log(CanSpawnBuilding(activeBuildingType, Utilsclass.GetMouseWorldPosition()));if(activeBuildingType.prefab != null && CanSpawnBuilding(activeBuildingType, Utilsclass.GetMouseWorldPosition())){// 在鼠标点击位置创建一个建筑实例Instantiate(activeBuildingType.prefab, Utilsclass.GetMouseWorldPosition(), Quaternion.identity);}}
}/// <summary>
/// 检查是否可以在给定位置生成建筑物
/// </summary>
/// <param name="buildingType">要生成的建筑物类型</param>
/// <param name="position">生成建筑物的位置</param>
/// <returns>如果可以生成建筑物,则返回 true,否则返回 false</returns>
private bool CanSpawnBuilding(BuildingType buildingType, Vector3 position)
{// 获取建筑物预制体的碰撞器BoxCollider2D boxCollider2D = buildingType.prefab.GetComponent<BoxCollider2D>();// 在指定位置使用盒形检测获取所有重叠的碰撞体Collider2D[] collider2DArray = Physics2D.OverlapBoxAll(position + (Vector3)boxCollider2D.offset, boxCollider2D.size, 0);// 判断是否有其他碰撞体与要生成的建筑物重叠,如果有则返回 falsebool isAreaClear = collider2DArray.Length == 0;if (!isAreaClear){return false;}// 在指定位置使用圆形检测获取所有在最小施工半径内的碰撞体collider2DArray = Physics2D.OverlapCircleAll(position, buildingType.minConstructionRadius);// 遍历所有与最小施工半径内碰撞的碰撞体foreach (Collider2D collider2D in collider2DArray){// 获取碰撞体上的 BuildingTypeHolder 组件BuildingTypeHolder buildingTypeHolder = collider2D.GetComponent<BuildingTypeHolder>();// 如果碰撞体上有 BuildingTypeHolder 组件if (buildingTypeHolder != null){// 检查该建筑物的类型是否与要生成的建筑物类型相同,如果是则返回 falseif (buildingTypeHolder.buildingType == buildingType){return false;}}}// 如果以上条件都满足,则可以生成建筑物,返回 truereturn true;
}

效果
在这里插入图片描述

创建一个总部

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

建造后实时显示生产速率

待续

建造前实时显示生产速率

建造消耗材料

错误提示

建筑生命值

设置敌人

源码

为了防止大家变懒,源码就不提供了,大家直接可以照着文章思路进行学习

完结

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

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

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

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

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

相关文章

基于SSM的旅游攻略网站设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

构建捡垃圾机器人的 ROS 2 项目

一、说明 本系列是关于学习如何使用 ROS2、Docker 和 Github 设计、设置和维护机器人项目。 先决条件 — ROS2 软件包的基本知识、实现发布者、订阅者、操作并连接它们。 我们之前在 ROS2 中了解了不同的部分。但是&#xff0c;在我们转向实际的基于硬件的项目之前&#xff0c;…

数字IC前端学习笔记:数字乘法器的优化设计(华莱士树乘法器)

相关阅读 数字IC前端https://blog.csdn.net/weixin_45791458/category_12173698.html?spm1001.2014.3001.5482 进位保留乘法器依旧保留着阵列的排列规则&#xff0c;只是进位是沿斜下角&#xff0c;如果能使用树形结构来规划这些进位保留加法器&#xff0c;就能获得更短的关键…

计算机网络八股

1、请你说说TCP和UDP的区别 TCP提供面向连接的可靠传输&#xff0c;UDP提供面向无连接的不可靠传输。UDP在很多实时性要求高的场景有很好的表现&#xff0c;而TCP在要求数据准确、对速度没有硬件要求的场景有很好的表现。TCP和UDP都是传输层协议&#xff0c;都是为应用层程序服…

树上游走最优策略问题:Cf1725J

https://codeforces.com/contest/1725/problem/J 首先要转化题目 发现题目本质是什么 不用回去 少走一条路径 传送 少走另一条路径 一开始猜的结论是这样 但这并不完整 传送本质是让我们把某些路径少走一遍 考虑这种情况&#xff0c;交于1点 #include<bits/stdc.…

gitgitHub

在git中复制CtrlInsert、粘贴CtrlShif 一、用户名和邮箱的配置 查看用户名 &#xff1a;git config user.name 查看密码&#xff1a; git config user.password 查看邮箱&#xff1a;git config user.email 查看配置信息&#xff1a; $ git config --list 修改用户名 git co…

Puppeteer基础知识(一)

Puppeteer基础知识&#xff08;一&#xff09; Puppeteer基础知识&#xff08;一&#xff09;一、简介二、其他一些自动化测试工具三、Puppeteer常用命令四、常见问题解决&#xff1a; 一、简介 Puppeteer 是一个强大而灵活的工具&#xff0c;可以用于网页爬虫、自动化测试、性…

不死马的利用与克制(基于条件竞争)及变种不死马

不死马即内存马&#xff0c;它会写进进程里&#xff0c;并且无限地在指定目录中生成木马文件 这里以PHP不死马为例 测试代码&#xff1a; <?phpignore_user_abort(true);set_time_limit(0);unlink(__FILE__);$file .test.php;$code <?php if(md5($_GET["pass…

Vue中如何进行数据可视化雷达图展示

在Vue中进行数据可视化雷达图展示 数据可视化是将数据以图形方式呈现的过程&#xff0c;雷达图是其中一种常用的图表类型&#xff0c;用于可视化多个维度的数据。Vue.js作为一个流行的JavaScript框架&#xff0c;提供了许多工具和库来实现数据可视化。本文将介绍如何使用Vue来…

华为云云耀云服务器L实例评测|SpringCloud相关组件——nacos和sentinel的安装和配置 运行内存情况 服务器被非法登陆尝试的解决

前言 最近华为云云耀云服务器L实例上新&#xff0c;也搞了一台来玩&#xff0c;期间遇到各种问题&#xff0c;在解决问题的过程中学到不少和运维相关的知识。 本篇博客介绍SpringCloud相关组件——nacos和sentinel的安装和配置&#xff0c;并分析了运行内存情况&#xff0c;此…

【ARM】(1)架构简介

前言 ARM既可以认为是一个公司的名字&#xff0c;也可以认为是对一类微处理器的通称&#xff0c;还可以认为是一种技术的名字。 ARM公司是专门从事基于RISC技术芯片设计开发的公司&#xff0c;作为知识产权&#xff08;IP&#xff09;供应商&#xff0c;本身不直接从事芯片生产…

Hive 【Hive(七)窗口函数练习】

窗口函数案例 数据准备 1&#xff09;建表语句 create table order_info (order_id string, --订单iduser_id string, -- 用户iduser_name string, -- 用户姓名order_date string, -- 下单日期order_amount int -- 订单金额 ); 2&#xff09;装载语句 i…

C++设计模式-装饰器(Decorator)

目录 C设计模式-装饰器&#xff08;Decorator&#xff09; 一、意图 二、适用性 三、结构 四、参与者 五、代码 C设计模式-装饰器&#xff08;Decorator&#xff09; 一、意图 动态地给一个对象添加一些额外的职责。就增加功能来说&#xff0c;Decorator模式相比生成子…

C++设计模式-工厂模式(Factory Method)

目录 C设计模式-工厂模式&#xff08;Factory Method&#xff09; 一、意图 二、适用性 三、结构 四、参与者 五、代码 C设计模式-工厂模式&#xff08;Factory Method&#xff09; 一、意图 定义一个用于创建对象的接口&#xff0c;让子类决定实例化哪一个类。Factory…

R实现数据分布特征的视觉化——多笔数据之间的比较

大家好&#xff0c;我是带我去滑雪&#xff01; 如果要对两笔数据或者多笔数据的分布情况进行比较&#xff0c;Q-Q图、柱状图、星形图都是非常好的选择&#xff0c;下面开始实战。 &#xff08;1&#xff09;绘制Q-Q图 首先导入数据bankwage.csv文件&#xff0c;该数据集…

【深蓝学院】手写VIO第2章--IMU传感器--作业

这次作业坑很多&#xff0c;作业说明的不清楚&#xff0c;摸索了很长时间才将此次作业完成&#xff0c;在这里进行记录。 1. T1 1.1 题干 1.2 解答 1.2.1 法1&#xff0c;ros related方法 不知道为什么我的launch不了&#xff0c;在imu_utils目录下面建立build后&#xff0…

【redis学习笔记】缓存

redis主要的三个应用场景 存储数据缓存消息队列&#xff08;redis本来是设计用来作为消息队列的&#xff09; redis常用作mysql的缓存 因为MySQL等数据库&#xff0c;效率比较低&#xff0c;所以承担的并发量就有限。一旦请求数量多了&#xff0c;数据库的压力就会很大&#…

代码随想录第36天 | 1049. 最后一块石头的重量 II ● 494. 目标和 ● 474.一和零

1049. 最后一块石头的重量 第一想法 /*** param {number[]} stones* return {number}*/ var lastStoneWeightII function (nums) {// 和分割两个和相等的子数组一样//dp[j]表示 背包总容量&#xff08;所能装的总重量&#xff09;是j&#xff0c;放进物品后&#xff0c;背的…

数据科学最佳实践:Kedro 的工程化解决方案 | 开源日报 No.47

leonardomso/33-js-concepts Stars: 58.4k License: MIT 这个项目是一个帮助开发者掌握 JavaScript 概念的资源库。该项目基于 Stephen Curtis 撰写的一篇文章&#xff0c;包含了对 33 个重要 JavaScript 概念全面深入地讲解&#xff0c;并被 GitHub 评为 2018 年最佳开源项目…

前端项目nginx部署

进入nginx下载地址:https://nginx.org/ 下载完安装包以后,解压在D盘中 双击进去> 将前端打包好的文件放在nginx的html文件夹中 可能80端口会被系统所占用 我们可以在nginx的conf文件夹中的nginx.conf文件中修改80为90 之后我们就可以在任务管理器中看到了 然后 localhost:…