主页 > 手机  > 

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

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

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

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

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

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

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

素材

链接: pan.baidu /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/资源类型/资源类型列表"); // 遍历资源类型列表,将每个资源类型添加到资源字典并初始化数量为0 foreach (ResourceType resourceType in resourceTypeList.list) { resourceAmountDictionary[resourceType] = 0; } TestLogResourceAmountDictionary(); // 测试输出资源字典 } private void Update() { if (Input.GetKeyDown(KeyCode.T)) { // 加载资源类型列表 ResourceTypeList resourceTypeList = Resources.Load<ResourceTypeList>("ScriptableObject/资源类型/资源类型列表"); // 将列表中第二个资源类型的数量增加2 resourceAmountDictionary[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); // 实例化资源UI resourceTransform.gameObject.SetActive(true); // 启用资源UI resourceTransform.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 Transform int 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); // 判断是否有其他碰撞体与要生成的建筑物重叠,如果有则返回 false bool 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) { // 检查该建筑物的类型是否与要生成的建筑物类型相同,如果是则返回 false if (buildingTypeHolder.buildingType == buildingType) { return false; } } } // 如果以上条件都满足,则可以生成建筑物,返回 true return 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(); // 查找并设置进度条的 Transform barTransform = transform.Find("bar"); // 查找并设置图标的 SpriteRenderer transform.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); // 设置图标的 SpriteRenderer transform.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){ // 支付不起该资源,返回 false return false; } } // 所有资源的数量都足够支付,返回 true return 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); // 判断是否有其他碰撞体与要生成的建筑物重叠,如果有则返回 false bool isAreaClear = collider2DArray.Length == 0; if (!isAreaClear) { errorMessage = "区域重叠!"; return false; } errorMessage = ""; // 如果以上条件都满足,则可以生成建筑物,返回 true return 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); // 创建箭头实例 } } }

箭头的脚本,主要功能是沿着目标敌人的方向飞行并对其造成伤害。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); }

效果

控制敌人生成 源码

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

完结

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

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

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

标签:

【用unity实现100个游戏之14】Unity2d做一个建造与防御类rts游戏由讯客互联手机栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“【用unity实现100个游戏之14】Unity2d做一个建造与防御类rts游戏