Unity吃豆人项目解析

来源:互联网 发布:罗麦品质365商城淘宝网 编辑:程序博客网 时间:2024/06/07 00:35

Unity吃豆人项目解析

基本来源
https://noobtuts.com/unity/2d-pacman-game
http://forum.china.unity3d.com/thread-13546-1-1.html

前言

游戏效果

这篇教程讲解如何在Unity中制作《吃豆人》游戏。该游戏最初发布于1980年10月,并很快风靡全球直至现在。Unity也用到了其中的形象作为UI:
UI

本文将使用Unity强大的2D功能,用简洁的代码来制做一个仿版《吃豆人》游戏。游戏会尽可能的简单,只关注迷宫、怪物、豆豆以及主角吃豆人。旨在发现实际开发中的问题并解决。

素材
游戏背景
游戏背景
吃豆人
游戏主角
敌人
敌人
只找到这一个。。。。。
豆子
豆子[豆子:我在这里!!!]

game start

新建项目 将主摄像机上的背景设置为黑色
将上述游戏背景图片移动到Unity中并设置:
设置
注意:Pixels Per Unit 值为8表示游戏世界的每个单位会填充8x8像素。我们会将该数值用于所有纹理。选择8是因为两个豆豆之间的距离是8像素,并且我们希望这是游戏中的一个单位。将锚点(Pivot)设为左上是便于后续使用。

物理特性

此时迷宫还只是一张图。不具有物理性质,所以吃豆人也无法在墙间游走。现在就为迷宫的每道墙都添加一个Collider。在检视面板中依次选择Add Component->Physics 2D->Box Collider 2D:我们希望每道墙都有单独的Collider。有两种办法可以实现。第一种是写个算法基于迷宫图片来生成Collider(求大神教我),第二种就是简单的手动添加。
在检视面板中点击Edit Collider 按钮,之后就可以在场景中使用绿色的点来调整Collider:
Collider
对迷宫的每道墙都重复以上步骤,依次选择Add Component->Physics 2D->Box Collider 2D,然后点击Edit Collider调整各Collider的形状直至与墙贴合。这里的诀窍就是设置Collider的大小和中心点时注意值都是1.25、1.5、1.75或2.00这样,而非1.24687或1.25788。
注意:如果后面吃豆人被困在迷宫或无法移动,就是因为迷宫的Collider没有完全设置好。
Collider完成图

添加我们的主角吃豆人并设置:
设置
每个方向的移动都需要一个动画:- 右- 左- 上- 下
将所有动画放在一张图中,一行就是一个方向的动画:
将Sprite Mode设为Multiple很重要,这会告诉Unity其中包含了多个吃豆人精灵。点击按钮打开Sprite Editor:
现在就让Unity对吃豆人精灵进行切片。选择Slice将Grid设为16x16,然后点击下面的Slice按钮:
切割吃豆人
精灵切割完成后关闭Sprite Editor。如果Unity提示Unapplied Import Settings,选择Apply。

现在项目视图中吃豆人精灵下有12个子精灵:
吃豆人精灵

吃豆人动画

现在可以创建4个方向的动画了。再提一下,我们需要的动画如下:
右(切片0,1,2)
左(切片3,4,5)
上(切片6,7,8)
下(切片9,10,11)

先创建向右的动画。在项目视图中选中前三个切片精灵,然后将它们拖拽到场景中。
同时拖拽几个精灵到场景中,Unity就会知道我们想创建动画,并自动询问动画保存的位置。将动画命名为right.anim并保存到新建的Animation目录。Unity会自动在场景中加入游戏对象pacman_0,并在项目视图中新增两个文件:
这里写图片描述
第一个文件是动画状态机,用于设置动画速度和混合树等。第二个的动画文件。
对余下的动画重复以上步骤。
Unity为每个动画都新建了游戏对象,但我们这里只需要一个。选中其余三个并删除。
在项目视图中也一样。动画和状态机各有4个。同样删除其它多余的三个。

吃豆人动画状态机

现在有4个动画文件,但Unity并不知道要播放哪个动画。所以我们需要一个具有4种状态的动画状态机:
•右
•左
•上
•下

还要添加变换让Unity知道何时切换动画。
注:Unity会在处于right状态时重复播放right动画。它依靠Transition来判断何时切换状态。这些都是Unity自动完成的,我们要做的就是在后面的脚本中告知它吃豆人的移动方向。

在项目视图中双击pacman_0动画状态机:
现在可在Animator查看状态机:
状态机中已经包含right状态了,将项目视图中Animation文件夹下的其他三个anim文件拖拽到Animator中新建状态:
动画状态

Mecanim最大的优点是它会完全自动管理动画状态。我们要做的就是告诉Mecanim注意吃豆人的移动方向。Mecanim会自动切换动画状态,不需我们操心。

创建参数

下面就来告诉Mecanim主意吃豆人的移动方向。移动方向参数类型是Vector2,下图展示了几种可能的移动方向:
这里写图片描述

本例中吃豆人只有4个移动方向。也就是说在动画状态机中仅需4种状态变换:

  DirY>0则向上移动  DirY<0则向下移动  DirX>0则向右移动  DirX<0则向左移动

在Animator界面中选择Parameters标签页。点击右边的+添加一个Float类型的参数命名为DirX,再添加一个Float参数命名为DirY:
Parameters参数

创建变换

我们希望Unity基于这些参数自动切换动画状态。变换就是用于实现这点的。例如,新建一个从left到right的变换,条件是DirX > 0。但要考虑到最完美的变换也会存在细微误差,因为浮点数的比较并不太完美,因此我们会使用DirX>0.1来判断。

从其它任何状态切换为right都会用到DirX > 0.1。所以借助Unity的Any State来节省一些工作量。
Any State几乎表示任何状态。所以创建一个从Any State到right的变换就相当于同时创建了left,up和down到right的变换。

右击Any State 选择Make Transition。然后将白色箭头拖拽到right状态:
点击白色箭头后在检视面板中设置变换的条件(即何时切换状态)为DirX > 0.1:
禁用Settings中的Can Transition To Self :
注:这样可以避免出现按下某个方向键时动画会一直重新开始的情况
因此,当吃豆人向右移动时(DirX > 0.1),状态机会切换到right状态播放向右移动的动画。

同样的步骤新建另外3个变换:
Any State到left,条件为DirX < -0.1
Any State 到up,条件为DirY > 0.1
Any State 到down,条件为 DirY < -0.1

现在Animator如下:
Animator
动画状态机就差不多完成了。还要进行最后一个调整,选中所有状态并在检视面板中修改Speed以便动画看起来不会太快:
这里写图片描述
点击Play就可以看到right动画:
play

吃豆人命名及摆放
在层次面板中选中pacman_0并重命名为pacman(右击选择重命名,或按F2)。
将其坐标设为 (14, 14) 这样就不会位于迷宫外了。

吃豆人物理

现在吃豆人也只是一张没有物理特性的图片,所以它不会移动。在吃豆人上新加一个Collider使其具有物理性质,然后周围的物体就可以与它进行碰撞而非直接穿过。

吃豆人可以到处移动。Rigidbody可用于处理重力、速度和其它推动它移动的力。物理世界所有可以移动的物体都需要一个Rigidbody组件。

在层次面板中选中pacman,依次点击Add Component->Physics 2D->Circle Collider 2D添加碰撞器,同样依次点击Add Component->Physics 2D->Rigidbody 2D添加Rigidbody。使用下图的设置:
设置信息
注意:Gravity Scale设置为0,否则主角位置会受力的改变而自动改变。(产生BUG,运行时吃豆人位置自行改变)
现在吃豆人可以与周围的物体进行碰撞了。这也会导致所有绑定到pacman的脚本中的OnCollisionEnter2D函数被调用。

Movement脚本

现在有几种让吃豆人移动的方法。最简单的就是创建脚本,并检测键盘方向键操作,然后按照对应的方向移动吃豆人。但这种操作不好。

我们来试试复杂点的办法。玩家按下某个方向键时,吃豆人都在相应方向移动一个完整单位。豆豆也会按照一个单位的间隔距离来摆放,所以吃豆人每次需要移动一个完整单位。

在层次面板中选中pacman,依次点击Add Component->New Script新建C#脚本PacmanMove。将脚本放在项目视图中新建的Scripts文件夹下:

我们需要一个公共变量以便后面在检视面板中修改移动速度:
还需要一个方法来判断吃豆人是否可以朝某个方向前进,或者前面是否有墙。例如,如果想知道前面是否有墙,就从吃豆人前方一单位距离处发出射线到吃豆人,看看是否撞到东西。如果撞到的是吃豆人自身则什么都没有,否则就是有墙。

    //判断前面是否有墙    bool validMove(Vector2 dir)    {        //获得自身位置        Vector2 pos = transform.position;        //发射射线从pos+dir到pos        RaycastHit2D hit = Physics2D.Linecast(pos + dir, pos);        return (hit.collider == cor2D);    }

注:这里只是简单的从距离吃豆人一单位的点 (pos + dir)发射到吃豆人自身(pos)。
将目的地保存到变量,然后在FixedUpdate函数中一步步移动,加入输入控制,设置动画参数。

using System.Collections;using System.Collections.Generic;using UnityEngine;public class Move : MonoBehaviour {    //移动速度    public float speed = 0.4f;    Vector2 dest = Vector2.zero;    Collider2D cor2D;    void Start()    {        dest = transform.position;        cor2D = GetComponent<Collider2D>();    }    void FixedUpdate()    {        Vector2 p = Vector2.MoveTowards(transform.position, dest, speed);        GetComponent<Rigidbody2D>().MovePosition(p);        if ((Vector2)transform.position == dest)                                      //BUG   float值有差值 出现差值后不能移动了        {            if (Input.GetKey(KeyCode.UpArrow) && validMove(Vector2.up))                dest = (Vector2)transform.position + Vector2.up;            if (Input.GetKey(KeyCode.RightArrow) && validMove(Vector2.right))                dest = (Vector2)transform.position + Vector2.right;            if (Input.GetKey(KeyCode.DownArrow) && validMove(Vector2.down))                dest = (Vector2)transform.position + Vector2.down;            if (Input.GetKey(KeyCode.LeftArrow) && validMove(Vector2.left))                dest = (Vector2)transform.position + Vector2.left;        }        Vector2 dir = dest - (Vector2)transform.position;                                   //bug 移动一会会旋转        //print("dir.x" + dir.x);        //print("dir.y" + dir.y);        GetComponent<Animator>().SetFloat("X", dir.x);        GetComponent<Animator>().SetFloat("Y", dir.y);    }    //判断前面是否有墙    bool validMove(Vector2 dir)    {        //获得自身位置        Vector2 pos = transform.position;        //发射射线从pos+dir到pos            RaycastHit2D hit = Physics2D.Linecast(pos + dir, pos);        return (hit.collider == cor2D);    }}

注:使用GetComponent获取pacman的Rigidbody组件用于移动(永远不要用transform.position来移动带有Rigidbody的游戏对象)。

到此吃豆人的移动就完成了。保存脚本后点击Play可以看到移动动画也正常了:
移动

保证吃豆人在最上层

在制作豆豆之前,要先保证吃豆人在所有物体上层。这是2D游戏,所以这里没有3D游戏中的Z值。也就是说Unity按照它自己的想法来绘制物体。我们要确保吃豆人绘制值所有物体上层。

有两种办法。改变Sprite Renderer的Sorting Layer 属性,或改变Order in Layer 属性。对于有很多对象的游戏来说使用Sorting Layer 更合适,这里我们只要将Order in Layer 改为1就好:
Order in Layer
注:Unity会按从低到高的顺序来绘制对象。所以将吃豆人的Order in Layer设为1后,Unity会先绘制Order为0的迷宫、豆豆等。这样吃豆人就可以永远在最上层。

豆豆

吃豆人吃的就是豆豆。就像现在的水果和食物。首先导入一张2x2像素的图作为豆豆并设置参数。
豆豆参数
然后将它拖拽到场景。我们希望在吃豆人走过豆豆时收到通知,所以在检视面板中依次点击 Add Component->Physics 2D->Box Collider 2D并选中Is Trigger:
注:启用IsTrigger的Collider只会接收碰撞信息,不与其它物体进行物理碰撞。
不论何时在吃豆人或怪物走过豆豆时Unity都会自动调用OnTriggerEnter2D函数。

依次点击Add Component->New Script新建C#脚本Pacdot,并将其放到Scripts文件夹下:

using System.Collections;using System.Collections.Generic;using UnityEngine;public class Pacdot : MonoBehaviour {    void OnTriggerEnter2D(Collider2D co2)    {        if (co2.name == "pacman")        {            Destroy(gameObject);        }    }}

重复添加豆豆并摆放后结果如下:
豆子位置

怪物
与吃豆人一样,怪物也有四个方向的动画:前后左右。
• 右
• 左
• 上
• 下
导入怪物图片并设置参数:
参数

创建动画

与上面吃豆人一样将Sprite Mode设为Multiple,在Sprite Editor 中以16x16的Grid切割怪物精灵:
切割
点击Apply后关闭Sprite Editor 。与上面吃豆人一样,将切片拖拽到场景中创建动画。

首先将切片0~1拖拽到场景创建向右移动到动画 right.anim,保存在BlinkyAnimation文件夹下。

重复以上步骤:
2~3是left
4~5是up
6~7是down

之后清除多余的内容

与吃豆人一样设置动画状态机
还是与之前吃豆人一样的步骤和设置。拖拽动画、创建参数并设置变换:
从Any State到right的变换,条件是DirX > 0.1
从Any State到left的变换,条件是DirX < -0.1
从Any State到up的变换,条件是DirY > 0.1
从Any State到down的变换,条件是DirY < -0.1

最终的动画状态机如下:
这里写图片描述

在检视面板中设置blinky坐标让其显示在迷宫中间:

怪物物理

怪物也需具有物理特性。同样在检视面板中依次点击Add Component->Physics 2D->Circle Collider 2D并设置以下属性:
这里写图片描述
注:启用Is Trigger是因为怪物可以穿过物体。
怪物也可在迷宫中移动,所以要添加Rigidbody。依次点击Add Component->Physics 2D->Rigidbody 2D:
这里写图片描述
设置Order in Layer为1
这里写图片描述

怪物移动

这里我们不清楚原作的AI机制,只是其中一些专注于四处移动,而另一些则专注于跟着吃豆人,或者还有些专门负责出现在吃豆人面前。

本教程中我们选用最简单的AI:在迷宫中四处移动。新建一个按路径点移动的脚本,让怪物按照路径移动。听起来很简单,但确实能很好的解决AI问题。路径越复杂,万件就越难躲避怪物。

新建C#脚本GhostMove,放在Scripts文件夹下。然后双击打开它:

using System.Collections;using System.Collections.Generic;using UnityEngine;public class GhostMove : MonoBehaviour {    //添加一个公共的Transform数组,以便在检视面板中设置路径点:    public Transform[] waypoints;    //索引变量来记录怪物当前走向第几个路径点:    int cur = 0;    //移动速度    public float speed = 0.3f;    void FixedUpdate()    {        //判断当前位置是否不等于路经点,不等于就继续移动等于就转向下一个路径点        if (transform.position != waypoints[cur].position)        {            Vector2 p = Vector2.MoveTowards(transform.position, waypoints[cur].position, speed);            GetComponent<Rigidbody2D>().MovePosition(p);        }        else cur = (cur + 1) % waypoints.Length;        Vector2 dir = waypoints[cur].position - transform.position;        GetComponent<Animator>().SetFloat("X", dir.x);        GetComponent<Animator>().SetFloat("Y", dir.y);    }    //碰到吃豆人就销毁它    void OnTriggerEnter2D(Collider2D co2)    {        if(co2.name=="吃豆人")        {            Destroy(co2.gameObject);        }    }}

注:可以通过waypoints[cur]来访问当前路径点。

添加路径点
为怪物添加一些路径点。新建游戏对象,重命名为Blinky_Waypoint0并为其设置Gizmo便于在场景中观察:
路径点
注:Gizmo只是一个可视化的辅助工具,运行时不可见。
将其复制多个,自行选择摆放位置。
因为移动脚本会在到达最后点路径点之后从第一个冲心开始,所以我们要设置循环。

在层次面板中选中blinky,将路径点依次拖拽到GhostMove的Waypoints变量:
这里写图片描述
点击Play可以看到怪物按照路径点移动:
这里写图片描述
也可以设置复杂的路径并设置多个怪物。

最终效果图:
这里写图片描述

原创粉丝点击