从零开始独立游戏开发学习笔记(二十)-Unity学习笔记(八)M_Studio教程2D入门(一)

学完后过来提醒,整个教程是以教会你 unity 的一些基本操作,介绍各种功能为目的。并不能当做 best pratice 来用,即使是修改了 3 次版本的手感,依然非常稀烂。整个教程充斥着,这里有更好的完善的解决方案,但是偏不用,就要用一些自己写的,通过其他方式完成实现的代码。但是其实很多时候换一种实现方式其实是为了介绍某个功能。比如巡逻功能,其实目的不是教你巡逻怎么做,而是教你如何在屏幕上显示空物体的位置方便查找。
了解这个真正的目的我觉得对理解教程有很大帮助(避免踩坑吧也是为了)。这样你看到迭代了 3 次的代码的时候,就应该把错误的代码也写一遍,而不是直接用最新的。以及当你看到一个功能实现的时候,你绝对不要去想噢原来这个功能要这么去做,而是去看用了哪些 unity 组件。
不过整体来讲确实学的难受,也是比较久远的教程了。后来我看了下 brackeys 也有一个类似的教程,就流畅并且优雅多了,如果有机会建议直接看 brackeys 的 2d 教程(去他的 ytb 频道上找一个叫做 How to make a 2d game 的播放列表,国内没有搬运(如果你在 b 站搜索 brackeys 2d 是可以搜到一个教程的,但是实际上 brackeys 有两个 2d 教程,一个叫做 2d tutorial,还有一个就是刚刚那个。这两个是不一样的,b 站那个是分散的介绍各种 2d 功能,而不是一个完整的游戏流程))。
---------以下是正文--------------
开始学习著名的小狐狸了。
开头相当一部分是在 Brackeys 教程里已经教授过的,就简单提一下。
1. 新建项目,导入素材
新建一个 2D 项目。导入 asset store 里的素材(导入界面为 package manager)。
导入后在 asset panel 里可以看到导入后的文件。
1.1 Pixels Per Unit
在 asset panel 中点击其中一个素材(在拖进游戏画面之前)可以在 inspector 里看到许多设置,首先讲一下这个 Pixels Per Unit。

其中,Unit 指的就是中央 scene panel 里的小网格。

因此 Pixels Per Unit 的含义就很明了,就是这个小网格代表多少个像素。
默认的 100 很显然太大了,画面会非常小,不适合操作,因此在这整个项目过程中,我们统一使用 16 的 PPU。
操作之后把背景图拖进去。
1.2 TileMap
2d 场景设置中最常用的就是 TileMap。直接在 hierachy 中创建,在 2d object 子项中,选第一个矩形。新建后可以发现出现了一个网格。

进行后续工作前可以把刚导入的背景图隐藏掉。
1.2.1 Tile Palette
打开 window -> 2D -> Tile Palette。
名字和含义很直观,把素材放进这里,就可以在场景中用素材画画了。
在一切操作之前,首先要创建一个新的 palette,这是会弹出资源管理器页面,因为 palette 相关素材都要放在一个文件夹里保管,因此这里可以新建一个文件夹用于存放该 palette。
导入 tileset 前,记得设置 PPU。
如果直接导入,会发现整个图片都被放在一个格子里,也就是说整个图片被识别为一个素材,这样显示燃是不合格的。因此除了更改 PPU,还得切图。在更改 PPU 的那个地方,先把 Sprite Mode 改成 multiple(因为我们有多个 Sprite),然后进入 Sprite Editor。
进入之后再左上角选择 slice,直接切割,会发现切成如下样式:

切的很好,但是,有一个小问题,假设我们想切成下面这样子呢:

切成这样子的原因很简单,因为可以复用素材,更加灵活。 5. 方法就是在切的时候,不要选择 automatic,而是 grid by cell size,这里我们选择 16。切完后记得 apply。 6. 拖进 Tile Palette 中,导入刚创建的文件夹里。上方工具里选择笔刷,然后下方点击选择 tile,然后鼠标移动到 scene panel 中就可以看到鼠标变成 tile 了,接下来的操作就很直观了。而且因为 tile 的大小和 PPU 都是 16,tile 可以和 grid 完全贴合,非常舒适。
1.3 成果
自由操作了一番后的成果:

2. 图层
2.1 画面比例设置
进入到 game panel,这里是我们实际游玩的时候会看到的画面。这里我们把比例调成 16:9。
此外,也可以对 camera 进行调控。比如说调控 size 的大小,也会影响游戏画面的大小,也就是能看到的画面变多(少)了。
2.3 sorting layer
也许你一经发现了,之前导入的背景图永远都在画面最前方,遮挡住了主要场景。这很不好。解决方案有两种:
改变 z 轴位置。(不易于管理)
使用 sorting layer。
之前 Brackeys 的教程里提到过 layer,layer 可以方便选取。比如说把一些无关紧要的东西分成 environment 层,鼠标框选就无法选中这一 layer 的 object。
不过这里我们讨论的是 sorting layer。
新建方式一样,不过要在 sorting layer 里添加:


和其他设计软件不一样的地方是,在 sorting layer 的逻辑里,越靠近下方的 layer,其实是越靠近人眼的。(当然,靠 index 来判断就不容易出错了)因此我们这里新建两个图层:

我们给背景的 sorting layer 换成 layer 1(Background),tilemap 的换成 Frontground。(注意 tilemap 在 grid 的子项里,grid 没法设置 sorting layer,要点开)
此外,即使是同一个 sorting layer(比如说都是 background),也可以设置 order in layer 来改变前后位置。逻辑也是越大越靠前。

2.4 成果
成果如下,突然好看:

3. 角色建立
3.1 放入游戏
角色素材已经有了,那么如何放到游戏中呢?
直接拖。
在 hierachy 里 create 一个 sprite 的 object。然后把素材拖动到 sprite renderer 组件中的 sprite 属性里。
以上两个的效果是一样的。
如果发现角色比较小,仔细想一想原因,是之前提到过的内容。
3.2 角色逻辑
虽然放进去了,但是开始游戏后这个狐狸和背景没有任何区别,没有重力也没有碰撞啥的,这些都要我们来添加。
3.2.1 重力
如果之前 Brackeys 的教程比较熟悉的话,这里应该想到添加 rigidbody,不过这一次我们使用的是 rigidbody 2d。
3.2.2 碰撞
首先给小狐狸简单点添加 box collider 2d,简单地 edit 一下 collider 的范围,不要那么大一个框。
给 tilemap 添加专用的 tilemap collider 2d。
3.3 成果
成果如下,草我放在了另一个没有 collider 的 tilemap 上了。

4. 角色移动
首先我们可以去 Project Setting -> Input Manager(老版本叫 Input) -> Axes 里看一下,这里是一些常用按键的设置。比如说进到 Horizontal 里可以看到方向键的左右,a 和 d,以及一些重力的设置。
说看一下就真的就只是看一下,暂时不用更改这里的东西,先看第一步。
4.1 脚本
新建一个文件夹叫 Scripts,用于存放之后的脚本。
给 player 新建一个脚本,进入 unity 编辑。
比起 Brackeys 的教程,这里给了一些不一样的移动控制方式,使用了刚刚看过的 Input Manager。

以上代码为判断水平方向是否移动,如果是 0,则说明没有按 Input Manager 里设置的按键(默认是左右方向键和 a,d)。-1 到 0 表示按了向左移动的方向键,0 到 1 反之。
注意这里用的是 GetAxis,还有一个 GetAxisRaw,后者只返回 -1,0,1。前者之所以会有小数,这个其实是灵敏度,因为有些游戏用摇杆玩的时候会有轻推和重推的区分,这是用来干这个的。不需要判断轻重的时候就使用 GetAxisRaw。
写好的代码如下:

通过添加速度来移动玩家。不过现在有一个问题,那就是移动着移动着玩家突然倒下了。
这是因为 tilemap 的 collider 不规则,人物很容易撞到。
为了解决这个问题,则去往 player 的 rigidbody 里,固定 player 的 z 轴旋转。(倒下是沿着 z 轴旋转的)。
小 tips:在游玩的过程中改变组件的属性,退出后会还原。但是有一个方法,可以在组件的右上角齿轮处选择 copy component,然后退出游戏后 paste component values。这样方便游玩的时候快速 debug
5. 角色方向(重点,包含一些重要的 C# 的语法知识)
回到 unity,调整一下 scale 的 x 值为 -1,发现人物就这么朝着左边了。(当然追求细节的话,这样做肯定是不够的的,比如说异色瞳要换颜色,发型,背包的位置等等),不过现在就这么用。
上一小节提到了 GetAxisRaw 返回的正是 -1,0,1,正好可以用在这里,不用再做 if 判断。

有几点需要注意:
scale 不叫 scale 叫 localScale,在 transform 里。
不能直接给 localScale.x 赋值(y,z 同理),必须给外层的 localeScale 赋值。原因可以参考这篇文章。
类似地,有一个方法是 PlayerRigid.transform.localScale.Set(1,1,1)。看起来没问题,也不会报错,但是实际没有效果。因为 localScale 返回的是一个类型为 velocity 的 copy 值,更改这个 copy 值并不会影响原始值。为什么会有这种怪异的表现可以参考这篇文章,根本原因是 transform.localeScale.Set() 会先用 get 得到一个 localeScale 的副本再用 set,而不是直接使用 set。实际上和第二点是一样的,不过这里 C# 没有给你报错,因为这里是调用方法了,C# 还没有那么智能判断出方法里的逻辑错误。
如果硬要写的话可以这么写:
var localeScale = PlayerRigid.transform.localScale; localeScale.Set(horizontalDirection, 1, 1); PlayerRigid.transform.localScale = localeScale;
总结下来其实主要还是理解两点:
Vector3 是个结构体,而结构体是值类型,无法被引用,调用方法的时候传递的只是一个副本。
执行
transform.localScale = **
的时候,调用的是set
。而当执行transform.localScale.**
的时候(如transform.localScale.**.x
),**
是通过get
得到的,一旦 localScale 后面跟着一个点,那就是使用了 get,如果这个属性是值类型的话就要小心了,尤其是struct
这种值类型却还可以使用点语法的。
6. 跳跃
跳跃和移动差不多,只不过判断改成了 GetButtonDown。GetAxis 也是可以的,但是如果一直按着跳跃键(默认为空格键)不放,那么 GetAxis 会一直返回 1,是一个一直按住的概念。很明显跳跃是一个一次事件,因此不能使用 GetAxis。代码如下,结合了上一小节学到的知识点。

目前代码有很大问题,因为按键判断放在了 fixedUpdate 里,因此很多时候按下去了没有被 catch 到,这个问题之前 Brackeys 的学习笔记里有讲过,不再赘述。暂时只是为了方便跟着教程走这么写。