U3D - TowerDefense

此文仅记录塔防游戏学习过程
参考教程:SiKi学院 如何制作塔防游戏(基于Unity5.5)

创建地图

  • 使用Cube作为基础单元(大小 4 * 4),创建一个10 * 10的基准地图

在这里插入图片描述

  • 创建敌人移动路径,起点、终点
    去掉起点、终点碰撞检测(Box Collider),以免和敌人发生碰撞关系。

在这里插入图片描述

调整相机角度和移动脚本

在这里插入图片描述
相关脚本

public class ViewController : MonoBehaviour
{

    [Header("相机移动速度")]
    [Tooltip("视野移动速度")]
    public float Speed = 20;

    [Header("相机远近速度")]
    [Tooltip("滚轮速度")]
    public float MouseSpeed = 60;

    // Start is called before the first frame update
    void Start()
    {
    }

    // Update is called once per frame
    void Update()
    {

        // WS: 控制X轴移动
        float h = Input.GetAxis("Horizontal");
        // AD: 控制Z轴移动
        float v = Input.GetAxis("Vertical");
        // 鼠标滚轮: 控制Y轴
        float mouse = -Input.GetAxis("Mouse ScrollWheel");
        // 使用世界坐标系 Space.World
        transform.Translate(new Vector3(h, mouse * MouseSpeed, v) * Time.deltaTime * Speed, Space.World);

    }
}

设置敌人(固定)移动路径和方向

在这里插入图片描述
相关脚本

public class WayPoints : MonoBehaviour
{

    // 为了方便使用, 直接将该属性公开
    public static Transform[] points;

    // 脚本对象被创建时调用
    private void Awake()
    {
        // 构建路径: 敌人移动方向
        points = new Transform[transform.childCount];
        for (int i = 0; i < points.Length; i++)
        {
            // 由于路径点定义的顺序正好与敌人移动方向一致
            // 这里可以直接使用下标为路径点赋值
            // 如果路径点与移动方向不一致时, 需要手动将每个路径点和移动方向一一对应
            points[i] = transform.GetChild(i);
        }
    }
}

创建敌人并指定移动路径

使用Sphere(球体)作为敌人,并按照WayPoints.points指定的路径进行移动

在这里插入图片描述
相关代码

public class Enemy : MonoBehaviour
{
    [Header("速度")]
    [Tooltip("敌人移动速度")]
    public float MoveSpeed = 10;

    // 规划好的路径
    private Transform[] points;

    // 当前前往的路径位置
    public int index = 0;

    // Start is called before the first frame update
    void Start()
    {
        // 初始化路径
        // 方向是数组的索引
        points = WayPoints.points;
    }

    // Update is called once per frame
    void Update()
    {
        Move();
    }

    private void Move()
    {
        // 抵达终点(暂不处理)
        if (index >= points.Length)
        {
            return;
        }

        // 得到下个坐标点和当前坐标点的位置偏移向量
        Vector3 toPos = points[index].position;
        Vector3 pos = (toPos - transform.position).normalized;
        // 按指定速度和帧率更新敌人位置
        transform.Translate(pos * Time.deltaTime * MoveSpeed);

        // 到达目标后进入下一个路径点
        if (Vector3.Distance(toPos, transform.position) < 0.2f)
        {
            index++;
        }
    }
}

这里省略多个敌人的不同配置, 可以根据个人喜好自定义.

GameManager游戏管理器

创建空物体来挂载游戏逻辑

敌人孵化器

  • 定义每波敌人所需的配置类 Wave, Wave是辅助类无需集成游戏逻辑, 也不需要挂载到游戏物体上
// 保存每波敌人生成所需的参数
[System.Serializable] // 必须指定为可序列化才会显示在Inspector面板中
public class Wave
{
    // 敌人预制体
    public GameObject prefab;
    
    // 生成数量
    public int count;

    // 本次生成中, 每个敌人生成时间间隔
    public float rate;
}
  • 给GameManager挂载EnemySpawner(孵化器), 并配置相关信息:敌人预制体、一波敌人数量、一波敌人生成速率

在这里插入图片描述
相关代码

// 敌人孵化器, 管理敌人生成逻辑
public class EnemySpawner : MonoBehaviour
{

    // 地图中敌人的数量
    public static int AliveEnemyCount = 0;

    [Header("敌人配置")]
    public Wave[] waves;

    [Header("起始位置")]
    [Tooltip("敌人出生位置")]
    public Transform StartPos;

    [Header("每波生成速率")]
    [Tooltip("每波敌人之间默认生成速率")]
    public float waveRate;

    void Start()
    {
    	// 开始协程调用
        StartCoroutine(SpawnEnemy());
    }

    // 生成敌人
    IEnumerator SpawnEnemy()
    {
        // 读取每一波敌人生成的配置
        foreach (Wave wave in waves)
        {
            // 按照数量生成一波敌人
            for (int i = 0; i < wave.count; i++)
            {
                // 使用预制体(wave.prefab)在指定位置(StartPos.position)生成无旋转(Quaternion.identity)游戏物体
                GameObject.Instantiate(wave.prefab, StartPos.position, Quaternion.identity);
                // 地图中敌人计数
                AliveEnemyCount++;
                // 一波内敌人生成间隔
                if (i < wave.count - 1)
                    yield return new WaitForSeconds(wave.rate);
            }

			// 如果不启用这个条件, 敌人会一次性全部生成完成
			// AliveEnemyCount 的修改由 Enemy 销毁时减少
            // 等待上一波敌人全部消失后再生成下一波敌人
            while (AliveEnemyCount > 0)
            {
                // 暂停0帧后重新判断入口条件
                yield return 0;
            }

            // 每波敌人之间间隔时长
            yield return new WaitForSeconds(waveRate);
        }
    }

}

修改的Enemy相关代码


public class Enemy : MonoBehaviour
{

    private void Move()
    {
		...
        if (index >= points.Length)
        {
            ReachDestination();
        }
    }

    // 抵达终点
    void ReachDestination()
    {
        // 销毁敌人游戏物体
        GameObject.Destroy(this.gameObject);
    }

	// 销毁事件
    void OnDestroy()
    {
        // (负)增量敌人存活数量
        EnemySpawner.AliveEnemyCount--;
    }
}

创建炮塔预制体

获取炮塔资源
制作步骤详见视频, 这里不再说明. 后面为基础炮台, 前面为升级后炮台
在这里插入图片描述

炮台选择UI

  1. 创建Canvas
  2. Canvas中添加一个Toggle组件,一个组件代表一种可以创建的炮台,在每个组件的Background中选择对应贴图,并在Background -> Image中使用贴图原大小,点击Set Native Size并调整到合适大小。
  3. 使用同样的方式创建所有炮台选项
  4. 创建空物体并添加组件Toggle Group,并把所有炮台选项拖动到该组件下并把Toggle -> Group指定为当前空物体。这样就能实现单选组互斥选择。
    在这里插入图片描述

定义炮台数据

炮台基础数据,目前只能升级一次

[System.Serializable]
public class TurretData
{
    [Header("原始炮台")]
    [Tooltip("原始炮台预制体")]
    public GameObject turretPrefab;

    [Header("原始价格")]
    public int cost;

    [Header("升级炮台")]
    [Tooltip("升级炮台预制体")]
    public GameObject upgradPrefab;

    [Header("升级价格")]
    public int constUpgrad;

    [Header("类型")]
    public TurretType turretType;
}

炮台类型

[System.Serializable]
public enum TurretType
{
    LASER_TURRET,
    MISSILE_TURRET,
    STANDARD_TURRET
}

新增(炮台)构造管理器

GameManager空物体上新增脚本BuildManagerBuildManager用来保存每种炮塔的构建数据,以及记录(UI中)当前选中的炮塔

public class BuildManager : MonoBehaviour
{
    [Header("激光炮塔")]
    public TurretData laserTurretData;

    [Header("导弹炮塔")]
    public TurretData missileTurretData;

    [Header("普通炮塔")]
    public TurretData standardTurretData;

    // 当前选择的(UI)炮台
    public TurretData selectedTurretData;

    public void OnLaserSelected(bool isOn)
    {
        ToggleSelectedTurretData(isOn, laserTurretData);
    }

    public void OnMissileSelected(bool isOn)
    {
        ToggleSelectedTurretData(isOn, missileTurretData);
    }

    public void OnStandardSelected(bool isOn)
    {
        ToggleSelectedTurretData(isOn, standardTurretData);
    }

    /// <summary>
    /// 切换选择中的UI炮塔图标
    /// </summary>
    /// <param name="isOn">是否选中</param>
    /// <param name="turretData">选中后指定的炮塔</param>
    private void ToggleSelectedTurretData(bool isOn, TurretData turretData)
    {
        if (selectedTurretData != turretData)
        {
            if (isOn)
                selectedTurretData = turretData;
        }
        else
        {
            selectedTurretData = null;
        }
    }
}

以上定义的几个以OnXxx()事件需要绑定到UI中对应的炮塔选择器上
在这里插入图片描述

在构造管理器中获取鼠标所在的方块

【注意】:原视频中MapCube层标记使用的是 **MapCube**而作者用的Load

public class BuildManager : MonoBehaviour
{
	...
    void Update()
    {
        // 按下鼠标左键
        bool pressLeftButton = Input.GetMouseButtonDown(0);
        // 当前鼠标点击在游戏物体上
        // 游戏界面中有可能存在UI挡住游戏物体
        bool mouseInGameObject = !EventSystem.current.IsPointerOverGameObject();

        if (pressLeftButton && mouseInGameObject)
        {
            // 从主摄像机中获取鼠标射线
            Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            // 射线和指定层的碰撞结果
            RaycastHit hit;
            // 射线与指定层碰撞检测
            bool isCollider = Physics.Raycast(ray, out hit, 1000, LayerMask.GetMask("Load"));
            if (isCollider)
            {
                // 获取鼠标所在的MapCube(Load)
                GameObject gameObject = hit.collider.gameObject;
            }
        }
    }
    ...
}

建造经济显示

不论是建造、升级、出售都需要对当前玩家的经济(游戏币/资源)做出调增。在UI界面新增游戏币显示
在这里插入图片描述
由于这个游戏比较简单,就不单独做游戏经济管理。在BuildManager中直接更新;
在这里插入图片描述
经济的修改交给单独函数去做,方便后续统一升级管理。

public class BuildManager : MonoBehaviour
{
	...
    [Header("游戏币UI组件")]
    public Text moneyText;

    [Header("游戏币")]
    public int money = 1000;
	...

    void Update()
    {
		...
		//扣除建造费用
		ChangeMoney(-selectedTurretData.cost);
		load.BuildTurret(selectedTurretData.turretPrefab);
		...
	}
	
    /// <summary>
    /// 改变Money的值
    /// </summary>
    /// <param name="incr">增量</param>
    private void ChangeMoney(int incr = 0)
    {
        money += incr;
        moneyText.text = "$ " + money;
    }
}

经济不足提示

经济不足通过UI闪烁提示,闪烁一轮即可需要把Loop Time取消。

由于作者(新手😓)使用Editor 2019.3.2f1与老师使用的不是同一个版本,有些操作不太一致。
就拿这个闪烁动画来说,添加帧后无论怎样修改,总是会还原回去。后来才知道需要先启用Keyframe Recoding Mode,也就是Animation窗口左上角那个圆点,编辑完成后再禁用。

在这里插入图片描述
动画状态机,通过(Trigger)参数Flicker触发动画,动画播放完毕后回到Empty
在这里插入图片描述
指定动画组件

public class BuildManager : MonoBehaviour
{
	...
    [Header("游戏币不足提示")]
    public Animator lessMoneyAnimator;

    void Update()
    {
    	...
		if (isCollider)
		{
			...
			//【注意】老师的视频中没有此判断,实际运行中可能出现 NullPointerException
            // 没有选中UI中的炮塔, 不可以建造
            if (null == selectedTurretData)
                return;
                
			if (money >= selectedTurretData.cost) 
			{
				...
			}
			else 
			{
				// Flicker 与动画状态机中指定的参数名一致
				lessMoneyAnimator.SetTrigger("Flicker");
			}
		}
    }
}

在这里插入图片描述

优化建造交互

建造炮塔

基础构建

public class Load : MonoBehaviour
{

    /// <summary>
    /// 搭载的炮台实例
    /// </summary>
    [HideInInspector]
    [Header("炮台")]
    public GameObject turretGo;

    /// <summary>
    /// 创建炮台
    /// </summary>
    /// <param name="turretPrefab">炮台预制体</param>
    public void BuildTurret(GameObject turretPrefab)
    {
        // 炮塔创建位置与当前位置保持一致且不旋转
        turretGo = GameObject.Instantiate(turretPrefab, transform.position, Quaternion.identity);
    }

}

构建特效

特效通过粒子系统实现
细节参考视频: 课时 19 : 18-利用粒子系统创建建造的特效在这里插入图片描述

public class Load : MonoBehaviour
{

    [HideInInspector]
    [Header("炮台")]
    public GameObject turretGo;

    [Header("建造特效")]
    public GameObject buildEffectPrefab;

    /// <summary>
    /// 创建炮台
    /// </summary>
    /// <param name="turretPrefab">炮台预制体</param>
    public void BuildTurret(GameObject turretPrefab)
    {
        // 炮塔创建位置与当前位置保持一致且不旋转
        turretGo = GameObject.Instantiate(turretPrefab, transform.position, Quaternion.identity);

        // 特效播放完毕后删除游戏物体
        GameObject effect = GameObject.Instantiate(buildEffectPrefab, transform.position, Quaternion.identity);
        GameObject.Destroy(effect, 1);
    }

}

炮塔检测敌人

  1. 为所有炮塔添加Layer(Turret),所有的敌人添加Layer(Enemmy)
  2. 为所有炮塔添加刚体碰撞器,并启用触发器。在进入触发器时按顺序添加敌人,离开触发器时删除敌人
  3. 指定碰撞检测层 Turret <-> Enemy
    在这里插入图片描述
public class Turret : MonoBehaviour
{

    [Header("敌人列表")]
    public List<GameObject> enemies = new List<GameObject>();

    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Enemy"))
            enemies.Add(other.gameObject);
    }


    private void OnTriggerExit(Collider other)
    {
        if (other.CompareTag("Enemy"))
            enemies.Remove(other.gameObject);
    }
}

标准炮台开火

攻击准备

为炮塔添加三个属性:

  • 攻击速率:attackRateTime,单位秒/次;N秒攻击一次
  • 子弹初始位置firePosition,由于炮塔头部需要瞄准敌人,所以这个位置应该在指定在炮塔头部内部
  • 子弹预制体:为标准炮塔创建球形子弹预制体
public class Turret : MonoBehaviour
{

    [Header("敌人列表")]
    public List<GameObject> enemies = new List<GameObject>();

    [Header("攻击速率(秒/次)")]
    public float attackRateTime = 1;

    [Header("子弹初始位置")]
    public Transform firePosition;

    [Header("子弹")]
    [Tooltip("子弹预制体")]
    public GameObject bulletPrefab;

    // 攻击计时器
    private float timer = 0;
    
    void Start()
    {
        // 把timer设置为attackRateTime可以在敌人进入视野内第一时间发起攻击
        timer = attackRateTime;
    }

    void Update()
    {
        // 累计等待攻击时间
        timer += Time.deltaTime;

        // 范围内发现敌人, 且子弹准备就绪
        if (timer >= attackRateTime && 0 < enemies.Count)
        {
            // 计算下次攻击时间
            timer -= attackRateTime;
            Attack();
        }
    }

    /// <summary>
    /// 发起攻击
    /// </summary>
    private void Attack()
    {
        // 生成子弹
        // 子弹朝向和旋转与开火位置一致
        GameObject.Instantiate(bulletPrefab, firePosition.position, firePosition.rotation);
    }

    /// <summary>
    /// 进入攻击范围
    /// </summary>
    /// <param name="other"></param>
    void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Enemy"))
            enemies.Add(other.gameObject);
    }

    /// <summary>
    /// 离开攻击范围
    /// </summary>
    /// <param name="other"></param>
    void OnTriggerExit(Collider other)
    {
        if (other.CompareTag("Enemy"))
            enemies.Remove(other.gameObject);
    }
}

让子弹飞

为子弹预制体添加子弹脚本Bullet

/// <summary>
/// 标准炮塔子弹
/// </summary>
public class Bullet : MonoBehaviour
{
    [Header("伤害")]
    public int damage = 50;

    [Header("速度")]
    [Tooltip("子弹出膛速度")]
    public float speed = 120;

    [Header("目标位置")]
    private Transform target;

    /// <summary>
    /// 设置攻击对象
    /// </summary>
    /// <param name="target">目标</param>
    public void SetTarget(Transform target)
    {
        this.target = target;
    }

    void Update()
    {
        // 面向敌人
        transform.LookAt(target);

        // 飞向敌人
        transform.Translate(Vector3.forward * speed * Time.deltaTime);
    }
}

创建子弹后需要给子弹设置攻击目标

public class Turret : MonoBehaviour
{
	...
    /// <summary>
    /// 发起攻击
    /// </summary>
    private void Attack()
    {
        // 生成子弹
        // 子弹朝向和旋转与开火位置一致
        GameObject go = GameObject.Instantiate(bulletPrefab, firePosition.position, firePosition.rotation);
        Bullet bullet = go.GetComponent<Bullet>();
        bullet.SetTarget(enemies[0].transform);
    }
	...
}

【注意】:由于作者地形较小,刚好测试时建造的炮塔覆盖了终点,从而发现一个小问题;后面会修正

MissingReferenceException: The object of type 'GameObject' has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.
Turret.Attack () (at Assets/Scripts/Turret.cs:54)
Turret.Update () (at Assets/Scripts/Turret.cs:41)

在这里插入图片描述

【修复Bug】:长时间未攻击敌人时,一次会发射多发子弹:

用求模运算(timer %= attackRateTime)代替减法运算 (timer -= attackRateTime)

public class Turret : MonoBehaviour
{
	...
    void Update()
    {
        // 累计等待攻击时间
        timer += Time.deltaTime;

        // 范围内发现敌人, 且子弹准备就绪
        if (timer >= attackRateTime && 0 < enemies.Count)
        {
            // 计算下次攻击时间
            // 这里使用取模运算, 防止timer长时间不攻击时, 一次发射多枚炮弹
            timer %= attackRateTime;
            Attack();
        }
    }
}

子弹命中敌人

  • 命中敌人后播放子弹爆炸特效
    给子弹添加刚体并将碰撞器设置为碰撞器
public class Bullet : MonoBehaviour
{
	...
    [Header("爆炸特效")]
    [Tooltip("命中敌人或敌人消失时爆炸特效")]
    public GameObject explosionEffectPrefab;

    void OnTriggerEnter(Collider other)
    {
        if ("Enemy" == other.tag)
        {
            // 敌人掉血
            other.GetComponent<Enemy>().TakeDamage(damage);

            // 自身销毁
            GameObject.Destroy(this.gameObject);

            // 播放爆炸特效
            GameObject effect = GameObject.Instantiate(explosionEffectPrefab, transform.position, transform.rotation);
            GameObject.Destroy(effect, 1);
        }
    }
}

直接使用触发器无法检测到与敌人的碰撞(速度太快导致检测不到碰撞?),修改为子弹与敌人的距离检测

public class Bullet : MonoBehaviour
{
	...
    [Header("命中距离")]
    [Tooltip("子弹与敌人小于该距离时检测为击中目标")]
    public float distanceArriveTarget = 1;

    void Update()
    {
		...
        CheckHitEnemy();
    }

    /// <summary>
    /// 击中敌人检测
    /// </summary>
    private void CheckHitEnemy()
    {
        Vector3 dir = target.position - transform.position;
        if (dir.magnitude < distanceArriveTarget)
        {
            // 敌人掉血
            target.GetComponent<Enemy>().TakeDamage(damage);
            Die();
        }
    }
    
    /// <summary>
    /// 子弹摧毁子弹
    /// </summary>
    private void Die()
    {
        // 自身销毁
        GameObject.Destroy(this.gameObject);

        // 播放爆炸特效
        GameObject effect = GameObject.Instantiate(explosionEffectPrefab, transform.position, transform.rotation);
        GameObject.Destroy(effect, 1);
    }
}

在这里插入图片描述

【修复Bug】:子弹攻击的敌人已经销毁,程序报NullPointerException

public class Bullet : MonoBehaviour
{
	...
	void Update()
    {
        // 当前被攻击的敌人已经销毁
        // 或被其他子弹打死
        // 或抵达最终点自行销毁
        if (null == target)
        {
            Die();
            return;
        }
        ...
    }
}

【修复Bug】:炮塔攻击的敌人已经销毁,程序报NullPointerException

public class Turret : MonoBehaviour
{
	...
    /// <summary>
    /// 发起攻击
    /// </summary>
    private void Attack()
    {
        // 攻击的敌人已经消失
        // 且没有可攻击对现象时等待下一轮攻击
        if (!CheckEnemies()) 
            return;
        ...
	}

    /// <summary>
    /// 检查是否还有可攻击目标
    /// </summary>
    /// <returns>true-已找到攻击目标, false-未找到攻击目标</returns>
    private bool CheckEnemies()
    {
        // 已经没有可攻击敌人
        if (0 == enemies.Count)
            return false;

        // 目标敌人可以被攻击
        if (null != enemies[0])
            return true;

        // 目标敌人不可被攻击
        // 清空无效敌人
        for (int i = enemies.Count - 1; i >= 0; i--)
            if (null == enemies[i])
                enemies.RemoveAt(i);

        // 是否还存在有效敌人
        return 0 < enemies.Count && null != enemies[0];
    }
}
  • 敌人死亡特效
    给敌人添加生命值(HealthPoint),和死亡特效
public class Enemy : MonoBehaviour
{
	...
    [Header("生命值")]
    [Tooltip("生命值")]
    public int healthPoint = 150;

    [Header("死亡特效")]
    [Tooltip("死亡瞬间特效")]
    public GameObject explosionEffect;
	...
    /// <summary>
    /// 受到伤害
    /// </summary>
    /// <param name="damage">伤害值</param>
    public void TakeDamage(int damage)
    {
        // 最后一击有可能被多枚子弹命中
        // 死亡特效播放一次即可
        if (0 >= healthPoint)
            return;

        // 扣除伤害
        healthPoint -= damage;

        // 死亡检测
        if (0 >= healthPoint)
        {
            Die();
        }
    }

    /// <summary>
    /// 敌人死亡处理
    /// </summary>
    private void Die()
    {
        // 死亡特效
        GameObject effect = GameObject.Instantiate(explosionEffect, transform.position, transform.rotation);
        GameObject.Destroy(effect, 1.5f);

        // 销毁自身
        GameObject.Destroy(this.gameObject);
    }
}

在这里插入图片描述

炮塔指向敌人

标准炮塔头部中心点位置通过添加空物体调整
在这里插入图片描述

public class Turret : MonoBehaviour
{
	...
    [Header("炮头")]
    [Tooltip("可旋转炮头")]
    public Transform head;
	
	...
	void Update()
    {
        // 炮头指向敌人
        if (0 < enemies.Count && null != enemies[0])
        {
            Vector3 targetPos = enemies[0].transform.position;
            // 把位置中高度与炮塔保持一致, 避免炮塔抬头或低头
            targetPos.y = transform.position.y;
            head.LookAt(targetPos);
        }
    	...
    }
}

2020年05月07日, 由于工作需要暂停学习


增加地图互动

鼠标移动到地图上显示颜色变化。

public class MapCube : MonoBehaviour
{
	// ...
    // 渲染器
    // renderer 已经存在,这里使用 renderer2
    private Renderer renderer2;

    // 原本的颜色
    private Color oldColor;
	// ...
	
    private void Start()
    {
        renderer2 = GetComponent<Renderer>();
        // 记录当前颜色
        oldColor = renderer2.material.color;
    }

    private void OnMouseEnter()
    {
        // 当前鼠标点击在游戏物体上
        // 游戏界面中有可能存在UI挡住游戏物体
        bool mouseInGameObject = !EventSystem.current.IsPointerOverGameObject();
        if (null == turretGo && mouseInGameObject)
        {
            // 显示交互颜色
            renderer2.material.color = Color.red;
        }
    }
	// ...
}

给敌人增加血条

在敌人预制体下创建Canvas,并在Canvas中使用Slider描述血条变化。
为敌人添加血条ui

敌人每次受到伤害更新Slider.value即可。

public class Enemy : MonoBehaviour
{
	//...

    [Header("生命值")]
    [Tooltip("生命值")]
    public float healthPoint = 150;
    private float healthOrigin;
	//...
    void Start()
    {
        //....
        healthOrigin = healthPoint;
        slider.value = 1;
    }
	//...
    public void TakeDamage(int damage)
    {
		//...
        // 扣除伤害
        healthPoint -= damage;
        // 更新血条
        slider.value = healthPoint / healthOrigin;
        //...
    }
	//...
}

炮塔升级

创建升级UI
炮塔升级UI

public class BuildManager : MonoBehaviour
{
	// ...
    [Header("升级UI")]
    public GameObject upgradeCanvas;

    [Header("升级按钮")]
    public Button upgradeButton;
    
    // 升级ui动画机
    private Animator upgradeCanvasAnimator;

    // 隐藏ui协程
    private Coroutine hideCoroutine;
    // ...
    
    void Update()
    {
		// ...
		// 由mapCube保存炮塔属性
		mapCube.BuildTurret(selectedTurretData);

        // 炮台升级
        // 地图上有炮台,并且没有选中要建造的炮台
        if (null != mapCube.turretGo && null == selectedTurretData)
        {
            // 显示升级UI
            if (!upgradeCanvas.activeInHierarchy || selectedMapCube != mapCube)
            {
                bool canUpgrade = !mapCube.isUpgraded;
                canUpgrade = canUpgrade && (money >= mapCube.turretData.costUpgrade);
                ShowUpgradeUI(mapCube.gameObject.transform.position, canUpgrade);
                selectedMapCube = mapCube;
            }
            // 再次点击就隐藏ui
            else
            {
                selectedMapCube = null;
                hideCoroutine = StartCoroutine(HideUpgradeUI());
            }
        }
		// ...
    }

    /// <summary>
    /// 显示升级UI
    /// </summary>
    /// <param name="pos">显示位置</param>
    /// <param name="isDisable">是否禁用升级按钮</param>
    private void ShowUpgradeUI(Vector3 pos, bool isDisable = false)
    {
        if (null != hideCoroutine)
            StopCoroutine(hideCoroutine);
        upgradeCanvas.SetActive(false);
        upgradeCanvas.SetActive(true);
        upgradeCanvas.transform.position = pos;
        upgradeButton.interactable = isDisable;
    }

    /// <summary>
    /// 隐藏升级UI
    /// </summary>
    private IEnumerator HideUpgradeUI()
    {
        upgradeCanvasAnimator.SetTrigger("Hide");
        yield return new WaitForSeconds(0.3f);
        upgradeCanvas.SetActive(false);
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是一个基本的塔防游戏的Java代码示例: ```java import java.awt.Color; import java.awt.Graphics; import java.awt.Image; import java.awt.Rectangle; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.util.ArrayList; import javax.swing.ImageIcon; import javax.swing.JFrame; import javax.swing.JPanel; public class TowerDefense extends JPanel implements Runnable, MouseListener { private static final long serialVersionUID = 1L; private static final int WIDTH = 800; private static final int HEIGHT = 600; private boolean running = false; private Image bgImage; private Image enemyImage; private Image towerImage; private ArrayList<Enemy> enemies = new ArrayList<Enemy>(); private ArrayList<Tower> towers = new ArrayList<Tower>(); public TowerDefense() { setBackground(Color.WHITE); setDoubleBuffered(true); addMouseListener(this); bgImage = new ImageIcon(getClass().getResource("background.png")).getImage(); enemyImage = new ImageIcon(getClass().getResource("enemy.png")).getImage(); towerImage = new ImageIcon(getClass().getResource("tower.png")).getImage(); } public static void main(String[] args) { JFrame frame = new JFrame("Tower Defense"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(WIDTH, HEIGHT); frame.setResizable(false); frame.setLocationRelativeTo(null); TowerDefense game = new TowerDefense(); frame.add(game); frame.setVisible(true); game.start(); } public synchronized void start() { running = true; new Thread(this).start(); } public synchronized void stop() { running = false; } public void run() { while (running) { update(); repaint(); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } private void update() { // Add new enemies every 100 frames if (GameFrame.getFrameCount() % 100 == 0) { enemies.add(new Enemy(enemyImage, WIDTH, HEIGHT)); } // Update enemies for (int i = 0; i < enemies.size(); i++) { Enemy enemy = enemies.get(i); enemy.update(); // If an enemy reaches the end, remove it and subtract a life if (enemy.getX() == 0) { enemies.remove(i); GameFrame.subtractLife(); } } // Update towers for (Tower tower : towers) { tower.update(enemies); } } public void paint(Graphics g) { super.paint(g); g.drawImage(bgImage, 0, 0, null); // Draw enemies for (Enemy enemy : enemies) { enemy.draw(g); } // Draw towers for (Tower tower : towers) { tower.draw(g); } } public void mouseClicked(MouseEvent e) { Tower tower = new Tower(towerImage, e.getX(), e.getY()); towers.add(tower); } public void mousePressed(MouseEvent e) {} public void mouseReleased(MouseEvent e) {} public void mouseEntered(MouseEvent e) {} public void mouseExited(MouseEvent e) {} public Dimension getPreferredSize() { return new Dimension(WIDTH, HEIGHT); } } ``` 上面的代码只是一个基本的框架,还需要实现一些其他的类和方法。你可以根据需要自己添加或修改代码。 上面的代码中,`Enemy`类表示敌人,`Tower`类表示防御塔。`GameFrame`类是一个静态类,用于计算游戏的帧数和减少生命值。`background.png`、`enemy.png`和`tower.png`是游戏需要的图像文件。 如果你想要了解更多关于Java游戏开发的知识,可以参考Java游戏开发的相关书籍或文档。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值