文章目录
- 前言
- 素材
- 新建项目
- 放置物品
- 放置不同物品类型
- 资源管理
- 管理和配置生成资源的信息
- 绘制资源UI
- 同步资源生成
- 绘制地图,优化场景
- 控制虚拟相机
- 添加建筑物按钮UI
- UI上放置建筑问题修复
- 添加点击事件
- 选中效果
- 箭头空物体效果
- 建造跟随鼠标显示
- 添加资源物体
- 实现树叶的随风摇摆
- 按附近资源数控制资源生成速度
- 建筑物放置不可重叠
- 创建一个总部
- 添加一些动画粒子效果
- 建造后实时显示生产速率
- 建造前实时显示生产速率
- 建造消耗材料
- 提示信息
- 错误提示信息
- 建筑生命值
- 设置敌人
- 创建防御箭塔
- 敌人血条和死亡
- 控制敌人生成
- 敌人抖动卡顿问题
- 给总部默认添加攻击功能
- 显示敌人生成位置
- 显示波数
- 敌人进攻方向提示
- 添加后处理效果
- 使用ShaderGraph实现发光效果
- 昼夜交替循环效果
- 建造时间
- 显示建造进度
- 图片跟着修改
- 建造进度
- 删除建筑
- 修复建筑
- 源码
- 完结
前言
欢迎来到本次教程,我将为您讲解如何使用 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;
}
效果
创建一个总部
效果
添加一些动画粒子效果
具体的效果可以按自己喜欢去添加
效果
建造后实时显示生产速率
绘制速率显示UI
效果
修改ResourceGeneratorData
public ResourceGeneratorData GetResourceGeneratorData(){// 返回资源生成器的数据return resourceGeneratorData;}public float GetTimerNormalized(){// 返回计时器的归一化值,即当前计时器值除以计时器最大值return timer / timerMax;}public float GetAmountGeneratedPerSecond(){// 返回每秒生成的数量,即 1 除以计时器最大值return 1 / timerMax;}
新增代码ResourceGeneratorOverlay,挂载在效率模板上
public class ResourceGeneratorOverlay : MonoBehaviour
{[SerializeField]private ResourceGenerator resourceGenerator;private Transform barTransform;private void Start(){// 获取资源生成器的数据ResourceGeneratorData resourceGeneratorData = resourceGenerator.GetResourceGeneratorData();// 查找并设置进度条的 TransformbarTransform = transform.Find("bar");// 查找并设置图标的 SpriteRenderertransform.Find("icon").GetComponent<SpriteRenderer>().sprite = resourceGeneratorData.resourceType.sprite;// 查找并设置文本的 TextMeshPro 组件,显示每秒生成的数量(保留一位小数)transform.Find("text").GetComponent<TextMeshPro>().SetText(resourceGenerator.GetAmountGeneratedPerSecond().ToString("F1"));}private void Update(){// 更新进度条的缩放比例,根据当前计时器的归一化值确定barTransform.localScale = new Vector3(resourceGenerator.GetTimerNormalized(), barTransform.localScale.y, 1);}
}
效果
建造前实时显示生产速率
绘制UI
修改ResourceGenerator
public static int GetNearbyResourceAmount(ResourceGeneratorData resourceGeneratorData,Vector3 position){// 获取附近的资源节点数量Collider2D[] collider2DArray = Physics2D.OverlapCircleAll(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);return nearbyResourceAmount;
}
新增ResourceNearbyOverlay脚本,挂载在UI上
using UnityEngine;
using TMPro;public class ResourceNearbyOverlay : MonoBehaviour
{private ResourceGeneratorData resourceGeneratorData;private void Awake(){Hide();}private void Update(){// 获取附近资源的数量int nearbyResourceAmount = ResourceGenerator.GetNearbyResourceAmount(resourceGeneratorData, transform.position);// 计算资源数量占最大资源量的百分比,并取整数值float percent = Mathf.RoundToInt((float)nearbyResourceAmount / resourceGeneratorData.maxResourceAmount * 100f);// 在界面上显示百分比文本transform.Find("text").GetComponent<TextMeshPro>().SetText(percent + "%");}public void Show(ResourceGeneratorData resourceGeneratorData){// 记录资源生成器的数据,以便后续使用this.resourceGeneratorData = resourceGeneratorData;// 激活显示该界面gameObject.SetActive(true);// 设置图标的 SpriteRenderertransform.Find("icon").GetComponent<SpriteRenderer>().sprite = resourceGeneratorData.resourceType.sprite;}public void Hide(){// 隐藏该界面gameObject.SetActive(false);}
}
修改BuildingGhost,调用
private ResourceNearbyOverlay resourceNearbyOverlay;resourceNearbyOverlay = transform.Find("效率模板").GetComponent<ResourceNearbyOverlay>();//处理BuildingManager中的事件
private void BuildingManager_OnActiveBuildingTypeChanged(object sender, BuildingManager.OnActiveBuildingTypeChangedEventArgs e)
{if (e.activeBuildingType.prefab == null){Hide();resourceNearbyOverlay.Hide();}else{Show(e.activeBuildingType.sprite);resourceNearbyOverlay.Show(e.activeBuildingType.resourceGeneratorData);}}
效果
建造消耗材料
新增消耗资源配置脚本
[System.Serializable]
public class ResourceAmount
{// 资源类型public ResourceType resourceType;// 消耗资源数量public int amount;
}
修改BuildingType
public ResourceAmount[] constructionResourceCostArray;
修改配置
修改ResourceManager
//判断资源是否够
public bool CanAfford(ResourceAmount[]resourceAmountArray){// 遍历所有资源类型和数量foreach (ResourceAmount resourceAmount in resourceAmountArray){if (GetResourceAmount(resourceAmount.resourceType) < resourceAmount.amount){// 支付不起该资源,返回 falsereturn false; }}// 所有资源的数量都足够支付,返回 truereturn true;
}//减少对应资源
public void SpendResources(ResourceAmount[] resourceAmountArray){// 遍历所有资源类型和数量foreach (ResourceAmount resourceAmount in resourceAmountArray){// 减少对应资源类型的数量resourceAmountDictionary[resourceAmount.resourceType] -= resourceAmount.amount;}
}
修改BuildingManager,调用
private void Update()
{if (Input.GetMouseButtonDown(0) && !EventSystem.current.IsPointerOverGameObject()){if(activeBuildingType.prefab != null && CanSpawnBuilding(activeBuildingType, Utilsclass.GetMouseWorldPosition())){if (ResourceManager.Instance.CanAfford(activeBuildingType.constructionResourceCostArray)){ResourceManager.Instance.SpendResources(activeBuildingType.constructionResourceCostArray);// 在鼠标点击位置创建一个建筑实例Instantiate(activeBuildingType.prefab, Utilsclass.GetMouseWorldPosition(), Quaternion.identity);}}}
}
效果,资源够了才可以建造
提示信息
绘制UI
新增TooltipUI代码,挂载在提示脚本,实现提示跟随鼠标和提示文本
using UnityEngine;
using TMPro;public class TooltipUI : MonoBehaviour
{private RectTransform rectTransform;// 文本组件,用于显示提示文字private TextMeshProUGUI textMeshPro;// 背景 UI 布局组件private RectTransform backgroundRectTransform;public static TooltipUI Instance {get;private set;}private void Awake(){Instance = this;// 查找并获取文本组件和背景 UI 布局组件textMeshPro = transform.Find("text").GetComponent<TextMeshProUGUI>();backgroundRectTransform = transform.Find("background").GetComponent<RectTransform>();rectTransform = GetComponent<RectTransform>();// 显示示例提示文字SetText("Hi there!");// Hide();}private void Update(){// 获取鼠标当前坐标Vector2 mousePosition = Input.mousePosition;// 将鼠标坐标转换为Canvas内的坐标RectTransform canvasRectTransform = rectTransform.parent as RectTransform;Vector2 canvasPosition;RectTransformUtility.ScreenPointToLocalPointInRectangle(canvasRectTransform, mousePosition, null, out canvasPosition);// 设置UI物体的位置为鼠标位置rectTransform.localPosition = canvasPosition;}private void SetText(string tooltipText){// 设置提示文字内容textMeshPro.SetText(tooltipText);// 强制更新文本组件网格textMeshPro.ForceMeshUpdate();// 获取文本组件渲染后的尺寸Vector2 textSize = textMeshPro.GetRenderedValues(false);// 根据渲染后的尺寸更新背景 UI 布局组件的尺寸 加点高度美化backgroundRectTransform.sizeDelta = textSize + new Vector2(8, 8);}public void Show(string tooltipText){gameObject.SetActive(true);SetText(tooltipText);}public void Hide(){gameObject.SetActive(false);}
}
效果
新增MouseEnterExitEvents,定义鼠标UI事件
using System;
using UnityEngine;
using UnityEngine.EventSystems;public class MouseEnterExitEvents : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{// 当鼠标进入UI物体时触发的事件public event EventHandler OnMouseEnter;// 当鼠标离开UI物体时触发的事件public event EventHandler OnMouseExit;// 当鼠标进入UI物体时调用该方法public void OnPointerEnter(PointerEventData eventData){// 如果有订阅事件的方法存在,则触发鼠标进入事件OnMouseEnter?.Invoke(this, EventArgs.Empty);}// 当鼠标离开UI物体时调用该方法public void OnPointerExit(PointerEventData eventData){// 如果有订阅事件的方法存在,则触发鼠标离开事件OnMouseExit?.Invoke(this, EventArgs.Empty);}
}
修改BuildingTypeSelectUI,测试调用
// 遍历建筑类型列表,创建对应的按钮
foreach (BuildingType buildingType in buildingTypeList.buildingTypeList)
{MouseEnterExitEvents mouseEnterExitEvents = btnTransform.GetComponent<MouseEnterExitEvents>();mouseEnterExitEvents.OnMouseEnter += (object sender,EventArgs e)=>{TooltipUI.Instance.Show(buildingType.nameString);};mouseEnterExitEvents.OnMouseExit += (object sender,EventArgs e)=>{TooltipUI.Instance.Hide();};
}
效果
修改BuildingType,新增建造需要的资源文本拼接
public string GetConstructionResourceCoststring(){string str ="";foreach (ResourceAmount resourceAmount in constructionResourceCostArray){str += resourceAmount.resourceType.nameString + ":" + resourceAmount.amount;}return str;
}
调用,修改BuildingTypeSelectUI
MouseEnterExitEvents mouseEnterExitEvents = btnTransform.GetComponent<MouseEnterExitEvents>();
mouseEnterExitEvents.OnMouseEnter += (object sender,EventArgs e)=>{TooltipUI.Instance.Show(buildingType.nameString + "\n" + buildingType.GetConstructionResourceCoststring());
};
mouseEnterExitEvents.OnMouseExit += (object sender,EventArgs e)=>{TooltipUI.Instance.Hide();
};
效果
问题,鼠标悬浮时会发现提示框闪烁,原因就是提示框遮挡了UI按钮
解决方法就是去除提示框的射线检测
效果
优化,不同资源显示不同的颜色
修改ResourceType
public string colorHex; // 提示颜色
修改BuildingType,提示资源加入颜色
public string GetConstructionResourceCoststring(){string str ="";foreach (ResourceAmount resourceAmount in constructionResourceCostArray){str += "<color=#"+resourceAmount.resourceType.colorHex+ ">" + resourceAmount.resourceType.nameString + ":" + resourceAmount.amount + "</color>";}return str;
}
效果
错误提示信息
修改BuildingManager
private void Update()
{if (Input.GetMouseButtonDown(0) && !EventSystem.current.IsPointerOverGameObject()){if(activeBuildingType.prefab != null){if(CanSpawnBuilding(activeBuildingType, Utilsclass.GetMouseWorldPosition(), out string errorMessage)){if (ResourceManager.Instance.CanAfford(activeBuildingType.constructionResourceCostArray)){ResourceManager.Instance.SpendResources(activeBuildingType.constructionResourceCostArray);// 在鼠标点击位置创建一个建筑实例Instantiate(activeBuildingType.prefab, Utilsclass.GetMouseWorldPosition(), Quaternion.identity);}}else{TooltipUI.Instance.Show(errorMessage);}}}
}private bool CanSpawnBuilding(BuildingType buildingType, Vector3 position, out string errorMessage)
{// 获取建筑物预制体的碰撞器BoxCollider2D boxCollider2D = buildingType.prefab.GetComponent<BoxCollider2D>();// 在指定位置使用盒形检测获取所有重叠的碰撞体Collider2D[] collider2DArray = Physics2D.OverlapBoxAll(position + (Vector3)boxCollider2D.offset, boxCollider2D.size, 0);// 判断是否有其他碰撞体与要生成的建筑物重叠,如果有则返回 falsebool isAreaClear = collider2DArray.Length == 0;if (!isAreaClear){errorMessage = "区域重叠!";return false;}errorMessage = "";// 如果以上条件都满足,则可以生成建筑物,返回 truereturn true;
}
效果
提示在指定时间消失
修改TooltipUI
private TooltipTimer tooltipTimer;private void Update(){// 。。。if (tooltipTimer != null){tooltipTimer.timer -= Time.deltaTime;if (tooltipTimer.timer <= 0) Hide();}
}public class TooltipTimer{public float timer;
}public void Show(string tooltipText, TooltipTimer tooltipTimer = null){this.tooltipTimer = tooltipTimer;gameObject.SetActive(true);SetText(tooltipText);
}
修改BuildingManager
private void Update()
{if (Input.GetMouseButtonDown(0) && !EventSystem.current.IsPointerOverGameObject()){if(activeBuildingType.prefab != null){if(CanSpawnBuilding(activeBuildingType, Utilsclass.GetMouseWorldPosition(), out string errorMessage)){if (ResourceManager.Instance.CanAfford(activeBuildingType.constructionResourceCostArray)){ResourceManager.Instance.SpendResources(activeBuildingType.constructionResourceCostArray);// 在鼠标点击位置创建一个建筑实例Instantiate(activeBuildingType.prefab, Utilsclass.GetMouseWorldPosition(), Quaternion.identity);}else{TooltipUI.Instance.Show("资源不够 " + activeBuildingType.GetConstructionResourceCoststring(), new TooltipUI.TooltipTimer{timer = 2f});}}else{TooltipUI.Instance.Show(errorMessage, new TooltipUI.TooltipTimer{timer = 2f});}}}
}
效果
建筑生命值
绘制血条UI
修改BuildingType
public int healthAmountMax;//最大生命值
添加HealthSystem脚本,生命值事件
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class HealthSystem : MonoBehaviour
{public event EventHandler OnDamaged; // 受伤事件public event EventHandler OnDied; // 死亡事件private int healthAmountMax; // 最大生命值private int healthAmount; // 当前生命值//受伤public void Damage(int damageAmount){healthAmount -= damageAmount; // 减少生命值healthAmount = Mathf.Clamp(healthAmount, 0, healthAmountMax); // 限制生命值在0和最大生命值之间OnDamaged?.Invoke(this, EventArgs.Empty); // 触发受伤事件if (IsDead()){OnDied?.Invoke(this, EventArgs.Empty); // 触发死亡事件}}private bool IsDead(){return healthAmount <= 0; // 判断是否死亡}public int GetHealthAmount(){return healthAmount; // 获取当前生命值}public float GetHealthAmountNormalized(){return (float)healthAmount / healthAmountMax; // 获取当前生命值的归一化值}//是否满血public bool IsFullHealth(){return healthAmount == healthAmountMax;}public void SetHealthAmountMax(int healthAmountMax, bool updateHealthAmount){this.healthAmountMax = healthAmountMax; // 设置最大生命值if (updateHealthAmount){healthAmount = healthAmountMax; // 如果需要更新当前生命值,将当前生命值设置为最大生命值}}
}
添加HealthBar,控制血条UI
using UnityEngine;public class HealthBar : MonoBehaviour
{[SerializeField]private HealthSystem healthSystem;private Transform barTransform;private void Awake(){barTransform = transform.Find("bar"); // 获取bar的Transform组件}private void Start(){healthSystem.OnDamaged += HealthSystem_OnDamaged; // 订阅受伤事件UpdateBar();UpdateHealthBarVisible();}private void HealthSystem_OnDamaged(object sender, System.EventArgs e){UpdateBar();UpdateHealthBarVisible();}private void UpdateBar(){barTransform.localScale = new Vector3(healthSystem.GetHealthAmountNormalized(), 1, 1); // 更新血条的缩放比例}private void UpdateHealthBarVisible(){if (healthSystem.IsFullHealth()){gameObject.SetActive(false); // 如果满血,则隐藏血条}else{gameObject.SetActive(true); // 如果不满血,则显示血条}}
}
新增Building脚本,控制建造受伤测试
using UnityEngine;public class Building : MonoBehaviour
{private BuildingType buildingType;private HealthSystem healthSystem;private void Start(){buildingType = GetComponent<BuildingTypeHolder>().buildingType; // 获取建筑类型healthSystem = GetComponent<HealthSystem>(); // 获取HealthSystem组件healthSystem.SetHealthAmountMax(buildingType.healthAmountMax, true); // 设置最大生命值healthSystem.OnDied += HealthSystem_OnDied; // 订阅死亡事件}private void Update(){if (Input.GetKeyDown(KeyCode.T)){healthSystem.Damage(10); // 测试受伤}}private void HealthSystem_OnDied(object sender, System.EventArgs e){Destroy(gameObject); // 销毁游戏对象}
}
运行效果
设置敌人
修改BuildingManager,获取总部Building组件
[SerializeField]private Building hqBuilding;//总部Building组件public Building GetHQBuilding(){return hqBuilding;
}
新建敌人脚本
using UnityEngine;public class Enemy : MonoBehaviour
{private Rigidbody2D rigidbody2d; // 敌人刚体组件private Transform targetTransform; // 目标建筑物的Transform组件,即敌人要攻击的建筑物private float lookForTargetTimer; // 查找目标的计时器private float lookForTargetTimerMax = 0.2f; // 查找目标的时间间隔// 创建一个新的敌人public static Enemy Create(Vector3 position){Transform pfEnemy = Resources.Load<Transform>("Enemy"); // 加载敌人预制体资源Transform enemyTransform = Instantiate(pfEnemy, position, Quaternion.identity); // 在指定位置生成敌人Enemy enemy = enemyTransform.GetComponent<Enemy>(); // 获取敌人脚本组件return enemy;}// 初始化敌人private void Start(){rigidbody2d = GetComponent<Rigidbody2D>(); // 获取敌人刚体组件targetTransform = BuildingManager.Instance.GetHQBuilding().transform; // 设置敌人攻击的目标建筑物为总部lookForTargetTimer = Random.Range(0f, lookForTargetTimerMax); // 随机设置查找目标的计时器}private void Update(){HandleMovement(); // 处理敌人的移动HandleTargeting(); // 处理敌人的目标选择}// 处理敌人的移动private void HandleMovement(){if (targetTransform != null) // 如果有目标,则向目标方向移动{Vector3 moveDir = (targetTransform.position - transform.position).normalized; // 计算向目标方向的移动向量float moveSpeed = 6f; // 移动速度rigidbody2d.velocity = moveDir * moveSpeed; // 更新敌人刚体组件的速度}else // 如果没有目标,则停止移动{rigidbody2d.velocity = Vector2.zero;}}// 处理敌人的目标选择private void HandleTargeting(){lookForTargetTimer -= Time.deltaTime; // 减去帧时间,更新查找目标的计时器if (lookForTargetTimer <= 0f) // 如果计时器结束了,则开始查找并选择新的目标{lookForTargetTimer += lookForTargetTimerMax; // 重置计时器LookForTargets(); // 查找目标}}// 当敌人与其他物体碰撞时触发的事件private void OnCollisionEnter2D(Collision2D collision){Building building = collision.gameObject.GetComponent<Building>(); // 获取碰撞到的建筑物if (building != null) // 如果碰撞到的是建筑物{// 减少建筑物的生命值,并销毁自身HealthSystem healthSystem = building.GetComponent<HealthSystem>();healthSystem.Damage(10);Destroy(gameObject);}}// 查找目标建筑物private void LookForTargets(){float targetMaxRadius = 10f; // 查找的最大半径Collider2D[] collider2DArray = Physics2D.OverlapCircleAll(transform.position, targetMaxRadius); // 在指定位置半径内查找碰撞体foreach (Collider2D collider2D in collider2DArray){Building building = collider2D.GetComponent<Building>(); // 获取碰撞体上的建筑物组件if (building != null) // 如果这是一个建筑物{// 判断是否是更优的攻击目标if (targetTransform == null){targetTransform = building.transform; // 如果原来没有目标,则更新为当前建筑物}else if (Vector3.Distance(transform.position, building.transform.position) <Vector3.Distance(transform.position, targetTransform.position)){targetTransform = building.transform; // 如果距离更近,则更新为当前建筑物}}}if (targetTransform == null) // 如果没有找到目标,则将目标设置为总部{targetTransform = BuildingManager.Instance.GetHQBuilding().transform;}}
}
修改UtilsClass,生成随机偏移量
public static Vector3 GetRandomDir(){// 生成一个随机方向的单位向量return new Vector3(Random.Range(-1f,1f), Random.Range(-1f,1f)).normalized;
}
生成敌人测试,修改BuildingManager
if (Input.GetKeyDown(KeyCode.T)){Vector3 enemySpawnPosition = UtilsClass.GetMouseWorldPosition() + UtilsClass.GetRandomDir() * 5f;Enemy.Create(enemySpawnPosition);
}
效果
创建防御箭塔
新建塔的脚本,主要功能是查找目标敌人并进行攻击。LookForTargets()方法用于在一定范围内查找敌人,通过遍历触发器碰撞体数组来获取目标敌人的组件,然后根据距离判断是否是更优的攻击目标。HandleTargeting()方法用于处理目标选择,其中有一个计时器来控制目标查找的时间间隔。Update()方法每帧调用HandleTargeting()方法来更新目标选择。
using UnityEngine;public class Tower : MonoBehaviour
{private Enemy targetEnemy; // 当前目标敌人private float lookForTargetTimer; // 目标查找计时器private float lookForTargetTimerMax = 0.2f; // 目标查找计时器最大值private Vector3 projectileSpawnPosition; // 子弹生成位置private float shootTimer; // 射击计时器[SerializeField] private float shootTimerMax; // 射击计时器最大值private void Awake(){projectileSpawnPosition = transform.Find("projectileSpawnPosition").position; // 找到子弹生成位置的Transform组件,并获取其坐标}// 每帧执行一次private void Update(){HandleTargeting(); // 处理目标选择HandleShooting(); // 处理射击}// 查找目标敌人private void LookForTargets(){float targetMaxRadius = 20f; // 查找的最大半径Collider2D[] collider2DArray = Physics2D.OverlapCircleAll(transform.position, targetMaxRadius); // 在指定位置半径内查找碰撞体foreach (Collider2D collider2D in collider2DArray) // 遍历所有的碰撞体{Enemy enemy = collider2D.GetComponent<Enemy>(); // 获取碰撞到的敌人组件if (enemy != null) // 如果这是一个敌人{// 判断是否是更优的攻击目标if (targetEnemy == null) // 如果原来没有目标,则更新为当前敌人{targetEnemy = enemy;}else if (Vector3.Distance(transform.position, enemy.transform.position) <Vector3.Distance(transform.position, targetEnemy.transform.position)) // 如果距离更近,则更新为当前敌人{targetEnemy = enemy;}}}}// 处理目标选择private void HandleTargeting(){lookForTargetTimer -= Time.deltaTime; // 减去帧时间,更新目标查找计时器if (lookForTargetTimer <= 0f) // 如果计时器结束了,则开始查找并选择新的目标{lookForTargetTimer += lookForTargetTimerMax; // 重置计时器LookForTargets(); // 查找目标}}// 处理射击private void HandleShooting(){shootTimer -= Time.deltaTime; // 减去帧时间,更新射击计时器if (shootTimer <= 0f) // 如果计时器结束了,则开始射击{shootTimer += shootTimerMax; // 重置计时器if (targetEnemy != null) ArrowProjectile.Create(projectileSpawnPosition, targetEnemy); // 创建箭头实例}}
}
修改UtilsClass,根据给定的向量计算角度(弧度转换为角度)
public static float GetAngleFromVector(Vector3 vector){// 根据给定的向量计算角度(弧度转换为角度)float radians = Mathf.Atan2(vector.y, vector.x);float degrees = radians * Mathf.Rad2Deg;return degrees;
}
箭头的脚本,主要功能是沿着目标敌人的方向飞行并对其造成伤害。Update()方法用于在每帧根据目标敌人的位置计算移动方向和速度,并更新箭头的位置。SetTarget()方法用于设置目标敌人的组件。OnTriggerEnter2D()方法用于检测碰到的对象是否是敌人,如果是则销毁箭头。Create()方法是静态方法,用于创建箭头实例,并设置目标敌人组件。
using UnityEngine;public class ArrowProjectile : MonoBehaviour
{private Enemy targetEnemy; // 目标敌人的组件private Vector3 lastMoveDir; // 上一次移动方向private float timeToDie = 2f; // 存活时间public static ArrowProjectile Create(Vector3 position, Enemy enemy){Transform pfArrowProjectile = Resources.Load<Transform>("箭"); // 加载箭头预制体Transform arrowTransform = Instantiate(pfArrowProjectile, position, Quaternion.identity); // 实例化箭头对象ArrowProjectile arrowProjectile = arrowTransform.GetComponent<ArrowProjectile>(); // 获取箭头的脚本组件arrowProjectile.SetTarget(enemy); // 设置目标敌人return arrowProjectile;}// 在每帧更新位置private void Update(){Vector3 moveDir;if (targetEnemy != null) // 如果有目标敌人{moveDir = (targetEnemy.transform.position - transform.position).normalized; // 计算当前移动方向lastMoveDir = moveDir; // 更新上一次移动方向}else{moveDir = lastMoveDir; // 没有目标敌人时继续使用上一次的移动方向}float moveSpeed = 20f; // 移动速度transform.position += moveDir * moveSpeed * Time.deltaTime; // 根据移动方向、速度和时间更新位置transform.eulerAngles = new Vector3(0, 0, UtilsClass.GetAngleFromVector(moveDir)); // 根据移动方向更新旋转角度timeToDie -= Time.deltaTime; // 减去帧时间,更新存活时间if (timeToDie < 0f){Destroy(gameObject); // 存活时间结束时销毁箭头对象}}// 设置目标敌人private void SetTarget(Enemy targetEnemy){this.targetEnemy = targetEnemy;}// 当进入触发器时检查碰到的对象是否是敌人private void OnTriggerEnter2D(Collider2D collision){Enemy enemy = collision.GetComponent<Enemy>(); // 获取触发器内的碰撞体中的敌人组件if (enemy != null) // 如果触发器碰到的是敌人,则销毁箭头{Destroy(gameObject);}}
}
效果
敌人血条和死亡
绘制敌人血条UI
添加脚本
修改ArrowProjectile,攻击敌人掉血
// 当进入触发器时检查碰到的对象是否是敌人
private void OnTriggerEnter2D(Collider2D collision)
{Enemy enemy = collision.GetComponent<Enemy>(); // 获取触发器内的碰撞体中的敌人组件if (enemy != null) // 如果触发器碰到的是敌人,则销毁箭头{//攻击敌人int damageAmount = 10;enemy.GetComponent<HealthSystem>().Damage(damageAmount);Destroy(gameObject);}
}
修改Enemy,添加敌人死亡事件
private HealthSystem healthSystem;// 初始化敌人
private void Start()
{healthSystem = GetComponent<HealthSystem>();healthSystem.OnDied += HealthSystem_OnDied;
}private void HealthSystem_OnDied(object sender,System.EventArgs e){Destroy(gameObject);
}
效果
控制敌人生成
添加敌人生成脚本
using System.Collections.Generic;
using UnityEngine;public class EnemyWaveManager : MonoBehaviour
{private enum State{WaitingToSpawnNextWave, // 等待生成下一波敌人SpawningWave // 正在生成敌人波次}[SerializeField] private List<Transform> spawnPositionTransformList; // 敌人生成位置列表private State state; // 当前状态private int waveNumber; // 波次数private float nextWaveSpawnTimer; // 下一波生成计时器private float nextEnemySpawnTimer; // 下一个敌人生成计时器private int remainingEnemySpawnAmount; // 剩余敌人生成数量private Vector3 spawnPosition; // 当前生成位置private void Start(){state = State.WaitingToSpawnNextWave; // 初始状态为等待生成下一波敌人nextWaveSpawnTimer = 3f; // 下一波生成计时器初始值为3秒}private void Update(){switch (state){case State.WaitingToSpawnNextWave:nextWaveSpawnTimer -= Time.deltaTime; // 减少下一波生成计时器if (nextWaveSpawnTimer <= 0f) // 当计时器小于等于0时开始生成新的波次{SpawnWave();}break;case State.SpawningWave:if (remainingEnemySpawnAmount > 0){nextEnemySpawnTimer -= Time.deltaTime; // 减少下一个敌人生成计时器if (nextEnemySpawnTimer <= 0f) // 当计时器小于等于0时生成敌人,并更新剩余敌人生成数量{nextEnemySpawnTimer = Random.Range(0f, 2f);Enemy.Create(spawnPosition + UtilsClass.GetRandomDir() * Random.Range(0f, 10f));remainingEnemySpawnAmount--;}}if (remainingEnemySpawnAmount <= 0) // 当剩余敌人生成数量小于等于0时,切换回等待生成下一波敌人的状态{state = State.WaitingToSpawnNextWave;}break;}}private void SpawnWave(){spawnPosition = spawnPositionTransformList[Random.Range(0, spawnPositionTransformList.Count)].position; // 随机选择一个生成位置nextWaveSpawnTimer = 10f; // 下一波生成计时器为10秒remainingEnemySpawnAmount = 5 + 3 * waveNumber; // 剩余敌人生成数量根据波次数计算state = State.SpawningWave; // 设置当前状态为正在生成敌人波次waveNumber++; // 波次数递增}
}
效果
敌人抖动卡顿问题
修改敌人Rigidbody2D 组件插值
在 Unity 中,Rigidbody2D
组件可以用来模拟游戏对象的物理效果。而插值配置(Interpolate)则是其中一项非常有用的属性。它控制的是当游戏对象发生变化时,如何让物理引擎进行计算和渲染。
具体来说,插值配置决定了游戏对象在物理引擎的计算过程中如何与渲染器协同工作。通常情况下,我们希望游戏对象在移动、旋转等变换过程中,物理引擎能够尽快地进行计算并渲染到屏幕上,以保证游戏的流畅性和稳定性。但是,对于某些情况下的快速移动或大量碰撞等操作,物理引擎可能无法及时完成计算,导致游戏画面出现卡顿、抖动等问题。
这时候,插值配置就可以发挥作用了。通过对插值的设置,我们可以让物理引擎在计算时加入一些插值计算,把当前对象的状态与上一次渲染的状态进行插值,从而消除因物理计算和渲染时间不一致而导致的画面抖动和卡顿问题,从而提升游戏体验。
在 Unity 中,Rigidbody2D
组件的插值配置一共有以下三种可选项:
-
None:不进行插值计算,直接使用物理引擎输出的结果进行渲染;
-
Interpolate:使用线性插值计算,在物理引擎的计算结果与渲染器之间进行插值,从而实现平滑过渡效果;
-
Extrapolate:使用线性插值计算,但是在物理引擎无法及时计算出结果时,会自动对游戏对象的当前状态进行推测,从而实现更加平滑的运动过渡效果。不过需要注意的是,过度推测会带来精度误差,可能导致游戏表现不太符合预期。
综上所述,插值配置可以帮助我们通过插值计算消除游戏画面中的抖动和卡顿问题,提升游戏的流畅性和稳定性。而具体采用哪种插值模式,则要根据具体的场景需求和性能表现来选择。
给总部默认添加攻击功能
挂载Tower脚本即可
配置攻击点
效果
显示敌人生成位置
配置显示样式
修改EnemyWaveManager
[SerializeField]private Transform nextWaveSpawnPositionTransform;//生成位置指示nextWaveSpawnPositionTransform.position = spawnPosition;
效果,在敌人生成位置出现圆圈提示
显示波数
绘制UI
修改EnemyWaveManager
public event EventHandler OnWaveNumberChanged;// 波次编号变化事件private void SpawnWave()
{nextWaveSpawnTimer = 10f; // 下一波生成计时器为10秒remainingEnemySpawnAmount = 5 + 3 * waveNumber; // 剩余敌人生成数量根据波次数计算state = State.SpawningWave; // 设置当前状态为正在生成敌人波次waveNumber++; // 波次数递增OnWaveNumberChanged?.Invoke(this, EventArgs.Empty);// 触发波次编号变化事件
}public int GetWaveNumber(){// 返回当前波次数return waveNumber;
}public float GetNextWaveSpawnTimer(){// 返回下一波敌人生成计时器的剩余时间return nextWaveSpawnTimer;
}
新增EnemyWaveUI脚本,控制波数提示UI
using UnityEngine;
using TMPro;public class EnemyWaveUI : MonoBehaviour
{[SerializeField] private EnemyWaveManager enemyWaveManager; // 敌人波次管理器private TextMeshProUGUI waveNumberText; // 显示波次编号的文本private TextMeshProUGUI waveMessageText; // 显示波次消息的文本private void Awake(){// 获取波次编号文本和波次消息文本的引用waveNumberText = transform.Find("波数").GetComponent<TextMeshProUGUI>();waveMessageText = transform.Find("计时器").GetComponent<TextMeshProUGUI>();}private void Start(){// 订阅敌人波次管理器的波次编号变化事件enemyWaveManager.OnWaveNumberChanged += EnemyWaveManager_OnWaveNumberChanged;}private void EnemyWaveManager_OnWaveNumberChanged(object sender, System.EventArgs e){// 当波次编号发生变化时,更新波次编号文本显示SetWaveNumberText("波数 " + enemyWaveManager.GetWaveNumber());}private void Update(){float nextWaveSpawnTimer = enemyWaveManager.GetNextWaveSpawnTimer();if (nextWaveSpawnTimer < 0f){// 如果下一波次生成的计时器小于0,则清空波次消息文本SetMessageText("");}else{// 显示下一波次生成的计时器SetMessageText("下波倒计时 " + nextWaveSpawnTimer.ToString("F1") + "s");}}private void SetMessageText(string message){// 更新波次消息文本显示waveMessageText.SetText(message);}private void SetWaveNumberText(string text){// 更新波次编号文本显示waveNumberText.SetText(text);}
}
效果
敌人进攻方向提示
绘制UI
修改EnemyWaveManager
public Vector3 GetSpawnPosition(){// 返回当前敌人生成位置return spawnPosition;
}
修改EnemyWaveUI
private RectTransform enemyWaveSpawnPositionIndicator; // 敌人指示器
private Camera mainCamera; // 主摄像机// 在 Start 方法中获取敌人指示器和主摄像机
enemyWaveSpawnPositionIndicator = transform.Find("敌人指示器").GetComponent<RectTransform>();
mainCamera = Camera.main;private void Update()
{// ...// 计算指向下一个生成位置的方向向量Vector3 dirToNextSpawnPosition = (enemyWaveManager.GetSpawnPosition() - mainCamera.transform.position).normalized;// 将指示器的位置设置为方向向量的长度乘以一个常数,这里是300enemyWaveSpawnPositionIndicator.anchoredPosition = dirToNextSpawnPosition * 300f;// 根据方向向量计算角度,并将指示器旋转到该角度enemyWaveSpawnPositionIndicator.eulerAngles = new Vector3(0, 0, UtilsClass.GetAngleFromVector(dirToNextSpawnPosition));// 计算当前位置与下一个生成位置之间的距离float distanceToNextspawnPosition = Vector3.Distance(enemyWaveManager.GetSpawnPosition(), mainCamera.transform.position);// 根据距离判断是否显示指示器enemyWaveSpawnPositionIndicator.gameObject.SetActive(distanceToNextspawnPosition > mainCamera.orthographicSize * 1.5f);
}
效果
添加后处理效果
相机勾选后处理效果
配置后处理效果
效果
使用ShaderGraph实现发光效果
使用ShaderGraph实现图片自发光效果
创建材质
记得开启URP配置的HDR效果
预览效果
应用,敌人和总部窗户发光效果
实现武器箭头局部发光
效果,箭头发光
最终效果
昼夜交替循环效果
新增全局2d灯光
新增昼夜循环(DayNightCycle)脚本代码
using UnityEngine;
using UnityEngine.Experimental.Rendering.Universal;
using UnityEngine.Rendering.Universal;public class DayNightCycle : MonoBehaviour
{[SerializeField] private Gradient gradient; // 渐变色[SerializeField] private float secondsPerDay = 10f; // 每一天的时间(秒)private Light2D light2d; // 2D 光源private float dayTime; // 当前时间private float dayTimeSpeed; // 时间速度private void Awake(){light2d = GetComponent<Light2D>(); // 获取 2D 光源组件dayTimeSpeed = 1 / secondsPerDay; // 计算每秒钟增加的时间}private void Update(){dayTime += Time.deltaTime * dayTimeSpeed; // 计算当前时间light2d.color = gradient.Evaluate(dayTime % 1f); // 根据当前时间和渐变色计算光源颜色}
}
配置参数
效果,每十秒切换一下全局灯光颜色
建造时间
一般我们不希望马上就建造出我们的建筑,而是经过一段时间的等待
修改BuildingType
public float constructionTimerMax; //施工需要的时间
配置
配置建筑施工模板
新建建筑施工脚本
using UnityEngine;public class BuildingConstruction : MonoBehaviour
{private BuildingType buildingType; // 建筑类型信息private float constructionTimer; // 施工计时器,表示还需要多少时间完成建筑private float constructionTimerMax; // 施工计时器的最大值private BoxCollider2D boxCollider2D; // 用于检测碰撞的组件private void Awake(){boxCollider2D = GetComponent<BoxCollider2D>(); // 获取碰撞体组件}private void Update(){constructionTimer -= Time.deltaTime;// 如果施工计时器小于等于0,表示建筑已经完成if (constructionTimer <= 0f){// 创建建筑物Instantiate(buildingType.prefab, transform.position, Quaternion.identity);// 销毁建筑施工物体Destroy(gameObject);}}private void SetBuildingType(BuildingType buildingType){this.buildingType = buildingType;// 设置施工计时器的最大值constructionTimerMax = buildingType.constructionTimerMax;// 初始化施工计时器constructionTimer = constructionTimerMax;// 设置碰撞体的偏移和大小,与建筑物预制体保持一致boxCollider2D.offset = buildingType.prefab.GetComponent<BoxCollider2D>().offset;boxCollider2D.size = buildingType.prefab.GetComponent<BoxCollider2D>().size;}public static BuildingConstruction Create(Vector3 position, BuildingType buildingType){// 加载建筑施工预制体Transform pfBuildingConstruction = Resources.Load<Transform>("建筑施工模板");// 实例化建筑施工物体Transform buildingConstructionTransform = Instantiate(pfBuildingConstruction, position, Quaternion.identity);BuildingConstruction buildingConstruction = buildingConstructionTransform.GetComponent<BuildingConstruction>();// 设置建筑类型buildingConstruction.SetBuildingType(buildingType);return buildingConstruction;}
}
修改BuildingManager
// 在鼠标点击位置创建一个建筑实例
// Instantiate(activeBuildingType.prefab, UtilsClass.GetMouseWorldPosition(), Quaternion.identity);
BuildingConstruction.Create(UtilsClass.GetMouseWorldPosition(), activeBuildingType);
效果,放置3秒后部署
显示建造进度
效果
修改BuildingConstruction
// 返回施工计时器的标准化值,即施工时间的比例(0到1之间)
public float GetConstructionTimerNormalized(){return constructionTimer / constructionTimerMax;
}
新增ConstructionTimerUI,控制进度UI
using UnityEngine;
using UnityEngine.UI;public class ConstructionTimerUI : MonoBehaviour
{[SerializeField] private BuildingConstruction buildingConstruction; // 建筑施工组件private Image constructionProgressImage; // 施工进度显示图片private void Awake(){// 获取施工进度显示图片组件constructionProgressImage = transform.Find("Canvas").Find("image").GetComponent<Image>();}private void Update(){// 更新施工进度显示constructionProgressImage.fillAmount = buildingConstruction.GetConstructionTimerNormalized();}
}
效果
图片跟着修改
修改
建造进度
效果
删除建筑
修复建筑
源码
为了防止大家变懒,源码就不提供了,大家直接可以照着文章思路进行学习
完结
赠人玫瑰,手有余香!如果文章内容对你有所帮助,请不要吝啬你的点赞评论和关注
,以便我第一时间收到反馈,你的每一次支持
都是我不断创作的最大动力。点赞越多,更新越快哦!当然,如果你发现了文章中存在错误
或者有更好的解决方法
,也欢迎评论私信告诉我哦!
好了,我是向宇
,https://xiangyu.blog.csdn.net
一位在小公司默默奋斗的开发者,出于兴趣爱好,于是最近才开始自习unity。如果你遇到任何问题,也欢迎你评论私信找我, 虽然有些问题我可能也不一定会,但是我会查阅各方资料,争取给出最好的建议,希望可以帮助更多想学编程的人,共勉~