项目经历(二)— 基于Unity的塔防游戏开发

这个项目算是笔者第一次完成度较高且评价还行的项目了,在本篇中我将重点介绍实现的思路,舍弃Unity中的配置方式,而且由于Unity软件已过期,讲述可能会略显抽象,望诸位见谅。推荐一下当时学习的地方SIKI学院,里面有很多利用Unity开发的教程。

1.目标

以Unity为基础,使用C#语言,开发出一款Windows版本单机3D塔防游戏。

2.逻辑

作为塔防类游戏,他应当有良好的内部逻辑,即怪物生成、移动、击杀、塔的建造、拆毁、攻击、输赢判别;也不能缺少与用户的交互,选择哪一关、建造什么类型的塔、在哪建塔等等。
给出两个逻辑框图:外部输入内部逻辑

3.代码

3.1 敌人代码

3.1.1移动

在Unity中,事先将起点、终点以及中间的所有拐点在地图上标出,作为一个GameObject的集合传入到敌人移动的脚本中,设置敌人的行动路线,总是朝着下一个目标点,当敌人与目标点的距离小于一定值时,更新目标点即集合的下一个值。移动中不断判断自身血量值,小于0时销毁自身,到达终点后销毁自身。在此脚本中,需要留有设置敌人移动速度、血量值、死亡效果等属性方便Unity设置。
为了敌人的移动好看,可以给模型添加一定的动作并设置为无限循环(笔者当时并没有深入研究Unity动作的时间控制,故采用了这样的方法)。

transform.Translate((positions[index].position - transform.position).normalized * Time.deltaTime * speed,Space.World);
/*此句用于控制敌人移动
*positions[index].position - transform.position 下一个位置至当前位置
*Time.deltaTime * speed  1S内的速度
*Space.World 按照世界坐标移动
*/
transform.LookAt(positions[index].position);//更改敌人的朝向
 GameObject.Destroy(this.gameObject);//销毁自身
  GameObject effect = GameObject.Instantiate(explosionEffect, transform.position, transform.rotation);
  //在当前地点当前角度实例化explosionEffect 即销毁效果

3.1.2 生成

能够让敌人移动和销毁后当然就是让敌人生成了,生成的逻辑采用了简单的这一次生成的敌人都被销毁即数量为0时生成下一波,因此我们需要定义的属性为敌人模型、敌人数目、敌人波次(肯定不能只有一波敌人吧),既然一波中生成的不止一个敌人,那就需要我们定义生成速率。

 for (int i=0;i<wave.count;i++) //遍历当前波次的每一个个体来生成
            {
                GameObject.Instantiate(wave.enemyPrefab, START.position, Quaternion.identity);  
                /*初始化游戏物体
                *wave.enemyPrefab 在Unity中设置的敌人模型
                *START.position  起始点
                *Quaternion.identity 旋转四元素为默认(0,0,0,0)个人是这么理解的
                */
                CountEnemyAlive++; //当前场上存活敌人数+1
                if (i != wave.count - 1)
                {
                    yield return new WaitForSeconds(wave.rate); //当生成一个非此波最后一个敌人时,等待设定的时间
                }
}

3.2 塔代码

因为塔代码中的建造代码属于交互类型,笔者将放在交互节代码进行介绍。

3.2.1 攻击

在Unity中可以给塔设置一个球体的攻击范围(假设已经有塔了),同时设置这个范围和敌人模型中心为刚体,这样他们就能产生碰撞作为触发源,(这也解释了为什么不设置为一个圆形,设置为圆形差生碰撞必须是点与线相交,需要设置成相同的高度,而与球体碰属于面,虽然范围会有一定的偏差但并不会影响过多),发生碰撞之后,塔进行攻击,步骤为先瞄准敌人(仅需动塔的上半部分),而后实例化出炮弹进行瞄准攻击,给实例化炮弹设置一定的时间间隔,这便是攻击速度,当然这个是对用炮弹攻击的塔,而对于激光类采用画线的方式进行解决,或者直接在敌人身上实例出效果;当敌人走出攻击范围,应当停止攻击;当敌人已经被消灭,应当更新敌人的数组攻击下一个敌人。
上面提到的攻击方式中,炮弹攻击方式需要拥有和敌人移动相似的移动脚本,只不过目标换为了自身位置到敌人所在位置,敌人已经被消灭时需要销毁自身,碰到敌人时需要销毁自身;其他两种方式只需要指向或者直接在敌人身上实例化即可。

 void OnTriggerEnter(Collider col)
    {
        if (col.tag=="Enemy")
        {
            enemys.Add(col.gameObject);   //当产生碰撞的是敌人,增加
        }
    }
    void OnTriggerExit(Collider col)
    {
        enemys.Remove(col.gameObject);   //当范围内有物体移出,减少  因为其他物体是不可能移出的
    }

代码中的:col.tag=="Enemy"是需要在Unity中设置的刚体标签

Vector3 targetPosition = enemys[0].GetComponent<EnemyMove>().AttackPosition.transform.position;  //瞄准的地方为第一个敌人的AttackPosition
targetPosition.y = head.position.y;   //塔不可以上下移动类似点头
head.LookAt(targetPosition);//改变瞄准方向

代码中的:enemys[0].GetComponent<EnemyMove>().AttackPosition"是需要在Unity敌人模型中设置的一个点

public void SetTarget(Transform _target)
    {
        this.target = _target;   //设置子弹目标
    }
    
GameObject shell = GameObject.Instantiate(attackPrefab, fire_position.position, fire_position.rotation); //在枪口实例化出子弹
shell.GetComponent<Shell>().SetTarget(enemys[0].transform);  //设置目标
 void UpdateEnemy()
    {
        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);// 将去除了空集合的新敌人集合赋值给原本的敌人集合
        }
    }
laserRendener.SetPositions(new Vector3 []{ fire_position.position, enemys[0].GetComponent<EnemyMove>().AttackPosition.transform.position }); //采用激光攻击的塔 采用从开火处至敌人攻击位置画线的方式
enemys[0].GetComponent<EnemyMove>().TakeDamage(damagerate*Time.deltaTime); //收到的伤害则按照时间计算

target.GetComponent<EnemyMove>().TakeDamage(damage); //采用炮弹攻击的则是固定伤害

3.2.2 属性

属性这一块没什么好说的,只要留出来诸如塔类型,建造费用、升级费用以及相应的拆毁费用等属性在Unity中进行设定即可,需要注意的是将所有塔类型罗列出来,这样在建造的时候才能知道。

3.3 交互代码

3.3.1 页面选择

看标题便知道是干啥了,每一个场景下需要干的事情不同,如关卡选择页,游戏界面页,可以在Unity中为各个页面进行编号,有关按键亦是如此,可以为按键单独添加一个函数,检测鼠标点下位置来进行响应。

SceneManager.LoadScene(1);   //跳转到页面1

public void OnExitGame()    //点击退出按键的函数
    {
#if UNITY_EDITOR
        UnityEditor.EditorApplication.isPlaying = false;   //在Unity中预览时退出的方式
#else
        Application.Quit();   //打包成APP后的退出方式
#endif
    }
    
public void OnButtonPause()   //点击暂停按键的函数
	{
    	pauseUI.SetActive(true);   //显示暂停界面UI为
        Time.timeScale = 0;        //设置游戏界面时间速度为0
    }

3.3.2 键盘输入

键盘输入主要用于两个方面,一是通过键盘进行视角的移动,二是通过键盘进行塔类型的选择。

void Update()
{
	float h = Input.GetAxis("Horizontal");    //左右方向键
    float v = Input.GetAxis("Vertical");      //上下方向键
    float mouse = Input.GetAxis("Mouse ScrollWheel");  //鼠标滚轮
    transform.Translate(new Vector3(h*speed, mouse*mousespeed, v*speed) * Time.deltaTime ,Space.World);   //设置移动的速率
}
if(Input.GetKeyDown(KeyCode.Q))
{
	turretone.isOn = true;    //当Q键被按下时,第一个塔被选择
}

3.3.3 与塔相关

与塔相关的无非三项,建造、升级、拆毁,而这三者均基于选择到了建造地点,Unity中可以为我们的物体打上标签,我们将所有可以建造的地方打上一个“MapCube”的标签表示可以用于建造,当我们选择了要建造的塔并点击到该物体时,进行金钱是否充足的判断,充足则建造,而如果此物体上已经有一个塔,那么就弹出升级和拆毁的UI界面,点击升级则升级,拆毁则销毁此物体,此外为了用户可以清楚地知道当前鼠标点在哪一块上,可以改变该物体颜色,鼠标移出后变回原样即可。

if(Input.GetMouseButtonDown(0))//检测鼠标按下
{
	Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);//采用视线检测的方法进行判断在哪一个物体
	RaycastHit hit;
	bool isCollider = Physics.Raycast(ray,out hit ,1000, LayerMask.GetMask("MapCube"));//得到点击的地方是否是MapCube
	if(isCollider)   //当点击的是MapCube
	{
		MapCube mapCube = hit.collider.GetComponent<MapCube>();
		if (SelectedTurretData != null && mapCube.TurretGo == null)// 当有选择塔并且这个MapCube上没有塔则建造
		{
           /* 判断是否能够建造
           * 建造就扣钱,生成塔
           * 不能就触发缺钱动画MoneyAnimator
           */ 
           if (money >= SelectedTurretData.cost)
           {
               ChangeMoney(-SelectedTurretData.cost);
               mapCube.BulidTurret(SelectedTurretData);
           }
           else
           {
               moneyAnimator.SetTrigger("MoneyAnimator");
           }
       }
       else if (mapCube.TurretGo != null)  //这个MapCube上有塔则显示升级、拆毁界面UI
       {
       ShowChangeUI(mapCube.transform.position, mapCube.isUpgrated);
       }
}
void ChangeMoney(int change)
    {
        money += change;
        moneyText.text =  money.ToString("###,###");// 根据参数将金钱改变,并且转化为1000位表示形式字符串
    }
void ChangeMoney(int change)
    {
        money += change;
        moneyText.text =  money.ToString("###,###");// 根据参数将金钱改变,并且转化为1000位表示形式字符串
    }
 public void BulidTurret(TurretData turretType)   //建造塔时调用的函数  根据传进来的参数确定塔的类型,并实例化塔和建造塔的特效,将塔的等级、建造费用、拆毁返现数据记录方便调用
{
     this.turretData = turretType;
     isUpgrated = false;
     TurretGo = GameObject.Instantiate(turretData.turretPrefab, transform.position, Quaternion.identity);
     level = 1;
     GameObject effect = GameObject.Instantiate(bulidEffect, transform.position, Quaternion.identity);
     cost = turretData.costUpgraded;
     back = turretData.back;
     Destroy(effect, 1);
}
public void UpgrateTurret()  //升级塔时调用的函数,与建造的区别就是要先拆除原来已有的塔,根据判断来确定现在应当是哪一等级的建造
    {
        if(isUpgrated==true)
        {
            return;
        }
        Destroy(TurretGo);
        if (level == 2)
        {
            TurretGo = GameObject.Instantiate(turretData.turretFinalPrefab, transform.position, Quaternion.identity);
            level = 3;
            cost = 0;
            back = turretData.backFinal;
            isUpgrated = true;
        }
        if (level==1)
        {
            TurretGo = GameObject.Instantiate(turretData.turretUpgradedPrefab, transform.position, Quaternion.identity);
            level = 2;
            cost = turretData.costFinal;
            back = turretData.backUpgrade;
        }
        GameObject effect = GameObject.Instantiate(bulidEffect, transform.position, Quaternion.identity);
        Destroy(effect, 1);
    }

当时在写完升级时,在Unity中进行测试,发现从一级塔会直接调到三级塔,排查后发现是连续判断出了问题,即刚开始是1,然后升级变为2,这时又继续进行了判断,进行了升级,故修改为先判断是否要2升3,再判断是否要1升2

void ShowChangeUI(Vector3 pos,bool isDisabled=false)   //当升级、拆毁界面出现时调用的函数   步骤为停止隐藏,并重新激活画布(相当于重启)  在点击位置显示
{
	StopCoroutine("HideChangeUI");
    ChangeCanvas.SetActive(false);
    ChangeCanvas.SetActive(true);
    ChangeCanvas.transform.position = pos;
    upgrateButton.interactable = !isDisabled;
}
//鼠标进入退出时的函数
private void OnMouseEnter()
{
	if(TurretGo==null&&EventSystem.current.IsPointerOverGameObject()==false)
	{
	mapcube_color.material.color = Color.green;
	}
}

private void OnMouseExit()
    {
        mapcube_color.material.color = Color.white;
    }

4.最终效果

首先先感谢一下小伙伴,笔者对于写代码还有些耐心,然鹅对于美学之类的就很随意,小伙伴做出来的地图和一些界面UI是真的漂亮,后面如果他开通了博客会放他的链接的,再次谢过!
参数调节界面:
在这里插入图片描述
游戏开始界面:
在这里插入图片描述
关卡选择界面
在这里插入图片描述
第一关游戏界面
在这里插入图片描述
暂停界面
在这里插入图片描述
失败界面
在这里插入图片描述

5.总结

网上有关各类工具的教程很多,是不可能学完的,最重要的是在使用工具的过程中思考,有着明确的逻辑才能举一反三,达到良好的效果。

发布了2 篇原创文章 · 获赞 0 · 访问量 47

猜你喜欢

转载自blog.csdn.net/baidu_40605776/article/details/104721448