这个项目算是笔者第一次完成度较高且评价还行的项目了,在本篇中我将重点介绍实现的思路,舍弃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.总结
网上有关各类工具的教程很多,是不可能学完的,最重要的是在使用工具的过程中思考,有着明确的逻辑才能举一反三,达到良好的效果。