yukamu 发表于 2022-7-26 15:52

初学者用Unity重制魂斗罗

一个月前,突然想进军游戏业,于是下载了大名鼎鼎的Unity,打开教程开始了我辉煌的游戏业一周游。大概2天后,我游戏业大佬的生涯结束了...
不过来都来了,总要带走点儿什么吧,至少要做一款游戏再去安详晚年。于是我选择重制魂斗罗!为什么选择魂斗罗呢?一方面是作为早期动作射击游戏的鼻祖大家都比较熟悉,另一方面魂斗罗可谓是麻雀虽小五脏俱全,从游戏性到细节都能都展现很多Unity的功能和使用技巧。也算让童年的回忆更加丰富了。
很多其他的教程都倾向与尽量少写代码来实现游戏,但我认为代码很难回避,大可不必谈代码色变。在接下来的内容中我会尽量讲解每一行代码。
跟随本教程会完成魂斗罗游戏的前两关,这些内容会大致覆盖完整游戏制作的80%的工作内容。
# 开发环境的搭建

Unity的开发环境比较简单,统共需要两个东西

[*]Unity Editor
[*]C#语言开发IDE,可在下面中任意选择一款


[*]Visual Studio (初学者推荐,简单易用无需配置)
[*]Visual Studio Code (开发人员推荐,简洁高性能)
## 下载Unity Hub

"Unity Hub"是开发环境的安装工具,可以支持不同版本的UnityEditor、Unity项目、以及Unity文档的安装和管理。

下载地址 Unity官网 https://unity.com/cn/download#how-get-started


## 选择合适的Unity Editor

安装并打开Unity Hub后,在左侧菜单选择“安装”


点击右侧中间的“安装编辑器”


这里需要注意,我们最好使用正式发布的最新LTS版本。
由于在某一个版本的Editor生成的项目,用另一个版本的Editor打开的时候,Editor需要重构项目,有时会产生一些错误需要手动调整。
所以我们最好使用一个稳定的版本,尽量避免切换不同版本的Editor。 LTS是长期维护版本的意思,应该首选。在本教程中我安装的是"2021.3.6f1c1"这个版本。 选择版本后会出现下面这个图。


红框是建议选择的内容。
对于初学者,强烈建议使用Windows平台安装Visual Studio作为开发IDE。
如果使用Mac平台Visual Studio想要正常使用需要安装Mono才能运行。所以建议使用VSCode作为开发IDE。等待安装完成。
## 创建项目

在左侧菜单选择“项目”


按照下图的步骤创建一个2D项目




在创建项目前,需要注册一个账号,并登陆。
项目模板选择“2D”。
如果步骤5,没有显示。可以继续“创建项目”,在下一步会需要安装“PlasticSCM”。不安装好像不能继续,安装后我们可以不用。## 打开项目

创建完项目后,继续选择左侧菜单的“项目”。
可以看到新创建的项目,点击项目名称启动Editor。
接下来我们开始正式进入魂斗罗的制作。

# 从第一个游戏画面入手

现在让我们回忆一下童年,从第一个游戏画面开始。


# 游戏分辨率

上面这张图是用"VirtuaNES"模拟器截图。图片大小为“256x240”像素,但截图会比实际游戏屏幕上下各多出8个像素,所以游戏的实际分辨率为"256x224"像素。
在FC中所有画面都是有一个个小的贴图拼接而成的。




   每一个最小的贴图尺寸为"8x8"像素。

   例如游戏中玩家操控的比尔和兰斯就是由11个最小贴图按照下面这个顺序画出来的。


   当然我们不用这么麻烦,对于每一个物品和角色动作扣出一个完整的贴图就可以了。

   这里介绍FC的分辨率主要是为了确定游戏视窗的分辨率。

   现在打开“Unity Editor”,在左侧"场景结构树"窗口中创建一个正方形。




      这个正方形的大小就是1个单位。在Unity中一个大小单位等于多少个像素是由贴图的设置决定的。这点我们在下面介绍贴图和精灵的时候会详细说明。
      这里我们只定义魂斗罗游戏画面中的"16x16"像素大小等于Unity中的一个大小单位。这个定义在后面会经常用到。
# 设定摄像机的大小

   现在我们需要设定摄像机镜头的大小,让它匹配魂斗罗的分辨率。但如果按照"256x224"像素来设置画面会非常小。我们在模拟器上玩的时候通常会将画面放大,可以根据你自己的体验确定放大的比例。我选择放大3倍,实际分辨率是"768x672"像素。

按照下面的操作创建一个分辨率并选择它。





现在可以看到"Scene"窗口中,表示摄像机的白色框框的大小已经变化了。

下面我们要设置摄像头和单位大小的关系,使得在摄像头范围内可以容纳"16x14"个单位。
为什么是“16x14”个单位呢?
因为我们定义"16x16"像素=1个单位,那么“256x224”像素="16x14"单位
上面设置的"768x672"像素的分辨率,只影响摄像头的宽高比和运行时显示的分辨率,而不影响摄像头和单位大小的关系。
这点要记住,避免混乱。"场景结构树"窗口中选择"Main Camera",右侧的"检查器(Ispector)"窗口会显示摄像头的详细配置。


这里需要关注3项内容:
1. 背景颜色,魂斗罗整体背景为黑色,需要把摄像头的背景颜色改为黑色。
2. 正交还是透视。选择透视会有景深效果在Z轴上近大远小,选择正交则没有景深Z轴位置不影响看到的物体大小。这里应选择正交(Orthographic)。
3. 摄像头"Size",这个Size指的是一半高的大小。我们希望能看到的是"16x14"单位,所以一半高就是7,Size设置为7那么整个高就是14。同时宽高比受到"768x672"这个设置的影响,等于“16/14”,所以根据这个比例,宽自动设置为16个单位。# 精灵和瓦片

在第一个游戏画面中,可以看到有很多远景、近景的内容。例如:星空、远山、丛林、岩石、水等等。这些很多都不与游戏角色进行交互,而且他们往往都是通过更小的画面组合、重复构成的 这些都可以通过瓦片(Tile)来进行绘制。

但在介绍瓦片之前需要先了解什么是“精灵(Sprite)”。

精灵是一个小的图片,游戏画面中所有可见的内容都是由精灵构建成的。无论的背景中的山峰还是比尔的身材,乃至射出去的子弹。这些都是由精灵展示出来的。

在2D游戏中精灵的来源是图片文件,一张完整的图片文件可以包含多个精灵,通过编辑器可以把这些精灵分割出来。这张图片就是"贴图(Texture)"。

那么上面的第一个游戏画面图片能不能作为一张"贴图"呢?理论上可以,但实际上不会。一张贴图要尽可能的简洁,不要有大量重复的内容。原则上贴图中的每个精灵有且只有一个。下面这张图就是后面使用的背景贴图。


刚刚我们介绍了三个概念"贴图"、"精灵"、"瓦片",接下来我们要从导入贴图文件开始。
## 创建资源目录

在Editor的"项目(Project)"窗口中,选择"Asset"目录,右键菜单中选择"Create"->"Folder",新文件夹命名为"Resources"。并在创建子文件夹"Sprites"。
注意!新文件夹命名为"Resources"是有特殊意义的。   
在Unity运行时,可以通过"Resources类"动态加载"Resources目录"下的资源。如果命名为其他名称则无法动态加载。 ## 导入"贴图文件"

下载本教程的资源包(资源包链接)。
   解压后"贴图文件"目录中保存了所有的贴图。这里我们选择"魂斗罗素材-第1关-背景.png"这个文件作为样例。
   打开Unity Editor在"项目(Project)"窗口中选中刚才创建的"Asset/Resources/Sprites"目录。然后拖拽贴图文件到"Project"窗口的内容区域。这样就导入了贴图文件。导入后界面如下图。


## 创建精灵

   贴图文件导入后,我们需要将文件中的精灵分割出来以方便后续使用。
   选中贴图文件,在对应的"检查器(Inspector)"中显示如下。


这里需要关注的是红色标记出的4个属性。

[*]Sprite Mode:选择"Multipe"

[*]Single: 整张贴图文件就是一个精灵
[*]Multipe: 贴图文件包含多个精灵,需要进一步切割。
[*]Polygon: 类似Single,但允许自定义精灵的网格轮廓。

[*]PPU(Pixels Pre Unit):
      这个属性表示贴图中多少个像素对应Unity中的1个单位。根据上面的设定我们输入"16"。这个值越大在Unity中精灵显示越小,反之则显示越大。
[*]Filter Mode:
      这个属性用于处理贴图时采用的抗锯齿算法。我们做的是像素游戏,每一个像素都是宝贵的,一定要把抗锯齿关闭,否则画面就糊了。所以选择"Point(no filter)"。
[*]Compression:
      这个选项是对贴图进行压缩,同样为了我们宝贵的像素,一定要关闭它。选择"None"。
   设定上面4个属性后点击下面的"Apply"保存。然后点击中间的"Sprite Editor"打开精灵编辑器。

   这个贴图文件中的内容都是以"16x16"像素为标准处理的。可以用自动分割的方法来生成精灵。


点击工具栏的"Slice"下拉按钮。在下拉窗口中需要关注2个属性。

[*]Type: 采用"Grid By Cell Size"->"16x16"

[*]Automatic: 自动识别精灵,将连续的图片识别为一个精灵,如果有透明背景区隔则识别为另一个精灵。
[*]Grid By Cell Size: 指定精灵像素大小,按照尺寸对贴图进行分割,分割后的区域如果有非透明的像素则识别为一个精灵。
[*]Grid By Cell Size: 指定贴图横向和纵向分割的次数。
[*]Isometric Grid: 等距网格。等距游戏我倒是知道,但这个选项是咋用的还没弄明白,暂时搁置把。

[*]Method: 这个属性决定了,分割时对已有的精灵如何处理,删除还是保留。
分割完成后,点击每一个精灵可以设置精灵的具体属性。这里我们可以不做任何修改,包括名称。因为在使用瓦片的时候我们不关心精灵叫什么名字。
对于大部分用于瓦片的精灵可以不命名,但其中用于动态瓦片的精灵最好还是起个名字,否则下一步就不好找了。
例如:水浪、闪烁的星星、近景丛林的动态效果。如果你玩儿魂斗罗没有看到过这些效果,那是因为美版和日版的区别。在日版中会有背景的动态效果,这也是我们需要复现的。    最后点击工具栏右侧的"Apply",然后关闭"Sprite Editor",就可以了。
   接下来,在"Project"窗口的贴图文件,点击文件右侧的箭头就可以展开其中的精灵。
## 创建瓦片+瓦片调色板

   上一步,我们创建了精灵,精灵是所有可现实内容的基础。现在我们开始精灵的第一个应用"瓦片"。瓦片用于绘制游戏背景,可以想象成每种瓦片就像一种颜色的颜料,我们把颜料挤到调色板上,然后通过画笔将颜色画到画布上。这个过程就是瓦片的使用过程。
   精灵(Sprite)
       ↓
       ↓ (创建)
   瓦片(Tile)
       ↓
       ↓ (放入)
   瓦片调色板(Tile Palette)
       ↓
       ↓ (画笔绘制)
   瓦片地图(Tile Map)1. 贴图(Texture)->瓦片调色板(Tile Palette) 的一条龙服务<span id="一条龙服务"><span>
      选择菜单"Window->2D->Tile Palette"打开"瓦片地图"窗口。然后找一个巴适的地方放好。我放到右边,和"检查器(Inspector)"肩并肩。



      点击"Create New Palette"下拉菜单,输入一个名字"背景",点击"Create"按钮。         这时会弹出文件选择框,让你选择一个目录用以存放瓦片地图的数据。这个文件不需要放到"Resources"目录下面,可以在"Asset"下新建一个"Tiles"目录。          在"Project"窗口中选择并拖拽"贴图文件"到新建的"瓦片调色板(Tile Palette)"中。         这时又一次弹出文件选择窗口,这次是要存放瓦片对象。
注意:瓦片不是精灵,不要放到Sprites目录下,要另选一个目录。
可以选择瓦片地图同样的目录"Tiles"。等待导入完成后,瓦片调色板窗口变为下图的样子。


      可以看到,生成的瓦片格局和贴图文件中一摸一样。但是这些瓦片都是静态的,丛林树叶和水岸波浪的动态效果不会出现。
      所以下面我们要手动创建"动态瓦片"。
2. 动态瓦片(Animated Tile)
      在"Project"窗口中,进入"Tiles"目录。里面都是刚才自动生成的瓦片对象和瓦片地图。自动生成的瓦片对象名称和对应的精灵命名一致。
      在目录内容的空白处点击鼠标右键,选择"Create->2D->Tiles->Animated Tile",命名为`"树叶-动-1"`。

      先说一下贴图中树叶对应的精灵。


      在贴图中树叶精灵由4组,分别对应动态效果中的4种图案。我给精灵起名时采用的命名规则是
`"树叶-动<位置编号>-<动画编号>"`
      例如: "树叶-动1-2"表示,这个是在2号位置,的第1幅动画精灵。
`"树叶-动1-1","树叶-动2-1","树叶-动3-1","树叶-动4-1"`
      这4个精灵构成了1号位置的完整动画,一会儿我们会用这4个精灵构建一个"动态瓦片",瓦片的名字叫做`"树叶-动-1"`

      在"Project"窗口的"Tiles"目录找到并选中刚才创建的动态瓦片`"树叶-动-1"`,打开"检查器(Inspector)"。


      动态瓦片需要指定构成动画的多个精灵,`"Number of Animated"`表示需要多少个精灵构成动画,也可以通过下面的`"+ -"`按钮来增减。
      点击精灵图标右下角的`"Select"`按钮可以选择精灵。这里我们按照顺序选择`"树叶-动1-1" ... "树叶-动4-1"`。一个动态精灵就创建好了。

      下一步,打开"瓦片调色板"窗口,把静态的树叶瓦片删掉(不删也行,动态瓦片放别处)。
      点击"瓦片调色板"窗口内上部的"Edit"按钮,打开对调色板的编辑功能。这种状态下,工具栏对应操作对象就是调色板,否则操作对象就是"瓦片地图(TileMap)"。
      点击"瓦片调色板"窗口内工具栏的"橡皮擦",涂掉树叶瓦片。
注意:涂掉瓦片只是在调色板种删除了瓦片,Tiles目录种的瓦片对象仍然存在,这些瓦片对象我们不会用到,可以删除。

      然后在"Project"窗口的"Tiles"目录找到刚刚创建的动态瓦片`"树叶-动-1"`,拖拽到"瓦片调色板"。
      接下来重复这一过程,完成树叶9个位置的动态瓦片。
      对于水岸的波浪效果和星星的闪烁也采用同样的方法。

      现在,调色板准备好了,开始画背景。
3. 瓦片地图(Tile Map)
      上面几步都是在为绘制背景做准备,这里我们要开始真正绘制地图了。
      在"场景"窗口点击鼠标右键,选择"2D Object->Tilemap->Rectangular"。瓦片地图有3种类型,方形、六边形、等距。对应3种不同类型的游戏,我们选择"方形"。


      在场景的结构树中会默认创建一个`Grid`对象,并在下面创建一个`Tilemap`子对象,把`Tilemap`改为`背景`。
`Grid`对象用来定义网格
`Tilemap`对象用来容纳具体的瓦片。可以允许多个`Tilemap`存在,处理不同层次的背景。      打开"瓦片调色板(Tile Palette)"在工具栏选择第三个"画笔"工具,在下面选择绿色的跳台瓦片。现在我们开始画。   
      跳台瓦片有4个,分别表示跳台不同的部分,依次选择不同的瓦片,然后点击"场景"窗口中的格子,就可以绘制了。   
      这样一个一个的绘制比较麻烦,点击工具栏第4个工具"区域绘制"然后在瓦片调色板中框选4个跳台瓦片,在场景中拖动,可以生成一片连续的背景。
按住`Shift`在场景中点击或拖动可以删除已画好的瓦片。下面参照第一个游戏画面来制作能看到的场景。完成后看上去是这个样子的:


      点击"Play"按钮,跑起来看看。看到水波浪的动态效果了么?

      完整的关卡地图需要一步步画出来,我是参照FC模拟器上的游戏画面一点一点画的。下面是画完后的第一关卡。


如果你细心会发现,有些地方瓦片间会有缝隙。处理这些缝隙可以通过下面几个方法。

[*]菜单"Edit->Project Settings->Quality"
      把不必要的质量Level删掉,下面属性的"(抗锯齿)Anit Aliasing"设置为"Disabled"。
[*]在场景树种选择`Grid`对象,将`Grid`组件的`Cell Gap`的"X Y"属性设置为`-0.001`。这会影响整体的定位,非常不推荐!!
[*] 选中瓦片的"贴图文件",将`PPU(Pixels Per Unit)`属性,设置为`15.99`。这样精灵会变得稍微大一点点,从而遮住缝隙。
# 角色精灵

## 导入贴图

   我们先导入人物精灵的贴图文件。在资源包目录中找到`"贴图文件/魂斗罗素材-通用和第1关精灵.png"`文件,导入到Unity的`Sprites`目录。
这里不要忘记设置贴图文件的属性
`Sprite Mode`=`Mutiple`
`PPU(Pixels Per Unit)`=`16`
`Filter Mode`=`Point(no filter)`
`Compression`=`None`
`Apply`   打开"Sprite Editor",这里在"Slice"中使用"Automatic"方式自动识别一下精灵。然后你会发现有些划分的挺好,有些就不行,需要手工处理一下。这里我们主要关注比尔的人物精灵,其他的以后再说。


## 精灵轴心(Pivot)

   精灵需要附加到一个"游戏对象(GameObject)"上才能真正显示出来,显示的位置依据游戏对象的位置确定。精灵轴心(Pivot)用来标明"游戏对象"和精灵的相对位置。具体而言相对游戏对象坐标(0,0)的位置就是精灵轴心所在的位置。
   我们先分割出Bill的站立精灵,把轴心(Pivot)的`Y值`设置到脚底,`X值`设置到与脸对齐。


## 创建人物游戏对象(GameObject)

   在"Project"窗口找到贴图文件,点击右侧的箭头展开后,将刚刚的精灵拖拽到"场景结构树"中。Unity会自动创建一个"游戏对象(GameObject)",并包含一个"Sprite Render"组件用于渲染精灵。


游戏对象(GameObject)的组件(Component):
`组件`是`游戏对象`中最总要的概念。"游戏对象"是一个载体,它确定在游戏中一个实体与其他实体的差别。这些差别具体体现在不同的"组件"上。"组件"确定了"游戏对象"的位置、显示内容、行为等等。
上图这个例子中,左边"角色-站"就是一个游戏对象,它的"查看器(Inspector)"中有两个组件`Transform`和`Sprite Render`。
`Transform`这个组件是固有组件不可删除,用来游戏对象的位置、旋转和缩放。
`Sprite Render`这个组件用于在组件位置显示精灵。
接下来我们还需要给这个游戏对象添加更多的组件来完成不同的行为。# 向量与坐标系

在解释如何运动之前,先来说说三维向量和Unity使用的坐标系。

[*]向量(Vector)
      一个"游戏对象"在Unity的三维空间中的位置用一个三维向量`Vector3`值表示。这个向量包含`x,y,z`三个值。分别表示在`X轴,Y轴,Z轴`上的坐标。   
      在2D游戏中,通常`Vector3`可以简化为`Vector2`,只包含`x,y`两个值。这可以表示在一个笛卡尔直角坐标系中的位置。
[*]Unity的三维坐标系
      即使在2D游戏中,完全不考虑`Z轴`也是不行的,例如:在旋转的时候,就是以`Z轴`进行旋转。这时候旋转的方向与`Z轴`的方向相关了。
      三维坐标系就是在笛卡尔直角坐标系的基础上,加上一个同时垂直与`X轴`和`Y轴`的`Z轴`。那么问题来了,`Z轴`的方向是什么?或者问`Z轴`在哪个方向上是正?

      在三维坐标系中有两种定义。"左手定则"和"右手定则"。


      按照上面这个图片展示的样子。用左手将拇指和食指摆出90°角,然后用中指摆出与"拇指"和"食指"同时90°角的姿态。然后用`拇指对应X轴正方向`、`食指对应Y轴正方向`这时`中指指向的方向就是Z轴正方向`,这就是"左手定则"。而"右手定则"就是用右手做出上述动作,右手中指的方向就是Z轴正方向。两个手的定则得出的Z轴正方向是相反的。
      Unity的三维坐标系使用的是左手定则。
简单说,当X指向右边,Y指向上边的时候,Z轴的正方向指向屏幕背面离你远去的方向。在Unity的"场景(Scene)"窗口,点击工具栏中的"2D"按钮取消2D模式。在场景画面中按住鼠标右键调整视角。看到右上角的坐标方向同时出现了`X,Y,Z轴`。


   向量Vector包含了一些固有的常量来表示方向:
Vector3.right: X轴正方向,长度为1。
Vector3.left: X轴负方向,长度为1。
Vector3.up: Y轴正方向,长度为1。
Vector3.down: Y轴负方向,长度为1。
Vector3.forward: Z轴正方向,长度为1。
Vector3.back: Z轴负方向,长度为1。

[*]旋转的方向
      选取刚刚建立的游戏对象"角色-站"。调整"检查器"窗口中`Transform->Rotation->Z`的值改为`45`。


      角色在Z轴上逆时针旋转了45°。这说明了,当旋转轴的方向`背向`你时,正向角度是逆时针旋转,负向角度是顺时针旋转,当旋转轴的方向`面向`你时旋转角度相反。由于在2D视角下`Z轴`永远`背向`你,所以对于我们而言对象的旋转都适用于`正逆负顺`的原则。
# 让人物动起来

让游戏对象运动需要通过写代码来实现,通常有几种方法:

[*]设置`transform.position`的值
`transform.position`是一个向量值`(Vector3)`,修改它的`x,y`值。这种方法需要自己写代码来计算对象当前应当所在的位置,在复杂的物理模型下这种计算是很难的。所以不推荐。
[*]`Rigidbody2D.MovePosition()`
      这个方法与上一个类似,不同的是会产生刚体碰撞效果。也不推荐。
[*]`Rigidbody2D.Velocity`
这是一个刚体的属性,表示给刚体设置一个速度(包括方向和大小)。刚体会按照速度的方向和大小移动,同时在移动持续的时候根据受力影响产生速度变化。在我们的游戏中,使用这个属性来移动角色。
   下面演示一下如何操作。下面的内容会开始写代码,我会尽量详细的说明。
## 刚体(Rigidbody)

   提到刚体就让我想起了初中物理中经常遇到的"刚体小球"和"光滑平面",这里你都可以用到。
   刚体(Rigidbody)可以使"游戏对象"应用物理引擎,施加物理效果,和碰撞体(Collider)一起进行碰撞检测。刚体和碰撞体都是区分3D和2D的,在这个游戏中我们使用的均是2D对象。

   首先我们给游戏对象添加刚体组件。选择"角色-站",在"检查器"中点击"Add Component",查找"Rigidbody 2D"组件。


   在组件选择框中查找"Rigidbody"会出现两个,`Rigidbody`和`Rigidbody 2D`。我们做的是2D像素横板射击游戏,所以这里选择`Rigidbody 2D`。

`Rigidbody 2D`有几个重要的属性我们需要了解:

[*]Body Type:

[*]Dynamic: 动态,这种模式下刚体会具有运行、受力、有限质量、物理碰撞等效果。
[*]Kinematic: 运动,这种模式下刚体可以运动、不会受力、质量无限、仅与"Dynamic"模式的其他刚体碰撞。
[*]Static: 静态,这种模式下刚体不能运动、不会受力、仅与"Dynamic"模式的其他刚体碰撞。

[*]Gravity Scale: 重力倍数,只在"Dynamic"模式下有效。重力的设置在菜单"Edit->Project Settings->Physics 2D->Gravity",默认在`Y轴`上是`-9.81`。也就是说`Gravity Scale=1`时,刚体会受到一个向下的大小为`9.81m/s^2`的重力加速度。这个数字将用于计算跳跃的高度和跳跃时施加的力。
[*]Collision Decection:碰撞检测
      由于游戏中的移动时并不是完全连续的,而时根据移动时间来判断当前应该的位置。这样会导致游戏卡顿时画面的跳跃,或这当速度过快时在两帧之间会移动很大距离导致跨越了某个碰撞体,导致的穿模现象。这个属性用来设置是否用额外的计算来避免卡顿时的穿模。

[*] Discrete: 离散方式,不管穿模的问题。
[*]Coutinuous: 连续方式,当移动的方向上有碰撞体则不会在两帧之间跨越穿模,需要额外的计算量。

[*]Constraints: 约束,禁止在某一个或几个轴方向上旋转。在2D刚体的物理模型中,由于受力位置的影响,对象会沿着`Z轴`旋转,我们的角色走着走着就摔倒了。勾选`Freeze Rotation Z`就可以避免。
添加完刚体后我们设置下列属性值:
`Body Type`=`Dynamic`
`Gravity Scale`=`0` : 这里设置为`0`是为了先编写移动代码,而不考虑碰撞和重力。后面应设为`1`。
`Collision Decection`=`Coutinuous`
`Constraints` 勾选 `Freeze Rotation Z` ## 代码组件

   在"Project"窗口新建目录"Asset/Scrpits"。在"Scripts"目录中点击鼠标右键,选择"Create->C# Script",给新文件命名为"Player"这个代码文件用于专门处理玩家角色的行为。然后选择"角色-站"添加组件,找到刚才的"Player"代码文件。这样我们就给游戏对象绑定了一段代码来控制它的行为。
在Unity中代码文件也是一种组件   下面我们要打开代码来修改它,首先我们需要选定编辑代码的工具。
   菜单`"Edit->Preferences->External Tools->External Script Editor"`,选择`"Microsoft Visual Studio"`。然后点击下面的`"Regenerate project files"`(这个按钮用来生成符合Visual Studio的C#工程文件,如果不按则只能打开单独的cs文件,不能相互引用)。
   回到`Scripts`目录,双击`Player`文件,打开"Visual Studio"看到如下界面。


   右上部的"解决方案'Contra'"就是`"Regenerate project files"`按钮产生的,不按就没有。
   下面的内容我预期读者至少有一门编程语言基础知识,能够了解`类、函数、变量、属性`这些基本概念。如果您对这些不了解,也可以尝试看下去,这些概念并不难,随着我的操作即使不懂也可以照葫芦画瓢。

   Player的代码分几个部分:

[*]包引用:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

      这部分表示我们的代码使用了系统和Unity引擎提供的内容。例如:下面要使用的`Vector2`就是在`UnityEngine`中定义的,通过`using UnityEngine;`这个命令可以使用其中的内容。

[*]类定义:
public class Player : MonoBehaviour
      类是一组属性、函数的集合,这里定义的类名称必须与文件名称一致。
实际上一个文件中可以定义多个不同名称的类,但至少要有一个类与文件名一致,否则Unity将这个文件作为组件时,找不到具体执行的类,从而报错。      所有组件类,都必须继承于`MonoBehaviour`类,也就是说必须遵守`MonoBehaviour`类中定义的规范和函数入口,这样Unity才能够在运行时调用它,具体的函数入口下面会介绍。



[*]函数:
void Start()
{
}

void Update()
{
}

函数包括一个具体的代码逻辑。Unity规定的常用函数入口如下:


[*]`Awake`: Unity在运行时创建一个"游戏对象"的同时也会创建其中的每一个组件,当一个代码组件被创建时,Unity引擎会调用`Awake`方法,这个方法只被调用一次。
[*]`Start`: 当脚本开始运行,这个方法被调用一次。`Start`与`Awake`的区别是:
1. `Awake`先被调用,当目前所有代码组件的`Awake`都被执行后,再调用`Start`函数。
2. 如果代码组件被"禁用(disabled)",`Awake`仍会被执行,`Start`不会被执行。 在运行时将代码组件设置为"可用(enabled)"时,`Start`将会被执行,但只会执行一次。
[*]`Update`: Unity在绘制每一帧画面之前都会调用`Update`函数。但由于运行时的性能不同,`Update`函数的调用频率也不同。在卡顿的情况下,这个函数被调用的间隔会增大。还要提一点,Unity获取设备输入(键盘、鼠标等)的时机就是在`Update`函数被执行之前。
[*]`LateUpdate`: 这个函数与`Update`类似,都是每一帧执行一次。区别在于`Update`在处理每帧的"动画(Animation)"之前,`LateUpdate`在处理"动画"之后。在"动画"中对"游戏对象"的修改会在`LateUpdate`中体现。
[*]`FixedUpdate`: 与`Update`不同,Unity会用固定的时间间隔调用这个函数,无论画面卡顿与否,Unity都会保证`FixedUpdate`被执行。具体执行的时间间隔可以通过配置修改。菜单`"Edit->Project Settings->Time->Fixed Timestep"`,默认值是`0.02`表示每`0.02秒`执行一次`FixedUpdate`函数。

## 添加代码让刚体移动

   我们先实现一个简单的移动逻辑,通过"WSAD"控制角色上下左右移动,当然这不符合魂斗罗的控制方式,这里只用来演示一下。
按键和移动
   接受按键的代码要写在`Update`函数中,在上面我们介绍了`Update`函数的作用,其中说到在`Update`被执行前,Unity会获得设备输入的信息。代码如下:
      void Start(){
      //移动速度
      float speed = 3f;

      //取得按键
      float axisX = Input.GetAxis("Horizontal");
      float axisY = Input.GetAxis("Vertical");
      
      //取得刚体组件
      Rigidbody2D rig = GetComponent<Rigidbody2D>();

      //计算刚体速度
      Vector2 velocity = speed * new Vector2(axisX, axisY);

      //设置刚体速度
      rig.velocity = velocity;
      }
写完代码记得用`"ctrl-s"`保存。   现在切换回"Unity Editor",将"角色-站"的"刚体"组件中的`"Gravity Scale"`改为`"0"`。否则它会一直往下掉。
   然后点击"Play"按钮。


   在"游戏(Game)"窗口中通过`"WSAD"`就可以移动角色。
去除滑动效果
   仔细观察移动的效果,在你松开按键后,角色仍然会滑动一小段距离。但我们希望的效果是"令行禁止",一旦松开按键角色立刻停止,按下按键角色立刻移动。
产生滑动的原因
`GetAxis`函数,会根据按键的时间返回一个`[-1,1]`之间的值。当没有按键是值是`0`,当按下按键后返回值会逐渐从`0`变为`1或-1`,通过在代码中加上`Debug`可以观察。float axisX = Input.GetAxis("Horizontal");
float axisY = Input.GetAxis("Vertical");
Debug.Log("Horizontal:" + axisX + "   Vertical:" + axisY);


在运行后Unity的"Console"窗口可以看到,按住`D`键时"Horizontal"的值时逐渐变为`1`的。同样在松开`D`键时,会逐渐变为`0`。这种平滑效果就是滑动的原因。   介绍两个方法消除滑动现象:

[*]将`GetAxis`替换为`GetAxisRaw`函数   
      与`GetAxis`函数不同`GetAxisRaw`函数返回的是按键的值,而不采用平滑效果。
      这个方法很简单,但会有另一个问题。当角色站在平台边缘,我们轻点按键,希望角色做一个小范围的移动,而不掉落平台。如果使用`GetAxisRaw`这个小范围移动的距离会比较长导致掉落。因为`GetAxisRaw`无法识别点击按键的时长,也就无法识别"轻点"这个动作。这需要第二个方法解决。
[*]调试`Axes`的属性
      菜单`"Edit->Project Settings->Input Manager->Axes(展开)->Horizontal(展开)"`
      介绍其中几个重要的属性:


[*]`Gravity`: 当按键松开时,"轴(Axis)值"回到"中性(neutral)值"的速度。这句话翻译自官方文档。简单讲就是松开按键时,返回值落回`0`的速度。
[*]`Sensitivity`: 灵敏度,就是按下按键时返回值从`0`到`1`的速度。
[*] `Snap`: 截断

[*] 如果此项不勾选。那么当按下反向的按键时,返回值会根据`Gravity`属性逐渐从`1`变为`0`,然后根据`Sensitivity`属性从`0`逐渐变为`-1`,有个刹车滑动的效果。
[*] 如果勾选此项,则返回值会**立即**从`1`变为`0`,然后根据`Sensitivity`属性,逐渐从`0`变为`-1`。

[*]`Dead`: 这个属性表示敏感度,在多大的范围内,返回值始终为`0`。主要用于摇杆设备,防止摇杆过于灵敏导致的晃动。
[*] 其他属性也在后续用到的时候再介绍。
      根据上面的属性介绍,我们可以把`Gravity`属性增大,而`Sensitivity`属性不变或适当减小来实现我们预期的效果。这里我让`Gravity=100`,`Sensitivity=3`,注意下面的`Vertical`轴也需要同样修改。
## 重力

   真正的魂斗罗游戏,可以左右自由移动,向上只能通过跳跃,所以我们要给角色加上重力效果。
   将"角色-站"的"刚体"组件中的`"Gravity Scale"`改为`"1"`,点击"Play"。我们可以看到角色一直在缓慢的下落。为什么会**缓慢**的下落呢?
`Update`函数中在每一帧设置了刚体的速度,没有按键的情况下,向下速度设置为`0`。
角色刚体持续受到向下的重力影响。在帧和帧的间隔时间内,受重力影响,速度由`0`变大,但在下一帧时又被设置为`0`。    所以会呈现出缓慢下落的动作。我们需要在代码中取消`Y轴`的速度设定。
   修改代码
      ...
      //计算刚体速度
      Vector2 velocity = new Vector2(speed * axisX, rig.velocity.y);
      ...
   修改后移动只应用到横向,纵向不改变刚体当前的速度。这样纵向就只受到重力影响。运行看看效果。
   我们会看到角色快速向下掉落。下面我们希望绿色的平台能够支撑住角色。
## 平台碰撞体(Collider)


[*]给精灵和瓦片设置物理形状
      瓦片在`Tilemap`中可以有物理形状,在碰撞器中瓦片物理形状就是碰撞器的形状。先在`Tiles`目录中选择一个瓦片。


`Collider Type`属性有三个值:

[*] None: 瓦片没有形状
[*] Sprite: 瓦片形状在"精灵(Sprite)"中定义,如果没有定义则会根据精灵图片将不透明的部分认为是瓦片形状。
[*] Grid: 瓦片形状就是瓦片格子的形状。在我们的游戏中就是"16x16"像素的正方形。
   仔细观察一下游戏的画面


   完整的瓦片高度`16像素`,但角色站立的位置高度是`10像素`。所以我们需要在"Sprite Editor"中手动设置形状。选择瓦片的"贴图文件",打开"Sprite Editor"。


      左上角的下拉选项中选择`"Custom Physics Shape"`,然后选择跳台精灵,按住鼠标左键拉出一个矩形,调整4个角让矩形覆盖刚才计算出的大小。给其他3个跳台精灵做同样操作。然后点击`"Apply"`保存。
      进入`"Tiles"`目录,选择所有的"静态瓦片",在"检查器"窗口批量设置`"Collider Type"`属性为`"None"`,然后再单独选择4个"跳台瓦片",设置`"Collider Type"`为`"Sprite"`。
      这样其他瓦片都不使用形状,只有跳台瓦片有形状。

[*]`Tilemap`中的碰撞机(Collider)
      在"场景结构树"中选择"背景Tilemap",添加组件`Tilemap Collider 2D`。


      在工具栏点击"球一样的按钮",然后在弹出的菜单中选择`Wireframe`,然后在左侧"场景结构树"中选择"背景Tilemap"。就可以看到绿色线条标出的跳台碰撞体。


      这里可以看到跳台碰撞体有多个矩形组成,为了提高计算效率这些矩形可以合成为一个大的矩形。
      在背景"背景Tilemap"中添加新组件"Composite Collider 2D",Unity会同时添加一个"Rigidbody 2D"组件。我们要把这个"Rigidbody 2D"的`"Body Type"`改为`Static`否则整个背景会向下掉落╮(╯▽╰)╭ 。然后在`"Tilemap Collider 2D"`组件中勾选`"Used By Composite"`,这是可以看到在`"Wireframe"`中连续的跳台"碰撞体"形成了一个完成的矩形。



[*]给人物角色添加碰撞机(Collider)
      在"场景结构树"中选择"角色-站",添加组件`"Box Collider 2D"`。
      在`"Box Collider 2D"`组件中点击图标




      拖拽绿色边框四个点,来调整碰撞体的大小。

      点击运行可以看到角色站到了跳台上。

      但是,最后我们还需要考虑给碰撞机设置"层(Layer)"。

[*]碰撞机的层(Layer)
      两个碰撞体(Collider)要能相互碰撞,需要几个条件:


[*]至少有一方对应的刚体模式为`"Dynamic"`。
[*] 两个`Collider`所属的"层(Layer)"要能够相互碰撞

      "角色-站"的刚体是`"Dynamic"`,背景跳台的刚体是`"Static"`,第一条满足。
      这两个"游戏对象"的层都默认设置为`Default`。


      默认情况下所有层与层都可以碰撞。但随着游戏进一步复杂,我们需要区分不同类别对象之间的碰撞关系,需要用到更多的层。   
      我们把所有跳台定义为"跳台"层,玩家角色定义为"玩家角色"层。




      在选择"Layer"的下拉菜单中选择"Add Layer...",然后在空白出填写新的"Layer"名称。这样就建立了新的"Layer"
      然后分别给"背景Tilemap"和"角色-站"设置层"跳台"和"玩家角色"。

      现在看一下层与层的碰撞关系。菜单`"Edit->Project Settting->Physics 2D->Layer Collision Matrix(在最下面,展开)"`


      我们可以只让"玩家角色"层和"跳台"层进行碰撞。修改如下图


      运行游戏,可以看到角色站到了跳台上,左右移动角色,移到边缘角色会下落。

好了到这里吧,休息!休息一下再说。

RhinoFreak 发表于 2022-7-26 15:53

很好很强大,期待更多~

JoshWindsor 发表于 2022-7-26 16:02

谢谢支持,正在写下一节

xiangtingsl 发表于 2022-7-26 16:04

写得不错,很棒[酷]

zifa2003293 发表于 2022-7-26 16:04

喜欢   可惜不懂编程纯支持
页: [1]
查看完整版本: 初学者用Unity重制魂斗罗