zoukankan      html  css  js  c++  java
  • 使用Unity创建塔防游戏(Part2)

    How to Create a Tower Defense Game in Unity – Part 2

    原文地址:https://www.raywenderlich.com/107529/unity-tower-defense-tutorial-part-2

      欢迎大家来查看,使用Unity创建塔防游戏(第二篇)。在第一篇的结尾,我们已经可以召唤和升级小怪兽,召唤一个敌人朝着饼干前进的敌人。

      但是这个敌人没有方向感,让人感觉怪怪的。接下来,我们要做的是召唤一波一波的敌人,然后令小怪兽能够消灭它们,都是为了保护你那块美味的饼干。

    准备工作

      用Unity打开你之前完成的工程,但如果你没看过Part1,先下载starter project ,然后打开TowerDefense-Part2-Starter这个工程。打开Scenes文件夹下的GameScene。

    让敌人有方向感

      在Part1的结尾,我们可以令敌人沿着路线前进,但它们毫无方向感。

      用VS打开脚本MoveEnemy.cs,添加下面的代码来解决这个问题。

        private void RotateIntoMoveDirection() 
        {
            // 1 
            Vector3 newStartPosition = waypoints[currentWaypoint].transform.position;
            Vector3 newEndPosition = waypoints[currentWaypoint + 1].transform.position;
            Vector3 newDirection = (newEndPosition - newStartPosition);
            // 2
            float x = newDirection.x;
            float y = newDirection.y;
            float rotationAngle = Mathf.Atan2(y, x) * 180 / Mathf.PI;
            // 3
            GameObject sprite = (GameObject)gameObject.transform.FindChild("Sprite").gameObject;
            sprite.transform.rotation = Quaternion.AngleAxis(rotationAngle, Vector3.forward);
        }

      RotateIntoMoveDirection 这个方法是将场景中敌人对象的角度进行旋转,让敌人看起来有方向感。我们一步一步地来看:

    1. 计算出下一个路标与当前路标之间的向量之差,敌人会沿着这个向量前往下一个路标。
    2. 计算敌人要旋转的角度,即敌人当前的方向与newDirection之间的夹角的度数。调用Mathf.Atan2 来计算,参数为newDirection的X坐标和Y坐标,但返回的结果是以弧度为单位的。因此,我们需要将结果乘以 180 / Math.PI ,将弧度转化为角度。 
    3. 最后,我们获取敌人对象的子对象——Sprite,令它围绕Z轴旋转的度数为rotationAngle。这里我们调用Quaternion.AngleAxis来完成旋转的工作,它的第一个参数就是我们之前计算出的角度。注意,为什么是将子对象旋转,而不是敌人对象?这是为了保证敌人的血条始终保持水平,接下来我们要为敌人添加血条了。

        

      将Update() 中的注释 // TODO: Rotate into move direction 替换成调用我们刚写好的函数—— RotateIntoMoveDirection

                    RotateIntoMoveDirection();

      保存好脚本,返回Unity,运行游戏,看敌人现在有方向感了。这样才算是朝着饼干前进。

      

      才一个小兵?这怎行,要来就来一大群。在一般的塔防游戏中,都是每一波敌人都是一大群。

    告知玩家——敌人来了

      在一大群敌人出现之前,我们应该先告知玩家——敌人来了。同时,我们需要显示这是第几波敌人,在界面的右上角显示。

      在脚本中,有不少需要用到波数的地方,我们先在GameManager的脚本组件GameManagerBehavior中添加有关波数的代码。

      用VS打开GameManagerBehavior.cs,然后添加下面两个变量:

        public Text waveLable;
        public GameObject[] nextWaveLabels;

      显示在屏幕右上角的波数会存储在waveLabel 这个变量中。 nextWaveLabels 这个数组保存了两个游戏对象。在一波新的敌人到来之前,它们会构成一个文字合并的动画,如下图所示:

      

      保存好脚本,返回Unity。选中Hierarchy视图中的GameManager,在Inspector面板中,点击Wave Label右侧的小圆圈,然后从弹出的Text对话框中的Scene标签页下选择 WaveLabel

      将NextWave LabelsSize 设置为2。就像刚才设置WaveLabel那样,将Element0设置为NextWaveBottomLabel ,将Element1设置为NextWaveTopLabel

      

      这是设置好数据的结果。

      当玩家输掉游戏的时候,它无法看到有关下一波敌人的信息。回到GameManagerBehavior.cs中,添加一个变量:

        public bool gameOver = false;

      gameOver这个变量表示玩家是否输掉了游戏。

      同样的,我们也要为wave这个私有变量添加一个属性,让wave中的值与游戏当前波数保持一致,再向GameManagerBehavior.cs添加以下代码:

        private int wave;
        public int Wave 
        {
            get { return wave; }
            set {
                wave = value;
                if (!gameOver) 
                {
                    for (int i = 0; i < nextWaveLabels.Length; i++)
                    {
                        nextWaveLabels[i].GetComponent<Animator>().SetTrigger("nextWave");
                    }
                }
                waveLable.text = "WAVE: " + (wave + 1);
            }
        }

      在上面的代码中,我们创建了一个私有变量,一个属性。这个属性的getter方法,我们已经习以为常了,但它的setter方法看起来有些棘手。

      先是更新了wave的值。接下来,判断游戏是否未结束,如果是的话,遍历nextWaveLabels中元素,这些元素都带有一个Animator组件。调用SetTrigger来触发动画。

      最后,我们设置waveLabel上的数值为 wave + 1。为什么呢?因为在程序中,变量的初始值可以是0,但是人们都是从1开始数数的。

      在Start()方法中设置这个属性的值:

            Wave = 0;

      将Wave的初始值设置为1。

      保存好脚本,返回Unity中,运行游戏。波数的确是从1开始的。

      

      对于玩家而言,首先要解决的是第一波敌人。 

    逐个创建敌人

      显然,我们现在要做的是创建一支敌军(由想吃掉你饼干的小虫子组成),但我们暂时无法做到。

      此外,当玩家刚消灭一波敌人的时候,先不要创建下一波敌人,至少现在是这样。

      于是,我们必须要知道游戏场景中是否还有敌人存在,我们为敌人对象添加Tags(标签)来区别于其他游戏对象。此外,在脚本中,可以通过标签名快速查找物体。

    为敌人对象添加标签

      在Project视图中,选中名为Enemy的prefab。在Inspector面板的顶部,点击Tag右边的下拉框,从弹出的对话框中选择Add Tag

            

      新建一个标签,命名为Enemy

          

      选中名为Enemy的prefab,在Inspector中将它的标签设置为我们刚才创建的标签——Enemy

    配置敌军的信息

      现在,我们需要定义有关敌军的类和变量。用VS打开SpawnEnemy.cs,在SpawnEnemy的上方添加一个新的类,如下面代码所示:

    [System.Serializable]
    public class Wave 
    {
        public GameObject enemyPrefab;
        public float spawnInterval = 2;
        public int maxEnemies = 20;
    }

      Wave这个类表示一支敌军,它有3个字段,enemyPrefab用于实例化敌人对象;每隔spawnInterval秒产生一个敌人,每波创建单个敌人的时间间隔可能是不同的;一波敌人的最大数量为maxEnemies

      这个类是序列化的,所以我们可以在Inspector面板中更改它的数据。

      接下来为SpawnEnemy这个类添加下列变量:

        public Wave[] waves;
        public int timeBetweenWaves = 5;
    
        private GameManagerBehavior gameManager;
    
        private float lastSpawnTime;
        private int enemiesSpawned = 0;

      这几个变量都是与创建敌人有关的。我们将各个级别的敌军存储在waves这个数组里;enemiesSpawned记录了已产生的敌人的数量;lastSpawnTime记录了还是上一个敌人产生的时间;

      玩家需要一些时间来消灭这些敌人,于是我们将timeBetweenWaves设置为5秒,即每隔5秒产生一波敌人。

      将Start()方法中的代码替换为以下代码:

            lastSpawnTime = Time.time;
            gameManager = GameObject.Find("GameManager").GetComponent<GameManagerBehavior>();

      我们将lastSpawnTime设置为当前时间,当场景加载完成后,Start()方法就会被执行。然后,我们获取了游戏对象GameManager的引用。

      向Update()方法中添加下列代码:

            // 1
            int currentWave = gameManager.Wave;
            if (currentWave < waves.Length)
            {   // 2
                float timeInterval = Time.time - lastSpawnTime;
                float spawnInterval = waves[currentWave].spawnInterval;
                if(((enemiesSpawned == 0 && timeInterval > timeBetweenWaves) ||
                    timeInterval > spawnInterval) && 
                    enemiesSpawned < waves[currentWave].maxEnemies)
                {   // 3
                    lastSpawnTime = Time.time;
                    GameObject newEnemy = (GameObject)Instantiate(waves[currentWave].enemyPrefab);
                    enemiesSpawned++;
                }
                // 4 
                if (enemiesSpawned == waves[currentWave].maxEnemies &&
                    GameObject.FindGameObjectWithTag("Enemy") == null) 
                {
                    gameManager.Wave++;
                    gameManager.Gold = Mathf.RoundToInt(gameManager.Gold * 1.1f);
                    enemiesSpawned = 0;
                    lastSpawnTime = Time.time;
                }
            }  // 5
            else
            {
                gameManager.gameOver = true;
                GameObject gameOverText = GameObject.FindGameObjectWithTag("GameWon");
                gameOverText.GetComponent<Animator>().SetBool("gameOver", true);
            }

      让我们一步一步来理解这段代码:

    1. 获得当前波数,并判断是否未到最后一波。
    2. 如果是这样的话,先计算距离上一个敌人的创建过去了多少时间,并且判断是否到了创建下一个敌人的时间。这取决于两个条件:一、如果已创建的敌人数量为0,并且timeInterval大于timeBetweenWaves;二、判断timeInterval是否大于spawnInterval。无论如何,前提是这波敌人尚未被创建完毕。
    3. 假如符合2中的条件,就以enemyPrefab为拷贝,实例化一个敌人对象,赋予敌人对象有关路标的信息,并且将已创建的敌人数量加1。
    4. 若所有敌人都已被创建,但场景中找不到标签为Enemy的游戏对象,说明这波敌人都已玩家消灭。我们就要准备创建下一波敌人,并且给予玩家金币数量增加百分之十。
    5. 玩家消灭了最后一波敌人,播放游戏胜利的动画。

    设置创建单个敌人的时间间隔

      保存好脚本,返回Unity,选中Hierarchy视图中的Road对象,在Inspector面板中,将数组WavesSize设置为4。

      接下来,依次为数组的4个元素赋值。将名为Enemy的prefab赋值给Enemy Prefab,分别设置Spawn IntervalMax Enemies的值如下:

    • Element 0: Spawn Interval: 2.5, Max Enemies: 5
    • Element 1: Spawn Interval: 2, Max Enemies: 10
    • Element 2: Spawn Interval: 2, Max Enemies: 15
    • Element 3: Spawn Interval: 1, Max Enemies: 5

      最终设置好的结果如下如图所示:

      

      我们可以通过上面的设置达到平衡游戏的目的。运行游戏,哈哈!那些小虫子正朝着你的饼干前进!

      

    可选项:添加不同种类的敌人

      塔防游戏里的敌人一般都不止一种。在我们工程的Prefab文件夹中还包含着另一种敌人的prefab,Enemy2

      选中Prefab文件夹中的Enemy2,在Inspector面板中,为它添加一个脚本组件,我们选择已有的MoveEnemy这个脚本。将Speed的值设置为3,将它的标签设置为Enemy。我们用这种快速前进的小虫子,让玩家保持警觉。

    更新玩家的血量——不要让我死的那么快

      现在,即使一大群小虫子抵达了你那美味的饼干,你的血量都丝毫未损。于是,当有小虫子碰了你那块饼干的时候,你就要受伤了。

      

      打开GameManagerBehavior.cs,添加下面两个变量。

        public Text healthLabel;
        public GameObject[] healthIndicator;

      我们用healthLabel来显示玩家当前的血量,healthIndicator用于表示5只正在啃你饼干的小虫子,比起一个简单的数字或血条,用它们来表示玩家的血量会更有趣一些。

    控制玩家的血量

      接下来,为 GameManagerBehavior 添加一个属性,用来管理玩家的血量。  

        private int health;
        public int Health 
        {
            get { return health; }
            set { 
                // 1
                if (value < health) {
                    Camera.main.GetComponent<CameraShake>().Shake();
                }
                // 2
                health = value;
                healthLabel.text = "HEALTH: " + health;
                // 3
                if (health <= 0 && !gameOver) 
                {
                    gameOver = true;
                    GameObject gameOverText = GameObject.FindGameObjectWithTag("GameOver");
                    gameOverText.GetComponent<Animator>().SetBool("gameOver", true);
                }
                // 4
                for (int i = 0; i < healthIndicator.Length; i++)
                {
                    if (i < Health)
                    {
                        healthIndicator[i].SetActive(true);
                    }
                    else 
                    {
                        healthIndicator[i].SetActive(false);
                    }
                }
            }
        }

      以上代码块用于管理玩家的血量,同样的,setter方法是这段代码的主体。

    1. 当玩家掉血的时候,我们使用CameraShake这个组件来制造一个很棒的晃动效果。(这个晃动效果是为了警告玩家,小虫子正在吃掉你的饼干。)这个脚本也被包含在我们的工程内,但本文不作介绍。
    2. 更新私有字段health的值,以及屏幕左上角的血量显示。
    3. 当玩家血量被扣光的时候,且游戏未结束,先设置 gameOver 的值为true,再触发游戏失败的动画。
    4. 将一只绿色的小怪物从饼干上移除。就可以做得简单点的话,我们可以只是隐藏它们,当我们需要为玩家加血的时候,就可以将重新它们显示出来。

      在Start()中初始化Health

            Health = 5;

      在游戏开始的时候,玩家的血量为5。

      有了这个属性,当小虫子抵达饼干的时候,我们就可以更新玩家的血量了。保存好脚本,在VS中打开MoveEnemy.cs这个脚本。

    更新玩家的血量

      将MoveEnemy.csUpdate()方法内部的注释:// TODO: deduct health ,替换成以下代码:

                    GameManagerBehavior gameManager = GameObject.Find("GameManager").GetComponent<GameManagerBehavior>();
                    gameManager.Health -= 1;

      这段代码是为了获取GameManagerBehavior对象,然后将Health的值减1。

      保存好脚本,返回Unity。

      选中Hierarchy视图中的GameManager对象,为Health Label 赋值,选择HealthLabel

      在Hierarchy视图中展开Cookie对象,注意不要选中它,我们只要让它下面的5个子对象显示出来即可。将这5个子对象拖拽赋值给GameManagerHealth Indicator数组。我们用5只正在开心地啃着饼干的青色小虫子来表示玩家的血量。玩家受到一次伤害,就减少一只青色的小虫子。

      运行游戏,让那些小虫子冲向饼干,什么都别做,直到游戏结束。

      

    小怪兽的战斗:消灭那些小虫子

      该召唤小怪兽?还是让小虫子前进?现在我们的小怪兽还是纸老虎,我们要做的是让小怪兽们能够消灭那些小虫子。

      我们先要把以下几件事情做好:

    • 给小虫子一个血条,让玩家能看出敌人的强弱。
    • 让小怪兽能够发现它攻击范围内的敌人们
    • 决定朝那个敌人开火
    • 无尽的子弹

    显示敌人的血条

      我们用两张图片来显示血条,一张是暗的,用于显示血条的背景,另一张是绿色较小的细长图片,我们通过缩放它的长度来与敌人当前血量匹配。

      将Project视图中的PrefabsEnemy拖到场景中。

      将ImagesObjectsHealthBarBackground拖拽到Hierarchy视图中的Emeny对象上,令HealthBarBackground作为Enemy的子对象。

      在Inspector面板中,将HealthBarBackgroundPosition设置为 (0, 1, -4)

      接下来选中Project视图中的ImagesObjectsHealthBar,确保它的Pivot被设置为Left。同样的,也将它作为Hierarchy视图中的Emeny对象的子对象,将它的Position设置为 (-0.63, 1, -5),将它的X Scale设置为125 

      为游戏对象HealthBar添加一个C#脚本,命名为HealthBar,后面我们需要在脚本中调整血条长度。

      现在我们将Hierarchy视图中的Emeny对象的坐标调整为(20, 0, 0) 

      点击Inspector面板顶部的Apply按钮,保存刚才对prefab的更改。回到Project视图,刚才我们所作的更改已经成为了Prefab的一部分。最后,删除Hierarchy视图中的Emeny对象。

              

      同上,我们也为PrefabEnemy2添加一个血条。

    调整血条的长度

      在VS中打开HealthBar.cs,添加下列变量:

        public float maxHealth = 100;
        public float currentHealth = 100;
        private float originalScale;

      maxHealth表示敌人的最大生命值,currentHealth则表示敌人的当前的生命值,originalScale记录的是血条图片的初始长度。

      在Start()方法中,为originalScale赋值:

            originalScale = gameObject.transform.localScale.x;

      这里,我们获取了HealthBar这个游戏对象的X Scale。

      在Update()方法中,我们通过缩放HealthBar的图片长度,令它与敌人的当前生命值匹配:

            Vector3 tmpScale = gameObject.transform.localScale;
            tmpScale.x = currentHealth / maxHealth * originalScale;
            gameObject.transform.localScale = tmpScale;

      以上代码能够简写为下面的代码么?

        gameObject.transform.localScale.x = currentHealth / maxHealth * originalScale;

      不行的,单独为localScale.x赋值的时候,编译器报错了。

      

      于是,我们只能够先用一个临时变量tmpScale获取localScale的值,然后为tmpScale.X赋值,最后将tmpScale赋值localScale

      保存好脚本,启动游戏。现在我们可以看到每个敌人都有了自己的血条。

      

      选中一个敌人对象Enemy(Clone),在Hierarchy视图将它展开,选中它的子对象HealthBar。在Inspector面板中调整Current Health这个变量的值,我们可以看到敌人的血条的长度随着Current Health的值变化。

      

    追踪射程内的敌人

      现在,小怪兽们需要知道它们的攻击目标在哪里。在我们做这件事之前,我们要先为小怪兽和敌人做一点准备工作。

      选中Project面板中的PrefabMonster,在Inspector面板中为它添加一个Circle Collider 2D组件,这是一个2D圆形碰撞体组件。

      将该圆形碰撞体的半径设置为2.5——这是小怪兽的射程。

      启用Is Trigger这个属性,目的是令此碰撞体用于触发事件,并且不会发生任何物理交互。如果不启用这个属性的话,就是会发生碰撞。

      最后,在Inspector面板的顶部,将Monster的Layer属性设置为Ignore Raycast。在弹出的对话框中选择Yes,change children。如果你不这样设置的话,碰撞体会响应鼠标点击事件,这是我们不需要的。小怪兽位于召唤点Openspot的上方,这个碰撞体又是小怪兽的组件,于是鼠标点击事件就会被碰撞体优先响应,而不是被Openspot响应。这样的结果是什么?上一篇文章中,Openspot通过响应鼠标点击事件,可以放置或升级小怪兽;想想看,放置小怪兽后不能对它升级,这是不是违背了之前的设定?

      

      为了令小怪兽的碰撞体能够检测到在它范围内的敌人,我们需要为敌人对象添加一个碰撞体和刚体。在两个碰撞体发生碰撞的时候,假如其中一个有附加刚体组件,那么就会触发碰撞事件。

      在Project面板中,选中PrefabEnemy,为它添加Rigid Body 2D组件,勾选Is Kinematic属性。这是为了令敌人对象不受Unity中的物理引擎影响。

      再添加一个Circle Collider 2D,半径设置为1。对PrefabEnemy2重复以上步骤。

      现在所有的设置都已完成,你的小怪兽们可以侦测到射程内的敌人。  

      还有一件事情要做:在脚本中告知小怪兽敌人是否被消灭,当它们的射程内没有敌人的时候,没必要一直开火。

      为EnemyEnemy2这两个prefab添加一个新的脚本组件,命名为EnemyDestructionDelegate

      在VS中打开这个脚本,为它添加一个委托的声明:

        public delegate void EnemyDelegate(GameObject enemy);
        public EnemyDelegate enemyDelegate;

      这里我们创建了一个委托,它包含了一个方法的声明,可以像变量一样传递。

      提示: 当我们需要让一个游戏对象灵活地通知另一个游戏对象做出改变,请使用委托吧。关于委托的更多知识点,你可以从这里学习到—— the Unity documentation

      再添加下面的方法:

        void OnDestroy() 
        {
            if (enemyDelegate != null)
            {
                enemyDelegate(gameObject);
            }
        }

      以上代码的目的是为了销毁一个游戏对象,如同Start()Update()方法一样,Unity会自动调用OnDestroy()这个方法。在这个方法中,我们先判断委托变量的值是否不为null。如果是这样的话,我们调用这个委托,将gameObject作为它的参数。所有注册过这个委托的游戏对象都会得知敌人对象被销毁了。

      保存好脚本,返回Unity。

    让你的小怪兽们能对敌人开火

      现在,小怪兽们能侦测到攻击范围内的敌人。为Monster prefab添加一个C#脚本组件,命名为ShootEnemies

      在VS中打开它,添加下面的代码,目的是引用命名空间Generics

      using System.Collections.Generic;

      添加一个集合变量,用于追中所有攻击范围内的敌人:

      public List<GameObject> enemiesInRanges;

      这个集合里面存储了攻击范围内所有的敌人对象。

      在Start()方法里对这个集合进行初始化。

        enemiesInRanges = new List<GameObject>();

      起先,小怪兽的射程内木有敌人,于是我们就创建了一个空的List。

      接下来是向这个List中添加元素,在脚本中添加下面的代码段:

        // 1
        void OnEnemyDestroy(GameObject enemy) {
            enemiesInRanges.Remove(enemy);
        }
    
        void OnTriggerEnter2D(Collider2D other) {
        // 2
            if (other.gameObject.tag.Equals("Enemy")){
                enemiesInRanges.Add(other.gameObject);
                EnemyDestructionDelegate del =
                    other.gameObject.GetComponent<EnemyDestructionDelegate>();
                del.enemyDelegate += OnEnemyDestroy;
            }
        }
        // 3
        void OnTriggerExit2D(Collider2D other) {
            if (other.gameObject.tag.Equals("Enemy")){
                enemiesInRanges.Remove(other.gameObject);
                EnemyDestructionDelegate del =
                    other.gameObject.GetComponent<EnemyDestructionDelegate>();
                del.enemyDelegate -= OnEnemyDestroy;
            }
        }

      这段代码分为3个小方法:

      1. 在OnEnemyDestroy()方法中,我们移除了enemiesInRange中的enemy对象。当有敌人经过小怪兽的射程时,方法OnTriggerEnter2D()就会被调用。

      2. 将敌人对象添加到enemiesInRange当中,并且将方法OnEnemyDestroy()添加到委托EnemyDestructionDelegate上。这是为了确保当敌人对象被销毁的时候,方法OnEnemyDestroy()会被调用。你的小怪兽们不需要为已死的敌人浪费火力。

      3. 在OnTriggerExit2D()方法中,我们将敌人对象enemy从当中enemiesInRange移除,并且移除之前添加到委托上方法。现在小怪兽们可以知道它射程内的敌人是哪些了。

      保存好脚本,启动游戏,看看我们之前做的行不行。召唤一只小怪兽,选中它,然后在Inspector面板中查看enemiesInRange这个变量的变化。

      

      就像数绵羊那样。围栏(Fence )和绵羊(sheep)都由OpenClipArt提供。

    为小怪兽选择开火的目标

      现在小怪兽们可以侦测到它射程之内的敌人,但问题是当有多个敌人存在它射程之内的时候,该怎么办?

      当然是对离饼干最近的敌人开火啦!

      在VS中打开MoveEnemy.cs,添加一个新的方法来完成这个任务:

        public float distanceToGoal() 
        {
            float distance = 0;
            distance += Vector3.Distance(
                gameObject.transform.position,
                waypoints[currentWaypoint + 1].transform.position);
            for (int i = currentWaypoint + 1; i < waypoints.Length - 1; i++){
                Vector3 startPosition = waypoints[i].transform.position;
                Vector3 endPosition = waypoints[i + 1].transform.position;
                distance += Vector3.Distance(startPosition, endPosition);
            }
            return distance;
        }

      这个方法计算出了敌人尚未走完的路有多长。我们使用了Distatnce这个方法来计算两个Vector3之间的距离。

      ·通过这个方法来决定小怪兽的攻击目标。但是,现在你的小怪兽们无法攻击敌人,什么事都做不了,这个问题在下一步中解决。

      

      保存好脚本,返回Unity中,我们需要为小怪兽们配备射击敌人的子弹。

    为小怪兽们配备无尽的子弹

      将 Images/Objects/Bullet1 拖拽到场景视图中。将它的Z坐标设置为-2,在游戏过程中,我们需要不断地产生新的子弹,X和Y坐标是在子弹产生时候设置的。

      为Bullet1添加一个名为 BulletBehavior 的C#脚本组件,将下面的变量添加到脚本中:

        public float speed = 10;
        public int damage;
        public GameObject target;
        public Vector3 startPosition;
        public Vector3 targetPosition;
    
        private float distance;
        private float startTime;
    
        private GameManagerBehavior gameManager;

      变量 speed 指的是子弹的飞行速度,damage 指的是子弹对敌人造成的伤害。

      Target、startPosition、 targetPosition 分别指的是:子弹的目标、初始坐标、目标的坐标。

      distance 和 startTime 这两个变量决定了子弹的当前坐标。当玩家消灭一个敌人的时候,我们通过操作 gameManager 这个变量来给予玩家奖励。

      在 Start() 方法中为这些变量赋值:

            startTime = Time.time;
            distance = Vector3.Distance(startPosition, targetPosition);
            GameObject gm = GameObject.Find("GameManager");
            gameManager = gm.GetComponent<GameManagerBehavior>();

      我们将 startTime 设置为当前时间;distance变量的值为 startPosition 和 targetPosition 之间的距离;最后,我们获取了GameManagerBehavior的实例。

      在Update()方法中,添加下面的代码来控制子弹的运动轨迹:

            // 1
            float timeInterval = Time.time - startTime;
            gameObject.transform.position = Vector3.Lerp(startPosition, targetPosition, timeInterval * speed / distance);
            
            // 2
            if (gameObject.transform.position.Equals(targetPosition))
            {
                if (target != null){
                    // 3
                    Transform healthBarTransform = target.transform.FindChild("HealthBar");
                    HealthBar healthBar = healthBarTransform.gameObject.GetComponent<HealthBar>();
                    healthBar.currentHealth -= Mathf.Max(damage, 0);
                    // 4
                    if (healthBar.currentHealth <= 0)
                    {
                        Destroy(target);
                        AudioSource audioSource = target.GetComponent<AudioSource>();
                        AudioSource.PlayClipAtPoint(audioSource.clip, transform.position);
    
                        gameManager.Gold += 50;
                    }
                }
                Destroy(gameObject);
            }
    1. 计算出子弹的当前位置,这里我们还是使用 Vector3.Lerp 这个方法。
    2. 当子弹击中目标的时候,我们会先验证目标是否还存在。
    3. 获取了目标的 HealthBar 组件,按子弹造成的伤害来削减目标的生命值。
    4. 当一个敌人的生命值减到零的时候,需要销毁这个敌人对象,然后播放一个音效,最后给予玩家金币奖励。

      保存好脚本,返回Unity中。

    来些更大的子弹

      假如等级高的小怪兽能发射较大的子弹,这是不是很酷呢?是的,我们能做到,因为这很简单。

      将 Hierarchy 视图中的 Bullet1 拖拽到Project 视图中的Prefab文件夹下,创造出一个子弹的prefab。删除场景中的子弹对象,我们已经不再需要它。

      利用 Bullet1 prefab再创建两个prefab,分别命名为 Bullet2Bullet3 。传统的CTRL + C,CTRL + V命令在这里行不通。选中Bullet1后,按下快捷键CTRL + D,(duplicate 复制的意思),按下CTRL + D 两次后,创建  Bullet2 和 Bullet3。因为Bullet2 和 Bullet3都是比较大的子弹接下来,我们要为这两个prefab设置新的子弹图片。

      选中Bullet2 ,在Inspector面板中,设置 Sprite Renderer 组件的Sprite为 Images/Objects/Bullet2。这样,Bullet2的样子会比Bullet1更大一些。

      同上,将Bullet3 prefab的sprite设置为 Images/Objects/Bullet3

      之前在编写Bullet Behavior脚本的时候,没有进行设置 Damage 这个变量的值,接下来,分别设置这三种子弹造成的伤害值。

      在Inspector面板中,对Bullet1 、Bullet2 、Bullet3 的Damage进行赋值,分别为10、15、20,或者随你的便。

      注意:级别越高的子弹造成的伤害越大。玩家需要将金币花在刀刃上,优先升级那些位置好的小怪兽们。

      

      子弹的大小与小怪兽的等级成正比。

    提升子弹的威力

       为不同等级的小怪兽分配威力不同的子弹,这样小怪兽越强,就能越快地消灭敌人。

      打开脚本 MonsterData.cs ,为 MonsterLevel 添加下面的变量:

        public GameObject bullet;
        public float fireRate;

      前者是指子弹的 prefab,后者是指小怪兽发射子弹的速率。保存好脚本,返回Unity,让我们完成对小怪兽的配置。

      在Project视图中选中Monster prefab。在Inspector面板中,展开Monster Data脚本组件中的Levels数组,将所有元素的Fire Rate都设置为1,分别设置Elements0、Elements1、Elements2的BulletBullet1Bullet2Bullet3

      配置好后的结果如下图所示:

      

    开火

      打开脚本ShootEnemies.cs,添加下面的变量:

        private float lastShotTime;
        private MonsterData monsterData;

      像这两个变量名所显示的那样,前者记录了小怪兽上一次开火的时间,后者的类型为MonsterData,这里包含了该小怪兽的子弹类型,发射速率等等数据。

      在Start()方法中为这两个变量赋值:

        lastShotTime = Time.time;
        monsterData = gameObject.GetComponentInChildren<MonsterData>();

      这里,我们设置lastShotTime为当前时间,然后获取了该游戏对象的MonsterData 组件。

      再添加下面的代码,令小怪兽能够对敌人开火:

        void Shoot(Collider2D target)
        {
            GameObject bulletPrefab = monsterData.CurrentLevel.bullet;
            // 1
            Vector3 startPosition = gameObject.transform.position;
            Vector3 targetPosition = target.transform.position;
            startPosition.z = bulletPrefab.transform.position.z;
            targetPosition.z = bulletPrefab.transform.position.z;
    
            // 2
            GameObject newBullet = (GameObject)Instantiate(bulletPrefab);
            newBullet.transform.position = startPosition;
            BulletBehavior bulletComp = newBullet.GetComponent<BulletBehavior>();
            bulletComp.target = target.gameObject;
            bulletComp.startPosition = startPosition;
            bulletComp.targetPosition = targetPosition;
    
            // 3
            Animator animator = monsterData.CurrentLevel.visualization.GetComponent<Animator>();
            animator.SetTrigger("fireShot");
            AudioSource audioSource = gameObject.GetComponent<AudioSource>();
            audioSource.PlayOneShot(audioSource.clip);
        }
    1. 获取了子弹的初始坐标和目标所在坐标,将这两个坐标的Z坐标设置为 bulletPrefab的Z坐标。之前我们设置bullet prefab的Z坐标的原因是为了表现一种层次感,子弹所处的位置要比小怪兽和敌人更低。
    2. 方法开头从MonsterData中获取了bulletPrefab,bulletPrefab创建出一个子弹对象。startPosition 和 targetPosition 赋值给我们创建出来的子弹对象。
    3. 让游戏更生动:当小怪兽开火的时候播放一个射击的动画和音效。

    整合所有的模块

      现在是时候该整合一切了,让你的小怪兽能够准确地朝着目标开火。

      往ShootEnemies.cs脚本的Update()方法中添加下面的代码:

            GameObject target = null;
            // 1
            float minimalEnemyDistance = float.MaxValue;
            foreach (GameObject enemy in enemiesInRange)
            {
                float distanceToGoal = enemy.GetComponent<MoveEnemy>().distanceToGoal();
                if (distanceToGoal < minimalEnemyDistance) 
                {
                    target = enemy;
                    minimalEnemyDistance = distanceToGoal;
                }
            }
            // 2
            if (target != null) 
            {
                if (Time.time - lastShotTime > monsterData.CurrentLevel.fireRate){
                    Shoot(target.GetComponent<Collider2D>());
                    lastShotTime = Time.time;
                }
                // 3
                Vector3 direction = gameObject.transform.position - target.transform.position;
                gameObject.transform.rotation = Quaternion.AngleAxis(
                    Mathf.Atan2(direction.y, direction.x) * 180 / Mathf.PI,
                    new Vector3(0, 0, 1));
            }

      让我们一步一步地来看这些代码:

    1. 决定小怪兽开火的目标,这里我们采用了寻找最小数的算法。先将 minimalEnemyDistance设置为float.MaxValue,这样就不会有比它更大的数出现了。遍历集合中的所有敌人,当循环结束的时候,我们就可以找出距离饼干最近的敌人。
    2. 当前时间与小怪兽上次开火的时间间隔大于射击速率的时候,调用Shoot方法, 再将lastShotTime设置为当前时间。
    3. 计算出小怪兽和目标之间的当前角度,然后旋转小怪兽,让小怪兽能够一直面对着目标。

      保存好脚本,启动游戏。看你的小怪兽们正在奋力地保护你的饼干。好样的,现在我们完成了整个工程。

      

    从这个项目中我们学到了什么

      从这里可以下载完整的项目。

      现在我们这个教程就要结束了,我们完成了一个很棒的塔防游戏。

      这个游戏我们还可以做出以下扩展:

       1. 添加更多种类的敌人和小怪兽

       2. 为敌人建立更多的通往饼干的道路

       3. 为小怪兽们设置更多的级别

      这些小小的扩展可以令我们的游戏更好玩。假如你以此教程为基础创造出了属于自己的新游戏,请在评论中分享你的链接,让大家都能够好好地体验一回。

      在这里你可以发现更多有趣的关于塔防游戏的想法。

       感谢大家抽出时间来完成这篇教程。希望大家能够提出更多好的想法,祝大家都能够愉快地杀敌。

  • 相关阅读:
    Android 6.0运行时权限第三方库的使用-----RxPermissions
    Android当下最流行的开源框架总结
    Android使用SVG小结
    Android框架之路——GreenDao3.2.2的使用
    15 个 Android 通用流行框架大全
    Android sqlite 使用框架
    Android数据库框架——GreenDao轻量级的对象关系映射框架,永久告别sqlite
    根据图片名字在drawable中得到图片
    Repeater控件最后一笔记录高亮显示
    MasterPage + UpdatePanel + FileUpload
  • 原文地址:https://www.cnblogs.com/lcxBlog/p/6185330.html
Copyright © 2011-2022 走看看