unity制作一款塔防游戏

news2024/11/24 2:44:47

文章目录

  • 介绍
  • 寻路系统
  • 怪物生成器
  • 制作3种初级炮台、3种升级炮台
  • 设置炮台属性
  • 选择炮台,添加监听事件
  • 炮弹追踪攻击敌人
  • 拖动鼠标实现相机视角转换
  • 鼠标光标放在cube上变色
  • 文字动画


介绍

在这里插入图片描述

关键技术:

寻路系统
生成怪物算法
粒子系统
line renderer制作追踪射线
相机视角移动、放大
炮弹追踪算法
粒子特效
按钮动画制作


寻路系统

设置几个基准点,用于偏移方向

  1. 定义一个Move方法
  2. 判断当前行数是否超过位置数组的长度,如果是则直接返回
  3. 根据当前位置与目标位置计算出移动方向,并乘以移动速度和时间,用transform.Translate方法进行移动
  4. 判断当前位置是否接近目标位置,如果是则将当前行数index加1
  5. 如果当前行数大于位置数组的长度减1,说明已经到达终点,调用ReachDestination方法
  6. 完成移动方法的定义
    在这里插入图片描述
  void Move()
    {
        if (index > positions.Length - 1) return;
        transform.Translate((positions[index].position - transform.position).normalized * Time.deltaTime * speed);
        if (Vector3.Distance(positions[index].position, transform.position) < 0.2f)
        {
            index++;
        }
        if (index > positions.Length - 1)
        {
            ReachDestination();
        }
    }

怪物生成器

一波一波生成敌人,相邻波时间间隔为wavaRate
相邻敌人的时间间隔为rate

序列化,设置每一波敌人的基本属性
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

//保存每一波敌人生成所需要的属性
[System.Serializable]
public class Wave  {
    public GameObject enemyPrefab;
    public int count;
    public float rate;
}

只有当前波完全销毁,才能有下一波攻势
// 这个脚本负责按波次生成敌人,并跟踪当前有多少敌人存活

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemySpawner : MonoBehaviour {

    // 一个静态变量,用于跟踪当前有多少敌人存活
    public static int CountEnemyAlive = 0;

    // 一个波次数组,指定每个波次要生成的敌人类型和数量
    public Wave[] waves;

    // 敌人生成的起始位置
    public Transform START;

    // 每个波次之间的时间间隔
    public float waveRate = 0.2f;

    // 生成敌人的协程的引用
    private Coroutine coroutine;

    void Start()
    {
        // 开始生成敌人的协程
        coroutine = StartCoroutine(SpawnEnemy());
    }

    public void Stop()
    {
        // 停止生成敌人的协程
        StopCoroutine(coroutine);
    }

    IEnumerator SpawnEnemy()
    {
        // 循环遍历每个波次的敌人
        foreach (Wave wave in waves)
        {
            // 为这个波次生成指定数量的敌人
            for (int i = 0; i < wave.count; i++)
            {
                // 在起始位置实例化敌人预制件
                GameObject.Instantiate(wave.enemyPrefab, START.position, Quaternion.identity);

                // 增加当前存活敌人的计数
                CountEnemyAlive++;

                // 在生成最后一个敌人之前,等待指定的时间
                if(i != wave.count - 1)
                    yield return new WaitForSeconds(wave.rate);
            }

            // 在这个波次的所有敌人被消灭之前,等待
            while (CountEnemyAlive > 0)
            {
                yield return 0;
            }

            // 等待指定的时间,然后开始下一个波次
            yield return new WaitForSeconds(waveRate);
        }

        // 在最后一个波次的所有敌人被消灭之前,等待
        while (CountEnemyAlive > 0)
        {
            yield return 0;
        }

        // 在GameManager实例上调用Win方法,以胜利结束游戏
        GameManager.Instance.Win();
    }
}

在这里插入图片描述


制作3种初级炮台、3种升级炮台

在这里插入图片描述

在这里插入图片描述


设置炮台属性

炮台预制体、炮台价格、升级后的预制体、升级价格、枚举三种炮台类型

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class TurretData  {
    public GameObject turretPrefab;
    public int cost;
    public GameObject turretUpgradedPrefab;
    public int costUpgraded;
    public TurretType type;
}
public enum TurretType
{
    LaserTurret,
    MissileTurret,
    StandardTurret
}

选择炮台,添加监听事件

在这里插入图片描述

	 public TurretData laserTurretData;
    public TurretData missileTurretData;
    public TurretData standardTurretData;

    //表示当前选择的炮台(要建造的炮台)
    private TurretData selectedTurretData;
  public void OnLaserSelected(bool isOn)
    {
        if (isOn)
        {
            selectedTurretData = laserTurretData;
        }
    }

    public void OnMissileSelected(bool isOn)
    {
        if (isOn)
        {
            selectedTurretData = missileTurretData;
        }
    }
    public void OnStandardSelected(bool isOn)
    {
        if (isOn)
        {
            selectedTurretData = standardTurretData;
        }
    }

炮弹追踪攻击敌人

以下是每个函数的作用:

  1. void OnTriggerEnter(Collider col):当有物体进入防御塔触发器范围内时,将其添加到敌人列表中。

  2. void OnTriggerExit(Collider col):当有物体离开防御塔触发器范围时,将其从敌人列表中移除。

  3. void Start():在开始时初始化计时器。

  4. void Update():在每一帧中更新防御塔的逻辑。

  5. void Attack():发射子弹攻击敌人。

  6. void UpdateEnemys():更新敌人列表,移除已经死亡的敌人。

在 Update 函数中:

  1. 如果敌人列表不为空且敌人列表中的第一个敌人不为空,则瞄准敌人的头部位置。

  2. 如果不使用激光攻击,则增加计时器。如果敌人列表不为空且计时器超过攻击间隔时间,则攻击敌人。

  3. 如果使用激光攻击且敌人列表不为空,则显示激光渲染器和激光特效,并攻击敌人。

  4. 如果敌人列表为空,则隐藏激光特效和激光渲染器。

在 Attack 函数中:

  1. 如果敌人列表中的第一个敌人为空,则更新敌人列表。

  2. 如果敌人列表不为空,则发射子弹攻击敌人,否则重置计时器。

在 UpdateEnemys 函数中:

  1. 创建一个空列表,用于存储已经死亡的敌人在敌人列表中的下标。

  2. 遍历敌人列表,找出已经死亡的敌人。

  3. 遍历已经死亡的敌人在敌人列表中的下标,将它们从敌人列表中移除。

// 引入命名空间
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 炮塔脚本
public class Turret : MonoBehaviour {

    // 炮塔攻击的敌人列表
    private List<GameObject> enemys = new List<GameObject>();

    // 敌人进入触发器
    void OnTriggerEnter(Collider col)
    {
        if (col.tag == "Enemy")
        {
            enemys.Add(col.gameObject);
        }
    }

    // 敌人离开触发器
    void OnTriggerExit(Collider col)
    {
        if (col.tag == "Enemy")
        {
            enemys.Remove(col.gameObject);
        }
    }

    // 炮塔攻击频率
    public float attackRateTime = 1;

    // 计时器
    private float timer = 0;

    // 子弹预制体
    public GameObject bulletPrefab;

    // 发射子弹的位置
    public Transform firePosition;

    // 炮塔头部
    public Transform head;

    // 是否使用激光武器
    public bool useLaser = false;

    // 激光武器伤害值
    public float damageRate = 70;

    // 激光武器的LineRenderer组件
    public LineRenderer laserRenderer;

    // 激光武器的特效
    public GameObject laserEffect;

    // 初始化
    void Start()
    {
        timer = attackRateTime;
    }

    // 更新
    void Update()
    {
        // 如果有敌人
        if (enemys.Count > 0 && enemys[0] != null)
        {
            // 瞄准第一个敌人
            Vector3 targetPosition = enemys[0].transform.position;
            targetPosition.y = head.position.y;
            head.LookAt(targetPosition);
        }
        // 如果不使用激光武器
        if (useLaser == false)
        {
            // 计时器累加
            timer += Time.deltaTime;
            // 如果有敌人并且计时器时间到
            if (enemys.Count > 0 && timer >= attackRateTime)
            {
                // 重置计时器
                timer = 0;
                // 进行攻击
                Attack();
            }
        }
        // 如果使用激光武器
        else if(enemys.Count>0)
        {
            // 开启激光线和特效
            if (laserRenderer.enabled == false)
                laserRenderer.enabled = true;
            laserEffect.SetActive(true);
            // 如果第一个敌人为空,更新敌人列表
            if (enemys[0] == null)
            {
                UpdateEnemys();
            }
            // 如果有敌人
            if (enemys.Count > 0)
            {
                // 激光线设置起始位置和结束位置
                laserRenderer.SetPositions(new Vector3[]{firePosition.position, enemys[0].transform.position});
                // 对第一个敌人造成伤害
                enemys[0].GetComponent<Enemy>().TakeDamage(damageRate *Time.deltaTime );
                // 特效跟随第一个敌人
                laserEffect.transform.position = enemys[0].transform.position;
                // 特效朝向炮塔
                Vector3 pos = transform.position;
                pos.y = enemys[0].transform.position.y;
                laserEffect.transform.LookAt(pos);
            }
        }
        // 如果没有敌人,关闭激光特效和线
        else
        {
            laserEffect.SetActive(false);
            laserRenderer.enabled = false;
        }
    }

    // 攻击
    void Attack()
    {
        // 如果第一个敌人为空,更新敌人列表
        if (enemys[0] == null)
        {
            UpdateEnemys();
        }
        // 如果有敌人
        if (enemys.Count > 0)
        {
            // 实例化子弹
            GameObject bullet = GameObject.Instantiate(bulletPrefab, firePosition.position, firePosition.rotation);
            // 设置子弹目标为第一个敌人
            bullet.GetComponent<Bullet>().SetTarget(enemys[0].transform);
        }
        else
        {
            // 如果没有敌人,重置计时器
            timer = attackRateTime;
        }
    }

    // 更新敌人列表
    void UpdateEnemys()
    {
        // 找到所有空的敌人,并将其索引加入空索引列表中
        List<int> emptyIndex = new List<int>();
        for (int index = 0; index < enemys.Count; index++)
        {
            if (enemys[index] == null)
            {
                emptyIndex.Add(index);
            }
        }
        // 根据空索引列表,移除空敌人
        for (int i = 0; i < emptyIndex.Count; i++)
        {
            enemys.RemoveAt(emptyIndex[i]-i);
        }
    }
}





拖动鼠标实现相机视角转换

在这里插入图片描述

把这个脚本,挂载到主相机上。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ViewController : MonoBehaviour {

    public float sizespeed = 1;    // 定义了一个名为sizespeed的公共(public)浮点型(float)变量,初始值为1
    public float mouseSpeed = 10;  // 定义了一个名为mouseSpeed的公共浮点型变量,初始值为10

    private Vector3 lastMousePosition;    // 定义了一个名为lastMousePosition的私有(private)Vector3类型变量

    // Update is called once per frame
    void Update () {    // 定义了一个名为Update的方法,在每一帧(frame)中被调用

        float mouse = -Input.GetAxis("Mouse ScrollWheel");    // 获取鼠标滚轮的输入值,并将其赋值给名为mouse的局部(local)浮点型变量

        // 鼠标中键按住拖动
        if (Input.GetMouseButton(2)) {    // 检测if语句中的条件是否为真,如果鼠标中键被按住,则执行大括号内的代码块

            Vector3 deltaMousePosition = Input.mousePosition - lastMousePosition;    // 获取当前鼠标位置和上一次鼠标位置之间的差值,并将其赋值给名为deltaMousePosition的局部Vector3类型变量

            transform.Translate(-deltaMousePosition.x * mouseSpeed * Time.deltaTime, -deltaMousePosition.y * mouseSpeed * Time.deltaTime, 0);    // 将摄像机的位置向左右和上下移动,移动的距离由鼠标的移动距离和鼠标速度决定

        }

        transform.Translate(new Vector3(0, mouse * sizespeed, 0) * Time.deltaTime, Space.World);    // 将摄像机的位置向上或向下移动,移动的距离由鼠标滚轮的输入值和大小速度决定

        lastMousePosition = Input.mousePosition;    // 将鼠标当前位置赋值给lastMousePosition变量,以便下一帧计算鼠标位置差值
    }
}

鼠标光标放在cube上变色

在这里插入图片描述

在这里插入图片描述

给cube添加脚本:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class aaa : MonoBehaviour {

    private Renderer renderer; // 渲染器组件

    void Start()
    {
        renderer = GetComponent<Renderer>(); // 获取自身的渲染器组件
    }

    // 当鼠标指针进入该物体的渲染范围内时执行
    void OnMouseEnter()
    {
        // 如果鼠标不在UI元素上,则将该物体的材质颜色改为红色
        if (EventSystem.current.IsPointerOverGameObject() == false)
        {
            renderer.material.color = Color.red;
        }
    }

    // 当鼠标指针离开该物体的渲染范围时执行
    void OnMouseExit()
    {
        // 将该物体的材质颜色改回白色
        renderer.material.color = Color.white;
    }
}


文字动画

在这里插入图片描述

在这里插入图片描述


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

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

相关文章

python基本操作1(速通版)

目录 一、input输入函数 二、格式化字符输出 三、函数的基本操作 1.return返回值的问题 2.参数传递 四、运算符 1.关系比较符 2.逻辑运算符 五、if语句 六、随机数 七、循环 1.while语句的基本应用 2.break语句 2.continue语句 3.猜拳游戏 4.三目运算符 6.for…

让input框只输入英文

解决扫码枪在中文输入法时扫码冲突 扫码枪在扫完码时会自动回车&#xff0c;这时如果是中文输入法就会触发输入法联想&#xff0c;再加一个回车&#xff0c;那么input框输入的就成中文了。如果可以控制input框只能输入英文那就好了。css有一个属性&#xff08;ime-mode&#xf…

ChatGpt论文指令,很全!

ChatGPT狂飙160天&#xff0c;世界已经不是之前的样子。 新建了人工智能中文站https://ai.weoknow.com 每天给大家更新可用的国内可用chatGPT资源 ChatGPT的能力大家肯定都听说过&#xff0c;很多学生应该都亲身体验过。它在自然语言处理方面的出色 除了写代码 写论文也是超…

【计算思维题】少儿编程 蓝桥杯青少组计算思维真题及详细解析第4套

少儿编程 蓝桥杯青少组计算思维真题及详细解析第4套 1、下面哪个图形与其它图形不同 A、 B、 C、 D、 答案:D 考点分析:主要考查小朋友们的观察能力,从给定的图中可以看到前三个选项都是90度直角,最后一个是锐角,所以答案D 2、下列哪个选项是由下图旋转得到的

位运算【算法基础】

目录 知识点&#xff1a; 题目&#xff1a; 模板 关于为什么负数要用补码 知识点&#xff1a; 如果想看整数n的二进制表示中的第k位&#xff08;从0开始&#xff09;是几&#xff1f; &#xff08;1&#xff09;把第k位右移到个位n>>k &#xff08;2&#xff09;看x…

大模型时代,「重识」云知声

在山海大模型发布会现场&#xff0c;黄伟有一句话令人印象深刻&#xff0c;“云知声的过去十年&#xff0c;就是为山海而生。 作者| 皮爷 出品|产业家 “谁能做成中国最好的大模型&#xff1f;”在今年3月一个北京投资人的内部分享会上&#xff0c;有人满怀期待地提出这样…

Java SPI 一 之SPI(Service Provider Interface)进阶 AutoService

​ 一、SPI&#xff08;Service Provider Interface&#xff09; 1.1 介绍 SPI&#xff08;Service Provider Interface&#xff09;&#xff0c;是JDK内置的一种 服务提供发现机制(为某个接口寻找服务实现的机制)&#xff0c;可以用来启用框架扩展和替换组件&#xff0c;其…

Admin.NET管理系统(c#+vue3)前后端学习笔记

我的学习笔记 - 9iAdmin.NET 欢迎学习交流&#xff08;一&#xff09;前端笔记1.1 关于.env的设置1.2 关于路由模式问题1.3 关于 vue.config.ts1.4 关于 打包&#xff08;pnpm run build&#xff09;溢出问题1.5 关于 打包&#xff08;pnpm run build&#xff09;后部署到IIS重…

article-六轴码垛机器人admas正逆运动学仿真

基座自由度、大臂摆动自由度、小臂摆动自由度、腕部Y轴摆动自由度、腕部Z轴摆动自由度及其腕部末端X轴旋转自由度 其导入过程为&#xff1a; 机器人三维模型总体有6个部分。打开机器人的SolidWork三维模型&#xff0c;依次另存为6个“Parasolid(x_t)”类型的文件。打开ADAMS/…

【MATLAB第36期】基于MATLAB的QOWOA-LSTM鲸鱼优化算法准反向策略的WOA优化LSTM时间序列预测模型 优势明显,注释详细,绘图丰富

【MATLAB第36期】基于MATLAB的QOWOA-LSTM鲸鱼优化算法准反向策略的QOWOA优化LSTM时间序列预测模型&#xff0c;优势明显&#xff0c;注释详细&#xff0c;绘图丰富 一、代码优势 1.使用优化后的QOWOA算法优化LSTM超参数&#xff08;学习率&#xff0c;隐藏层节点&#xff0c;…

2020下半年上午题

2020下半年 d a b 小阶向大阶对齐 b b 平均cpi: MIPS: d c 公加验&#xff0c;私解签 加密防止被动攻击&#xff0c;认证防止主动攻击 a 访问控制包括&#xff1a;授权&#xff0c;确定存取权限&#xff0c;实施存取权限 c a c a 先申请先得 b b 著作权包括&…

OpenCV使用SURF和SIFT算法报错解决记录

OpenCV使用SURF和SIFT算法报错解决记录 1.报错代码&#xff0c;使用以下两种写法都会报错 # 创建SIFT和SURF特征提取器 # 写法1 sift cv2.xfeatures2d.SIFT_create() surf cv2.xfeatures2d.SURF_create() # 写法2 sift cv2.SIFT_create() surf cv2.SURF_create()第一种报…

架构整洁之道下篇(实现细节)

目录 1.实现细节 1.1.数据库只是实现细节 1.2.Web是实现细节 1.3.应用程序框架是实现细节 1.4.案例分析&#xff1a;视频销售网站 1.5.拾遗 1.5.1.按层封装 1.5.2.按功能封装 1.5.3.端口和适配器 1.5.4.按组件封装 1.5.5.组织形式和封装的区别 2.总结 1.实现细节 …

13_Uboot移植

目录 查找NXP官方的开发板默认配置文件 编译NXP官方开发板对应的uboot 烧写验证与驱动测试 SD卡和EMMC驱动检查 LCD驱动检查 网络驱动 在U-Boot中添加自己的开发板 添加开发板默认配置文件 添加开发板对应的头文件 添加开发板对应的板级文件夹 修改mx6ull_alientek_…

Vue——状态管理库Pinia

写在前面&#xff1a;本文参考小满大牛的pinia专栏 一、Vuex与Pinia Vuex 和 Pinia 均是 Vue.js 的状态管理库&#xff0c;它们为 Vue 应用程序提供了一种集中式的、可预测的状态管理解决方案。 Vuex 是 Vue.js 官方推荐的状态管理库之一。它的核心概念包括 state、mutation…

【C++初阶】类与对象(中)之取地址及const取地址操作符重载

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前学习C和算法 ✈️专栏&#xff1a;C航路 &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章对你有帮助的话 欢迎 评论&#x1f4ac; 点赞&#x1…

架构整洁之道中篇(组件构建原则软件架构)

目录 1.组件构建原则 1.1.组件 1.2.组件聚合 1.3.组件耦合 2.软件架构 2.1.什么是软件架构&#xff1f; 2.2.独立性 2.3.划分边界 2.4.策略与层次 2.5.业务逻辑 2.6.尖叫的软件架构 2.7.整洁架构 2.8.层次与边界 2.9.Main组件 2.10.测试边界 2.11.整洁的嵌入式…

Edgedetect2

边缘检测&#xff0c;检查数据变化&#xff0c;用异或实现 对于 8 位矢量中的每个位&#xff0c;检测输入信号何时从一个时钟周期变为下一个时钟周期&#xff08;检测任何边沿&#xff09;。输出位应在发生 0 到 1 转换后设置周期。 以下是一些示例。为清楚起见&#xff0c;in…

HNU-电路与电子学-小班4

第四次小班讨论 一、题目 1、书 3-41、3-62 2、书 4-23、4-26 3、设计一个时序电路。该电路仅在连续三个或三个以上时钟期间&#xff0c;且两个输入信号 X1 和 X2 相同时&#xff0c;输出信号 Z 为 1&#xff0c;其余情况 Z 为 0。试做出该电路的 Mealy 机和 Moore 机状态…

Windows:设置右键用RStudio打开文件和文件夹

0. 前言 在使用RStudio写R脚本的时候总是要先打开它&#xff0c;再通过它打开脚本和文件夹&#xff0c;感觉不是很方便。由于VSCode以及其他软件都可以整合到右键菜单中打开文件或文件夹&#xff0c;因此就折腾了一下怎么在右键中使用RStudio打开文件&#xff0c;下面是效果展…