学习笔记13-AR版的坦克对战
作业素材
官方Tank Tutourial的素材
实现效果
代码仓库
Gif
演示视频
实现过程
环境配置
首先,我们导入Vuforia的包,根据课上学到的内容,配置好AR的环境其次因为电脑上的摄像头配合AR太不方便了,所以我打算配置好实验环境,部分时候使用手机来测试。 打开File > Build Settings,我们可以看到Unity可以编译出多平台下的游戏,这里我们需要使用的是ANDROID,点开,若没有安装,其会指导我们打开下载页面下载对应安装包,安装好后界面如下,到这里基本的环境
摇杆制作
手机上就不像我们的电脑可以使用键盘,如何进行交互就是值得思考的点,最终我打算使用较多手机游戏中使用的摇杆,首先创建一个UI > Button,把里面的Text换成Image,我们通过编写关于拖动的脚本,根据Image的偏移量来计算移动,效果如下
脚本呢也较为简单,主要使用到了OnDrag来完成
public class MoveCtrl : MonoBehaviour, IDragHandler, IEndDragHandler { //图标移动最大半径 public float maxRadius = 200; //初始化背景图标位置 private Vector2 moveBackPos; private float horizontal = 0; private float vertical = 0; public float Horizontal { get { return horizontal; } } public float Vertical { get { return vertical; } } // Use this for initialization void Start () { //初始化位置 moveBackPos = transform.parent.transform.position; } // Update is called once per frame void Update () { horizontal = transform.localPosition.x; vertical = transform.localPosition.y; } /// <summary> /// 当鼠标开始拖拽时 /// </summary> /// <param name="eventData"></param> public void OnDrag(PointerEventData eventData) { //获取鼠标位置与初始位置之间的向量 Vector2 oppsitionVec = eventData.position - moveBackPos; //获取向量的长度 float distance = Vector3.Magnitude(oppsitionVec); //最小值与最大值之间取半径 float radius = Mathf.Clamp(distance, 0, maxRadius); //限制半径长度 transform.position = moveBackPos + oppsitionVec.normalized * radius; } /// <summary> /// 当鼠标停止拖拽时 /// </summary> /// <param name="eventData"></param> public void OnEndDrag(PointerEventData eventData) { transform.position = moveBackPos; transform.localPosition = Vector3.zero; } }
这样我们只需在场景控制器中的Update加上对应根据Vertical和Horizontal来移动的代码即可实现摇杆控制玩家。
场景制作
我的游戏场景打算使用ImageTarget,当识别到相应图像,游戏场景就会显示。为了使用Nav Mesh, 先把Image Target的Mesh Filter的Mesh改成Plane。这样我们就可以使用Unity的导航与寻路了。 创建好游戏对象之后,我们把物体不能在上移动的对象在Navigation的Object中调成以下属性 Bake结果如下
代码完成
到这里我们就可以开始写我们的代码了,这一次的作业我使用了导演及工厂模式,部分借鉴了师兄的优秀博客,这里说说部分类的代码
Tank
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class Tank : MonoBehaviour { private float hp; public float getHp() { return hp; } public void setHp(float hp) { this.hp = hp; } }
Player
当生命值小于0时,GameOver,坦克爆炸
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.AI; public class Player : Tank { public void init() { this.gameObject.SetActive (true); setHp(300f); } void Start() { setHp(300f); } void Update () { if (getHp() <= 0 && gameObject.activeSelf) { this.gameObject.SetActive(false); ParticleSystem explosion = Singleton<AllFactory>.Instance.getTankPs (); //获取爆炸的粒子系统 explosion.transform.position = transform.position; //设置粒子系统位置 explosion.Play(); ((FirstSceneController)GameDirector.getInstance ().currentSceneController).gameOver = true; } } }
Enemy
public class Enemy : Tank { private Vector3 target; private bool gameover; //根据FirstSceneController中的gameOver,判断游戏是否结束 private AllFactory allFactory; public void init() { setHp (100f);//设置初始生命值 StopAllCoroutines ();//停止所有协程 StartCoroutine(shoot());//开始射击的协程 } void Start() { setHp(100f);//设置初始生命值 allFactory = Singleton<AllFactory>.Instance; StartCoroutine(shoot());//开始射击的协程 } void Update () { gameover = ((FirstSceneController)GameDirector.getInstance ().currentSceneController).gameOver; if (!gameover) { target = ((FirstSceneController)GameDirector.getInstance().currentSceneController).getPlayerPos(); // 感知 if (getHp() <= 0 && gameObject.activeSelf) { //思考 allFactory.recycleTank (this.gameObject); ((FirstSceneController)GameDirector.getInstance ().currentSceneController).GetScore (); StopCoroutine(shoot()); } else { NavMeshAgent agent = GetComponent<NavMeshAgent>(); agent.SetDestination(target); } } else { NavMeshAgent agent = GetComponent<NavMeshAgent>(); agent.velocity = Vector3.zero; agent.ResetPath(); } } IEnumerator shoot() { // 行为 while(!gameover && getHp() > 0) { for (float i = 2; i > 0; i -= Time.deltaTime) { yield return 0; } if (Vector3.Distance(transform.position, target) < 15) { //距离判断是否攻击 GameObject bullet = allFactory.getBullet(tankType.Enemy); bullet.transform.position = new Vector3(transform.position.x, 1.5f, transform.position.z) + transform.forward * 1.5f; bullet.transform.forward = transform.forward; Rigidbody rb = bullet.GetComponent<Rigidbody>(); rb.AddForce(bullet.transform.forward * 20, ForceMode.Impulse); } } } }
AllFactory
AllFactory把所有坦克、子弹和两个爆炸集中到一起管理
using System.Collections; using System.Collections.Generic; using UnityEngine; public enum tankType:int {Player, Enemy} public class AllFactory : MonoBehaviour { public GameObject player; //玩家坦克 public GameObject bullet; //子弹 public Transform imageTarget; private Dictionary<int, GameObject> usingTanks; private Dictionary<int, GameObject> freeTanks; private Dictionary<int, GameObject> usingBullets; private Dictionary<int, GameObject> freeBullets; private List<ParticleSystem> psContainer; private List<ParticleSystem> tankPsContainer; void Awake() { usingTanks = new Dictionary<int, GameObject>(); freeTanks = new Dictionary<int, GameObject>(); usingBullets = new Dictionary<int, GameObject>(); freeBullets = new Dictionary<int, GameObject>(); psContainer = new List<ParticleSystem>(); tankPsContainer = new List<ParticleSystem> (); } void Start() { } public GameObject getPlayer() {//获取玩家坦克 return player; } public GameObject getTank() { if (freeTanks.Count == 0) { GameObject newTank = Instantiate (Resources.Load ("Prefabs/Enemy")) as GameObject; usingTanks.Add(newTank.GetInstanceID(), newTank); newTank.transform.parent = imageTarget; newTank.transform.localScale = new Vector3 (0.02f, 0.02f, 0.02f); newTank.transform.localPosition = new Vector3(((float)Random.Range(-4, 4))/10, 0, ((float)Random.Range(-4, 4))/10); return newTank; } foreach (KeyValuePair<int, GameObject> pair in freeTanks) { pair.Value.SetActive(true); freeTanks.Remove(pair.Key); usingTanks.Add(pair.Key, pair.Value); pair.Value.transform.parent = imageTarget; pair.Value.transform.localPosition = new Vector3(((float)Random.Range(-4, 4))/10, 0, ((float)Random.Range(-4, 4))/10); pair.Value.GetComponent<Enemy> ().init (); return pair.Value; } return null; } public GameObject getBullet(tankType type) { if (freeBullets.Count == 0) { GameObject newBullet = Instantiate (Resources.Load ("Prefabs/Bullet")) as GameObject; newBullet.GetComponent<Bullet>().setTankType(type); usingBullets.Add(newBullet.GetInstanceID(), newBullet); newBullet.transform.parent = imageTarget; return newBullet; } foreach (KeyValuePair<int, GameObject> pair in freeBullets) { pair.Value.SetActive(true); pair.Value.GetComponent<Bullet>().setTankType(type); freeBullets.Remove(pair.Key); usingBullets.Add(pair.Key, pair.Value); pair.Value.GetComponent<Rigidbody> ().velocity = Vector3.zero; return pair.Value; } return null; } public ParticleSystem getPs() { for (int i = 0; i < psContainer.Count; i++) { if (!psContainer[i].isPlaying) { return psContainer[i]; } } GameObject explore = Instantiate (Resources.Load ("Prefabs/ShellExplosion")) as GameObject; explore.transform.parent = imageTarget; ParticleSystem newPs = explore.GetComponent<ParticleSystem> (); psContainer.Add(newPs); return newPs; } public ParticleSystem getTankPs() { for (int i = 0; i < tankPsContainer.Count; i++) { if (!tankPsContainer[i].isPlaying) { return tankPsContainer[i]; } } GameObject explore = Instantiate (Resources.Load ("Prefabs/TankExplosion")) as GameObject; explore.transform.parent = imageTarget; ParticleSystem newPs = explore.GetComponent<ParticleSystem> (); tankPsContainer.Add(newPs); return newPs; } public void recycleTank(GameObject tank) { tank.SetActive (false); usingTanks.Remove(tank.GetInstanceID()); freeTanks.Add(tank.GetInstanceID(), tank); ParticleSystem explosion = getTankPs (); //获取爆炸的粒子系统 explosion.transform.position = tank.transform.position; //设置粒子系统位置 explosion.Play(); } public void recycleBullet(GameObject bullet) { usingBullets.Remove(bullet.GetInstanceID()); freeBullets.Add(bullet.GetInstanceID(), bullet); bullet.GetComponent<Rigidbody>().velocity = Vector3.zero; bullet.SetActive(false); } public void recycleAllTanks() { List<int> keyList = new List<int>(usingTanks.Keys); foreach (int key in keyList) { if (usingTanks.ContainsKey (key)) { usingTanks [key].SetActive (false); freeTanks.Add(key, usingTanks [key]); usingTanks.Remove(key); } } } public void recycleAllBullets() { List<int> keyList = new List<int>(usingBullets.Keys); foreach (int key in keyList) { if (usingBullets.ContainsKey (key)) { usingBullets [key].SetActive (false); freeBullets.Add(key, usingBullets [key]); usingBullets.Remove(key); } } } public void reset() { recycleAllTanks (); recycleAllBullets (); player.GetComponent<Player> ().init (); } }
FirstSceneController
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; public class FirstSceneController : MonoBehaviour, ISceneController { public MoveCtrl move; public GameObject tank; public Button btn; public bool gameOver = false; private AllFactory allFactory; GUIStyle headerStyle; private int Count = 0; private int Score = 0; // Use this for initialization void Awake () { GameDirector director = GameDirector.getInstance (); director.currentSceneController = this; allFactory = Singleton<AllFactory>.Instance; tank = allFactory.getPlayer (); btn.GetComponent<Button>().onClick.AddListener (delegate() { this.attackHandler(); }); } void Start () { Count = 0; headerStyle = new GUIStyle(); headerStyle.fontSize = 100; headerStyle.alignment = TextAnchor.MiddleCenter; } public void loadResources() { } public void GetScore() { ++Score; } void GameOver () { this.gameOver = true; } public Vector3 getPlayerPos() { return tank.transform.position; } // Update is called once per frame void Update () { if (gameOver) return; if (Count++ % 120 == 0) { allFactory.getTank (); } float ver = -move.Horizontal; float hor = move.Vertical; Vector3 direction = new Vector3 (hor, 0, ver); if (direction != Vector3.zero) { //控制转向 tank.transform.rotation = Quaternion.Lerp (tank.transform.rotation, Quaternion.LookRotation (direction), Time.deltaTime * 20); //向前移动 tank.transform.Translate (Vector3.forward * Time.deltaTime * 5); } } void OnGUI() { GUI.Label(new Rect(Screen.width - 500, 80, 400, 110), "Score: " + Score, headerStyle); if (gameOver) { GUI.Label (new Rect (Screen.width / 2 - 200, Screen.height / 2 - 100, 400, 110), "Game Over!", headerStyle); if (GUI.Button (new Rect (Screen.width / 2 - 150, Screen.height / 2 + 30, 300, 120), "Replay")) { allFactory.reset (); Score = 0; Count = 0; gameOver = false; } } } void attackHandler() { if (gameOver) return; GameObject bullet = allFactory.getBullet(tankType.Player);//获取子弹,设置其发出坦克的类型 bullet.transform.position = new Vector3(tank.transform.position.x, 1.5f, tank.transform.position.z) + tank.transform.forward * 1.5f; bullet.transform.forward = tank.transform.forward; Rigidbody rb = bullet.GetComponent<Rigidbody>(); rb.AddForce(bullet.transform.forward * 20, ForceMode.Impulse);//给子弹一个向前的力,实现子弹向前移动 } }
到这里,这周的作业就基本完成了,游戏的场景图像如下有兴趣的可以从我的Github下载玩一下