22.防御塔逻辑

  1. 22.游戏场景-防御塔逻辑
    1. 22.1 数据模型准备
      1. 在Resource下准备好三种塔的三个级别的预制体和图片。
      2. 创建excel表,配置塔的id,名字,建塔需要的钱,攻击力,攻击范围,攻击间隔,升级后下一个塔的id ,图片路径,预制体路径,攻击方式类型,攻击特效路径。去网站上转成json。取名TowerInfo.json存储到StreamAssets文件夹下。
      3. 创建塔数据脚本TowerInfo,变量名要和配置表一一对应
      4. 在GameDataMgr脚本中,关联一个TowerInfo列表。在构造函数中用Json数据管理器初始化读取。
    2. 22.2 防御塔逻辑
      1. 创建TowerObject脚本,继承MonoBehaviour。代表塔对象
      2. TowerObject脚本中声明炮塔头部的变换组件 head、开火点的变换组件 gunPoint 以及炮塔头部旋转速度 roundSpeed。声明了塔数据变量 info,用于存储炮塔的相关数据。声明了怪物对象变量 targetObj 和怪物对象列表targetObjs,分别用于存储当前攻击的单个目标和多个目标。声明变量 nowTime,用于计时,判断攻击间隔时间。声明变量 monsterPos,用于记录怪物位置。
      3. 给每个塔预制体添加开火点空对象。给各个他添加TowerObject脚本,关联头部和开火点,第三种塔群体攻击不需要旋转攻击可以不关联头部。
      4. TowerObject脚本中定义初始化炮台数据方法,外部传入炮台数据赋值
      5. 获得怪物列表可以用碰撞器范围检测,也可以通过游戏关卡管理器作为观察者。在GameLevelMgr脚本中创建怪物对象列表来管理。之前怪物数量相关的逻辑都要进行修改成和怪物列表相关。定义添加移除寻找情况怪物方法。寻找怪物方法有重载,外部传入位置和攻击范围,可能返回怪物和怪物列表用于单体攻击和群体攻击,里面的逻辑是遍历怪物列表判断怪物没死且和怪物距离小于攻击范围,单体寻找直接找到就返回。群体装到列表在返回。
      6. 在MonsterPoint脚本的创建怪物方法时,修改怪物数量逻辑为在关卡管理器加怪物对象到列表中。在MonsterObject 脚本死亡事件方法时修改怪物数量逻辑为在关卡管理器怪物列表中移除当前怪物。
      7. 在Resource特效文件夹下创建两个开火特效
      8. TowerObject脚本中,在 Update() 方法首先判断炮塔的攻击方式。如果攻击方式是单体攻击(info.atkType == 1),进入单体攻击逻辑:检查是否有目标可攻击,如果没有或者目标已死亡或者目标超出攻击范围,则调用 GameLevelMgr.Instance.FindMonster() 方法寻找新的目标。如果没有找到可攻击的目标,则返回,不进行旋转。计算怪物位置,并将炮塔头部朝向怪物位置,使用 Quaternion.Slerp() 方法实现平滑旋转。判断炮塔头部和怪物位置之间的夹角是否小于一定范围,同时判断攻击间隔条件是否满足。如果满足条件,则让目标受伤、播放音效、创建开火特效,并记录当前时间为开火时间。如果攻击方式是群体攻击(info.atkType != 1),进入群体攻击逻辑:调用 GameLevelMgr.Instance.FindMonsters() 方法查找多个目标。如果找到目标,并且攻击间隔条件满足,则创建开火特效,并让所有目标受伤。记录当前时间为开火时间。
    3. 22.3 造塔点逻辑
      1. 找到一个特效作为造塔点预制体,创建TowerPoint造塔点脚本,挂载上去。添加球形触发器。
      2. TowerPoint脚本中定义了一个游戏对象变量 towerObj,用于保存当前塔的游戏对象。初始值为 null。定义了一个塔的数据变量 nowTowerInfo,用于保存当前塔的数据。初始值为 null。定义了一个造塔id列表chooseIDs,用于保存可以建造的三个塔的ID。要在挂载的脚本中配置造塔点id列表数据。
      3. TowerBtn脚本中添加初始化塔按钮方法。该方法接受两个参数:id 和 inputStr。根据传入的 id,从游戏数据管理器(GameDataMgr)中获取对应的塔信息(TowerInfo)。将获取到的塔图片资源设置给按钮的 imgPic 的 sprite 属性。将塔的价格(info.money)和字符串 “¥” 进行组合,并设置给按钮的 txtMoney 的 text 属性,用于显示按钮上的金钱信息。将传入的 inputStr 设置给按钮的 txtTip 的 text 属性,用于显示按钮上的提示信息。判断当前玩家的金钱是否足够购买该塔:如果不足,则将按钮的 txtMoney 的 text 属性设置为 “金钱不足”,用于显示在按钮上。如果足够,则保持按钮的 txtMoney 的 text 属性不变。
      4. 在GamePanel 脚本中定义存储当前选中的造塔点和标识是否需要检测造塔输入。
      5. GamePanel脚本中定义用于更新当前选中造塔点的界面显示的方法。首先,将传入的TowerPoint对象赋值给nowSelTowerPoint变量,这个变量表示当前选中的造塔点。然后,通过判断nowSelTowerPoint是否为null来确定传入的数据是否为空。如果是空,则将checkInput设为false,表示不需要检测造塔输入;同时隐藏下方的造塔按钮。否则,将checkInput设为true,表示需要检测造塔输入;同时显示下方的造塔按钮。接下来,根据所选造塔点的状态进行不同的处理:如果没有造过塔(即nowSelTowerPoint.nowTowerInfo == null),则对每个造塔按钮进行设置。遍历towerBtns列表,将每个按钮的gameObject设为激活状态,并调用InitInfo方法初始化按钮的信息。初始化信息中传入了nowSelTowerPoint.chooseIDs[i]作为参数,以及字符串”数字键” + (i + 1)作为按钮的显示文本。这样就会显示具有不同造塔ID和对应数字键提示的按钮。如果已经造过塔(即nowSelTowerPoint.nowTowerInfo != null),则只显示一个造塔按钮,并设置其信息。首先将所有的造塔按钮设为非激活状态。然后,将towerBtns列表中索引为1的按钮(中间的按钮)设为激活状态,并调用InitInfo方法初始化按钮的信息。初始化信息中传入了nowSelTowerPoint.nowTowerInfo.nextLev作为参数,以及字符串”空格键”作为按钮的显示文本。
      6. 在各个玩家预制体上添加角色控制器,注意角色可以触发到造塔点。
      7. TowerPoint脚本中定义用于创建塔方法。接受一个整数参数id,代表要创建的塔的ID。根据传入的id从GameDataMgr.Instance.towerInfoList中获取对应的塔信息info。检查如果玩家的金钱不足以建造这座塔(即info.money > GameLevelMgr.Instance.player.money),则直接返回,不进行后续的建造操作。如果玩家的金钱足够建造这座塔,首先通过GameLevelMgr.Instance.player.AddMoney(-info.money)扣除相应的金钱。判断当前构建点是否已经存在塔对象(即towerObj != null)。如果存在,则销毁原有的塔对象(通过Destroy(towerObj))。通过Instantiate方法实例化一个塔对象,并使用Resources.Load(info.res)加载塔的资源文件。该塔对象位于当前脚本所在的游戏对象的位置(即this.transform.position),并将其旋转角度设置为默认角度(Quaternion.identity)。通过towerObj.GetComponent().InitInfo(info)方法对塔对象进行初始化,将塔的信息传递给该方法。将info赋值给nowTowerInfo,记录当前塔的数据。最后,根据nowTowerInfo.nextLev的值来判断塔是否建造完成。如果nextLev不为0,则调用UIManager.Instance.GetPanel().UpdateSelTower(this)方法来更新游戏界面上的内容,传递当前选中的造塔点作为参数。否则,调用UIManager.Instance.GetPanel().UpdateSelTower(null)方法来清空选中的造塔点。
      8. TowerPoint脚本中定义触发器进入和退出方法。假如走到了造塔点,但是有塔且不能升级,就直接返回,否则的话的话调用游戏界面的更新当前选择塔面板,传入自身。如果离开了造塔点,调用游戏界面的更新当前选择塔面板,直接传空。
      9. GamePanel 脚本中重写的Update方法中处理玩家输入来建造塔。调用base.Update()是为了确保基类(父类)的Update方法也会被执行,以便处理其他逻辑。通过检查checkInput变量的值来确定是否需要进行输入检测。如果checkInput为false,则直接返回,不进行后续的输入处理。如果当前选中的造塔点没有造过塔(即nowSelTowerPoint.nowTowerInfo == null),则会检测玩家按下数字键1、2、3的情况。如果玩家按下了数字键1,则调用nowSelTowerPoint.CreateTower方法,并传入nowSelTowerPoint.chooseIDs[0]作为参数;如果玩家按下了数字键2,则调用nowSelTowerPoint.CreateTower方法,并传入nowSelTowerPoint.chooseIDs[1]作为参数;如果玩家按下了数字键3,则调用nowSelTowerPoint.CreateTower方法,并传入nowSelTowerPoint.chooseIDs[2]作为参数。这样就实现了根据玩家按键来选择不同的造塔选项。如果当前选中的造塔点已经造过塔,那么只会检测玩家是否按下了空格键。如果玩家按下了空格键,则调用nowSelTowerPoint.CreateTower方法,并传入nowSelTowerPoint.nowTowerInfo.nextLev作为参数。这样就实现了通过按下空格键来升级当前已经造好的塔。
      10. MonsterObject脚本,添加怪物一死就加钱的逻辑。
      11. MonsterObject脚本,受伤方法添加判断,假如怪物已经死了直接返回
      12. PlayerObject脚本的刀枪事件中,添加要怪物没死才让他受伤的逻辑
    4. 22.4 代码
      1. TowerBtn
      2. TowerInfo
      3. TowerInfo.json
      4. TowerObject
      5. TowerPoint
      6. GamePanel

22.游戏场景-防御塔逻辑


22.1 数据模型准备

在Resource下准备好三种塔的三个级别的预制体和图片。

创建excel表,配置塔的id,名字,建塔需要的钱,攻击力,攻击范围,攻击间隔,升级后下一个塔的id ,图片路径,预制体路径,攻击方式类型,攻击特效路径。去网站上转成json。取名TowerInfo.json存储到StreamAssets文件夹下。

[
    {"id":1,"name":"加农炮1","money":200,"atk":5,"atkRange":8,"offsetTime":1,"nextLev":2,"imgRes":"TowerImg/1_1","res":"Tower/1_1","atkType":1,"eff":"eff/Fire1"},
    {"id":2,"name":"加农炮2","money":300,"atk":10,"atkRange":8,"offsetTime":1,"nextLev":3,"imgRes":"TowerImg/1_2","res":"Tower/1_2","atkType":1,"eff":"eff/Fire1"},
    {"id":3,"name":"加农炮3","money":400,"atk":20,"atkRange":8,"offsetTime":1,"nextLev":0,"imgRes":"TowerImg/1_3","res":"Tower/1_3","atkType":1,"eff":"eff/Fire1"},
    {"id":4,"name":"机枪炮1","money":200,"atk":2,"atkRange":8,"offsetTime":0.2,"nextLev":5,"imgRes":"TowerImg/2_1","res":"Tower/2_1","atkType":1,"eff":"eff/Fire1"},
    {"id":5,"name":"机枪炮2","money":300,"atk":4,"atkRange":8,"offsetTime":0.2,"nextLev":6,"imgRes":"TowerImg/2_2","res":"Tower/2_2","atkType":1,"eff":"eff/Fire1"},
    {"id":6,"name":"机枪炮3","money":400,"atk":6,"atkRange":8,"offsetTime":0.2,"nextLev":0,"imgRes":"TowerImg/2_3","res":"Tower/2_3","atkType":1,"eff":"eff/Fire1"},
    {"id":7,"name":"魔法炮1","money":200,"atk":3,"atkRange":8,"offsetTime":2,"nextLev":8,"imgRes":"TowerImg/3_1","res":"Tower/3_1","atkType":2,"eff":"eff/Fire2"},
    {"id":8,"name":"魔法炮2","money":300,"atk":6,"atkRange":8,"offsetTime":2,"nextLev":9,"imgRes":"TowerImg/3_2","res":"Tower/3_2","atkType":2,"eff":"eff/Fire2"},
    {"id":9,"name":"魔法炮3","money":400,"atk":9,"atkRange":8,"offsetTime":2,"nextLev":0,"imgRes":"TowerImg/3_3","res":"Tower/3_3","atkType":2,"eff":"eff/Fire2"}
]

创建塔数据脚本TowerInfo,变量名要和配置表一一对应

public class TowerInfo
{
    public int id;
    public string name;
    public int money;
    public int atk;
    public int atkRange;
    public float offsetTime;
    public int nextLev;
    public string imgRes;
    public string res;
    public int atkType;
    public string eff;
}

在GameDataMgr脚本中,关联一个TowerInfo列表。在构造函数中用Json数据管理器初始化读取。

/// <summary>
/// 专门用来管理数据的类
/// </summary>
public class GameDataMgr
{
    private static GameDataMgr instance = new GameDataMgr();
    public static GameDataMgr Instance => instance;

    //所有塔的数据
    public List<TowerInfo> towerInfoList;

    private GameDataMgr()
    {
        //读取塔的数据
        towerInfoList = JsonMgr.Instance.LoadData<List<TowerInfo>>("TowerInfo");
    }
}

22.2 防御塔逻辑

创建TowerObject脚本,继承MonoBehaviour。代表塔对象

public class TowerObject : MonoBehaviour

TowerObject脚本中声明炮塔头部的变换组件 head、开火点的变换组件 gunPoint 以及炮塔头部旋转速度 roundSpeed。声明了塔数据变量 info,用于存储炮塔的相关数据。声明了怪物对象变量 targetObj 和怪物对象列表targetObjs,分别用于存储当前攻击的单个目标和多个目标。声明变量 nowTime,用于计时,判断攻击间隔时间。声明变量 monsterPos,用于记录怪物位置。

// 炮台头部 用于旋转 指向目标
public Transform head;
// 开火点 用于释放攻击特效的位置
public Transform gunPoint;
// 炮台头部旋转速度 可以写死 也可以配在表中
private float roundSpeed = 20;

// 炮台关联的数据
private TowerInfo info;

// 当前要攻击的目标
private MonsterObject targetObj;
// 当前要攻击的目标们
private List<MonsterObject> targetObjs;

// 用于计时的 用来判断攻击间隔时间
private float nowTime;

// 用于记录怪物位置
private Vector3 monsterPos;

给每个塔预制体添加开火点空对象。给各个他添加TowerObject脚本,关联头部和开火点,第三种塔群体攻击不需要旋转攻击可以不关联头部。


TowerObject脚本中定义初始化炮台数据方法,外部传入炮台数据赋值

/// <summary>
/// 初始化炮台相关数据
/// </summary>
/// <param name="info"></param>
public void InitInfo(TowerInfo info)
{
    this.info = info;
}

获得怪物列表可以用碰撞器范围检测,也可以通过游戏关卡管理器作为观察者。在GameLevelMgr脚本中创建怪物对象列表来管理。之前怪物数量相关的逻辑都要进行修改成和怪物列表相关。定义添加移除寻找情况怪物方法。寻找怪物方法有重载,外部传入位置和攻击范围,可能返回怪物和怪物列表用于单体攻击和群体攻击,里面的逻辑是遍历怪物列表判断怪物没死且和怪物距离小于攻击范围,单体寻找直接找到就返回。群体装到列表在返回。

public class GameLevelMgr
{
    private static GameLevelMgr instance = new GameLevelMgr();
    public static GameLevelMgr Instance => instance;

    private List<MonsterObject> monsterList = new List<MonsterObject>();

    private GameLevelMgr()
    {

    }

    public bool CheckOver()
    {
        for (int i = 0; i < points.Count; i++)
        {
            if (!points[i].CheckOver())
                return false;
        }

        if (monsterList.Count > 0)
            return false;

        Debug.Log("游戏胜利");
        return true;
    }

    public void AddMonster(MonsterObject obj)
    {
        monsterList.Add(obj);
    }

    public void RemoveMonster(MonsterObject obj)
    {
        monsterList.Remove(obj);
    }

    public MonsterObject FindMonster(Vector3 pos, int range)
    {
        for (int i = 0; i < monsterList.Count; i++)
        {
            if (!monsterList[i].isDead && Vector3.Distance(pos, monsterList[i].transform.position) <= range)
            {
                return monsterList[i];
            }
        }
        return null;
    }

    public List<MonsterObject> FindMonsters(Vector3 pos, int range)
    {
        List<MonsterObject> list = new List<MonsterObject>();
        for (int i = 0; i < monsterList.Count; i++)
        {
            if (!monsterList[i].isDead && Vector3.Distance(pos, monsterList[i].transform.position) <= range)
            {
                list.Add(monsterList[i]);
            }
        }
        return list;
    }

    public void ClearInfo()
    {
        points.Clear();
        monsterList.Clear();
        nowWaveNum = maxWaveNum = 0;
        player = null;
    }
}

在MonsterPoint脚本的创建怪物方法时,修改怪物数量逻辑为在关卡管理器加怪物对象到列表中。在MonsterObject 脚本死亡事件方法时修改怪物数量逻辑为在关卡管理器怪物列表中移除当前怪物。

public class MonsterPoint : MonoBehaviour
{
    private void CreateMonster()
    {
        MonsterInfo info = GameDataMgr.Instance.monsterInfoList[nowID - 1];

        GameObject obj = Instantiate(Resources.Load<GameObject>(info.res), this.transform.position, Quaternion.identity);
        MonsterObject monsterObj = obj.AddComponent<MonsterObject>();
        monsterObj.InitInfo(info);

        GameLevelMgr.Instance.AddMonster(monsterObj);

        --nowNum;
        if( nowNum == 0 )
        {
            if (maxWave > 0)
                Invoke("CreateWave", delayTime);
        }
        else
        {
            Invoke("CreateMonster", createOffsetTime);
        }
    }
}

public class MonsterObject : MonoBehaviour
{
    public void DeadEvent()
    {
        GameLevelMgr.Instance.RemoveMonster(this);
        Destroy(this.gameObject);

        if(GameLevelMgr.Instance.CheckOver())
        {
            GameOverPanel panel = UIManager.Instance.ShowPanel<GameOverPanel>();
            panel.InitInfo(GameLevelMgr.Instance.player.money, true);
        }
    }
}

在Resource特效文件夹下创建两个开火特效

TowerObject脚本中,在 Update() 方法首先判断炮塔的攻击方式。如果攻击方式是单体攻击(info.atkType == 1),进入单体攻击逻辑:检查是否有目标可攻击,如果没有或者目标已死亡或者目标超出攻击范围,则调用 GameLevelMgr.Instance.FindMonster() 方法寻找新的目标。如果没有找到可攻击的目标,则返回,不进行旋转。计算怪物位置,并将炮塔头部朝向怪物位置,使用 Quaternion.Slerp() 方法实现平滑旋转。判断炮塔头部和怪物位置之间的夹角是否小于一定范围,同时判断攻击间隔条件是否满足。如果满足条件,则让目标受伤、播放音效、创建开火特效,并记录当前时间为开火时间。如果攻击方式是群体攻击(info.atkType != 1),进入群体攻击逻辑:调用 GameLevelMgr.Instance.FindMonsters() 方法查找多个目标。如果找到目标,并且攻击间隔条件满足,则创建开火特效,并让所有目标受伤。记录当前时间为开火时间。

void Update()
{
    // 单体攻击逻辑
    if (info.atkType == 1)
    {
        // 没有目标 或者 目标死亡 或者 目标超出攻击距离
        if (targetObj == null ||
            targetObj.isDead ||
            Vector3.Distance(this.transform.position, targetObj.transform.position) > info.atkRange)
        {
            // 寻找新的目标
            targetObj = GameLevelMgr.Instance.FindMonster(this.transform.position, info.atkRange);
        }

        // 如果没有找到任何可以攻击的对象 那么炮台就不应该旋转
        if (targetObj == null)
            return;

        // 得到怪物位置,偏移Y的目标是希望 炮台头部不要上下倾斜
        monsterPos = targetObj.transform.position;
        monsterPos.y = head.position.y;
        // 让炮台的头部 旋转起来
        head.rotation = Quaternion.Slerp(head.rotation, Quaternion.LookRotation(monsterPos - head.position), roundSpeed * Time.deltaTime);

        // 判断 两个对象之间的夹角 小于一定范围时  才能让目标受伤 并且攻击间隔条件要满足
        if (Vector3.Angle(head.forward, monsterPos - head.position) < 5 &&
            Time.time - nowTime >= info.offsetTime)
        {
            // 让目标受伤
            targetObj.Wound(info.atk);
            // 播放音效
            GameDataMgr.Instance.PlaySound("Music/Tower");
            // 创建开火特效
            GameObject effObj = Instantiate(Resources.Load<GameObject>(info.eff), gunPoint.position, gunPoint.rotation);
            // 延迟移除特效
            Destroy(effObj, 0.2f);

            // 记录开火时间
            nowTime = Time.time;
        }
    }
    // 群体攻击逻辑
    else
    {
        // 查找目标们
        targetObjs = GameLevelMgr.Instance.FindMonsters(this.transform.position, info.atkRange);

        if (targetObjs.Count > 0 &&
            Time.time - nowTime >= info.offsetTime)
        {
            // 创建开火特效
            GameObject effObj = Instantiate(Resources.Load<GameObject>(info.eff), gunPoint.position, gunPoint.rotation);
            // 延迟移除特效
            Destroy(effObj, 0.2f);

            // 让目标们受伤
            for (int i = 0; i < targetObjs.Count; i++)
            {
                targetObjs[i].Wound(info.atk);
            }

            // 记录开火时间
            nowTime = Time.time;
        }
    }
}

22.3 造塔点逻辑

找到一个特效作为造塔点预制体,创建TowerPoint造塔点脚本,挂载上去。添加球形触发器。

TowerPoint脚本中定义了一个游戏对象变量 towerObj,用于保存当前塔的游戏对象。初始值为 null。定义了一个塔的数据变量 nowTowerInfo,用于保存当前塔的数据。初始值为 null。定义了一个造塔id列表chooseIDs,用于保存可以建造的三个塔的ID。要在挂载的脚本中配置造塔点id列表数据。

//造塔点关联的 塔对象
private GameObject towerObj = null;
//造塔点关联的 塔的数据
public TowerInfo nowTowerInfo = null;
//可以建造的三个塔的ID是多少
public List<int> chooseIDs;

TowerBtn脚本中添加初始化塔按钮方法。该方法接受两个参数:id 和 inputStr。根据传入的 id,从游戏数据管理器(GameDataMgr)中获取对应的塔信息(TowerInfo)。将获取到的塔图片资源设置给按钮的 imgPic 的 sprite 属性。将塔的价格(info.money)和字符串 “¥” 进行组合,并设置给按钮的 txtMoney 的 text 属性,用于显示按钮上的金钱信息。将传入的 inputStr 设置给按钮的 txtTip 的 text 属性,用于显示按钮上的提示信息。判断当前玩家的金钱是否足够购买该塔:如果不足,则将按钮的 txtMoney 的 text 属性设置为 “金钱不足”,用于显示在按钮上。如果足够,则保持按钮的 txtMoney 的 text 属性不变。

/// <summary>
/// 初始化 按钮信息的方法
/// </summary>
/// <param name="id"></param>
/// <param name="inputStr"></param>
public void InitInfo(int id, string inputStr)
{
    TowerInfo info = GameDataMgr.Instance.towerInfoList[id - 1];
    imgPic.sprite = Resources.Load<Sprite>(info.imgRes);
    txtMoney.text = "¥" + info.money;
    txtTip.text = inputStr;
    //判断 钱够不够
    if (info.money > GameLevelMgr.Instance.player.money)
        txtMoney.text = "金钱不足";
}

在GamePanel 脚本中定义存储当前选中的造塔点和标识是否需要检测造塔输入。

//当前进入和选中的造塔点 
private TowerPoint nowSelTowerPoint;

//用来标识  是否检测 造塔输入的
private bool checkInput;

GamePanel脚本中定义用于更新当前选中造塔点的界面显示的方法。首先,将传入的TowerPoint对象赋值给nowSelTowerPoint变量,这个变量表示当前选中的造塔点。然后,通过判断nowSelTowerPoint是否为null来确定传入的数据是否为空。如果是空,则将checkInput设为false,表示不需要检测造塔输入;同时隐藏下方的造塔按钮。否则,将checkInput设为true,表示需要检测造塔输入;同时显示下方的造塔按钮。接下来,根据所选造塔点的状态进行不同的处理:如果没有造过塔(即nowSelTowerPoint.nowTowerInfo == null),则对每个造塔按钮进行设置。遍历towerBtns列表,将每个按钮的gameObject设为激活状态,并调用InitInfo方法初始化按钮的信息。初始化信息中传入了nowSelTowerPoint.chooseIDs[i]作为参数,以及字符串”数字键” + (i + 1)作为按钮的显示文本。这样就会显示具有不同造塔ID和对应数字键提示的按钮。如果已经造过塔(即nowSelTowerPoint.nowTowerInfo != null),则只显示一个造塔按钮,并设置其信息。首先将所有的造塔按钮设为非激活状态。然后,将towerBtns列表中索引为1的按钮(中间的按钮)设为激活状态,并调用InitInfo方法初始化按钮的信息。初始化信息中传入了nowSelTowerPoint.nowTowerInfo.nextLev作为参数,以及字符串”空格键”作为按钮的显示文本。

/// <summary>
/// 更新当前选中造塔点 界面的一些变化
/// </summary>
public void UpdateSelTower(TowerPoint point)
{
    //根据造塔点的信息 决定 界面上的显示内容
    nowSelTowerPoint = point;

    //如果传入数据是空
    if (nowSelTowerPoint == null)
    {
        checkInput = false;
        //隐藏下方造塔按钮
        botTrans.gameObject.SetActive(false);
    }
    else
    {
        checkInput = true;
        //显示下方造塔按钮
        botTrans.gameObject.SetActive(true);

        //如果没有造过塔
        if (nowSelTowerPoint.nowTowerInfo == null)
        {
            for (int i = 0; i < towerBtns.Count; i++)
            {
                towerBtns[i].gameObject.SetActive(true);
                towerBtns[i].InitInfo(nowSelTowerPoint.chooseIDs[i], "数字键" + (i + 1));
            }
        }
        //如果造过塔
        else
        {
            for (int i = 0; i < towerBtns.Count; i++)
            {
                towerBtns[i].gameObject.SetActive(false);
            }
            towerBtns[1].gameObject.SetActive(true);
            towerBtns[1].InitInfo(nowSelTowerPoint.nowTowerInfo.nextLev, "空格键");
        }
    }
}

在各个玩家预制体上添加角色控制器,注意角色可以触发到造塔点。

TowerPoint脚本中定义用于创建塔方法。接受一个整数参数id,代表要创建的塔的ID。根据传入的id从GameDataMgr.Instance.towerInfoList中获取对应的塔信息info。检查如果玩家的金钱不足以建造这座塔(即info.money > GameLevelMgr.Instance.player.money),则直接返回,不进行后续的建造操作。如果玩家的金钱足够建造这座塔,首先通过GameLevelMgr.Instance.player.AddMoney(-info.money)扣除相应的金钱。判断当前构建点是否已经存在塔对象(即towerObj != null)。如果存在,则销毁原有的塔对象(通过Destroy(towerObj))。通过Instantiate方法实例化一个塔对象,并使用Resources.Load(info.res)加载塔的资源文件。该塔对象位于当前脚本所在的游戏对象的位置(即this.transform.position),并将其旋转角度设置为默认角度(Quaternion.identity)。通过towerObj.GetComponent().InitInfo(info)方法对塔对象进行初始化,将塔的信息传递给该方法。将info赋值给nowTowerInfo,记录当前塔的数据。最后,根据nowTowerInfo.nextLev的值来判断塔是否建造完成。如果nextLev不为0,则调用UIManager.Instance.GetPanel().UpdateSelTower(this)方法来更新游戏界面上的内容,传递当前选中的造塔点作为参数。否则,调用UIManager.Instance.GetPanel().UpdateSelTower(null)方法来清空选中的造塔点。

/// <summary>
/// 建造一个塔
/// </summary>
/// <param name="id"></param>
public void CreateTower(int id)
{
    TowerInfo info = GameDataMgr.Instance.towerInfoList[id - 1];
    //如果钱不够 就不用建造了
    if (info.money > GameLevelMgr.Instance.player.money)
        return;

    //扣钱
    GameLevelMgr.Instance.player.AddMoney(-info.money);
    //创建塔
    //先判断之前是否有塔  如果有 就删除
    if (towerObj != null)
    {
        Destroy(towerObj);
        towerObj = null;
    }
    //实例化塔对象
    towerObj = Instantiate(Resources.Load<GameObject>(info.res), this.transform.position, Quaternion.identity);
    //初始化塔
    towerObj.GetComponent<TowerObject>().InitInfo(info);
    
    //记录当前塔的数据
    nowTowerInfo = info;

    //塔建造完毕 更新游戏界面上的内容
    if (nowTowerInfo.nextLev != 0)
    {
        UIManager.Instance.GetPanel<GamePanel>().UpdateSelTower(this);
    }
    else
    {
        UIManager.Instance.GetPanel<GamePanel>().UpdateSelTower(null);
    }
}

TowerPoint脚本中定义触发器进入和退出方法。假如走到了造塔点,但是有塔且不能升级,就直接返回,否则的话的话调用游戏界面的更新当前选择塔面板,传入自身。如果离开了造塔点,调用游戏界面的更新当前选择塔面板,直接传空。

private void OnTriggerEnter(Collider other)
{
    //如果现在已经有塔了 就没有必要再显示升级界面 或者造塔界面了
    if (nowTowerInfo != null && nowTowerInfo.nextLev == 0)
        return;
    UIManager.Instance.GetPanel<GamePanel>().UpdateSelTower(this);
}

private void OnTriggerExit(Collider other)
{
    //如果不希望游戏界面下方的造塔界面显示 直接传空
    UIManager.Instance.GetPanel<GamePanel>().UpdateSelTower(null);
}

GamePanel 脚本中重写的Update方法中处理玩家输入来建造塔。调用base.Update()是为了确保基类(父类)的Update方法也会被执行,以便处理其他逻辑。通过检查checkInput变量的值来确定是否需要进行输入检测。如果checkInput为false,则直接返回,不进行后续的输入处理。如果当前选中的造塔点没有造过塔(即nowSelTowerPoint.nowTowerInfo == null),则会检测玩家按下数字键1、2、3的情况。如果玩家按下了数字键1,则调用nowSelTowerPoint.CreateTower方法,并传入nowSelTowerPoint.chooseIDs[0]作为参数;如果玩家按下了数字键2,则调用nowSelTowerPoint.CreateTower方法,并传入nowSelTowerPoint.chooseIDs[1]作为参数;如果玩家按下了数字键3,则调用nowSelTowerPoint.CreateTower方法,并传入nowSelTowerPoint.chooseIDs[2]作为参数。这样就实现了根据玩家按键来选择不同的造塔选项。如果当前选中的造塔点已经造过塔,那么只会检测玩家是否按下了空格键。如果玩家按下了空格键,则调用nowSelTowerPoint.CreateTower方法,并传入nowSelTowerPoint.nowTowerInfo.nextLev作为参数。这样就实现了通过按下空格键来升级当前已经造好的塔。

protected override void Update()
{
    base.Update();
    //主要用于造塔点 键盘输入 造塔
    if (!checkInput)
        return;

    //如果没有造过塔 那么久检测1 2 3 按钮去建造塔
    if (nowSelTowerPoint.nowTowerInfo == null)
    {
        if (Input.GetKeyDown(KeyCode.Alpha1))
        {
            nowSelTowerPoint.CreateTower(nowSelTowerPoint.chooseIDs[0]);
        }
        else if (Input.GetKeyDown(KeyCode.Alpha2))
        {
            nowSelTowerPoint.CreateTower(nowSelTowerPoint.chooseIDs[1]);
        }
        else if (Input.GetKeyDown(KeyCode.Alpha3))
        {
            nowSelTowerPoint.CreateTower(nowSelTowerPoint.chooseIDs[2]);
        }
    }
    //造过塔 就检测空格键 去建造
    else
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            nowSelTowerPoint.CreateTower(nowSelTowerPoint.nowTowerInfo.nextLev);
        }
    }
}

MonsterObject脚本,添加怪物一死就加钱的逻辑。

//死亡
public void Dead()
{
    isDead = true;
    //停止移动
    //agent.isStopped = true;
    agent.enabled = false;
    //播放死亡动画
    animator.SetBool("Dead", true);
    
    //播放音效
    GameDataMgr.Instance.PlaySound("Music/dead");
    //加钱——我们之后通过关卡管理类 来管理游戏中的对象 通过它来让玩家加钱 
    GameLevelMgr.Instance.player.AddMoney(10);
}

MonsterObject脚本,受伤方法添加判断,假如怪物已经死了直接返回

//受伤
public void Wound(int dmg)
{
    if (isDead)
        return;

    //减少血量
    hp -= dmg;
    //播放受伤动画
    animator.SetTrigger("Wound");

    if( hp <= 0 )
    {
        //死亡
        Dead();
    }
    else
    {
        //播放音效
        GameDataMgr.Instance.PlaySound("Music/Wound");
    }
}

PlayerObject脚本的刀枪事件中,添加要怪物没死才让他受伤的逻辑

/// <summary>
/// 专门用于处理刀武器攻击动作的伤害检测事件
/// </summary>
public void KnifeEvent()
{
    //进行伤害检测
    Collider[] colliders = Physics.OverlapSphere(this.transform.position + this.transform.forward + this.transform.up, 1, 1 << LayerMask.NameToLayer("Monster"));

    //播放音效
    GameDataMgr.Instance.PlaySound("Music/Knife");

    //暂时无法继续写逻辑了 因为 我们没有怪物对应的脚本
    for (int i = 0; i < colliders.Length; i++)
    {
        //得到碰撞到的对象上的怪物脚本 让其受伤
        MonsterObject monster = colliders[i].gameObject.GetComponent<MonsterObject>();
        if (monster != null && !monster.isDead)
        {
            monster.Wound(this.atk);
            break;
        }
    }
}

public void ShootEvent()
{
    //进行摄像检测 
    //前提是需要有开火点
    RaycastHit[] hits = Physics.RaycastAll(new Ray(gunPoint.position, this.transform.forward), 1000, 1 << LayerMask.NameToLayer("Monster"));

    //播放开枪音效
    GameDataMgr.Instance.PlaySound("Music/Gun");

    for (int i = 0; i < hits.Length; i++)
    {
        //得到碰撞到的对象上的怪物脚本 让其受伤
        MonsterObject monster = hits[i].collider.gameObject.GetComponent<MonsterObject>();
        if (monster != null && !monster.isDead)
        {
            //进行打击特效的创建
            GameObject effObj = Instantiate(Resources.Load<GameObject>(GameDataMgr.Instance.nowSelRole.hitEff));
            effObj.transform.position = hits[i].point;
            effObj.transform.rotation = Quaternion.LookRotation(hits[i].normal);
            Destroy(effObj, 1);

            monster.Wound(this.atk);
            break;
        }
    }
}

22.4 代码

TowerBtn

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

/// <summary>
/// 组合控件 主要方便我们控制 造塔相关 UI的更新逻辑
/// </summary>
public class TowerBtn : MonoBehaviour
{
    public Image imgPic;

    public Text txtTip;

    public Text txtMoney;

    /// <summary>
    /// 初始化 按钮信息的方法
    /// </summary>
    /// <param name="id"></param>
    /// <param name="inputStr"></param>
    public void InitInfo(int id, string inputStr)
    {
        TowerInfo info = GameDataMgr.Instance.towerInfoList[id - 1];
        imgPic.sprite = Resources.Load<Sprite>(info.imgRes);
        txtMoney.text = "¥" + info.money;
        txtTip.text = inputStr;
        //判断 钱够不够
        if (info.money > GameLevelMgr.Instance.player.money)
            txtMoney.text = "金钱不足";
    }
}

TowerInfo

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

public class TowerInfo
{
    public int id;
    public string name;
    public int money;
    public int atk;
    public int atkRange;
    public float offsetTime;
    public int nextLev;
    public string imgRes;
    public string res;
    public int atkType;
    public string eff;
}

TowerInfo.json

[
    {"id":1,"name":"加农炮1","money":200,"atk":5,"atkRange":8,"offsetTime":1,"nextLev":2,"imgRes":"TowerImg/1_1","res":"Tower/1_1","atkType":1,"eff":"eff/Fire1"},
    {"id":2,"name":"加农炮2","money":300,"atk":10,"atkRange":8,"offsetTime":1,"nextLev":3,"imgRes":"TowerImg/1_2","res":"Tower/1_2","atkType":1,"eff":"eff/Fire1"},
    {"id":3,"name":"加农炮3","money":400,"atk":20,"atkRange":8,"offsetTime":1,"nextLev":0,"imgRes":"TowerImg/1_3","res":"Tower/1_3","atkType":1,"eff":"eff/Fire1"},
    {"id":4,"name":"机枪炮1","money":200,"atk":2,"atkRange":8,"offsetTime":0.2,"nextLev":5,"imgRes":"TowerImg/2_1","res":"Tower/2_1","atkType":1,"eff":"eff/Fire1"},
    {"id":5,"name":"机枪炮2","money":300,"atk":4,"atkRange":8,"offsetTime":0.2,"nextLev":6,"imgRes":"TowerImg/2_2","res":"Tower/2_2","atkType":1,"eff":"eff/Fire1"},
    {"id":6,"name":"机枪炮3","money":400,"atk":6,"atkRange":8,"offsetTime":0.2,"nextLev":0,"imgRes":"TowerImg/2_3","res":"Tower/2_3","atkType":1,"eff":"eff/Fire1"},
    {"id":7,"name":"魔法炮1","money":200,"atk":3,"atkRange":8,"offsetTime":2,"nextLev":8,"imgRes":"TowerImg/3_1","res":"Tower/3_1","atkType":2,"eff":"eff/Fire2"},
    {"id":8,"name":"魔法炮2","money":300,"atk":6,"atkRange":8,"offsetTime":2,"nextLev":9,"imgRes":"TowerImg/3_2","res":"Tower/3_2","atkType":2,"eff":"eff/Fire2"},
    {"id":9,"name":"魔法炮3","money":400,"atk":9,"atkRange":8,"offsetTime":2,"nextLev":0,"imgRes":"TowerImg/3_3","res":"Tower/3_3","atkType":2,"eff":"eff/Fire2"}
]

TowerObject

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

public class TowerObject : MonoBehaviour
{
    //炮台头部 用于旋转 指向目标
    public Transform head;
    //开火点 用于释放攻击特效的位置
    public Transform gunPoint;
    //炮台头部旋转速度 可以写死 也可以配在表中
    private float roundSpeed = 20;
    
    //炮台关联的数据
    private TowerInfo info;

    //当前要攻击的目标
    private MonsterObject targetObj;
    //当前要攻击的目标们
    private List<MonsterObject> targetObjs;

    //用于计时的 用来判断攻击间隔时间
    private float nowTime;

    //用于记录怪物位置
    private Vector3 monsterPos;

    /// <summary>
    /// 初始化炮台相关数据
    /// </summary>
    /// <param name="info"></param>
    public void InitInfo(TowerInfo info)
    {
        this.info = info;
    }
    
    void Update()
    {
        //单体攻击逻辑
        if (info.atkType == 1)
        {
            //没有目标 或者 目标死亡 或者 目标超出攻击距离
            if( targetObj == null ||
                targetObj.isDead ||
                Vector3.Distance(this.transform.position, targetObj.transform.position) > info.atkRange)
            {
                targetObj = GameLevelMgr.Instance.FindMonster(this.transform.position, info.atkRange);
            }

            //如果没有找到任何可以攻击的对象 那么炮台就不应该旋转
            if (targetObj == null)
                return;

            //得到怪物位置,偏移Y的目标是希望 炮台头部不要上下倾斜
            monsterPos = targetObj.transform.position;
            monsterPos.y = head.position.y;
            //让炮台的头部 旋转起来
            head.rotation = Quaternion.Slerp(head.rotation, Quaternion.LookRotation(monsterPos - head.position), roundSpeed * Time.deltaTime);
            
            //判断 两个对象之间的夹角 小于一定范围时  才能让目标受伤 并且攻击间隔条件要满足
            if( Vector3.Angle(head.forward, monsterPos - head.position) < 5 &&
                Time.time - nowTime >= info.offsetTime )
            {
                //让目标受伤
                targetObj.Wound(info.atk);
                //播放音效
                GameDataMgr.Instance.PlaySound("Music/Tower");
                //创建开火特效
                GameObject effObj = Instantiate(Resources.Load<GameObject>(info.eff), gunPoint.position, gunPoint.rotation);
                //延迟移除特效
                Destroy(effObj, 0.2f);

                //记录开火时间
                nowTime = Time.time;
            }
        }
        //群体攻击逻辑
        else
        {
            targetObjs = GameLevelMgr.Instance.FindMonsters(this.transform.position, info.atkRange);

            if( targetObjs.Count > 0 &&
                Time.time - nowTime >= info.offsetTime)
            {
                //创建开火特效
                GameObject effObj = Instantiate(Resources.Load<GameObject>(info.eff), gunPoint.position, gunPoint.rotation);
                //延迟移除特效
                Destroy(effObj, 0.2f);

                //让目标们受伤
                for (int i = 0; i < targetObjs.Count; i++)
                {
                    targetObjs[i].Wound(info.atk);
                }

                //记录开火时间
                nowTime = Time.time;
            }
        }
    }
}

TowerPoint

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

public class TowerPoint : MonoBehaviour
{
    //造塔点关联的 塔对象
    private GameObject towerObj = null;
    //造塔点关联的 塔的数据
    public TowerInfo nowTowerInfo = null;
    //可以建造的三个塔的ID是多少
    public List<int> chooseIDs;

    /// <summary>
    /// 建造一个塔
    /// </summary>
    /// <param name="id"></param>
    public void CreateTower(int id)
    {
        TowerInfo info = GameDataMgr.Instance.towerInfoList[id - 1];
        //如果钱不够 就不用建造了
        if (info.money > GameLevelMgr.Instance.player.money)
            return;

        //扣钱
        GameLevelMgr.Instance.player.AddMoney(-info.money);
        //创建塔
        //先判断之前是否有塔  如果有 就删除
        if(towerObj != null)
        {
            Destroy(towerObj);
            towerObj = null;
        }
        //实例化塔对象
        towerObj = Instantiate(Resources.Load<GameObject>(info.res), this.transform.position, Quaternion.identity);
        //初始化塔
        towerObj.GetComponent<TowerObject>().InitInfo(info);
        
        //记录当前塔的数据
        nowTowerInfo = info;

        //塔建造完毕 更新游戏界面上的内容
        if(nowTowerInfo.nextLev != 0)
        {
            UIManager.Instance.GetPanel<GamePanel>().UpdateSelTower(this);
        }
        else
        {
            UIManager.Instance.GetPanel<GamePanel>().UpdateSelTower(null);
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        //如果现在已经有塔了 就没有必要再显示升级界面 或者造塔界面了
        if (nowTowerInfo != null && nowTowerInfo.nextLev == 0)
            return;
        UIManager.Instance.GetPanel<GamePanel>().UpdateSelTower(this);
    }

    private void OnTriggerExit(Collider other)
    {
        //如果不希望游戏界面下方的造塔界面显示 直接传空
        UIManager.Instance.GetPanel<GamePanel>().UpdateSelTower(null);
    }
}

GamePanel

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class GamePanel : BasePanel
{
    public Image imgHP;
    public Text txtHP;

    public Text txtWave;
    public Text txtMoney;

    //hp的初始宽 可以在外面去控制它 到底有多宽
    public float hpW = 500;

    public Button btnQuit;

    //下方造塔组合控件的父对象 主要用于控制 显隐
    public Transform botTrans;

    //管理 3个复合控件
    public List<TowerBtn> towerBtns = new List<TowerBtn>();

    //当前进入和选中的造塔点 
    private TowerPoint nowSelTowerPoint;

    //用来标识  是否检测 造塔输入的
    private bool checkInput;

    public override void Init()
    {
        //监听按钮事件
        btnQuit.onClick.AddListener(() =>
        {
            //隐藏游戏界面
            UIManager.Instance.HidePanel<GamePanel>();
            //返回到开始界面
            SceneManager.LoadScene("BeginScene");
            //其它

        });

        //一开始隐藏下方和造塔相关的UI
        botTrans.gameObject.SetActive(false);
        //锁定鼠标
        Cursor.lockState = CursorLockMode.Confined;
    }

    /// <summary>
    /// 更新安全区域血量函数
    /// </summary>
    /// <param name="hp">当前血量</param>
    /// <param name="maxHP">最大血量</param>
    public void  UpdateTowerHp(int hp, int maxHP)
    {
        txtHP.text = hp + "/" + maxHP;
        //更新血条的长度
        (imgHP.transform as RectTransform).sizeDelta = new Vector2((float)hp / maxHP * hpW, 38);
    }

    /// <summary>
    /// 更新剩余波数
    /// </summary>
    /// <param name="nowNum">当前波数</param>
    /// <param name="maxNum">最大波数</param>
    public void UpdateWaveNum(int nowNum, int maxNum)
    {
        txtWave.text = nowNum + "/" + maxNum;
    }

    /// <summary>
    /// 更新金币数量
    /// </summary>
    /// <param name="money">当前获得的金币</param>
    public void UpdateMoney(int money)
    {
        txtMoney.text = money.ToString();
    }


    /// <summary>
    /// 更新当前选中造塔点 界面的一些变化
    /// </summary>
    public void UpdateSelTower( TowerPoint point )
    {
        //根据造塔点的信息 决定 界面上的显示内容
        nowSelTowerPoint = point;

        //如果传入数据是空
        if(nowSelTowerPoint == null)
        {
            checkInput = false;
            //隐藏下方造塔按钮
            botTrans.gameObject.SetActive(false);
        }
        else
        {
            checkInput = true;
            //显示下方造塔按钮
            botTrans.gameObject.SetActive(true);

            //如果没有造过塔
            if (nowSelTowerPoint.nowTowerInfo == null)
            {
                for (int i = 0; i < towerBtns.Count; i++)
                {
                    towerBtns[i].gameObject.SetActive(true);
                    towerBtns[i].InitInfo(nowSelTowerPoint.chooseIDs[i], "数字键" + (i + 1));
                }
            }
            //如果造过塔
            else
            {
                for (int i = 0; i < towerBtns.Count; i++)
                {
                    towerBtns[i].gameObject.SetActive(false);
                }
                towerBtns[1].gameObject.SetActive(true);
                towerBtns[1].InitInfo(nowSelTowerPoint.nowTowerInfo.nextLev,"空格键");
            }
        }
       
    }


    protected override void Update()
    {
        base.Update();
        //主要用于造塔点 键盘输入 造塔
        if (!checkInput)
            return;

        //如果没有造过塔 那么久检测1 2 3 按钮去建造塔
        if( nowSelTowerPoint.nowTowerInfo == null )
        {
            if( Input.GetKeyDown(KeyCode.Alpha1) )
            {
                nowSelTowerPoint.CreateTower(nowSelTowerPoint.chooseIDs[0]);
            }
            else if (Input.GetKeyDown(KeyCode.Alpha2))
            {
                nowSelTowerPoint.CreateTower(nowSelTowerPoint.chooseIDs[1]);
            }
            else if (Input.GetKeyDown(KeyCode.Alpha3))
            {
                nowSelTowerPoint.CreateTower(nowSelTowerPoint.chooseIDs[2]);
            }
        }
        //造过塔 就检测空格键 去建造
        else
        {
            if( Input.GetKeyDown(KeyCode.Space) )
            {
                nowSelTowerPoint.CreateTower(nowSelTowerPoint.nowTowerInfo.nextLev);
            }
        }
    }
}


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏