《3D游戏编程与设计》ai坦克

作业要求

  • 使用“感知-思考-行为”模型,建模 AI 坦克
  • 场景中要放置一些障碍阻挡对手视线
  • 坦克需要放置一个矩阵包围盒触发器,以保证 AI 坦克能使用射线探测对手方位
  • AI 坦克必须在有目标条件下使用导航,并能绕过障碍。(失去目标时策略自己思考)
  • 实现人机对战

项目代码

游戏场景设计

在unity资源商店中搜索Tanks! Tutorial并下载,其内含坦克和地图模型可以直接使用。

接着在unity导入地图,并对地图中的游戏对象设置Navigation,如果是障碍物需要设置为not walkable,并最后对地图进行bake。



为坦克添加Nav Mesh Agent 组件

为坦克实现自动寻路,当AI坦克发现玩家之后,只需要为NavMeshAgent设置destination,AI坦克便能够自动寻路,前往玩家所在位置。

AI坦克代码

AI坦克最初是巡逻状态,在预设的几个点中随机选取并移动。当发现玩家后,开始追击,若玩家进入设计范围,则AI坦克开始射击。

public class AITank : Tank {

    public delegate void recycle(GameObject tank);
    public static event recycle recycleEvent;

    private Vector3 target;
    private bool gameover;

    // 巡逻点
    private static Vector3[] points = { new Vector3(37.6f,0,0), new Vector3(40.9f,0,39), new Vector3(13.4f, 0, 39),
        new Vector3(13.4f, 0, 21), new Vector3(0,0,0), new Vector3(-20,0,0.3f), new Vector3(-20, 0, 32.9f), 
        new Vector3(-37.5f, 0, 40.3f), new Vector3(-37.5f,0,10.4f), new Vector3(-40.9f, 0, -25.7f), new Vector3(-15.2f, 0, -37.6f),
        new Vector3(18.8f, 0, -37.6f), new Vector3(39.1f, 0, -18.1f)
    };
    private int destPoint = 0;
    private NavMeshAgent agent;
    private bool isPatrol = false;

    private void Awake()
    {
        destPoint = UnityEngine.Random.Range(0, 13);
    }

    // Use this for initialization
    void Start () {
        setHp(100f);
        StartCoroutine(shoot());
        agent = GetComponent<NavMeshAgent>();
    }

    private IEnumerator shoot()
    {
        while (!gameover)
        {
            for(float i = 1; i > 0; i -= Time.deltaTime)
            {
                yield return 0;
            }
            // 当敌军坦克距离玩家坦克不到20时开始射击
            if(Vector3.Distance(transform.position, target) < 20)
            {
                GameObjectFactory mf = Singleton<GameObjectFactory>.Instance;
                GameObject bullet = mf.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);
            }
        }
    }

    // Update is called once per frame
    void Update () {
        gameover = GameDirector.getInstance().currentSceneController.isGameOver();
        if (!gameover)
        {
            target = GameDirector.getInstance().currentSceneController.getPlayerPos();
            if (getHp() <= 0 && recycleEvent != null)
            {//如果npc坦克被摧毁,则回收它
                recycleEvent(this.gameObject);
            }
            else
            {
                if(Vector3.Distance(transform.position, target) <= 30)
                {
                    isPatrol = false;
                    //否则向玩家坦克移动
                    agent.autoBraking = true;
                    agent.SetDestination(target);
                }
                else
                {
                    patrol();
                }
            }
        }
        else
        {
            NavMeshAgent agent = GetComponent<NavMeshAgent>();
            agent.velocity = Vector3.zero;
            agent.ResetPath();
        }
    }

    private void patrol()
    {
        if(isPatrol)
        {
            if(!agent.pathPending && agent.remainingDistance < 0.5f)
                GotoNextPoint();
        }
        else
        {
            agent.autoBraking = false;
            GotoNextPoint();
        }
        isPatrol = true;
    }

    private void GotoNextPoint()
    {
        agent.SetDestination(points[destPoint]);
        destPoint = (destPoint + 1) % points.Length;
    }
}


子弹类

public class Bullet : MonoBehaviour {
    // 子弹伤害半径
    public float explosionRadius = 3f;
    private tankType type;

    public void setTankType(tankType type)
    {
        this.type = type;
    }

    private void Update()
    {
        if(this.transform.position.y < 0 && this.gameObject.activeSelf)
        {
            GameObjectFactory mf = Singleton<GameObjectFactory>.Instance;
            // 落地爆炸
            ParticleSystem explosion = mf.getPs();
            explosion.transform.position = transform.position;
            explosion.Play();
            mf.recycleBullet(this.gameObject);
        }
    }

    void OnCollisionEnter(Collision other)
    {
        // 获得单实例工厂
        GameObjectFactory mf = Singleton<GameObjectFactory>.Instance;
        ParticleSystem explosion = mf.getPs();
        explosion.transform.position = transform.position;

        // 获取爆炸范围内的所有碰撞体
        Collider[] colliders = Physics.OverlapSphere(transform.position, explosionRadius);
        for(int i = 0; i < colliders.Length; i++)
        {
            if(colliders[i].tag == "tankPlayer" && this.type == tankType.Enemy || colliders[i].tag == "tankEnemy" && this.type == tankType.Player)
            {
                // 根据击中坦克与爆炸中心的距离计算伤害值
                float distance = Vector3.Distance(colliders[i].transform.position, transform.position);//被击中坦克与爆炸中心的距离
                float hurt = 100f / distance;
                float current = colliders[i].GetComponent<Tank>().getHp();
                colliders[i].GetComponent<Tank>().setHp(current - hurt);
            }
        }

        explosion.Play();
        if (this.gameObject.activeSelf)
        {
            mf.recycleBullet(this.gameObject);
        }
    }
}


工厂模式

通过单实例工厂GameObjectFactory来统一管理玩家player、 AI坦克、子弹、爆炸粒子系统等游戏对象。

public class GameObjectFactory : MonoBehaviour {
    // 玩家
    public GameObject player;
    // npc
    public GameObject tank;
    // 子弹
    public GameObject bullet;
    // 爆炸粒子系统
    public ParticleSystem ps;

    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 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>();
    }

    // Use this for initialization
    void Start () {
        //回收坦克的委托事件
        AITank.recycleEvent += recycleTank;
    }

    public GameObject getPlayer()
    {
        return player;
    }

    public GameObject getTank()
    {
        if(freeTanks.Count == 0)
        {
            GameObject newTank = Instantiate<GameObject>(tank);
            usingTanks.Add(newTank.GetInstanceID(), newTank);
            //在一个随机范围内设置坦克位置
            newTank.transform.position = new Vector3(Random.Range(-100, 100), 0, Random.Range(-100, 100));
            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.position = new Vector3(Random.Range(-100, 100), 0, Random.Range(-100, 100));
            return pair.Value;
        }
        return null;
    }

    public GameObject getBullet(tankType type)
    {
        if (freeBullets.Count == 0)
        {
            GameObject newBullet = Instantiate(bullet);
            newBullet.GetComponent<Bullet>().setTankType(type);
            usingBullets.Add(newBullet.GetInstanceID(), newBullet);
            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);
            return pair.Value;
        }
        return null;
    }

    public ParticleSystem getPs()
    {
        for(int i = 0; i < psContainer.Count; i++)
        {
            if (!psContainer[i].isPlaying) return psContainer[i];
        }
        ParticleSystem newPs = Instantiate<ParticleSystem>(ps);
        psContainer.Add(newPs);
        return newPs;
    }

    public void recycleTank(GameObject tank)
    {
        usingTanks.Remove(tank.GetInstanceID());
        freeTanks.Add(tank.GetInstanceID(), tank);
        tank.GetComponent<Rigidbody>().velocity = new Vector3(0, 0, 0);
        tank.SetActive(false);
    }

    public void recycleBullet(GameObject bullet)
    {
        usingBullets.Remove(bullet.GetInstanceID());
        freeBullets.Add(bullet.GetInstanceID(), bullet);
        bullet.GetComponent<Rigidbody>().velocity = new Vector3(0, 0, 0);
        bullet.SetActive(false);
    }
}

场记

场记SceneController内容也比较简单,主要负责通知工厂初始化各种游戏对象,如player、AI坦克等,并初始化主摄像机。

public class SceneController : MonoBehaviour, IUserAction {

    public GameObject player;
    private bool gameOver = false;
    private int enemyCount = 6;
    private GameObjectFactory mf;
    private MainCameraControl cameraControl;


    private void Awake()
    {
        GameDirector director = GameDirector.getInstance();
        director.currentSceneController = this;
        mf = Singleton<GameObjectFactory>.Instance;
        player = mf.getPlayer();
        cameraControl = GetComponent<MainCameraControl>();
        cameraControl.setTarget(player.transform);
    }

    // Use this for initialization
    void Start () {
        for(int i = 0; i < enemyCount; i++)
        {
            GameObject gb = mf.getTank();
            cameraControl.setTarget(gb.transform);
        }
        Player.destroyEvent += setGameOver;

        // 初始化相机位置
        cameraControl.SetStartPositionAndSize();
    }
    
    // 更新相机位置
    void Update () {
        Camera.main.transform.position = new Vector3(player.transform.position.x, 15, player.transform.position.z);
    }

    public Vector3 getPlayerPos()
    {
        return player.transform.position;
    }

    public bool isGameOver()
    {
        return gameOver;
    }
    public void setGameOver()
    {
        gameOver = true;
    }

    public void moveForward()
    {
        player.GetComponent<Rigidbody>().velocity = player.transform.forward * 20;
    }
    public void moveBackWard()
    {
        player.GetComponent<Rigidbody>().velocity = player.transform.forward * -20;
    }

    public void turn(float offsetX)
    {
        float y = player.transform.localEulerAngles.y + offsetX * 5;
        float x = player.transform.localEulerAngles.x;
        player.transform.localEulerAngles = new Vector3(x, y, 0);
    }

    public void shoot()
    {
        GameObject bullet = mf.getBullet(tankType.Player);
        // 设置子弹位置
        bullet.transform.position = new Vector3(player.transform.position.x, 1.5f, player.transform.position.z) + player.transform.forward * 1.5f;
        // 设置子弹方向
        bullet.transform.forward = player.transform.forward;

        // 发射子弹
        Rigidbody rb = bullet.GetComponent<Rigidbody>();
        rb.AddForce(bullet.transform.forward * 20, ForceMode.Impulse);
    }
}


游戏效果图



项目代码
演示视频

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容