【Unity3D开发小游戏】《植物大战僵尸游戏》Unity开发教程_植物大战僵尸环境unity创建-程序员宅基地

技术标签: Unity  # Unity3D开发小游戏  Unity3D开发小游戏  

推荐阅读

一、前言

今天我们将要使用Unity3D做一个植物大战僵尸游戏,为了保持简单,我们将去掉其他花里胡哨的东西,比如菜单、关卡、过长动画等,并专注于植物与僵尸的战斗。

效果图:

在这里插入图片描述

二、源码

UI资源:
https://wwr.lanzoui.com/idvmDop2dwb
密码:5mrb

源代码:
https://wwr.lanzoui.com/iAVD0op2dxc
密码:evq2

Unity3D交流QQ群:1040082875

三、正文

版本

Unity5.0.1f1

1.主摄像机设置

首先在层次面板中选择Main Camera,然后设置背景色为黑色,并且调整大小,如下图所示:在这里插入图片描述

2.创造草地

在这里插入图片描述
注意:右击图像,选择另存为。保存到项目的Assets/Sprites文件夹。

让我们在项目区选择它:
在这里插入图片描述
然后Inspector面板修改导入设置:

在这里插入图片描述
注:Pixels to Unit 值设成32,这意味着在游戏世界中32x32是一个比较合适的单位。我们将使用这个值作为我们所有的纹理的单位值。

之后,我们可以将图片从项目区拖入到场景中:
在这里插入图片描述
它在我们游戏的左下角,所以我们会把它定位在(0, 0):
在这里插入图片描述
添加Sorting Layer层
我们正在制作一个2D游戏,所以我们不能用三维做高度区分,也不能轻易区分背景和前景。所以,我们将使用Sorting Layer告诉Unity它应该先显示哪些元素。例如,我们应该先画背景图,然后再画植物。(因为植物在背景之上)。

我们点击草坪,在Inspector面板中可以看到Sprite Renderer组件的Sorting Layer组件:
在这里插入图片描述
让我们添加Sorting Layer,点击加号,输入Background,添加Background层,并将其移动到顶部,如下所示:
在这里插入图片描述
之后,我们再次选择草坪,并分配先前创建的Background层:
在这里插入图片描述
注意:Unity从上到下画层,因此背景中的任何内容都会显示在草坪的上面

3.草坪音效

我们需要一种方法来确定是否单击了草坪。在Unity中有各种不同的方法来实现这一点,但最简单的方法是使用OnMouseUpAsButton函数,如果单击了草,则由Unity自动调用该函数。

现在这里有一件事要记住:只有当游戏对象有对撞机时,Unity才能做到这一点。所以让我们选择Add Component->Physics 2D->Box Collider 2D,然后在在Inspector面板中启用Is Trigger 选项:

  • Is Triggers
    在这里插入图片描述
    注意:Is Trigger意味着草坪会收到各种各样的碰撞信息,但实际上不会发生碰撞。所以,如果僵尸走进草坪,并不会与其碰撞。

复制草坪
现在我们的游戏中只有一片草坪。让我们在Hierarchy面板中,选择Duplicate然后把它放在(1, 0),然后我们将再次复制它,并将其放置在(2, 0),然后继续复制,直到铺满整个场景:
在这里插入图片描述
注意:重要的是所有的草坪的坐标都是整数,比如(2, 3),而不可以是(2.01, 3.023):

4.生命值脚本

僵尸应该能够攻击植物,并且开火的植物也可以攻击僵尸。

我们需要创建一个Health.cs脚本

我们可以通过右键单击项目区然后选择Create->C# Script:
在这里插入图片描述
我们给它起个名字Health.cs然后拖入到新的Scripts文件夹:
在这里插入图片描述
我们可以通过双击脚本打开脚本,然后修改脚本:

using UnityEngine;
using System.Collections;

public class Health : MonoBehaviour {
    

    // Use this for initialization
    void Start () {
    
    
    }
    
    // Update is called once per frame
    void Update () {
    
    
    }
}

我们不需要Start或者Update函数,所以让我们移除这两个函数。
相反,我们将添加一个int变量cur,该变量跟踪当前的生命值,并添加一个减少该变量的函数。
如果当前的生命值低于0,应摧毁该对象:

using UnityEngine;
using System.Collections;

public class Health : MonoBehaviour {
    
    // Current Health
    [SerializeField]
    int cur = 5;

    public void doDamage(int n) {
    
        // Subtract damage from current health
        cur -= n;

        // Destroy if died
        if (cur <= 0)
            Destroy(gameObject);
    }
}

注:我们使用[SerializeField]属性让Unity知道我们希望能够在Inspector面板中可以修改cur变量,这通常是通过public访问权限,但在这种情况下,我们不希望其他脚本能够访问cur变量,其他脚本只能使用doDamage函数来改变血量的值。

5.创建向日葵植物

向日葵图片

由于我们正在开发一个2D游戏,动画是非常容易的。

对于我们的向日葵,我们只需要一个Idle的动画,它的头部轻微移动:
在这里插入图片描述
注意:右击图像,选择另存为。并将其保存在项目的Assets/Sprites文件夹。

让我们在项目区然后在Inspector面板中修改导入设置

在这里插入图片描述
这个Filter Mode和Format影响图片的分辨率。
设置Sprite Mode为Multiple告诉Unity,这一张照片可以分割成几个向日葵。

切片

我们可以打开Inspector面板中按Sprite Editor按钮

之后,我们可以在Sprite Editor中看到我们的向日葵:在这里插入图片描述
我们要做的就是打开Slice菜单,将类型设置为格网像素大小为32 x 32,之后,我们按下Slice按钮:
在这里插入图片描述
然后按Apply最后关闭 Sprite Editor编辑器。

现在我们可以在项目中看到我们的向日葵有两个子节点:
在这里插入图片描述

6.向日葵动画

从这两个切片创建一个Unity动画是非常容易的。我们所要做的就是在项目区选中两个切片,然后把他们拖到场景中:
在这里插入图片描述
一旦我们把它拖到场景中,Unity会询问我们将动画保存到哪个位置。
我们可以在我们的项目区创造一个新的SunflowerAnimation文件夹,然后将动画保存为idle.anim
Unity会为我们创建两个文件:
在这里插入图片描述
这就是我们的动画,如果我们按下Play:
在这里插入图片描述
注意:如果要改变动画播放的速度,可以在SunflowerAnimation文件夹中双击sunflower_0,选择idle状态,然后在Inspector面板更改speed

7.添加物理效果、tag与生命值

我们的植物应该是物理世界的一部分,这意味着我们必须分配一个Collider给它。
一个Collider确保物体会与植物发生碰撞,而植物也会与其他物体发生碰撞。

让我们选择植物,然后在Inspector面板按下Add Component->Physics 2D->Box Collider 2D:
在这里插入图片描述
我们还需要找出某个游戏对象是一个植物,还是一个僵尸,还是其他对象。
这就需要Unity标签系统了。
如果我们看看Inspector,我们可以看到当前标记是Untagged:
在这里插入图片描述
我们选择Plant标签。
在标记列表中,然后添加Plant标记到可用标签列表,
再次选择plant,然后从标记列表中分配标记:
在这里插入图片描述
最后在Inspector面板那里Add Component->Scripts->Health
僵尸可以在稍后可以对植物造成伤害:
在这里插入图片描述
注:我们的向日葵刚刚被随意放置在现场的某个地方。我们将把它保存在那里,下一步我们将生成阳光

5.生成阳光

画阳光
向日葵植物每隔几秒钟就会产生一个新的阳光,所以让我们先画一个阳光:
在这里插入图片描述
注意:右击图像,选择另存为。并将其保存在项目的Assets/Sprites文件夹。

导入设置中设置图片的导入属性:
在这里插入图片描述
Foreground Layer前景层
我们希望太阳被画在背景前面和植物前面。让我们点击添加排序层。
再一次创造另一个Foreground分选层,并将Foreground移至列表的底部:
在这里插入图片描述
之后,我们可以再次选择sun并分配Foreground排序层:
在这里插入图片描述
创建预制件

让我们把阳光拖到Hierarchy一次,创建一个游戏对象:
在这里插入图片描述
然后按下Add Component->Physics 2D>Circle Collider 2D
碰撞器可以让阳光可以接收碰撞信息:
在这里插入图片描述
注意:在实际的植物大战僵尸游戏中,阳光并没有真正地与植物或僵尸发生碰撞。我们可以通过在Circle Collider 2D中勾选Is Trigger。这意味着阳光接收到碰撞信息,但从未真正与任何物体发生碰撞。僵尸将能够穿过它,而不是与它相撞。

之后,我们创建一个新的文件Prefabs,将sun拖到Prefabs文件中创建一个预制体:
在这里插入图片描述
现在我们可以将sun从场景中删除。

注意:稍后将通过使用实例化功能生成阳光。

8.生成阳光

我们希望向日葵每隔几秒钟产生一个新的阳光。这种行为可以通过脚本实现。让我们选择向日葵,然后点击Add Component->New Script:
在这里插入图片描述
我们给它起个名字SunSpawn
然后将其移动到项目的Scrips文件夹中。之后,我们双击打开它:

using UnityEngine;
using System.Collections;

public class SunSpawn : MonoBehaviour {
    

    // Use this for initialization
    void Start () {
    
    
    }
    
    // Update is called once per frame
    void Update () {
    
    
    }
}

我们将添加一个公共GameObject变量,允许我们稍后在Inspector面板中指定预制体:

public class SunSpawn : MonoBehaviour {
    
    public GameObject prefab;
    ...

这个实例化函数允许我们将预置体加载到场景中。
这个InvokeRepeting函数允许我们从某个时候开始,重复调用某个函数。
例如,如果我们想在1秒内第一次生成,然后每2秒重复一次,我们将使用
InvokeRepeting(“Spawn”,1,2)
我们希望在10秒内产生第一个阳光,然后每10秒继续生成更多的阳光,下面是我们的代码:

using UnityEngine;
using System.Collections;

public class SunSpawn : MonoBehaviour {
    
    public GameObject prefab;

    // Use this for initialization
    void Start() {
    
        // Spawn first Sun in 10 seconds, repeat every 10 seconds
        InvokeRepeating("Spawn", 10, 10);
    }
    
    void Spawn() {
    
        // Load prefab into the Scene
        // -> transform.position means current position
        // -> Quaternion.identity means default rotation
        Instantiate(prefab,
                    transform.position,
                    Quaternion.identity);
    }
}

现在我们保存脚本,点击sun对象,然后查看Inspector面板。
脚本有一个预制体插槽,因为我们的预制体变量是公开的。
让我们从项目区拖着我们的SunPrefab预制件到脚本的插槽中:
在这里插入图片描述
如果我们按下Play然后我们每10秒就能看到一个新的太阳生成。

阳光移动
阳光生成后,它应该慢慢地消失在屏幕的顶端。我们将使用 Rigidbody 2D。
刚体通常被用于物理世界中的任何东西,而这些东西应该是在移动的。

让我们在Inspector那里单击Add Component->Physics2D->Rigidbody2D。我们将为它分配以下属性:
在这里插入图片描述
现在我们要做的就是给刚体一些速度(这是movement direction * speed),以下图像显示某些运动方向所需的不同矢量:
在这里插入图片描述
我们将创建另一个C#脚本,命名为DefaultVelocity然后用它来设定速度:

using UnityEngine;
using System.Collections;

public class DefaultVelocity : MonoBehaviour {
    
    // The Velocity (can be set from Inspector)
    public Vector2 velocity;

    void FixedUpdate () {
    
        GetComponent<Rigidbody2D>().velocity = velocity;
    }
}

注意:我们在每个FixedUpdate调用此函数,以便无论发生什么情况,该脚本所附加的内容都将尝试继续移动。这对于僵尸来说是很有用的,当他们跑进一个植物时,可能无法继续移动,但是我们的脚本将确保他们在植物被摧毁后再次开始移动。

之后,我们再次选择Sun预制体,然后单击Add Component->Scripts->Default Velocity
我们指定一个速度向上 (进入y方向):
在这里插入图片描述
如果我们按下Play然后,我们现在可以看到太阳阳光生成,然后向上移动:
在这里插入图片描述
收集阳光
当我们谈到阳光的时候,我们必须做的最后一件事就是让Player收集它。
我们将需要一个全局变量得分,以跟踪多少阳光被收集,每当玩家点击阳光,我们将增加这个数的大小。

Unity有OnMousedown函数,该函数确定我们是否单击了GameObject。
所以让我们创建一个新的C#脚本SunCollect然后使用OnMouseDown函数:

using UnityEngine;
using System.Collections;

public class SunCollect : MonoBehaviour {
    
    // Global score
    public static int score = 100;

    void OnMouseDown() {
    
        // Increase Score
        score += 20;

        // Destroy Sun
        Destroy(gameObject);
    }
}

注:我们必须使用静态以使分数变量全局化。这意味着其他脚本可以使用SunCollection.score获取到数值。最初的分数是100,所以玩家有一些阳光来开始建造植物。

一旦我们将脚本添加到SunPrefab中,我们就可以按Play,等待太阳生成阳光,然后点击它收集它。

现在向日葵还在场景中,但我们不希望它从一开始就在那里。
游戏应该在启动后自动生成它。
因此,让我们从它拖到Prefabs文件夹:
在这里插入图片描述

6.植物设计

植物图片
好吧,让我们再创造一种植物,这种植物能够向僵尸射击。和往常一样,我们将从绘制所有动画开始。顶部的两个切片是idle动画,底部的两个切片是开发动画:
在这里插入图片描述
注意:右击图像,选择另存为。并将其保存在项目的Assets/Sprites文件夹。

导入设置
在这里插入图片描述
点击Sprite Editor,然后使用以下设置对其进行切片:
在这里插入图片描述

7.植物动画

创造动画
这一次,我们有两个动画在我们的形象。前两个切片是idle动画,最后两个切片是开火动画。

将前两个切片拖入到场景中然后创建空闲动画:
在这里插入图片描述
命名为idle.anim并将其保存在一个新FiringplantAnimation文件夹中

射击动画的操作方式也是一样的:
在这里插入图片描述
如果我们按下Play然后我们可以在游戏中看到我们的两个动画:
在这里插入图片描述
注意:它们仍然是两个不同的游戏对象,但是我们很快就会处理好的。

清理对象
让我们在层级面板上删除firingplant_2,然后去项目区删除firingplant_2动画控制器,删完以后的样子:
在这里插入图片描述
在这里插入图片描述
注意:这么做只是为了整洁,不删除也没有影响

修改动画控制器
在Project区,双击firingplant_0动画控制器,打开动画控制器,可以在项目区看到当前状态机firingplant_0:
在这里插入图片描述
现在我们看到的就是idle状态。如果我们想让它在某个时候播放射击动画,那么我们将再创建一个状态,我们将firing.anim动画也拖入到动画控制器:
在这里插入图片描述
我们需要某种参数来帮助Unity确定何时进入射击状态。
我们将选择参数选项卡,然后在右边单击+,添加一个Trigger类型的参数给它起个名字IsFiring:

在这里插入图片描述
注意:触发器是只触发一次的参数。稍后,我们将在脚本中设置此参数。

当IsFiring参数被触发,我们将切换到射击状态。让我们右键单击idle状态,选择Make Transition然后将白色箭头拖到firing状态:
在这里插入图片描述
之后,点击白色箭头,我们将禁用Has Exit Time属性并设置条件IsFiring:
在这里插入图片描述
注意:禁用Has Exit Time,是因为转换不应该在一段时间之后自动发生

现在,我们再从firing到idle。这一次我们将开启Has Exit Time
因为我们希望动画状态机从firing到idle在下一秒自动执行。
我们还将设置Exit Time为1:
在这里插入图片描述
这是我们最后的状态机:
在这里插入图片描述
注意:状态机现在可以根据设置的IsFiring参数自动切换idle和firing状态

8.植物的物理和Tag

添加碰撞器
在这里插入图片描述
设置Tag为Plant:
在这里插入图片描述
添加Health.cs脚本给植物,以便僵尸稍后能够对植物造成伤害:
在这里插入图片描述

9.植物的子弹

画个子弹
我们将使用16x16px纹理作为子弹:

在这里插入图片描述
注意:右击图像,选择另存为。并将其保存在项目的Assets/Sprites文件夹。

导入设置

在这里插入图片描述
子弹预制体
我们将把子弹纹理拖到场景中,然后将其拖回项目区,它创建了一个Prefab:
在这里插入图片描述
之后,我们可以再次从层次面板中删除它。

子弹物体
添加碰撞器和刚体:
在这里插入图片描述
注意:我们启用了Is Trigger所以子弹会穿过植物而不是与它们相撞。

发射子弹
子弹一被实例化就应该飞向右边。
我们已经有了一个脚本,通过使用刚体的velocity。
所以让我们选择添加DefaultVelocity.cs脚本,指定一个速度让它向右飞:
在这里插入图片描述
注意:这是基于组件的开发的美妙之处。我们可以写一次脚本,然后重复使用它在我们的游戏中的各种实体。

碰撞造成伤害

为了检查碰撞,我们将使用Unity的OnTriggerEnter2D函数
一旦子弹飞进另一个GameObject,就会调用该函数
在这种情况下,我们将检查它是否是僵尸(通过比较标签)然后降低它的生命值。

让我们创建一个新的C#脚本,命名为BulletDamage.cs然后双击打开,不需要Stat函数和Update函数删除,然后添加一个OnTriggerEnter2D函数:

using UnityEngine;
using System.Collections;

public class BulletDamage : MonoBehaviour {
    

    void OnTriggerEnter2D(Collider2D co) {
    
        // Zombie?
        if (co.tag == "Zombie") {
    
            // Deal Damage, destroy Bullet
            co.GetComponent<Health>().doDamage(1);
            Destroy(gameObject);
        }
    }
}

注意:我们使用GetComponent找到僵尸上面的组件Health,然后我们通过调用doDamage功能。之后,我们通过以下方法将子弹从游戏中销毁

我们可以通过选择Add Component->Scripts->Bullet Damage:
在这里插入图片描述

10.植物发射子弹

既然我们有了子弹,我们就可以让我们的开火植物一看到僵尸就射它。
我们新添加一个脚本Firing.cs,然后双击打开它:

using UnityEngine;
using System.Collections;

public class Firing : MonoBehaviour {
    

    // Use this for initialization
    void Start () {
    
    
    }
    
    // Update is called once per frame
    void Update () {
    
    
    }
}

首先,我们得找出植物右边是否有僵尸。
我们可以通过使用RaycastAll功能。一个RaycastAll射出一条射线穿过这个世界,然后发现它是否击中了任何东西。
所以,如果我们从屏幕的最右边向植物发射射线,如果它与标有“僵尸”标签的物体相撞,那么植物就应该在发射子弹。

这是我们的功能:

bool zombieInFront() {
    
    // Raycast from the right of the game to the plant
    Vector2 origin = new Vector2(9.5f, transform.position.y);
    RaycastHit2D[] hits = Physics2D.RaycastAll(origin, -Vector2.right);

    // Find out if any Zombie was hit
    foreach (RaycastHit2D hit in hits) {
    
        if (hit.collider != null &&
            hit.collider.gameObject.tag == "Zombie")
            return true;
    }
    return false;
}

注意:RaycastAll函数返回所有点击,而不像Raycast函数那样只返回第一个点击。我们也可以从植物向屏幕右侧射出一束光线。但是从右向植物发射更好,因为如果我们给植物增加一个碰撞器,我们就会遇到麻烦,在这种情况下,射线只会击中植物本身。我们使用-Vector2.right。因为它是向左。我们使用向量(9.5f, pos.y)作为光线投射的位置,因为这是草的右边界,还有植物的高度。

我们还需要一个公共的Prefab变量,这个变量将被设置为子弹预制体:

public class Firing : MonoBehaviour {
    
    // The Bullet Prefab
    public GameObject bulletPrefab;    
    ...
}

现在我们可以写我们的Shoot函数,实例化生成子弹,并设置IsFiring参数在我们的动画状态机中,
每当它看到僵尸时:

void Shoot() {
    
    if (zombieInFront()) {
    
        // Animation
        GetComponent<Animator>().SetTrigger("IsFiring");

        // Instantiate Bullet
        Instantiate(bulletPrefab, transform.position, Quaternion.identity);
    }
}

如果我们把它们放在一起的话,情况是这样的:

using UnityEngine;
using System.Collections;

public class Firing : MonoBehaviour {
    
    // The Bullet Prefab
    public GameObject bulletPrefab;
    
    // Shooting Interval
    public float interval = 0.5f;

    // Use this for initialization
    void Start () {
    
        // Try to shoot every few seconds
        InvokeRepeating("Shoot", 0, interval);
    }

    bool zombieInFront() {
    
        // Raycast from the right of the game to the plant
        Vector2 origin = new Vector2(9.5f, transform.position.y);
        RaycastHit2D[] hits = Physics2D.RaycastAll(origin, -Vector2.right);

        // Find out if any Zombie was hit
        foreach (RaycastHit2D hit in hits) {
    
            if (hit.collider != null &&
                hit.collider.gameObject.tag == "Zombie")
                return true;
        }
        return false;
    }
    
    void Shoot() {
    
        if (zombieInFront()) {
    
            // Animation
            GetComponent<Animator>().SetTrigger("IsFiring");

            // Instantiate Bullet
            Instantiate(bulletPrefab, transform.position, Quaternion.identity);
        }
    }
}

然后我们点击植物,将我们的预制体子弹拖入到预制体插槽中:
在这里插入图片描述
注意:一旦我们加入僵尸,我们就可以尝试一下射击了。

然后将植物firingplant拖入到prefab文件夹,做成一个预制体。

11.生成菜单

在这里插入图片描述
现在我们创建菜单,并实现需要的功能。

Build Info组件
我们的建造菜单将需要每个植物的价格和一个小预览图像。
现在这种数据必须存储在某个地方。
要做到这一点,最好的方法是将其存储在每个植物的预制体中。

让我们选择我们的两个植物预制体,然后点击添加组件,新脚本,命名为BuildInfo,然后双击打开:

using UnityEngine;
using System.Collections;

public class BuildInfo : MonoBehaviour {
    
    public Texture previewImage;
    public int price;
}

我们将使用以下预览图像:
在这里插入图片描述在这里插入图片描述
注意:右击图像,选择另存为。并将其保存在项目的Assets/Sprites文件夹。

导入设置选中两幅图片:
在这里插入图片描述
然后我们分别选择我们的Build Info组件,分配以下属性:
在这里插入图片描述
向日葵
在这里插入图片描述
寒冰射手

12.绘制菜单

为了显示菜单名,我们将使用OnGUI函数:

  • 画阳光和设置阳光分数
  • 每种植物
  • 画出植物的样子和需要的阳光数量

让我们先梳理一下步骤:

  • 使用GUILayout将菜单定位在最上面的中心
  • 使用Horizontal 画出框框(带有方框式)
  • 画阳光图像和阳光的分数
  • 画出每个植物
  • 绘制按钮预览图像和价格

注意:GUILayout.Button只能显示一个图像或者一个文本,不能两个都显示,我们希望显示预览图和价格,我们将使用GUIContent

我们点击Main Camera,选择添加组件,新建一个脚本,命名为BuildMenu.cs

这是我们的BuildMenu脚本:

using UnityEngine;
using System.Collections;

public class BuildMenu : MonoBehaviour {
    
    // Sun Image
    public Texture sunImage;

    // Plant Prefabs
    public BuildInfo[] plants;

    void OnGUI() {
    
        // Begin Gui
        GUILayout.BeginArea(new Rect(Screen.width/2 - 100, -7, 200, 100));
        GUILayout.BeginHorizontal("box");

        // Draw the Sun
        GUILayout.Box(new GUIContent(SunCollect.score.ToString(), sunImage));

        // Draw each Plant's BuildInfo
        foreach (BuildInfo bi in plants) {
    
            GUILayout.Button(new GUIContent(bi.price.ToString(), bi.previewImage));
        }

        // End Gui
        GUILayout.EndHorizontal();
        GUILayout.EndArea();
    }
}

注意:植物预制板是一个数组(因此[ ])。这意味着这不仅仅是一个而是一组。

保存脚本后,我们将把我们的太阳图像和植物预制板从我们的项目区进入脚本的插槽:
在这里插入图片描述
如果我们按下Play然后我们就可以看到我们的菜单了:
在这里插入图片描述
启用和禁用按钮
现在有更多的东西要在这里实现。首先,每个按钮应该启用,只有当玩家收集到足够的太阳支付它。这可以很容易地通过使用GUI.enabled:

// Draw each Plant's BuildInfo
foreach (BuildInfo bi in plants) {
    
    GUI.enabled = SunCollect.score >= bi.price;
    GUILayout.Button(new GUIContent(bi.price.ToString(), bi.previewImage));
}

注意:这会将一个bool值(真或假)分配给GUI.enabled。bool值是通过判断SunCollect.score是否植物的价格要高来判断是否为真。

如果我们按下Play,我们就可以看到我们的按钮被禁用,除非我们有足够的阳光:

13.生成植物

让我们来处理最重要的部分:生成植物。
玩家应首先单击“生成”菜单中的“植物”按钮,然后单击“草地”。之后,植物应该建在那块草地上。

因此,首先,我们需要跟踪玩家想要建造的植物:

// Currently building...
public static BuildInfo cur;

注:public static因为我们希望以后能够从另一个脚本访问它。

一旦玩家点击一个按钮,我们就会设置cur变量为玩家想要创建的植物:

// Draw each Plant's BuildInfo
foreach (BuildInfo bi in plants) {
    
    GUI.enabled = SunCollect.score >= bi.price;
    if (GUILayout.Button(new GUIContent(bi.price.ToString(), bi.previewImage)))
        cur = bi;
}

14.草地的脚本

现在,我们将不得不等待玩家点击草地。
如果他点击了一个,那么我们想要构建BuildInfo.cur 植物。
让我们选择场景中的所有草块,然后单击Add Component->New Script。
我们给它起个名字Grass,再一次将它移到我们的Scripts 文件夹中,然后打开它。
我们将使用OnMouseUpAsButton函数以确定是否单击了草地:

using UnityEngine;
using System.Collections;

public class Grass : MonoBehaviour {
    

    void OnMouseUpAsButton() {
    
        // Build Stuff
    }
}

现在我们将找出是否有什么东西要建,如果有,我们将建造它,并让玩家支付阳光:

using UnityEngine;
using System.Collections;

public class Grass : MonoBehaviour {
    

    void OnMouseUpAsButton() {
    
        // Is there something to build?
        if (BuildMenu.cur != null) {
    
            // Build it
            Instantiate(BuildMenu.cur.gameObject, transform.position, Quaternion.identity);
            SunCollect.score -= BuildMenu.cur.price;
            BuildMenu.cur = null;
        }
    }
}

注意:我们可以从任何位置访问BuildMenu的cur变量。这起作用是因为public static。我们用实例化使用草地的位置(transform.position),默认的旋转(Quaternion.identity)。之后我们清除cur变量,因为我们已经完成了创建。

如果我们按下Play然后我们现在可以创建一些植物:
在这里插入图片描述

15.僵尸

画僵尸
现在我们一直在等待的是:僵尸:
和往常一样,我们将从绘制一个包含所有动画帧的图像开始。第一行包含行走动画,第二行包含攻击动画:
在这里插入图片描述
注意:右击图像,选择另存为。并将其保存在项目的Assets/Sprites文件夹。

导入设置:
在这里插入图片描述
图像将被切片为32 x 32网格:
在这里插入图片描述

16.僵尸动画

让我们创建moving动画,通过选择前4个切片并将它们拖到场景中来创建动画:
在这里插入图片描述
我们会把它命名为moving.anim存放在一个新的ZombieAnimation文件夹

接下来的两片将是attacking动画:
在这里插入图片描述
我们会把它命名为attacking.anim存放在一个新的ZombieAnimation文件夹

在这里插入图片描述
清理
与往常一样,我们将在场景中删除zombie_4,在项目文件中删除zombie_4的动画状态机

17.僵尸动画状态机

我们前面已经创建了很多次状态机了,已经很熟悉了:
在这里插入图片描述
参数是:

  • Trigger类型的IsAttacking变量

这些Transitions 是:

  • 进攻:=IsAttacking
  • 进攻到移动:= Exit Time 1.0

这意味着只要我们说IsAttacking,动画状态机将播放进攻动画一秒钟。然后它会自动回到移动状态。

18.僵尸物理

我们的僵尸应该是物理世界的一部分,这意味着它需要一个BoxCollider2D。
它也应该四处移动,这意味着它需要一个Rigidbody 2D:

在这里插入图片描述

19.僵尸移动

僵尸应该向左走。当然,我们可以再次使用我们的DefaultVelocity脚本,选择Add Component->Scripts->Default Velocity然后分配一个速度,让它慢慢地向左走:
在这里插入图片描述

20.僵尸攻击脚本

一旦僵尸与植物相撞,它就应该每秒钟攻击一次。

添加一个新脚本ZombieAttacking.cs,然后双击打开,不需要Start函数和Update函数,删除,然后添加OnCollisionStay2D函数:

using UnityEngine;
using System.Collections;

public class ZombieAttacking : MonoBehaviour {
    

    void OnCollisionStay2D(Collision2D coll) {
    
        // Collided with a Plant?
        if (coll.gameObject.tag == "Plant") {
    
            // Do Stuff...
        }
    }
}

僵尸站在植物前应播放攻击动画:

void OnCollisionStay2D(Collision2D coll) {
    
    // Collided with a Plant?
    if (coll.gameObject.tag == "Plant") {
    
        // Play Attack Animation
        GetComponent<Animator>().SetTrigger("IsAttacking");
    }
}

它也应该对植物造成伤害,但每秒钟只能造成一次:

using UnityEngine;
using System.Collections;

public class ZombieAttacking : MonoBehaviour {
    
    // Last Attack Time
    float last = 0;

    void OnCollisionStay2D(Collision2D coll) {
    
        // Collided with a Plant?
        if (coll.gameObject.tag == "Plant") {
    
            // Play Attack Animation
            GetComponent<Animator>().SetTrigger("IsAttacking");
            // Deal damage once a second
            if (Time.time - last >= 1) {
    
                coll.gameObject.GetComponent<Health>().doDamage(1);
                last = Time.time;
            }
        }
    }
}

注意:我们只是用Time.time找出从那时起已经过去了多少时间最后的时间,然后进入植物 Health脚本,调用doDamage函数并重置最后的时间到了。

还记得我们是如何检查僵尸射击和子弹记录上的标签?
让我们添加僵尸Tag到我们的僵尸,以确保它可以被子弹射中:
在这里插入图片描述
我们还将添加一个Health脚本:
在这里插入图片描述
如果我们按下Play建造一个向日葵
然后我们可以看到僵尸将如何尝试攻击到它。
如果我们建了一个寒冰射手,那么我们就可以看到它是如何在几枪之后杀死僵尸的!

我们还将通过复制几个僵尸,他们将僵尸定位在水平的右边,这样他们在步行几秒钟后才能到达:
在这里插入图片描述
如果我们按下Play然后我们就可以看到游戏的行动:
在这里插入图片描述

21.摘要

我们刚刚创造了一个干净而简单的植物大战僵尸游戏。
该教程相当长,但仍然有许多功能和改进可以添加到游戏。
和往常一样,现在是你让游戏变得有趣的时候了!

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/q764424567/article/details/100770946

智能推荐

自研多模态追踪算法 PICO 为「手柄小型化」找到新思路-程序员宅基地

文章浏览阅读6.1k次。作者:张韬、林泽一 、闻超 、赵洋研发背景作为头戴的追踪配件,VR手柄可以通过HMD(头戴显示设备)的inside-out光学追踪定位原理,计算出手柄的空间运动轨迹,同时结合6轴传感器实现6DoF空间定位。与此同时,结合手柄控制器的物理按键、马达反馈、摇杆等,用户还能获得逼真、细腻的触觉反馈,进一步增强虚拟现实人机交互的能力以及沉浸感,这也是目前无手柄方案所难以实现的。目前主流VR手柄的追踪技术方..._pico手柄定位原理

C语言程序设计A课程试卷 4,C语言程序设计A课程教学模拟卷2及答案-程序员宅基地

文章浏览阅读349次。scanf(\); }假定结构类型struct Worker 的定义如下:struct Worker { char name[15]; int age; float pay;};函数功能:五、按题目要求编写函数(每小题6分,共12分)1. 根据函数原型“double Mean(double a[M][N],int m,int n)”,编写函数定义,要求返回二维数组a[m][n]中所有..._三、写出下列每个程序运行后的输出结果(每小题6分,共30分)试题 24还未作答满

词形变换和词干提取工具(英文)_treetagger word pos-程序员宅基地

文章浏览阅读3.5k次。转载自: http://www.cnblogs.com/kaituorensheng/p/3437807.html词形变换和词干提取工具(英文)在信息检索和文本挖掘中,需要对一个词的不同形态进行归并,即词形规范化,从而提高文本处理的效率。例如:词根run有不同的形式running、ran另外runner也和run有关。这里涉及到两个概念:词形变化:把一个_treetagger word pos

Python程序中结束while循环的两种方法是条件不成立和分支结构-程序员宅基地

文章浏览阅读2k次。Python的分支(条件)语句及循环语句顺序结构的程序能解决计算、输出等问题,但不能做判断再选择,对于要先做判断再选择的问题就要使用分支结构,分支结构的执行是计算机依据一定的条件选择执行路径。1、单分支语句if 判断条件:语句块执行过程:首先执行判断条件,当判断条件成立【结果为真的时候】会执行语句块,若条件不成立则不执行。2、双分支语句if 判断条件:语句块1else:语句块2执行过程:首先执行判..._python中结束while循环的两种方法

[最全]安卓axml图解,一张图搞懂安卓二进制xml,(AndroidManifest,Layout,Drawable,Anim,raw,xml,etc...)_axmldec-程序员宅基地

文章浏览阅读2.9k次,点赞6次,收藏6次。APK是什么APK(全称:Android application package,Android应用程序包)是Android操作系统使用的一种应用程序包文件格式,用于分发和安装移动应用及中间件。APK 文件基于 ZIP 文件格式,它与JAR文件的构造方式相似,互联网媒体类型是:application/vnd.android.package-archive。以上是百度百科的解释apk是安卓的应用程序,它的本质就是一个zip文件,只是它可以被安卓系统识别、解释和运行。下图是apk内部的目录结构今天_axmldec

边界值分析法-程序员宅基地

文章浏览阅读5.7k次,点赞4次,收藏20次。1.定义 边界值分析法就是对输入或输出的边界值进行测试的一种黑盒测试方法。通常边界值分析法是作为对等价类划分法的补充,这种情况下,测试用例来自等价类的边界。 在测试技术中,边界值分析法与同等价类划分法有这同意重要的地位,测试工作中频繁使用的程度与等价类划分法基本一致,每使用一次等价类划分法都应该对应使用边界值分析法,对着两个方法结合的深入理解,以及灵活使用也是软件测试工作的基础。2.设计思想 根据大量的测试统计数据,很多错误是发生在输入或输出范围的边界上,而不是发生在输入/输出范围的中间区域。因此针对各_边界值分析

随便推点

【一维信号分类】Matlab一维信号CNN-LSTM二分类_1d cnn lstm-程序员宅基地

文章浏览阅读612次。考虑到Matlab搭建1D CNN-LSTM模型的教程较少,此程序是为了方便学习怎么搭建网络、测试等等,使用的数据量较少,并且数据本身也易于分类,换成自己的数据时需要根据实际情况调整网络,也可有偿帮忙替换数据。1、加载数据,一共为200个正常样本和200个异常样本,训练集为80%,即160正常和160异常,一共320条数据;Matlab一维信号CNN-LSTM分类,使用1D CNN-LSTM对一维信号(如语音信号、心电图信号)进行二分类源程序。唯一渠道为咸鱼售卖。4、测试,输出准确率,并绘制混淆矩阵。_1d cnn lstm

蒲峰投针实验-程序员宅基地

文章浏览阅读398次。  今天突然遇到了这个问题,就总结一下:设我们有一个以平行且等距木纹铺成的地板(如图),现在随意抛一支长度比木纹之间距离小的针,求针和其中一条木纹相交的概率。这就是蒲丰投针问题(又译“布丰投针问题”)。  逻辑推导的优雅证明:找一根铁丝完成一根圆圈,使其直径恰恰等于平行线间的距离d,可以想象的到,对于这样的圆圈来说,无论怎么扔下,都将和平行线有两个交点。因此,如果投下的次数为n,那么相交的次数..._蒲松投针实验

windows下eclipse远程连接linux上的hadoop集群_liunx虚拟机上的hadoop集群连接windows上eclipse-程序员宅基地

文章浏览阅读3.7k次。1、在win7下装一个eclipse(用juno-32位版吧)2、把插件hadoop-eclipse-plugin-1.2.1.jar放到eclipse的plugins下。3、把hadoop-1.2.1解压到win7系统的任意一个位置。4、打开eclipse,配置mapreduce的路径,这个路径就是hadoop-1.2.1解压后放的路径。5、然后打开视图_liunx虚拟机上的hadoop集群连接windows上eclipse

mysql8.0总是密码错误_解决MySQL8.0安装第一次登陆修改密码时出现的问题-程序员宅基地

文章浏览阅读3.2k次,点赞2次,收藏5次。下面给大家介绍下mysql 8.0.16 初次登录修改密码mysql数据库初始化后初次登录需要修改密码初次登录会碰到下面这个错误ql> alter user root identified by ‘password';ERROR 1820 (HY000): You must reset your password using ALTER USER statement before execu..._mysql8初始密码登录报错

java class查看器_java class文件查看工具-程序员宅基地

文章浏览阅读2.9k次,点赞2次,收藏4次。非常好用,编译效果很好。如果你需要直接查看编译过的JAVA代码但又没源代码,用它就没错了,解压可用,强大的反编译功能,可以把class文件编译成java文件,而且支持层级关系,在打开子类的情况下,直接点击父类名称,即可进入父类文件 轻巧方便~是我使用过最好的反编译软件~~程序运行的容器只能识别class文件,而我们在编写的代码的时候实际上是java文件,那么就需要这个java文件编译成class文..._java class文件查看器

java 自定义异常拦截器_spring-boot自定义异常拦截器-程序员宅基地

文章浏览阅读428次。@ControllerAdvice注解增强类在spring 3.2中,新增了@ControllerAdvice注解,可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,并应用到所有@RequestMapping中一、介绍创建MyControllerAdvice,并添加 @ControllerAdvice注解。package com.lx.cont..._java 自定义异常拦截器

推荐文章

热门文章

相关标签