进度

导入素材

素材名:Sunny Land(Free)

处理图片

  • 设置背景,将 back 图片的每单元多少个像素点改为 16。
  • 处理地图元素,将 tileset 的每单元多少个像素点改为 16。并将其按照 16*16 像素的大小进行切割。放到 Tile Palette 中。如果格子之间有缝隙的话,可以将 Grid 的 Cell Size 都改成 0.99。
  • 注意后面的人物也需要将每单元像素数量改为 16。

    图层

    第一图层:Inspector 面板里面 Tag 的右边 Layer
    第二图层:Renderer 组件里面的 Sorting Layer,越往下,越会绘制在前面,比如背景图就应该是最上面的图层。
    第三级别:上面两个都相等的话,还可以修改 Order in Layer。数字越大,越在前面。
    将背景放到第二图层的 BackGround,地图和主角都放到 FrontGround。

    设置碰撞

    为主角设置 Rigidbody 2D 和 Box Collider,为 tilemap 设置 Tilemap Collider 2D,这里可以将草之类的不会产生碰撞的物体在新建一个 tilemap。
    后面在走上坡的时候,需要将人物分成两个碰撞体,代码中出现的也是下面的球形碰撞体。

    角色控制器

    版本一 (简单的人物移动和跳跃)

    移动就用 rigidbody 的 velocity 就可以了。
  • 第一点要注意的是 GetAxisRaw 只返回 0,-1,1,所以非常适合用来改变人物的朝向。
  • 关于为什么不需要判断 if(axis != 0),其实原因只是因为一个浮点数用 != 或者 == 来判断看着很不爽,而且加不加其实是一样的,不加还要更保险一点,下面证明为什么加不加是一样的。
    • 最终都会停下来,但是停下的原因不太一样,将加了 if 判断的设为 A 情况,不加的设为 B 情况。axis 会在方向键抬起之后,有一个平滑的过度到 0 的过程,所以 B 情况就是从 1->无限趋向于0->0。而 A 情况按道理来说应该只有 1->无限趋向于0 的过程,所以最终应该还是会保留一点速度的,但事实却不是这样,一是因为有摩擦力的作用,也就说即使在 Awake 里面给 Player 设定了一个速度,运行后它的速度也是递减到 0 的;二是 axis 最终无限趋向于 0 的值太小了,而 rigidbd.velocity 可能只保留有限位小数,最终截取到了 0。这个在我将地面的摩擦力改为 0 之后,将判断条件改为 if(Mathf.Abs(axis)>=1e-48f ),-48 左右是一个分界点,更大的话最终会保留一点点速度,但更小的话,最终就会变成 0。
    • 所以说,A 情况 ( 这里用 A 情况会感觉变量未定义 > < ),一是会导致浮点数直接做相等的运算;二是多一次 if 判断;三是不符合 axis 一个平滑过渡到 0 的过程。
  • 关于什么时候需要 * Time.deltaTime,因为这里是设置的速度,所以不应该 * 时间的,速度 * 时间应该是位移才对。
  • 出现的第一个问题就是人物在碰到坑的时候容易发生 Z 轴上的旋转,这需要将人物身上的 Rigidbody 的 Constraints 的 Freeze Rotation 给勾选上,这样就不会发生旋转了。
  • 出现的第二个问题,人物在移动时容易被一块一块的小方格碰撞体的缝隙所阻挡,也就是走着走着就走不动了。完美解决方法是在 tileMap 上再加一个 Composite Collider 2D,它会自动添加一个 Rigidbody 2D,需要将 Body Type 设置为 Static,不然地图会跟着一起掉下去的。最后将 Tilemap Collider 2D 的 Use By Composite 给勾选上。这样就可以将 tilemap 的碰撞体从一小块一小块变成一大块,提高了效率,也不会绊到主角行走。
  • 关于跳跃高度,也可以修改 Rigidbody 2D 里面的重力来改变。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    public class PlayerController : MonoBehaviour
    {
    private Rigidbody2D rigidbd;
    public float MoveSpeed;
    private void Awake()
    {
    rigidbd = this.GetComponent<Rigidbody2D>();
    }

    private void Update()
    {
    float axis = Input.GetAxis("Horizontal");
    float axis_direction = Input.GetAxisRaw("Horizontal");

    // 设置主角移动,这里不需要加 axis 是否等于 0 的判断,不然会出现漂移
    // 我觉得这里也不需要 * Time.Deltatime,因为如果改变的是位移的话,是需要 * 时间的,但这里改变的是速度。
    rigidbd.velocity = new Vector2(axis * MoveSpeed , rigidbd.velocity.y);
    Debug.Log(rigidbd.velocity);

    // 设置主角方向旋转
    transform.localScale = axis_direction == 0 ? new Vector3(transform.localScale.x,transform.localScale.y,transform.localScale.z) :
    new Vector3(axis_direction,transform.localScale.y,transform.localScale.z);
    }
    }

版本二 (动画状态机)

关于动画状态机的设置,我看很多教程都是先确定几个状态,然后进行连线,连完线再设置参数,再想每根线之间的转化应该受到什么参数影响,但其实按照那样的方法,最终我都不能确定所有情况是不是都考虑到了。而且教程中也会把动画机的 param 改变放在输入检测里面,就是说可能在控制人物移动的逻辑里就加上关于动画的控制,但我觉得动画应该是处于显示层的吧,他的直接原因是人物的位置和速度,如果通过这两项去确定动画状态的话,就可以和键盘输入事件解耦,而且也非常方便的去确定动画状态改变的条件。
状态机如图所示:

参数有三个:

  • IsOnGround (bool) 是否在地面上
  • YaxisSpeed (float) 人物 Y 轴上的速度
  • XaxisSpeed (float) 人物 X 轴上的速度

状态转化条件如下:

  • Idle->Run : IsOnGround 为 true,X 速度大于 0.0001(因为没有等于的情况,所以我都是以极小值做分界线)
  • Run->Idle : IsOnGround 为 true,X 速度小于 0.0001
  • Run->Jump : IsOnGround 为 false,Y 速度大于 -0.0001 (其实是为了包含 0 ,因为在跳跃最高点速度为 0,这个时候还是期望使用跳跃的动画的)
  • Jump->Fall : IsOnGround 为 false,Y 速度小于 -0.0001
  • Fall->Jump : IsOnGround 为 false,Y 速度大于 -0.0001(因为要考虑到多段跳,所以有这个转化)
  • Jump->Idle : IsOnGround 为 true (其实一开始是没有这一条线的,但是考虑到如果是以一个切线的方式跳到平台上,那么 Jump 之后就不会有 fall 的状态,但这样不好的就是贴墙会动画不对,但墙体和平台边沿和下方都不应该是属于 Ground 才对)
  • Idle->Jump : IsOnGround 为 false,Y 速度大于 -0.0001
  • fall->Idle : IsOnGround 为 true

其他注意事项:

  • 所有的动画都要把 Loop Time 给勾上。
  • 所有的箭头都要把 Has Exit Time 给取消勾选,再将 Transition Duration 的时间改为 0,这样两个动画之间的衔接就是瞬时的。
  • 可以新建一个 ConstNames 的类,里面保存同名的常量字符串,这样可以在写代码的时候自动补全,而不用担心自己拼错单词。
  • 如果是通过 GetAxisRaw 的值来改变动画的话,那不把输入事件和动画耦合还有一个好处,因为 GetAxisRaw 在键盘松开的瞬间,就会变成 0,但这个时候人物其实还是有速度的,动画却会变成 Idle 状态,就会出现漂移的效果,如果是使用 GetAxis 的话区别不大,因为 GetAxis 的值是一个平滑的降到 0 的过程。
  • 在上坡的时候会发现 IsOnGround 会一直跳变,原因是这个坡的碰撞体其实是下 图1 的样子,将 Composite Collider 2D 中的 Vertex Distance 改到 0.16 左右,就会变成平滑的上坡了。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 文件名:PlayerController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
private Rigidbody2D rigidbd;
private Collider2D coll;
private Animator animotor;
public float MoveSpeed;
public float JumpSpeed;
public LayerMask groundMask;
private void Awake()
{
rigidbd = this.GetComponent<Rigidbody2D>();
// rigidbd.velocity = new Vector2(10, 0); // 检测摩擦力的影响

coll = this.GetComponent<Collider2D>();
animotor = this.GetComponent<Animator>();
}

private void Update()
{
float axis = Input.GetAxis("Horizontal");
float axis_direction = Input.GetAxisRaw("Horizontal");

// 设置主角移动,这里不需要加 axis 是否等于 0 的判断
// if( axis!= 0 ) { rigidbd.velocity = new Vector2(axis * MoveSpeed, rigidbd.velocity.y); }
// if(Mathf.Abs(axis)>=1e-48f ) { rigidbd.velocity = new Vector2(axis * MoveSpeed, rigidbd.velocity.y); }

// 我觉得这里也不需要 * Time.Deltatime,因为如果改变的是位移的话,是需要 * 时间的,但这里改变的是速度。
rigidbd.velocity = new Vector2(axis * MoveSpeed , rigidbd.velocity.y);

// 设置主角方向旋转
transform.localScale = axis_direction == 0 ? new Vector3(transform.localScale.x,transform.localScale.y,transform.localScale.z) :
new Vector3(axis_direction,transform.localScale.y,transform.localScale.z);

// 设置跳跃
if (Input.GetButtonDown(ConstNames.JumpButton))
{
rigidbd.velocity = new Vector2(rigidbd.velocity.x, JumpSpeed);
}

// 最后根据人物的位置和速度来改变人物的动画状态
SetAnimatorParam();
}

void SetAnimatorParam()
{
animotor.SetBool(ConstNames.IsOnGround, coll.IsTouchingLayers(groundMask));
animotor.SetFloat(ConstNames.XaxisSpeed, Mathf.Abs(rigidbd.velocity.x));
animotor.SetFloat(ConstNames.YaxisSpeed, rigidbd.velocity.y);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
// 文件名:ConstNames.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ConstNames
{
public static readonly string IsOnGround = "IsOnGround";
public static readonly string YaxisSpeed = "YaxisSpeed";
public static readonly string XaxisSpeed = "XaxisSpeed";
public static readonly string JumpButton= "Jump";
}

版本三(优化手感,增加二段跳)

  • 先把人物的摩擦力设为 0,这样就不能日墙上升了。但之前我以为这样贴墙下落的动画应该会不对,但实际是对的,因为下面的球体是检测是否碰到墙的,而球体会被上面的矩形碰撞体给挡住碰不到墙。
  • 关于脚下的小球的大小问题,如果太大的话,会出现在同一次 FixUpdate 调用时已经执行了 Jump(),但还是检测在地面上的情况。导致变成 3 段跳。

镜头跟踪

方法一:自己写一个 camera controller,但会有各种问题,不如用 cinemachine 哈哈哈。

  • 新建一个 2D Camera。
  • 进一步建好场景,因为是卷轴游戏,所以左右多复s制几个背景出来。然后加一个 Polygon Collider 2D 的碰撞体。主要要勾上 Is Trigger。再给 cinemachine 摄像机添加一个 Cinemachine Confiner 组件,将刚刚添加的 碰撞体放到 Bounding Sharp 2D 里面。这时候试一试就会发现,这个碰撞体限制的是摄像机的位置,也就是说如果碰撞体完全贴合背景图的话,人物移动到边缘,还是能够看到屏幕一半的天空盒。所以我是将碰撞体设置到正好看不到天空盒的地方。
  • 设置相机,将 Follow 设置为 Player,然后 Dead Zone Width 和 Dead Zone Height,也就是设置焦点超过了哪个范围会移动摄像头。 修改 Lans 和 Screen 到合适的位置和视野。
  • 因为是卷轴游戏,所以不想让镜头上下移动,暂时将 Dead Zoom Height 设置成了最大。
  • 最后放一张我超喜欢的一景

拾取物的设定

因为在做的过程中发现细节其实挺多的,而且总是在突然冒出新的想法,一开始想要将代码就搭建的很舒服,但却发现越写越乱,所以暂时停更,等全部完成之后再进行梳理。代码仓库

参考

B站:2D-Controller