演示
资源包:链接:https://pan.baidu.com/s/15MMtYeKkNk5xChvCx0EckQ?pwd=d1ub 提取码:d1ub
对应视频教学:01-开始介绍和创建工程_哔哩哔哩_bilibili
功能简介
分为蓝,紫,粉,红四批敌人,每一批的敌人都比前一批的数量要多,并且速度要快,血量要多,当一批敌人死光了,才会出来第二批敌人,一共有三种炮塔每个金额为70,80,90,初始金额为1000,选择炮塔类型,点击Cube,即可以插放,再次点击时候可以选择升级或拆除,由于地图过大,可以一共上下左右键来控制地图前后左右视角,用鼠标滑轮来控制上下视角,把四批敌人杀光才可以通关成功,否则失败。
制作细节详解
Cube创建基本的地图
创建一个空物体记作"MapCube",把与地图map相关的都放进去,创建一个cube,进行ctrl+d复制,要按住ctrl进行拖拽(一米一米的移动否则将随意移动)。
创建敌人行走的路
定位两个位置,起始点/终点,然后随机连起来选择一些个MapCube删除掉,然后在空的路上边还是用Cube(Road纯黑材质)连出这条路来,该长的长,该短的短。
把上面的敌人走的路径放到一个新建的"RoadCube"
然后做两个Cube,命名为Start 和 End,调整好大小,材质,放置在起始点和终点正上方,(注意把这两个的Collider取消掉,就不会和下面创建的敌人做碰撞了)
控制游戏的视野(视野移动和放大缩小)
由于地图还是蛮大的,在这里添加地图上下前后移动的功能,给玩家提供便利,具体情况如下所示:
这里创建一个"Scripts"文档,把脚本都放进去,首先创建一个"ViewController"脚本来控制视野。
代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ViewController : MonoBehaviour {
public float speed = 1;
public float mouseSpeed = 60;
// Update is called once per frame
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);
}
}
敌人的路径管理
让敌人按照我们设计的路线行走,这里我们直接在拐弯的地方添加一些关键点就可以按照这些点一个个向下移动,把这些路径点都放入到"Waypoints中"
然后在这里添加一个脚本去管理这些路径点,
代码如下
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Waypoints : MonoBehaviour {
public static Transform[] positions;
//脚本被载入时调用(最早的执行函数)
void Awake()
{
//注意这里如果用transform.GetComponent这种方法,会把自身的组件也带上,所以要用下面的方式0
positions = new Transform[transform.childCount];//先从孩子点位里获得数组大小
for (int i = 0; i < positions.Length; i++)
{
positions[i] = transform.GetChild(i);
}
}
}
创建敌人,控制敌人的移动
这里简单的就拿做不同颜色的小球,当作不同的敌人,然后创建一个预制体"Prefab"文件夹,把不同的敌人放进文件夹中,如下所示:
为了让敌人之间区分,给小球涂上不同的颜色,给每种敌人创建一个材质:
控制每个敌人的移动,这里创建一个"Enemy"脚本,代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class Enemy : MonoBehaviour {
public float speed = 10;
public float hp = 150;
private float totalHp;
public GameObject explosionEffect;
private Slider hpSlider;
private Transform[] positions;
private int index = 0;
// Use this for initialization
void Start () {
positions = Waypoints.positions;
totalHp = hp;
hpSlider = GetComponentInChildren<Slider>();
}
// Update is called once per frame
void Update () {
Move();
}
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();
}
}
//达到终点
void ReachDestination()
{
GameManager.Instance.Failed();
GameObject.Destroy(this.gameObject);
}
void OnDestroy()
{
EnemySpawner.CountEnemyAlive--;
}
public void TakeDamage(float damage)
{
if (hp <= 0) return;
hp -= damage;
hpSlider.value = (float)hp / totalHp;
if (hp <= 0)
{
Die();
}
}
void Die()
{
GameObject effect = GameObject.Instantiate(explosionEffect, transform.position, transform.rotation);
Destroy(effect, 1.5f);
Destroy(this.gameObject);
}
}
创建敌人孵化器管理敌人的生成
创建四种敌人,每种敌人用不同的颜色表示,每种颜色的敌人血量和移动速度是不一样的,这就需要我们创建单独的脚本来保存每一波敌人的属性,脚本记为"Wave"。
代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//保存每一波敌人生成所需要的属性
[System.Serializable]
public class Wave {
public GameObject enemyPrefab;
public int count;
public float rate;
}
接着我们创建一个生成器,管理敌人一波一波的生成,这里创建一个空物体,设置为“GameManager",创建一个"Enemy Spawner"脚本拖入"GameManager"中,
可以设置每种敌人的数量和速度。
代码如下
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.Instance.Win();
}
}
创建三种炮台Prffab
首先在Prefab文件中创建三种炮台和三种炮台升级后的炮台:将第一个激光炮塔放置到0点上的Cube位置进行调整,j将资源里的材质啥的往合适的地方塞,弄好看了然后命名并设置成LaserTurret预制体,将升级后的命名为LaserTurretUpgraded,同理弄完MissileTurret、MissileTurretUpgraded、StandardTurret、StandardTurretUpgraded。
创建炮塔选择的UI
要在场景中创建炮台,首先要有UI界面,然后才能对炮台进行选择,创建一个新文档"Canvas"然后鼠标右击UI选项,选择"Toggle"开关按钮,名为LaserToggle,将Is On的去掉勾选。来代表三种炮塔的选择。它下面用Label表示介绍,用Text表示价格。ackground里的Image-Source Image设置成资源里的LaserBeamerIcon,可点击Image-Set Native Size将其图片设置为原生大小后再进行调整。将Checkmark大小和Background改成一致(用那个Alt键充满的方式),将里面的Image-Source Image改成一种被遮罩的图片(这里用的一个圆的Knob),修改颜色中的α值透明度,代表着选中后的效果。复制出两个LaserToggle,更换一下背景图片做出MissileToggle和StandardToggle。在Canvas下创建一个空物体,命名为TurretSwitch,在它上创建一ToggleGroup组件来包含这个三开关,位置摆放在Canvas居右,设置好三个炮台开关的分组,都选中后,在Toggle下的Group将TurretSwitch拉过来,这样就可以单选了。
创建炮台的数据类
创建炮台数据类,用来保存炮台相关数据,创建脚本TurretData:
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,//标准炮台
}
监听炮塔选择的事件
在GameManager中再创建一个BuildManager脚本:(测试的时候可以将selectedTurredData设成public,方便在Inspector面板选择UI炮台时能看出来是否有数据)
检测鼠标点击到了哪个Cube上
在MapCube预制体上添加一个Layer图层,叫MapCube,然后将其图层选择为MapCube。
控制开始按钮1和退出时按钮的点击事件处理
金钱的管理
下边代码有设定钱了,把它显示在UI界面上,Canvas下新建个Text命名为Money,设置一下字体和居右,在BuildManager代码新加入一个方法ChangeMoney和字段moneyText,在Inspector将Money拖入至该字段。
//放置原理:点击的时候在鼠标的位置发射出来一条射线,看一下射线和哪个Node发生了碰撞,发生碰撞后要去检查下这个Node上是否为空,再做处理
//在MapCube添加一个Layer(图层),叫做MapCube,然后选择为它,这样在利用射线做检测时只检测对MapCube的碰撞。
public class BuildManager : MonoBehaviour
{
public TurretData laserTurretData;//在Inspector面板将预制体拉入,并填写其它相关数据
public TurretData missileTurretData;//在Inspector面板将预制体拉入,并填写其它相关数据
public TurretData standardTurretData;//在Inspector面板将预制体拉入,并填写其它相关数据
//表示当前选择的炮台(要建造的炮台)
private TurretData selectedTurredData;//UI上显示和选择的炮台,写三个炮台的选择方法,通过注册三个炮台的Toggle事件来识别哪个被选择了
private int money = 1000;
public Text moneyText;
public Animator moneyAnimator;
void ChangeMoney(int change=0)
{
money += change;
moneyText.text = "$ " + money;
}
void Update()
{
if (Input.GetMouseButtonDown(0))
{
//如果鼠标在UI上面,则不做处理; EventSystem.current返回的是Hierarchy里EventSystem里EventSystem(Script)组件。
//IsPointerOverGameObject表示鼠标是否按在了UI上
if (EventSystem.current.IsPointerOverGameObject() == false)
{
//开发炮台的建造,首先判断鼠标点击到了哪个MapCube上,就要使用射线检测了,得到一个射线ray
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);//把鼠标的点转化成射线
RaycastHit hit;
//Physics.Raycast来进行射线检测,(射线,RaycastHit射线检测跟什么东西做了碰撞的结果,maxDistance最大距离,layerMask和哪一层做射线检测如不指定就是和所有的层)
bool isCollider = Physics.Raycast(ray,out hit, 1000, LayerMask.GetMask("MapCube"));//得到是否碰撞到MapCube上
if (isCollider)
{
MapCube mapCube = hit.collider.GetComponent<MapCube>();//得到点击的mapCube
if(mapCube.turretGo == null && selectedTurredData != null)//可以创建
{
if (money > selectedTurredData.cost)
{
money -= selectedTurretData.cost;
mapCube.BuildTurret(selectedTurredData);
}
else//提示钱不够
{
moneyAnimator.SetTrigger("Flicker");
}
}
else
{
//TODO 升级处理
}
}
}
}
}
//在Canvas里的设备里有On Value Changed里添加GameManager,然后选择对应的下面方法,只要是点击设备值发生改变了,也就是is on发生改变了,都会触发
public void OnLaserSelected(bool isOn)
{
if (isOn)
{
selectedTurredData = laserTurretData;
}
}
public void OnMissileSelected(bool isOn)
{
if (isOn)
{
selectedTurredData = missileTurretData;
}
}
public void OnStandardSelected(bool isOn)
{
if (isOn)
{
selectedTurredData = standardTurretData;
}
}
}
控制子弹跟敌人的碰撞处理,让子弹碰到敌人就爆炸
在Bullet预制体上添加一个Rigidbody刚体,取消勾选Use Gravity,在Bullet中处理触发检测OnTriggerEnter方法,定义好爆炸的特效字段explosionEffectPrefab,在Enemy中添加TakeDamage方法表示受到了伤害,Enemy脚本全部代码如下:
public class Enemy : MonoBehaviour
{
public float speed = 10;//每秒移动10米
public float hp = 150;
private float totalHp;
private Transform[] positions;
private int index = 0;//默认的位置
public GameObject explosionEffect;//爆炸特效
private Slider hpSlider;//血条
void Start()
{
//获取到小球行走的路径点
positions = Waypoints.positions;
totalHp = hp;
hpSlider = GetComponentInChildren<Slider>();//从子物体中寻找Slider物体装配上
}
void Update()
{
Move();
}
void Move()
{
//if (index > positions.Length - 1) return;//当到达最后一个位置
//(目标位置 - 当前位置)得到一个向量.单位化每次移动1,取得单位向量之后再做计算
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();
}
}
//到达目的地,游戏就失败了
void ReachDestination()
{
GameManager.Instance.Faild();
GameObject.Destroy(this.gameObject);
}
//被打掉销毁
private void OnDestroy()
{
EnemySpawner.CountEnemyAlive--;
}
//表示受到了伤害
public void TakeDamage(float damage)
{
if (hp <= 0) return;
hp -= damage;
hpSlider.value = (float)hp / totalHp;//hpSlider.value是一个0~1的值,所以它以百分比来计算得到
if(hp <= 0)
{
Die();
}
}
void Die()
{
GameObject effect = GameObject.Instantiate(explosionEffect, transform.position, transform.rotation);
Destroy(effect, 1.5f);
Destroy(this.gameObject);
}
}
添加爆炸特效
修改检测碰撞方式,创建一个Particle System命名为ExplosionEffect特效,然后将其拉入到Bullet的Bullet字段中,可以将Bullet下Rigidbody下的Collision Detection(碰撞检测)改成Continuous(连续的)或者Continuous Dynamic(动态的)这样对高速移动的物体检测更加准确,把Bullet图层设为Turret。
敌人添加血条显示
创建一个Canvas,把Canvas里的Render Mode修改为World Space,就可以调节画布的大小了,下面创建一个Slider,它不需要交互,所以把Slider(Script)下面Intractable(可交互的)取消勾选,Canvas调整成和Slider差不多大,将Slider下的Handle Slide Area(手柄滑动区)移除,Background取消勾选,Slider是依靠Slider(Script)下面Value来控制条长的,调整Fill Area长度与默认充满,可以把Fill(前置背景)的Image中Color改为绿色,然后把Canvas整体移到各个Enemy预制体下面,调整Canvas的位置和大小。
创建炮塔升级的UI按钮
创建一个Canvas命名为UpgradeCanvas,设置成World Space,下面创建两个Button,命名为ButtonUpgrade和ButtonDestroy,里面Image(Script)去掉,下面Text是升级和拆除,弄好后可拖到在炮塔下面进行编辑,方便定位位置和调整大小,完成后再挪出来。
控制升级面板显示
在BuildManager里进行控制,添加两个引用,public GameObject upgradeCanvas 和 public Button buttonUpgrade,在Inspector面板注册上,脚本里继续添加ShowUpgradeUI 和 HideUpgradeUI 显示/隐藏按钮方法,添加 OnUpgradeButtonDown 和 OnDestroyButtonDown 升级/拆按钮按下方法,然后在Inspector面板把后两个方法注册给两个按钮。
BuildManager脚本全部代码如下:
/放置原理:点击的时候在鼠标的位置发射出来一条射线,看一下射线和哪个Node发生了碰撞,发生碰撞后要去检查下这个Node上是否为空,再做处理
//在MapCube添加一个Layer(图层),叫做MapCube,然后选择为它,这样在利用射线做检测时只检测对MapCube的碰撞。
public class BuildManager : MonoBehaviour
{
public TurretData laserTurretData;
public TurretData missileTurretData;
public TurretData standardTurretData;
//表示当前选择的炮台(要建造的炮台)
private TurretData selectedTurredData;//UI上显示和选择的炮台
public Text moneyText;
public Animator moneyAnimator;
private int money = 1000;
public GameObject upgradeCanvas;//升级UI画板
public Button buttonUpgrade;
private MapCube selectedMapCube;//3D场景中选择的炮台
private Animator upgradeCanvasAnimator;//升级UI显示隐藏的动画转换状态机
void ChangeMoney(int change=0)
{
money += change;
moneyText.text = "$ " + money;
}
private void Start()
{
upgradeCanvasAnimator = upgradeCanvas.GetComponent<Animator>();//得到状态机
}
void Update()
{
if (Input.GetMouseButtonDown(0))
{
//如果鼠标在UI上面,则不做处理; EventSystem.current得到的是EventSystem模块里EventSystem那个组件。
if (EventSystem.current.IsPointerOverGameObject() == false)//IsPointerOverGameObject表示鼠标是否按在了UI上
{
//开发炮台的建造,首先判断鼠标点击到了哪个MapCube上,就要使用射线检测了,得到一个射线ray
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);//把鼠标的点转化成射线
RaycastHit hit;
//Physics.Raycast来进行射线检测,(射线,RaycastHit射线检测跟什么东西做了碰撞的结果,maxDistance最大距离,layerMask和哪一层做射线检测如不指定就是和所有的层)
bool isCollider = Physics.Raycast(ray,out hit, 1000, LayerMask.GetMask("MapCube"));//得到是否碰撞到MapCube上
if (isCollider)
{
MapCube mapCube = hit.collider.GetComponent<MapCube>();//得到点击的mapCube
if(mapCube.turretGo == null && selectedTurredData != null)//可以创建
{
if (money > selectedTurredData.cost)
{
ChangeMoney(-selectedTurredData.cost);
mapCube.BuildTurret(selectedTurredData);
}
else//提示钱不够
{
//TODO
moneyAnimator.SetTrigger("Flicker");
}
}
else if(mapCube.turretGo != null)//如果上边有炮台,那么判断是否做升级处理
{
if(mapCube.turretGo == selectedMapCube && upgradeCanvas.activeInHierarchy)//如果第二次点击此炮台了并且UI的激活属性是true
{
StartCoroutine("HideUpgradeUI");//将UI隐藏,用协程的方式
}
else
{
//否则显示升级/拆除UI面板,第二个参数的bool值与是否有炮台判断相符,所以不再if判断直接传即可
ShowUpgradeUI(mapCube.transform.position, mapCube.isUpgraded);
}
selectedMapCube = mapCube;//把点击的炮台赋给点击的炮台
}
}
}
}
}
//在Canvas里的设备里有On Value Changed里添加GameManager,然后选择对应的下面方法,只要是点击设备值发生改变了,就会触发
public void OnLaserSelected(bool isOn)
{
if (isOn)
{
selectedTurredData = laserTurretData;
}
}
public void OnMissileSelected(bool isOn)
{
if (isOn)
{
selectedTurredData = missileTurretData;
}
}
public void OnStandardSelected(bool isOn)
{
if (isOn)
{
selectedTurredData = standardTurretData;
}
}
void ShowUpgradeUI(Vector3 pos, bool isDisableUpgrade=false)
{
StopCoroutine("HideUpgradeUI");//搜索下面的HideUpgradeUI协程方法有没有在运行,有的话先给暂停掉,没有也不会影响。
//设置画布禁用,为的是切换到新的炮台时候,状态机会初始化一下,能有一个激活弹出UI的效果,这里调用状态机里show的时候,可能HideUpgradeUI还在播放,
//为了防止冲突故在上面加上一个暂停的协程方法。
upgradeCanvas.SetActive(false);
upgradeCanvas.SetActive(true);//设置画布显示
pos.y = pos.y + 4;
upgradeCanvas.transform.position = pos;//设置画布位置
buttonUpgrade.interactable = !isDisableUpgrade;//开启或者禁用升级按钮
}
IEnumerator HideUpgradeUI()
{
upgradeCanvasAnimator.SetTrigger("Hide");
yield return new WaitForSeconds(0.8f);//消失的效果结束后再去调用下面
upgradeCanvas.SetActive(false);//隐藏的时候不能直接把画布禁用,不然就无法播放禁用的动画了
}
public void OnUpgradeButtonDown()//按下升级触发的方法
{
if(money >= selectedMapCube.turretData.costUpgraded)//如果大于升级所需要的钱
{
ChangeMoney(-selectedMapCube.turretData.costUpgraded);
selectedMapCube.UpgradeTurret();
}
else
{
moneyAnimator.SetTrigger("Flicker");
}
StartCoroutine("HideUpgradeUI");//把UI隐藏掉
}
public void OnDestroyButtonDown()//按下拆除触发的方法
{
selectedMapCube.DestroyTurret();
StartCoroutine("HideUpgradeUI");
}
}
设计游戏结束时候的UI界面
在主Canvas下创建一个空物体命名为End,让其与Canvas画布大小保持一致(用Alt填充方法),在它下面创建一个Image命名为Bg,修改颜色和透明度,创建一个Text命名为Message居中,创建两个Button命名为ButtonRetry、ButtonMenu,修改这俩下面的Text内容为重玩和菜单,选择End创建动画show,做一个背景慢慢显示,Text和俩Button从外进来的效果,
控制失败界面的显示
public class GameManager : MonoBehaviour
{
public GameObject endUI;//结束的UI画面
public Text endMessage;
public static GameManager Instance;
private EnemySpawner enemySpawner;
private void Awake()
{
Instance = this;
enemySpawner = GetComponent<EnemySpawner>();
}
public void Win()
{
endUI.SetActive(true);//设置为true后,它下面的Animator动画已经勾上了,会自动播放
endMessage.text = "胜 利";
}
public void Faild()
{
enemySpawner.Stop();//停止生成敌人。
endUI.SetActive(true);
endMessage.text = "失 败";
}
public void OnButtonRetry()
{
endUI.SetActive(false);
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);//重新加载当前场景
}
public void OnButtonMenu()
{
SceneManager.LoadScene("MainMenu");
}
}
添加胜利界面和重玩,菜单按钮的点击
游戏胜利的条件是所有的敌人都生成了并且都死亡了,在EnemySpawner脚本中的SpawnEnemy协程方法中写一个对敌人数量的while循环,如果还有,就截止到此返回,如果没了就往下执行GameManager.Instance.Win() 方法。
开发菜单场景
控制开始按钮和退出按钮的点击事件处理
GameMenu脚本全部代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class GameMenu : MonoBehaviour {
public void OnStartGame()
{
SceneManager.LoadScene(1);
}
public void OnExitGame()
{
#if UNITY_EDITOR
UnityEditor.EditorApplication.isPlaying = false;
#else
Application.Quit();
#endif
}
}