阶段学习总结-坦克大战(2D)案例
这是前几天学习的案例,这两天学习了NGUI和UGUI,被UI搞得很烦躁,但是今天还是将前几天学习的坦克大战总结以下。这个游戏是小时候的经典红白机游戏的复刻,见截图:
一.游戏物体
游戏中包含地图元素(墙、障碍、水、空气墙、水等)、敌方坦克、我方坦克、核心等一系列物体,在选择进入游戏后,由一个空物体生成场景-各种游戏物体,之后各游戏物体按照自身带着的脚本和组件进行运动或产生和玩家的交互,下面主要是分析刚才的内容。
二.主菜单界面
public class Option : MonoBehaviour { //位置1 public Transform pos1; //位置2,这两个位置用于记录坦克图片所在位置 public Transform pos2; //根据坦克图片所在位置对应好选择的游戏模式,模式有两种,这个案例只设计了其中的一种 private int choice = 1; // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { //按下W键坦克图片移动到上方指向“1 PLAYER”选项 if (Input.GetKeyDown(KeyCode.W)) { transform.position = pos1.position; choice = 1; } //按下S键坦克图片移动到下方指向“2 PLAYER”选项 if (Input.GetKeyDown(KeyCode.S)) { transform.position = pos2.position; choice = 2; } //按下空格键加载游戏场景,案例中只设计了一个玩家的游戏场景,两个玩家的场景稍作更改即可得到 if(Input.GetKeyDown(KeyCode.Space) && choice == 1) { SceneManager.LoadScene(1); } } }
这段脚本挂载在上面第一张图中的指向游戏模式的小坦克上,用于选择游戏模式并进入游戏场景。
二.游戏场景的生成
public class MapCreation : MonoBehaviour { //0,home 1,墙 2,障碍 3,出生效果 4,河流 5,草 6,空气墙 public GameObject[] items; //用于记录已经加载好的场景资源的位置信息,防止在同一个位置重复加载物体 private List<Vector3> itemPositionList = new List<Vector3>(); //在Awake函数中加载资源,这个函数在Start之前调用 private void Awake() { //CreateItem函数见下方,就是封装了生成游戏物体、将游戏物体父类设置为空物体便于管理、将游戏物体的位置添加到list中三个步骤 //这里生成核心,并在核心左右各生成一个墙 CreateItem(items[0], new Vector3(0, -8, 0), Quaternion.identity); CreateItem(items[1], new Vector3(-1, -8, 0), Quaternion.identity); CreateItem(items[1], new Vector3(1, -8, 0), Quaternion.identity); //这里生成玩家 GameObject gameObj = CreateItem(items[3], new Vector3(-2, -8, 0), Quaternion.identity); gameObj.GetComponent<Born>().isEnemy = false; //这里生成敌人 CreateItem(items[3], new Vector3(-10, 8, 0), Quaternion.identity); CreateItem(items[3], new Vector3(0, 8, 0), Quaternion.identity); CreateItem(items[3], new Vector3(10, 8, 0), Quaternion.identity); //延迟调用敌人的生成函数,并每隔5秒生成一个新的敌人 InvokeRepeating("CreateEnemy", 4, 5); //循环在核心周围生成墙,用墙将核心包围 for (int i = -1; i <= 1; i ++) CreateItem(items[1], new Vector3(i, -7, 0), Quaternion.identity); //循环生成空气墙,限制敌人和玩家的活动范围 for (int i = -11; i <= 11; i++) { CreateItem(items[6], new Vector3(i, 9, 0),Quaternion.identity); CreateItem(items[6], new Vector3(i, -9, 0), Quaternion.identity); } for (int i = -8; i <= 8; i++) { CreateItem(items[6], new Vector3(-11, i, 0), Quaternion.identity); CreateItem(items[6], new Vector3(11, i, 0), Quaternion.identity); } //在随机位置生成墙、障碍、空气和草,各生成21个 for(int i = 0;i <= 20; i++) CreateItem(items[1], CreateRandomPosition(), Quaternion.identity); for (int i = 0; i <= 20; i++) CreateItem(items[2], CreateRandomPosition(), Quaternion.identity); for (int i = 0; i <= 20; i++) CreateItem(items[4], CreateRandomPosition(), Quaternion.identity); for (int i = 0; i <= 20; i++) CreateItem(items[5], CreateRandomPosition(), Quaternion.identity); } //生成游戏物体的方法封装 private GameObject CreateItem(GameObject item,Vector3 v,Quaternion r) { GameObject go = Instantiate(item, v, r); go.transform.SetParent(gameObject.transform); itemPositionList.Add(v); return go; } //获取随机位置的方法 private Vector3 CreateRandomPosition() { //最外圈不产生游戏物体 //循环遍历,随机生成位置,校验到随机位置没有物体即可返回 while (true) { Vector3 v = new Vector3(Random.Range(-9, 10), Random.Range(-8, 9), 0); if (!itemPositionList.Contains(v)) { return v; } } } //生成敌人的方法,在3个位置中随机一个位置生成敌人 private void CreateEnemy() { int position = Random.Range(0, 3); switch (position) { case 0: Instantiate(items[3], new Vector3(-10, 8, 0), Quaternion.identity); break; case 1: Instantiate(items[3], new Vector3(0, 8, 0), Quaternion.identity); break; case 2: Instantiate(items[3], new Vector3(10,8, 0), Quaternion.identity); break; } } }
这段代码挂载在一个空物体上,游戏开始时场景中只有这个空物体和摄像机等,接下来由脚本生成各种游戏物体。
三.不同游戏物体的脚本和组件
1.GameManager
/// <summary> /// 用于管理游戏的进程和界面UI的显示 /// </summary> public class GameManager : MonoBehaviour { //玩家剩余生命值 public int playerLife = 3; //玩家得分 public int playScore = 0; //玩家是否死亡 public bool isDead = false; //玩家生成预制体 public GameObject born; //游戏是否结束 public bool isDefeated = false; //分数显示和生命值显示 public Text playerScoreText; public Text playerLifeText; //游戏结束的图片 public GameObject GameOverImg; //使用单例模式,只能有一个实例,同时静态化便于其他脚本调用 public static GameManager instance; public static GameManager Instance { get { return instance; } set { instance = value; } } private void Awake() { Instance = this; } // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { //游戏结束 if (isDefeated) { //显示游戏结束的图片 GameOverImg.SetActive(true); //开启协程回到主菜单 StartCoroutine(BackMain()); return; } //分数更新和生命值更新 playerLifeText.text = playerLife.ToString(); playerScoreText.text = playScore.ToString(); //玩家死亡调用的方法 if (isDead) Recover(); } /// <summary> /// 回到主菜单的协程 /// </summary> IEnumerator BackMain() { //等待两秒 yield return new WaitForSeconds(2); //加载场景 SceneManager.LoadScene(0); } /// <summary> /// 玩家被打死后调用的方法 /// </summary> private void Recover() { //校验玩家剩余生命值,小于0结束游戏,否则复活玩家 if(playerLife <= 0) { isDefeated = true; } else { //重新生成玩家 GameObject go = Instantiate(born, new Vector3(-2,-8,0), Quaternion.identity); go.GetComponent<Born>().isEnemy = false; //设置玩家又复活了 isDead = false; //扣除剩余生命值 playerLife--; } } }
2.核心
public class Heart : MonoBehaviour { //核心死亡后会更换图片,所以获取renderer组件 private SpriteRenderer renderer; //用于在核心死亡后更换的图片精灵 public Sprite deadSprite; //核心被击中时的爆炸特效 public GameObject explosionPrefab; // Start is called before the first frame update void Start() { renderer = GetComponent<SpriteRenderer>(); } // Update is called once per frame void Update() { } /// <summary> /// 核心的死亡方法 /// </summary> private void Die() { //更换图片精灵 renderer.sprite = deadSprite; //设置游戏结束,GameManager是自己写的游戏控制脚本 GameManager.instance.isDefeated = true; //播放爆炸特效 Instantiate(explosionPrefab, transform.position, transform.rotation); } }
可以看到,核心会被击中,因此需要进行碰撞检测,需要添加相应的collider组件
3.玩家和敌人的生成
/// <summary> /// 管理玩家和敌人的生成 /// </summary> public class Born : MonoBehaviour { //玩家和敌人的预制体 public GameObject playerPrefab; public GameObject[] enemyPrefabs; //是否是敌人 public bool isEnemy; // Start is called before the first frame update void Start() { //在等待0.8s后调用玩家和敌人的生成函数,0.8s是为了生成动画有足够的时间播放和设置这个预制体生成的是否是敌人 Invoke("TankBorn", 0.8f); //生成游戏物体后销毁特效 Destroy(gameObject, 0.8f); } // Update is called once per frame void Update() { } /// <summary> /// 生成坦克的方法 /// </summary> private void TankBorn() { //根据isEnemy的布尔值确定是生成玩家还是敌人,敌人有多种类型,随机生成 if (isEnemy) { Instantiate(enemyPrefabs[Random.Range(0, 2)], transform.position, transform.rotation); }else Instantiate(playerPrefab,transform.position,transform.rotation); } }
这个脚本挂载在坦克出生特效上,生成坦克时也是先生成出生特效,特效播放0.8s后再根据isEnemy布尔值生成坦克。
4.玩家坦克
/// <summary> /// 用于控制玩家的移动、发射、死亡等 /// </summary> public class Player : MonoBehaviour { //水平轴和竖直轴的值 private float h = 0; private float v = 0; //移动速度 public float moveSpeed = 1; //无敌时间 public float timeDefended = 3; //坦克朝向,根据坦克朝向旋转图片和发射炮弹 private Vector3 tankTowards = new Vector3(0,0,0); //子弹预制体 public GameObject bulletPrefab; //发射子弹的间隔 private float timeVal = 0; //爆炸特效 public GameObject explosionPrefab; //无敌预制体 private GameObject defendedPrefab; //是否无敌 private bool isDefended = true; // Start is called before the first frame update void Start() { defendedPrefab = GameObject.FindGameObjectWithTag("Shield"); } // Update is called once per frame void Update() { //无敌情况下 if(isDefended) { //减少无敌时间 timeDefended -= Time.deltaTime; //校验剩余无敌时间 if(timeDefended <= 0) { //取消无敌状态和不显示无敌特效 isDefended = false; defendedPrefab.SetActive(false); } } } /// <summary> /// 在这个方法中书写控制玩家移动的代码,避免碰撞时出现的抽搐 /// </summary> private void FixedUpdate() { //如果游戏已经结束,坦克不受玩家控制 if (GameManager.instance.isDefeated) return; //移动的方法 Move(); //调整坦克朝向,旋转游戏物体 transform.eulerAngles = tankTowards; //校验发射子弹的时间间隔 if(timeVal > 0.4f) { //发射子弹攻击的方法 Attack(); } //发射冷却时间相应增加 timeVal += Time.fixedDeltaTime; } /// <summary> /// 坦克的攻击方法 /// </summary> private void Attack() { //空格键发射子弹 if (Input.GetKeyDown(KeyCode.Space)) { //生成子弹,并朝着玩家当前朝向发射 GameObject bullet = Instantiate(bulletPrefab, transform.position, transform.rotation); bullet.GetComponent<Bullet>().isEnemyBullet = false; //子弹发射冷却时间归零 timeVal = 0; } } /// <summary> /// 坦克的移动方法 /// </summary> private void Move() { //获取水平轴值 h = Input.GetAxis("Horizontal"); //水平移动 transform.Translate(Vector3.right * h * Time.fixedDeltaTime * moveSpeed ,Space.World); //设置坦克朝向 if (h < 0) tankTowards.z = 90; if (h > 0) tankTowards.z = -90; //已经水平移动的情况下无法同时上下移动,也就是以水平移动为主 if (h != 0) return; //竖直方向上的移动同理 v = Input.GetAxis("Vertical"); transform.Translate(Vector3.up * v * Time.fixedDeltaTime * moveSpeed,Space.World); if (v < 0) tankTowards.z = 180; if (v > 0) tankTowards.z = 0; } /// <summary> /// 坦克的死亡方法 /// </summary> private void Die() { //如果坦克无敌,不会死亡 if (isDefended) return; //设置游戏结束 GameManager.instance.isDead = true; //产生爆炸特效 Instantiate(explosionPrefab, transform.position, transform.rotation); //死亡 Destroy(gameObject); } }
5.敌人坦克
/// <summary> /// 用于控制敌人的移动和自动发射子弹 /// </summary> public class Enemy : MonoBehaviour { //水平轴和竖直轴 private float h = 0; private float v = -1; //移动速度和子弹发射间隔 public float moveSpeed = 1; public float timeVal = 0; //自动改变前进方向的时间 public float moveDirectionChangeTime = 0f; //坦克朝向 private Vector3 tankTowards = new Vector3(0, 0, 0); //子弹预制体 public GameObject bulletPrefab; //爆炸预制体 public GameObject explosionPrefab; // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { //每3秒自动发射一颗子弹 if (timeVal > 3f) { Attack(); timeVal = 0; } timeVal += Time.deltaTime; } private void FixedUpdate() { //调用运动方法 Move(); //调整坦克朝向 transform.eulerAngles = tankTowards; } /// <summary> /// 坦克的攻击方法 /// </summary> private void Attack() { //生成子弹 GameObject bullet = Instantiate(bulletPrefab, transform.position, transform.rotation); bullet.GetComponent<Bullet>().isEnemyBullet = true; } /// <summary> /// 坦克的移动方法 /// </summary> private void Move() { //坦克的移动方向任意,但是向下的可能性更高,每隔4秒自动改变移动方向 if(moveDirectionChangeTime >= 4f) { //0-7的随机数决定坦克的移动方向,0向上,1、2向左,3、4向右,5、6、7向下 int moveDirectionChange = Random.Range(0, 8); if(moveDirectionChange == 0) { h = 0; v = 1; }else if(moveDirectionChange == 1 || moveDirectionChange == 2) { h = -1; v = 0; } else if (moveDirectionChange == 3 || moveDirectionChange == 4) { h = 1; v = 0; } else { h = 0; v = -1; } //将改变移动方向的计时归零 moveDirectionChangeTime = 0; } else { //增加计时器 moveDirectionChangeTime += Time.fixedDeltaTime; } //坦克进行水平移动,直到碰到障碍物为止 transform.Translate(Vector3.right * h * Time.fixedDeltaTime * moveSpeed, Space.World); //设置坦克朝向 if (h < 0) tankTowards.z = 90; if (h > 0) tankTowards.z = -90; if (h != 0) return; //坦克进行竖直方向移动 transform.Translate(Vector3.up * v * Time.fixedDeltaTime * moveSpeed, Space.World); //设置坦克朝向 if (v < 0) tankTowards.z = 180; if (v > 0) tankTowards.z = 0; } /// <summary> /// 坦克的死亡方法 /// </summary> private void Die() { //产生爆炸特效 Instantiate(explosionPrefab, transform.position, transform.rotation); //坦克死亡时玩家得分 GameManager.instance.playScore++; //死亡 Destroy(gameObject); } /// <summary> /// 碰撞的方法,这个方法让敌人坦克相互碰到时进行转向,避免坦克扎堆 /// </summary> /// <param name="collision"></param> private void OnCollisionEnter2D(Collision2D collision) { //遇到其他敌人坦克 if(collision.gameObject.tag == "Enemy") { //设置转向计时器为4,马上转向 moveDirectionChangeTime = 4f; } } }
6.子弹预制体
/// <summary> /// 用于控制子弹的生命周期 /// </summary> public class Bullet : MonoBehaviour { //子弹发射速度 public float moveSpeed = 10; //子弹有两种,玩家子弹和敌人子弹 public bool isEnemyBullet = true; // Start is called before the first frame update void Start() { } // Update is called once per frame void Update() { //子弹一直朝前运动 transform.Translate(transform.up * moveSpeed * Time.deltaTime,Space.World); } /// <summary> /// 碰撞的方法,根据子弹碰到的物体的标签类型确定子弹碰到的物体 /// </summary> /// <param name="collision"></param> private void OnTriggerEnter2D(Collider2D collision) { switch (collision.tag) { //如果是玩家,判断是否是敌人子弹,是敌人子弹调用玩家的死亡方法并销毁子弹 case "Tank": if (isEnemyBullet) { collision.SendMessage("Die"); Destroy(gameObject); } break; //如果是核心,判断是敌人子弹时调用核心的死亡方法并销毁子弹 case "Heart": if (isEnemyBullet) { collision.SendMessage("Die"); Destroy(gameObject); } break; //如果是敌人,判断是玩家子弹时调用敌人的死亡方法 case "Enemy": if (!isEnemyBullet) { collision.SendMessage("Die"); Destroy(gameObject); } break; //如果是墙,销毁墙和子弹 case "Wall": Destroy(collision.gameObject); Destroy(gameObject); break; //如果是障碍物,销毁子弹,子弹无法击毁障碍物 case "Barar": Destroy(gameObject); break; //如果是其他子弹,销毁当前子弹(对弹) case "Bullet": Destroy(gameObject); break; default: break; } } }
7.爆炸特效
/// <summary> /// 用于控制爆炸特效的生命周期 /// </summary> public class Explosion : MonoBehaviour { // Start is called before the first frame update void Start() { //0.167秒后销毁游戏物体,留出足够的时间播放动画 Destroy(gameObject, 0.167f); } // Update is called once per frame void Update() { } }