Skip to content

学习笔记13-AR版的坦克对战

作业素材

官方Tank Tutourial的素材

实现效果

代码仓库

Github

Gif

演示视频

Youku

实现过程

环境配置

首先,我们导入Vuforia的包,根据课上学到的内容,配置好AR的环境其次因为电脑上的摄像头配合AR太不方便了,所以我打算配置好实验环境,部分时候使用手机来测试。 打开File > Build Settings,我们可以看到Unity可以编译出多平台下的游戏,这里我们需要使用的是ANDROID,点开,若没有安装,其会指导我们打开下载页面下载对应安装包,安装好后界面如下,到这里基本的环境 屏幕快照 2018-06-19 上午9.12.46

摇杆制作

手机上就不像我们的电脑可以使用键盘,如何进行交互就是值得思考的点,最终我打算使用较多手机游戏中使用的摇杆,首先创建一个UI > Button,把里面的Text换成Image,我们通过编写关于拖动的脚本,根据Image的偏移量来计算移动,效果如下 屏幕快照 2018-06-19 上午9.19.44 屏幕快照 2018-06-19 上午9.19.40

脚本呢也较为简单,主要使用到了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的导航与寻路了。 屏幕快照 2018-06-19 上午9.24.22 创建好游戏对象之后,我们把物体不能在上移动的对象在Navigation的Object中调成以下属性 屏幕快照 2018-06-19 上午9.26.24 Bake结果如下 屏幕快照 2018-06-19 上午9.27.52

代码完成

到这里我们就可以开始写我们的代码了,这一次的作业我使用了导演及工厂模式,部分借鉴了师兄的优秀博客,这里说说部分类的代码

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下载玩一下 20180616_145805