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

news2025/1/11 11:03:36

前言

欢迎来到本次教程,我将为您讲解如何使用 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/资源类型/资源类型列表");

        // 遍历资源类型列表,将每个资源类型添加到资源字典并初始化数量为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);
}

效果
在这里插入图片描述

控制敌人生成

源码

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

完结

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

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

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

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

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

相关文章

聊聊分布式架构——BIO到NIO的演进

目录 I/O I/O模型 BIO示例 BIO与NIO比较 NIO的三大核心 NIO核心之缓冲区 Buffer常用子类&#xff1a; Buffer常用API Buffer中的重要概念 NIO核心之通道 FileChannel 类 FileChannel常用方法 NIO核心之选择器 概述 应用 NIO非阻塞原理分析 服务端流程 客户端…

【Java8】线程问题排查分析

文章目录 thread dump简介JDK 工具jstackjattachjvisualvm Java程序CPU消耗较高&#xff0c;怎么快速看出是那个线程导致的呢&#xff1f;我们可以使用命令 jstack/jattach来快速定位问题 thread dump简介 thread dump 是 Java 进程的所有线程状态的快照。每个线程的状态都通过…

java基础-第1章-走进java世界

一、计算机基础知识 常用的DOS命令 二、计算机语言介绍 三、Java语言概述 四、Java环境的搭建 JDK安装图解 环境变量的配置 配置环境变量意义 配置环境变量步骤 五、第一个Java程序 编写Java源程序 编译Java源文件 运行Java程序 六、Java语言运行机制 核心机制—Java虚拟机 核…

【LeetCode高频SQL50题-基础版】打卡第2天:第11-15题

文章目录 【LeetCode高频SQL50题-基础版】打卡第2天&#xff1a;第11-15题⛅前言 员工奖金&#x1f512;题目&#x1f511;题解 学生们参加各科测试的次数&#x1f512;题目&#x1f511;题解 至少有5名直接下属的经理&#x1f512;题目&#x1f511;题解 确认率&#x1f512;题…

tcpdump(二)命令行参数讲解(一)

一 tcpdump实战详解 1、我们做抓包,一般都需要指定条件,保证对系统的CPU、内存、磁盘资源不会产生过大的响应备注&#xff1a; 遇到过tcpdump持续抓包导致系统挂了2、条件&#xff1a;1) tcpdump的 基础命令选项参数2) 真正的 过滤条件 ① 参数学习思路 思路&#xff1a;…

Window Anaconda 安装pytorch 启用cuda 终究手段

1.首先你的电脑要有NVIDIA 的显卡.没有就走吧,你如果不是window &#xff0c;也走吧&#xff0c;不一定教程管用。 2.然后要明白&#xff0c;有两种CUDA版本&#xff0c;一个叫运行时api&#xff0c;一个是驱动api 2.1 运行时cuda 版本查看 &#xff08;是你跑深度学习模型或其…

关于 Vue-iClient-MapboxGL 的使用注意事项

官网&#xff1a;https://iclient.supermap.io/web/apis/vue/zh/api/guide/installation.html 关于图的使用&#xff0c;其余的引入步骤不再赘述&#xff0c;仅说注意事项。 推荐使用的是全局引入&#xff0c;也就是完整引入 因为单独引入我踩了不少坑&#xff0c;比如说 cs…

swoole进行性能查看火焰图tideways_xhprof xhgui

D:\dnmp\services\php\Dockerfile D:\dnmp\services\php\php.ini 在php的配置文件里面增加tideways_xhprof拓展&#xff1a; [xhprof] ;xhprof.output_dir /var/log/php/xhprof.logextensiontideways_xhprof.so在php配置文件里面加上xhgui的header&#xff1a; 这样就能开启…

golang 编译器 汉化

1、找到左上角file选项&#xff0c;点击选中settings进行单机 2、找到settings中找到plugins选中进行点击 3、再框中输入chinese进行搜索&#xff0c;出结果后找到如下图所示&#xff0c;点击进行安装 4、安装完成后进行重启ide&#xff0c;完美解决

Docker linux 安装

sudo yum update sudo yum clean all sudo yum makecache#安装依赖 sudo yum install -y yum-utils device-mapper-persistent-data lvm2 #添加官方存储库 sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo#安装-跳过一些异常依赖…

计算机视觉: 基于隐式BRDF自编码器的文生三维技术

论文链接: MATLABER: Material-Aware Text-to-3D via LAtent BRDF auto-EncodeR 背景 得益扩散模型和大量的text - image 成对的图片&#xff0c; 现在文生2D的模型已经比较成熟的框架和模型&#xff0c;主流的技术比如说stable diffusion 和 midjourney 以及工业领域runway 等…

Idea下面git的使用:变基、合并、优选、还原提交、重置、回滚、补丁

多分支和分支切换 变基和合并 变基是把本项目的所有提交都列出来按顺序一个个提交到目标分支上去 而合并是把两个分支合并起来&#xff0c;但是旧的分支还是可以启动其他分支&#xff0c;在旧的分支上继续开发 master: A -- B -- C -- M/ feature: D -- Emaster: A -…

2023全新小红书图集和视频解析去水印网站源码

2023全新小红书图集和视频解析去水印网站源码 小红书视频图集解析网站源码&#xff0c;在红书看到好看的图片以及好看的头像&#xff0c;但是直接下载又有水印就非常难受&#xff0c;这个可以一键解析去除水印&#xff0c;支持统计解析次数&#xff0c;本地接口。 源码下载&a…

大厂秋招真题【BFS+DP】华为20230921秋招T3-PCB印刷电路板布线(留学生专场)

华为20230921秋招T3-PCB印刷电路板布线&#xff08;留学生专场&#xff09; 题目描述与示例 题目描述 在PCB印刷电路板设计中&#xff0c;器件之间的连线&#xff0c;要避免线路的阻抗值增大&#xff0c;而且器件之间还有别的器任和别的干扰源&#xff0c;在布线时我们希望受…

深度学习入门:基于Python的理论与实现【笔记】

深度学习入门:基于Python的理论与实现这本数的阅读笔记 根据自己的情况总结的,可能有些简单的就没做笔记。 目录 NumPyMatplotlib感知机NumPy 在深度学习的实现中,经常出现数组和矩阵的计算。NumPy的数组类(numpy.array)中提供了很多便捷的方法,在实现深度学习时,我们将使…

力扣 -- 516. 最长回文子序列

解题步骤&#xff1a; 参考代码&#xff1a; class Solution { public:int longestPalindromeSubseq(string s) {int ns.size();vector<vector<int>> dp(n,vector<int>(n));//记得从下往上填表for(int in-1;i>0;i--){//记得i是小于等于j的for(int ji;j&l…

reactjs开发环境搭建

Reactjs是一个前端web页面应用开发框架工具集&#xff0c;其支持前端构建页面以及后端构建页面两种常用的开发场景&#xff0c;其中&#xff0c;支持reactjs的开发框架包括next.js、remix、gatsby以及其他&#xff0c;本文主要描述next.js开发环境的搭建&#xff0c;next.js是一…

java docker图片叠加水印中文乱码

java docker图片叠加水印中文乱码 技术交流博客 http://idea.coderyj.com/ 1.由于项目需要后端需要叠加图片水印,但是中文乱码,导致叠加了之后 中文是框框 2.经过多方查找基本都说在 linux下安装字体就解决了,但是尝试了均无效 3.后来忽然想到我的项目是用docker打包部署的,不…

seata框架

Seata简介&#xff1a;&#xff08;Seata | Seata&#xff08;官方网站&#xff09;&#xff09; Seata 是一款开源的分布式事务解决方案&#xff0c;致力于在微服务架构下提供高性能和简单易用的分布式事务服务。 相关术语&#xff1a; TC (Transaction Coordinator) - 事务…

嵌入式养成计划-32-网络编程----域套接字模型------抓包工具--wireshark

六十九、 域套接字模型 69.1 域套接字的概念 只能做一台主机内的进程间通信&#xff0c;协议族&#xff08;地址族&#xff09;指定为&#xff1a;AF_UNIX AF_LOCALbsp-lcd&#xff1a; s类型文件&#xff0c;就是域套接字如果客户端不手动绑定&#xff0c;则操作系统不会创建…