【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

智能推荐

oracle 12c 集群安装后的检查_12c查看crs状态-程序员宅基地

文章浏览阅读1.6k次。安装配置gi、安装数据库软件、dbca建库见下:http://blog.csdn.net/kadwf123/article/details/784299611、检查集群节点及状态:[root@rac2 ~]# olsnodes -srac1 Activerac2 Activerac3 Activerac4 Active[root@rac2 ~]_12c查看crs状态

解决jupyter notebook无法找到虚拟环境的问题_jupyter没有pytorch环境-程序员宅基地

文章浏览阅读1.3w次,点赞45次,收藏99次。我个人用的是anaconda3的一个python集成环境,自带jupyter notebook,但在我打开jupyter notebook界面后,却找不到对应的虚拟环境,原来是jupyter notebook只是通用于下载anaconda时自带的环境,其他环境要想使用必须手动下载一些库:1.首先进入到自己创建的虚拟环境(pytorch是虚拟环境的名字)activate pytorch2.在该环境下下载这个库conda install ipykernelconda install nb__jupyter没有pytorch环境

国内安装scoop的保姆教程_scoop-cn-程序员宅基地

文章浏览阅读5.2k次,点赞19次,收藏28次。选择scoop纯属意外,也是无奈,因为电脑用户被锁了管理员权限,所有exe安装程序都无法安装,只可以用绿色软件,最后被我发现scoop,省去了到处下载XXX绿色版的烦恼,当然scoop里需要管理员权限的软件也跟我无缘了(譬如everything)。推荐添加dorado这个bucket镜像,里面很多中文软件,但是部分国外的软件下载地址在github,可能无法下载。以上两个是官方bucket的国内镜像,所有软件建议优先从这里下载。上面可以看到很多bucket以及软件数。如果官网登陆不了可以试一下以下方式。_scoop-cn

Element ui colorpicker在Vue中的使用_vue el-color-picker-程序员宅基地

文章浏览阅读4.5k次,点赞2次,收藏3次。首先要有一个color-picker组件 <el-color-picker v-model="headcolor"></el-color-picker>在data里面data() { return {headcolor: ’ #278add ’ //这里可以选择一个默认的颜色} }然后在你想要改变颜色的地方用v-bind绑定就好了,例如:这里的:sty..._vue el-color-picker

迅为iTOP-4412精英版之烧写内核移植后的镜像_exynos 4412 刷机-程序员宅基地

文章浏览阅读640次。基于芯片日益增长的问题,所以内核开发者们引入了新的方法,就是在内核中只保留函数,而数据则不包含,由用户(应用程序员)自己把数据按照规定的格式编写,并放在约定的地方,为了不占用过多的内存,还要求数据以根精简的方式编写。boot启动时,传参给内核,告诉内核设备树文件和kernel的位置,内核启动时根据地址去找到设备树文件,再利用专用的编译器去反编译dtb文件,将dtb还原成数据结构,以供驱动的函数去调用。firmware是三星的一个固件的设备信息,因为找不到固件,所以内核启动不成功。_exynos 4412 刷机

Linux系统配置jdk_linux配置jdk-程序员宅基地

文章浏览阅读2w次,点赞24次,收藏42次。Linux系统配置jdkLinux学习教程,Linux入门教程(超详细)_linux配置jdk

随便推点

matlab(4):特殊符号的输入_matlab微米怎么输入-程序员宅基地

文章浏览阅读3.3k次,点赞5次,收藏19次。xlabel('\delta');ylabel('AUC');具体符号的对照表参照下图:_matlab微米怎么输入

C语言程序设计-文件(打开与关闭、顺序、二进制读写)-程序员宅基地

文章浏览阅读119次。顺序读写指的是按照文件中数据的顺序进行读取或写入。对于文本文件,可以使用fgets、fputs、fscanf、fprintf等函数进行顺序读写。在C语言中,对文件的操作通常涉及文件的打开、读写以及关闭。文件的打开使用fopen函数,而关闭则使用fclose函数。在C语言中,可以使用fread和fwrite函数进行二进制读写。‍ Biaoge 于2024-03-09 23:51发布 阅读量:7 ️文章类型:【 C语言程序设计 】在C语言中,用于打开文件的函数是____,用于关闭文件的函数是____。

Touchdesigner自学笔记之三_touchdesigner怎么让一个模型跟着鼠标移动-程序员宅基地

文章浏览阅读3.4k次,点赞2次,收藏13次。跟随鼠标移动的粒子以grid(SOP)为partical(SOP)的资源模板,调整后连接【Geo组合+point spirit(MAT)】,在连接【feedback组合】适当调整。影响粒子动态的节点【metaball(SOP)+force(SOP)】添加mouse in(CHOP)鼠标位置到metaball的坐标,实现鼠标影响。..._touchdesigner怎么让一个模型跟着鼠标移动

【附源码】基于java的校园停车场管理系统的设计与实现61m0e9计算机毕设SSM_基于java技术的停车场管理系统实现与设计-程序员宅基地

文章浏览阅读178次。项目运行环境配置:Jdk1.8 + Tomcat7.0 + Mysql + HBuilderX(Webstorm也行)+ Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。项目技术:Springboot + mybatis + Maven +mysql5.7或8.0+html+css+js等等组成,B/S模式 + Maven管理等等。环境需要1.运行环境:最好是java jdk 1.8,我们在这个平台上运行的。其他版本理论上也可以。_基于java技术的停车场管理系统实现与设计

Android系统播放器MediaPlayer源码分析_android多媒体播放源码分析 时序图-程序员宅基地

文章浏览阅读3.5k次。前言对于MediaPlayer播放器的源码分析内容相对来说比较多,会从Java-&amp;amp;gt;Jni-&amp;amp;gt;C/C++慢慢分析,后面会慢慢更新。另外,博客只作为自己学习记录的一种方式,对于其他的不过多的评论。MediaPlayerDemopublic class MainActivity extends AppCompatActivity implements SurfaceHolder.Cal..._android多媒体播放源码分析 时序图

java 数据结构与算法 ——快速排序法-程序员宅基地

文章浏览阅读2.4k次,点赞41次,收藏13次。java 数据结构与算法 ——快速排序法_快速排序法