【Unity3D开发小游戏】贪吃蛇

博客分类: Unity3D-Game 阅读次数: comments

【Unity3D开发小游戏】贪吃蛇

@[TOC]

一、前言

贪吃蛇游戏是一款经典的益智游戏,有PC和手机等多平台版本。既简单又耐玩。该游戏通过控制蛇头方向吃蛋,从而使得蛇变得越来越长。 那么如何用unity做一个贪吃蛇游戏呢,就跟随作者一起实现以下吧。

效果图 在这里插入图片描述

二、资源下载

UI资源和源代码请搜索QQ群:1040082875下载

三、正文

游戏介绍

这篇文章将讲解怎么使用Unity制作简单的贪吃蛇游戏。贪吃蛇是一种街机游戏,最早的原型诞生于1976年。正如大多数街机游戏一样,它开发简单,且娱乐性强(至少克森的童年时玩它玩过来的)。

Unity版本

在本章教程中,我们将使用Unity5.0.04版本来制作。对于旧的版本也可以正常运行,不过建议大家还是使用Unity5.0以上的版本。

项目设置

让我们开始吧。首先先创建一个项目: 在这里插入图片描述 将该工程命名为“snake”,路径由你们来设置,这里我设置的是C盘根目录下,选择2D开发,然后点击创建项目按钮: 在这里插入图片描述 我选择场景中的Main Camera(主相机),然后再Inspector面板中修改相机的Background为黑色背景,最后调整Size和Position,如下图所示(注意参数要一样,方便后续跟进):

在这里插入图片描述

提示:Size是相机缩放调节的参数

添加边界

我们将使用下面两张图片来制作我们的边框:

提示:图片资源可以搜索QQ群:1040082875下载

我们再一次在Assets下选择这两张图片,如下所示: 在这里插入图片描述 之后,我们可以在Inspector面板中改变他们的导入设置,改变参数如下图所示: 在这里插入图片描述 提示:Pixels Per Unit 是在图片中的一个像素与世界坐标中的一个单位之间的比例尺。贪吃蛇每移动一步将对应游戏世界坐标上的一个单位。这就是为什么我们要把Pixels Per Unit设置为1的原因。

现在,我们可以制作我们的边框了。首先把Assets下的两张图片拖拽到Hierarchy面板下,拖拽两次(你也可以通过复制的方式实现),如下图所示: 在这里插入图片描述 提示:使用border_horizontal来制作顶部和底部的边框,使用border_vertical来制作左边和右边的边框。

让我们为它们重命名一下,方便查找。如下图所示(Top是顶部,Bottom是底部,Left是左边,right是右边): 在这里插入图片描述 现在,它们在游戏中只是一张一张的图片,毫无卵用,现在就让我们为这些图片添加Colliders(碰撞器)组件,让这些图片变为一堵堵墙吧。

首先先在Hierarchy面板中选择那四张图片,如下图所示: 在这里插入图片描述 好,把它们都选中之后,在Inspector面板中找到Add Component按钮,点击它,然后找到Physics2D,最后点击Box Collider2D即可,如下图所示: 在这里插入图片描述 刚刚我们所做的操作,不用写任何一行代码,便能让一张毫无卵用的图片编程了一堵墙,太感谢Unity这个强大的游戏引擎了。

创建食物预制体

我们不想让我们的贪吃蛇饿死,因此,让我们在游戏中随机生成一些食物,提供给蛇食用吧。和上面的操作一样,我们将使用一张图片来制作食品。在我们的教程中,他只是一个像素的色块:

提示:图片资源可以搜索QQ群:1040082875下载

还是老样子,将它的导入设置修改一下,如下图所示: 在这里插入图片描述 好吧,让我们把food拖到场景中,Unity会自动的帮我们在Hierarchy面板中创建一个相对应的游戏物体,如下所示(你的位置也许跟下图不一样,这根据你拖拽的位置而定): 在这里插入图片描述 每当贪吃蛇碰到food(也就是食物)的时候,应当获得一些相应的信息。因此我们也要给

food(食物)添加Collider(碰撞器)组件。

一个游戏物体没有Collider(碰撞器)组件,那么它只是一个可视化物体(就是没有交互功能的物体),它不是物理世界的一部分。一旦我们为游戏物体添加了Collider(碰撞器)组件,它如一堵墙,任何物体都不能穿透它,且能通过碰撞检测事件来进行交互,如:OnCollisionEnter2D、OnCollisionStay2D等等。假如我们勾选了Is Trigger,它便如水一般,可以穿透它,且能通过触发检测事件来进行交互,如:OnTriggerEnter2D、OnTriggerStay等等。

当贪吃蛇穿过food(食物)时,贪吃蛇应该得到一些通知(就是所谓的响应事件)。然而食物不能像墙一样不能穿过它,因此我们现在要做的就是为food(食物)添加Collider(碰撞器),并且勾上 Is Trigger: 在这里插入图片描述 好了,现在我们不想让food(食物)在游戏一开始就出现。因此我们把它做成一个预制体,以便我们使用Instantiate函数来生成它,每当我们需要它的时候。现在,让我们把food(食物)重命名为“FoodPrefab”,然后把它拖到Assets文件夹下: 在这里插入图片描述 现在我们可以删除Hierarchy面板中的FoodPrefad了,因为我们暂时不需要它了。

生成食物

让我们在游戏开始之后,间隔几秒就在随机的地方生成一个food(食物)。那么,就让我们创建一个脚本来控制食物的生成吧。我们将把脚本放置在Main Camera下(因为Main Camera始终在游戏场景中)。首先,在Hierarchy中选择Main Camera,然后再Inspector面板中招到Add Component按钮,点击New Script,在Name的输入框中输入SpawnFood,脚本类型选择C Sharp,如下所示:

在这里插入图片描述 然后打开该脚本(双击即可):

using UnityEngine;
using System.Collections;

public class SpawnFood : MonoBehaviour {

// Use this for initialization
    void Start () {

}

// Update is called once per frame
    void Update () {

}
}

我们不需要Update()函数,把它删除了:

using UnityEngine;
using System.Collections;

public class SpawnFood : MonoBehaviour {

// Use this for initialization
    void Start () {

}
}

这个脚本需要获取食物预制体,因此,我们将添加一个类型为GameObject类型的公开变量:

using UnityEngine;
using System.Collections;

public class SpawnFood : MonoBehaviour {
    // Food Prefab
    public GameObject foodPrefab;

// Use this for initialization
    void Start () {

}
}

食物应该是在边界内生成的。因此,我们也需要在我们的脚本中通过一些变量获得边框位置的相应信息,如下所示:

using UnityEngine;
using System.Collections;

public class SpawnFood : MonoBehaviour {
    // Food Prefab
    public GameObject foodPrefab;

// Borders
    public Transform borderTop;
    public Transform borderBottom;
    public Transform borderLeft;
    public Transform borderRight;

// Use this for initialization
    void Start () {

}
}

提示:我们已经将他们声明为Transform类型了,因此我们如borderTop.transform.posion这样调用position了,直接borderTop.position即可。

让我们创建Spawn()函数在边界内生成food(食物)。首先我们通过x变量来获取左边界和右边界之间的随机位置信息,然后通过y变量来获取上边界和下边界之间的随机位置信息。然后我们便在该位置生成food(食物):

// Spawn one piece of food
void Spawn() {
    // x position between left & right border
    int x = (int)Random.Range(borderLeft.position.x,
                              borderRight.position.x);

// y position between top & bottom border
    int y = (int)Random.Range(borderBottom.position.y,
                              borderTop.position.y);

// Instantiate the food at (x, y)
    Instantiate(foodPrefab,
                new Vector2(x, y),
                Quaternion.identity); // default rotation
}

提示:x 和 y通过使用(int)强制转换来确保该food(食物)生成的位置是整数,如(1,2),而不是带有小数点的形式,如(1.234, 2.74565)。

现在,让我们的脚本在每几秒后调用Spawn()函数,我们可以通过使用 InvokeRepeating() 函数来做:

// Use this for initialization
void Start () {
    // Spawn food every 4 seconds, starting in 3
    InvokeRepeating("Spawn", 3, 4);
}

生成食物

InvokeRepeating()函数用于在每几秒内重复调用某个函数。第一个参数是函数的名字,第二个参数是第一次调用的时间,第三个参数是间隔调用的时间。在上面的代码中,在游戏开始后3秒调用Spawn函数,然后每4秒再重复调用Spawn函数。

下面是SpawnFood函数的完整代码:

using UnityEngine;
using System.Collections;

public class SpawnFood : MonoBehaviour {
    // Food Prefab
    public GameObject foodPrefab;

// Borders
    public Transform borderTop;
    public Transform borderBottom;
    public Transform borderLeft;
    public Transform borderRight;

// Use this for initialization
    void Start () {
        // Spawn food every 4 seconds, starting in 3
        InvokeRepeating("Spawn", 3, 4);
    }

// Spawn one piece of food
    void Spawn() {
        // x position between left & right border
        int x = (int)Random.Range(borderLeft.position.x,
                                  borderRight.position.x);

// y position between top & bottom border
        int y = (int)Random.Range(borderBottom.position.y,
                                  borderTop.position.y);

// Instantiate the food at (x, y)
        Instantiate(foodPrefab,
                    new Vector2(x, y),
                    Quaternion.identity); // default rotation
    }
}

现在让我们保存脚本,然后回到Inspector面板中,你将会发现SpawnFood脚本下多了几个卡槽,现在我们要做的就是找到对应的预制体,将其拖进卡槽中,如下图所示: 在这里插入图片描述 好吧,现在让我们点击Play按钮,然后等待几秒钟,我们将能看到游戏场景中有了一些小点点(食物): 在这里插入图片描述

创建贪吃蛇

接下来让我们来完成游戏中最重要的部分:贪吃蛇。和上面一样,将图片保存到Assets文件夹下,然后修改导入设置:

提示:图片资源可以搜索QQ群:1040082875下载

snake的导入设置如下(参数要一致): 在这里插入图片描述 现在,我们可以拖snake图片到场景中,你将会看到如下图所示:

在这里插入图片描述 到目前为止,我们做好了蛇头。那么,让我们把它命名为“Head”方便查找: 在这里插入图片描述 贪吃蛇应该是物理世界的一部分,因此让我们为它添加Collider(碰撞器)组件,具体操作不用说了吧(Add -> Physics 2D -> Box Collider 2D): 在这里插入图片描述 提示:将碰撞器的Size大小调整为(0.7, 0.7),以便它不与边界发生直接的碰撞,让蛇能在边框上爬行。如果把它调整为(1,1),当蛇到边界的时候便直接Game Over。所以,你懂的。

现在,我们要让蛇动起来,在物理世界中要想移动,那就得添加Rididbody(刚体)组件,刚体组件负责管理例如Gravity(重力)、Velocity(速度)和forces(力)。我们可以通过Add Component -> Physics 2D -> Rigidbody 2D来为其添加刚体。然后为蛇设置如下参数: 在这里插入图片描述

  1. 我们把Gravity Scale(重力大小)设置为 0,因为我们不想让蛇往屏幕下面掉。
  2. 将Is Kinematic勾选上,用于禁用刚体的物理行为。我们只需要知道贪吃蛇与谁发生碰撞,不需要Unity的物理系统能帮我们做任何事情。(因为要想发生碰撞检测事件,其中一个游戏物体上必须要有刚体组件)

最终蛇将会包括很多个小元素,总会有一个头,和几个小元素组成的尾部,如下所示:

组成蛇身

在尾部元素和头部之间,唯一不同的是,稍后我们将添加一个脚本。

现在,让我们把Hierarchy中的Head拖到Assets文件夹下做成一个预制体,并且命名为“TailPrefab”。用于当蛇吃到food(食物)时加载该预制体添加到Head的尾部:

在这里插入图片描述 提示:当我们制作好预制体后,确保一下Hierarchy面板中的Head是否被改名字了,如果被Unity自动修改了名字,那就将它改回“Head”。

好了,让我们在Hierarchy面板中选择Head,,然后在Inspecotr中找到Add Component按钮,为其添加Snake脚本(Add Component -> New Script): 在这里插入图片描述 双击打开脚本:

using UnityEngine;
using System.Collections;

public class Snake : MonoBehaviour {

// Use this for initialization
    void Start () {

}

// Update is called once per frame
    void Update () {

}
}

让我们使用using导入一些命名空间,后面我们需要:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

public class Snake : MonoBehaviour {

// Use this for initialization
    void Start () {

}

// Update is called once per frame
    void Update () {

}
}

现在,让我们为该脚本添加一个Move()函数,用于蛇的移动,然后我们在Upadate()中调用,因为直接在Upadate()中调用Move()函数的话,这将会导致蛇移动得非常的快,因此我们便用到了InvokeRepeating()函数来调用Move()函数,这个函数之前我们已经介绍过了,在这里,我们让Move每300毫秒调用一次:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

public class Snake : MonoBehaviour {

// Use this for initialization
    void Start () {
        // Move the Snake every 300ms
        InvokeRepeating("Move", 0.3f, 0.3f);    
    }

// Update is called once per frame
    void Update () {

}

void Move() {
        // Do Movement Stuff..
    }
}

蛇应该会朝一些方向移动,因此,让我们在Move()函数中定义一个方向变量来控制蛇的移动方向:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Linq;

public class Snake : MonoBehaviour {
    // Current Movement Direction
    // (by default it moves to the right)
    Vector2 dir = Vector2.right;

// Use this for initialization
    void Start () {
        // Move the Snake every 300ms
        InvokeRepeating("Move", 0.3f, 0.3f);    
    }

// Update is called once per frame
    void Update () {

}

void Move() {
        // Move head into new direction
        transform.Translate(dir);
    }
}

提示:transform.Translate 意味着根据一个向量来位移,上面的向量是Vector2.right。以为着朝右边(X轴的正轴)移动一个单位。

如果我们现在点击Play按钮,将会看到如下图所示变化: 在这里插入图片描述 然而,用户应该可以通过按下某一个方向键来控制蛇朝某一个方向移动,因此我们依据上面的方法来创建用户按下方向键响应事件:

// Update is called once per Frame
void Update() {
    // Move in a new Direction?
    if (Input.GetKey(KeyCode.RightArrow))
        dir = Vector2.right;
    else if (Input.GetKey(KeyCode.DownArrow))
        dir = -Vector2.up;    // '-up' means 'down'
    else if (Input.GetKey(KeyCode.LeftArrow))
        dir = -Vector2.right; // '-right' means 'left'
    else if (Input.GetKey(KeyCode.UpArrow))
        dir = Vector2.up;
}

现在点击Play按钮进行测试(记得按下方向键蛤): 在这里插入图片描述

蛇的尾部

现在让我们思考一下蛇的尾部该如何操作。首先,让我们假设我们有一个蛇,它有由一个Head(头)和3个尾部组成:

ooox 现在,一旦Head(头)移动,它的尾部元素也将移动,它尾部是这样工作的,当前尾部向前移动一位,它的后一个元素就移动到它原来的位置,如下面分析所示:

step 1: ooox // snake didn’t move yet step 2: ooo x // head moved to the right step 3: oo ox // first tail follows step 4: o oox // second tail follows step 5: ooox // third tail follows 但是,这样工作的话,会让代码变得非常的复炸,让我们使用一个小小的技巧,让我们的工作变得更简单吧。我们让尾部作为一个整体,只要Head(头)移动一位,尾部作为一个整体也移动一个单位,这样便变得简单多了,如下面分析所示:

step 1: ooox // snake didn’t move yet step 2: ooo x // head moved to the right step 3: ooox // last tail element moved into the gap 现在看上去像是一个简单的算法。在每一次移动时调用。

首先,我们将需要一些数据结构,用于追踪(存放)所有尾部元素,如下所示:

// Keep Track of Tail
List<Transform> tail = new List<Transform>();

注意:List泛型类位于System.Collections.Generic命名空间中,因此之前我们使用using System.Collections.Generic是非常重要的。

让我们添加一个变量用于存储蛇头的移动前的位置,然后让尾部最后一个元素移动到蛇头移动前的位置,然后移除尾部最后一个元素在列表中的原来的位置(妈蛋,绕,不知道大伙们能否明白),然后:

void Move() {
    // Save current position (gap will be here)
    Vector2 v = transform.position;

// Move head into new direction (now there is a gap)
    transform.Translate(dir);

// Do we have a Tail?
    if (tail.Count > 0) {
        // Move last Tail Element to where the Head was
        tail.Last().position = v;

// Add to front of list, remove from the back
        tail.Insert(0, tail.Last());
        tail.RemoveAt(tail.Count-1);
    }
}

这是我们贪吃蛇教程中最复杂的部分,不过我们差不多完成了。

喂蛇

我们将使用O你TriggerEnter2D()函数来接收碰撞信息(它将用于当蛇撞到墙和穿过食物的时候调用)。

每当蛇触碰到食物的时候,我们将使用相同的操作(上面的思路),在间隙的地方实例化一个新的尾部元素,思路如下图所示:

ooo x // gap oooox // gap filled with new element 去理解它是非常重要的,我们不让蛇在吃到食物后立马再吃到食物,就像我们的方向键检测按下一样,我们将等待它把当前动作完成之后。因此,我们需要一个新的变量,当蛇吃到食物后让它变为true:

// Did the snake eat something?
bool ate = false;

我们也需要一个公用的变量去存放尾部元素物体,用于当蛇碰到食物时再尾部生成该物体:

// Did the snake eat something?
bool ate = false;

// Tail Prefab
public GameObject tailPrefab;

注意:这两个变量是在我们的Snake脚本中定义的。

现在,让我们编写O你TriggerEnter2D()函数的代码。具体用于当蛇碰到食物的时候,让ate变量的值变为true,然后删除触碰到的food物体。如果碰到的物体时边界的话,我们也让它做一些事情(当然目前还没有让它做什么事情):

void OnTriggerEnter2D(Collider2D coll) {
    // Food?
    if (coll.name.StartsWith("FoodPrefab")) {
        // Get longer in next Move call
        ate = true;

// Remove the Food
        Destroy(coll.gameObject);
    }
    // Collided with Tail or Border
    else {
        // ToDo 'You lose' screen
    }
}

注意:我们使用coll.name.StartsWith()函数,因为我们要检测该物体是食物还是边界。当然,更好的方式是使用tag去判断,但是为了简单起见,我们就直接比较字符串即可。

好了,让我们修改我们的Move()函数,判断蛇是否吃到了食物(也就是ate是否为true),然后在我们的尾部生成一个尾部元素,最后让ate变为原来的false值:

void Move() {
    // Save current position (gap will be here)
    Vector2 v = transform.position;

// Move head into new direction (now there is a gap)
    transform.Translate(dir);

// Ate something? Then insert new Element into gap
    if (ate) {
        // Load Prefab into the world
        GameObject g =(GameObject)Instantiate(tailPrefab,
                                          v,
                                              Quaternion.identity);

// Keep track of it in our tail list
        tail.Insert(0, g.transform);

// Reset the flag
        ate = false;
    }
    // Do we have a Tail?
    else if (tail.Count > 0) {
        // Move last Tail Element to where the Head was
        tail.Last().position = v;

// Add to front of list, remove from the back
        tail.Insert(0, tail.Last());
        tail.RemoveAt(tail.Count-1);
    }
}

注意:Instantiate()函数用于在Unity中生成游戏物体,第一个参数是要生成的物体,第二个参数是该物体生成的位置,第三个参数是该物体生成时的Rotation值。

现在让我们在Hierarchy面板中找到Head,然后把Assets下名为“TailPrefab”的预制体拖拽到Tail Prefab变量的卡槽中去: 在这里插入图片描述 现在点击Play按钮畅玩吧~!! 在这里插入图片描述

总结

贪吃蛇是一个神奇的游戏 通过该游戏的简单制作,我们学到了好多东西 比如InvokeRepeating()函数、OnTriggerEnter2D()函数的使用、碰撞器、简单的2D物理系统等等。