部分功能例如目录跳转,回到顶部功能在这里有问题
追求阅读体验可以转到 ✨本人主战场!✨
✨✨目录
-
一、入门卷
-
二、杂项卷
-
三、最后
一、入门卷
回到顶部
-
前言
-
准备资源
-
Tilemap 地图布置,刚体组件
-
角色移动跳跃脚本,以及刚体,碰撞器等组件添加(包含射线检测,解决手感问题)
-
角色添加动画逻辑,以及动画组件(包含动画状态机设置)
-
相机跟随 Player 移动(简单代码实现)
-
游戏场景切换
[入门卷] 0. 前言
回到顶部
本卷以 Sunnyland 素材为例,简单总结了一下 Unity 在 2D 游戏制作方面的基本使用。
[入门卷] 1. 准备资源
回到顶部
新建2D项目 FirstGameSunnyland
A. 导入资源
1. 添加资源
菜单栏 Window -> Asset Store -> Search online 搜索 Sunnyland -> 添加至我的资源
2. 导入素材
-> 在 Unity 中打开 或者 在Unity编辑器中 菜单栏 Window -> Package Manager
-> 左上角 Packages 选择 My Assets -> 选中 Sunny Land Download -> Import
-> All, Import
B. 整理文件夹
我习惯在 Asset 文件夹中新建 _Game 文件夹,前缀 _ 按对应字典排序在前面,会出现在比较靠前的位置,这个不做强制要求。
在 _Game 中暂时新建 Animations,Prefabs,Scripts,Maps,Scenes,Materials 6个文件夹。
我还把外面的两个文件夹拖了进来
C. 资源格式设置
在游戏中一般都会使用相同的 Pixels Per Units,默认是 100,这里我使用 16,可以根据需求更改,不同资源格式可以不一样,在使用资源时注意更改即可。
D. 编辑器视图布局
我之前使用的是在默认布局基础上做了一点更改,可以在右上角,Layout 点击 Default 设置。
可以右键下方 Project 或 Console,Add Tab -> Project,我喜欢用两个 Project
下面再加几个常用的窗口:
-
Window -> 2D ->
Tile Palette
(可以拖拽到自己喜欢的位置) -
Window -> Animation ->
Animation
(动画制作) -
Window -> Animation ->
Animator
(动画状态机控制)
注意:没必要跟我一模一样,这只是我目前的习惯,你完全可以按照你的喜好来
[入门卷] 2. Tilemap 地图布置,Tilemap 碰撞体组件
回到顶部
A. Tilemap 地图布置
xxx/Sunnyland/artwork/Environment 下有一些资源。
-
(1). 背景
可以拖一张背景图片 back 到 Scene 场景中,重命名为 Background,在 Inspector 中,Transform 组件右上角 三点左击 Reset 可以重置 Transform 中的属性。这里将 Scale 设置为 (3,3,3)
,复制两份(可以手动复制,或者 Ctrl+D
),按住 V 会出现锚点,拖动这个锚点往想去的那个地方拖拽(有类似吸附效果),将 3个背景稍微整齐排列。
-
(2). Tileset 瓦片集
资源自带的一个 tileset,已经切割好了,不过这里为了介绍基本使用,我们可以再切割一次
-
(3). 素材切割
点击 tileset,在 Inspector 中,将 Sprite Mode 改为 Multiple,点击 Sprite Editor 进行切割。
Slice -> Type 选中 Grid By Cell Size
-> Pixel Size 改为 x16 y16(因为我将素材都改为了 16像素每单位)
-> 最后点击 Slice 切割 -> Apply
-
(4). Sprite 图层 Layer
这里简单介绍一下图层的概念,Unity 中的图层渲染顺序是从上开始往下渲染的,也就是说越下面的图层越在上面,或者你也可以更改 Orider in Layer
,数字越大越在上面
选择 Tilemap_BaseMap
这里在 Inspector -> xxx Renderer(如 Tilemap Renderer
) 中的 Sorting Layer 中从上到下添加了 Background,Environment,Foreground
在 Hierarchy 窗口中右键创建空物体 Create Empty 重命名为 Background,来管理其他 3个背景,其他按格式 bg 重命名
选中 Background 在 Inspector 窗口中添加组件 Sprite Renderer,设置 Sorting Layer 为 Background
-
(5). 地图布置
- 这里好像自带了一个
Main Palette
,可以暂时不用管他,在 Tile Palette 窗口中Create New Palette
,我将其命名为 BaseMap
,放入 _Game/Maps/BaseMap
中,将切割好的 tileset 素材拖入 Tile Palette 窗口对应的 Palette 中。
-
在 Hierarchy 窗口中,右键 2D Object -> Tilemap -> Rectangular,重命名 Tilemap 为 Tilemap_BaseMap。
-
接下来可以使用 Tile Palette 窗口中对应的 Palette 资源在刚刚创建的 Grid 中的 Tilemap 上绘制了,面板上面有很多工具,如笔刷,橡皮。注意:Tile Palette 窗口中 Active Tilemap 要选择需要绘制的 Tilemap。
你还可以选择 Edit 选项去编辑 Tile Palette 如果你去试试,会很简单
-
这里布置地图如下
不过你完全可以按你的喜好来,不要让文章限制你的想法,我这里布置的比较随意,因为可能用不到这么大的地图,本卷主要以介绍基本使用和逻辑为主
2. Tilemap 碰撞体组件
为 Tilemap_BaseMap 添加 [复合] 碰撞体组件。
点击 Tilemap_BaseMap,在 Inspector 窗口下面,Add Component,搜索 Tilemap Collider 2D
并添加,这里勾选下面的 Used By Composite
选项(防止碰撞体之间的卡住现象,可以自行试一下,例如,有时角色冻结 Z 轴,移动会卡住,不冻结会是绕 Z轴旋转的现象),若勾选了此选项,还需要添加 Composite Collider 2D 组件
,但此时会自动添加 Rigidbody 2D,默认会有重力,这不是我们想要的,简单的可以将 Rigidbody 2D 中的 Body Type 改为 Static
,或将重力设为 0
。
将鼠标放在 Hierarchy 物体对象上左边会有小眼睛,点击可以隐藏该组件
你现在可以试试隐藏 Background 然后选择 Tilemap_BaseMap 查看刚刚设置的 复合 碰撞体组件了
[入门卷] 3. 角色移动跳跃脚本,以及刚体,碰撞器等组件添加(包含射线检测,解决手感问题)
回到顶部
在 …/Sunnyland/artwork/Sprites/player/idle 中找到 Player 的 “闲置状态” 素材,注意 Pixels Per Unit 的设置 这里统一 16。将 第一张素材图片拖入 Scene 中,或者在 hierarchy 中右键创建 2D Object -> Sprites -> Square 或者随便选一个,这里选的是 Square(这个只是设置 Renderer 中的 Sprite,后面要改,所以随便选择),将 Sprite 设置为第一张素材图片,可以拖拽,将 Sorting Layer 设置为 Foreground。
注意命名该对象为 Player
A. 刚体,碰撞器组件
-
添加
Rigidbody 2D
【重力设置为 3】,Capsule Collider 2D
-
在 Rigidbody 2D 中,设置 Collision Detection -> Continuous(让检测更频繁),Interpolate -> Interpolate(落地会有一点凹陷,然后恢复,模拟更真实的效果)。在 Constraints 中,勾选 Freeze Rotation,防止 Z 轴翻滚。
-
在 Capsule Collider 2D 中,点击 Edit Collider 可以设置 碰撞体形状,设置成合适的大小。(如果看不清,可以先隐藏 背景 Background)
-
给 Player 添加材质。在 …/_Game/Materials/ 中右键 Create -> 2D -> Physics Material 2D,命名为 M_ZeroFriction,我一般喜欢将材质命名为 M 开头,将 Friction 设为 0。拖到 Capsule Collider 2D 的 Material 上。
B. 按键设置
Edit
-> Project Settings
-> Input Manager
-> Axes
展开
这里会用到里面自带 Horizontal 和 Jump
再添加一个 Crouch 按键用来触发 下蹲。随便用鼠标右击一个按键例如 Jump -> Duplicate Array Element
,复制一份,再更改 Name 为 Crouch
,Positive Button 为 s,Alt Positive Button,是替代按键,暂时不用设置。
C. 为 Player 添加脚本组件(暂时不包括动画)
(1). 配置编辑器
✨ 配置 VsCode 参考链接
如果是 Visual Studio 例如(Visual Studio 2019)直接使用,不用配置,这里使用的是 VS 2019
Edit -> Preferences -> Analysis > External Tools
-> External Script Editor
(2). 示例代码
-
在 Project 中右键 Create -> C# Script(拖到 Player 上添加组件),或者 直接在 Inspector 中添加。这里选择前者,后者会默认创建在 Assets 目录,还要手动移动到 Scripts 文件夹中,所以就直接右键在 指定文件夹中创建了。(双击组件属性 Script 或 脚本文件进入 Vscode 编写脚本)
-
PlayerController.cs 移动示例代码如下(包括,水平移动,转向,跳跃,下蹲,射线检测)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
// --- Private ----------------------------------
private Rigidbody2D m_Rb; // Player 刚体组件
private CapsuleCollider2D m_CapsuleCollider2D; // 胶囊碰撞体组件
// 检测相关
[Header("【只能看不能改】")]
[SerializeField] private bool m_BTurnDirection = false; // Player 是否转向
[SerializeField] private bool m_BDoJump = false; // 是否执行跳跃相关
[SerializeField] private bool m_BPressedCrouch = false; // 是否按下 下蹲键
[SerializeField] private bool m_BOnGround = false; // 是否在地面
[SerializeField] private int m_CurrentAllowAirJumpCount = 0;// 当前 允许跳空中跃次数
[SerializeField] private Vector2 m_CapsuleCollider2DSize; // 保存碰撞体 初始大小
[SerializeField] private Vector2 m_CapsuleCollider2DOffset; // 保存碰撞体 初始偏移
[SerializeField] private bool m_BTopHasWall = false; // 上方是否有墙
// --- Public -----------------------------------
[Header("【移动参数】")]
public float m_HorizontalSpeedFactorPerSecond = 300.0f; // 水平移动速度
public float m_JumpSpeedPerSecond = 520.0f; // 跳跃速度
public int m_AllowAirJumpCount = 1; // 允许跳空中跃次数
[Header("【检测相关】")]
public LayerMask m_LMGround; // 地面图层蒙版
public float m_CheckGround_Left_XOffset = -0.4f; // 地面检测 射线 x 左偏移
public float m_CheckGround_Right_XOffset = 0.2f; // 地面检测 射线 x 右偏移
public float m_CheckGround_YOffset = -0.95f; // 地面检测 射线 y 偏移
public float m_CheckGround_Distance = 0.1f; // 地面检测 射线发射距离
public float m_CheckTopHasWall_Distance = 0.4f; // 检测上方是否右墙 射线 距离
public float m_CheckTopHasWall_Left_XOffset = -0.4f; // 地面检测 射线 x 左偏移
public float m_CheckTopHasWall_Right_XOffset = 0.35f; // 地面检测 射线 x 右偏移
// Start is called before the first frame update
private void Start()
{
// 获取组件
m_Rb = GetComponent<Rigidbody2D>();
m_CapsuleCollider2D = GetComponent<CapsuleCollider2D>();
// 设置参数
m_CurrentAllowAirJumpCount = m_AllowAirJumpCount;
m_CapsuleCollider2DSize = m_CapsuleCollider2D.size;
m_CapsuleCollider2DOffset = m_CapsuleCollider2D.offset;
}
private void FixedUpdate()
{
Move();
}
// Update is called once per frame
private void Update()
{
Check();
}
// --- public -------------------------------------------
/// <summary>
/// @breif 移动
/// </summary>
public void Move()
{
HorizontalMove(); // 水平移动
Jump(); // 跳跃
Crouch(); // 下蹲
}
/// <summary>
/// @brief 检测部分
/// </summary>
public void Check()
{
CheckInput(); // 输入检测
CheckOnGround(); // 检测是否在地面
CheckTopHasWall(); // 检测头上是否有墙壁
}
// --- private ------------------------------------------
/// <summary>
/// @brief 输入检测
/// </summary>
private void CheckInput()
{
// 检测跳跃键【GetButtonDown 这个函数,一直按下也只会算一次,需要松开再按才算下一次】
//Debug.LogWarning(Input.GetButtonDown("Jump"));
if (Input.GetButtonDown("Jump") && m_CurrentAllowAirJumpCount > 0)
{
// 如果可以跳跃,执行跳跃相关
m_BDoJump = true;
}
// 检测下蹲键
m_BPressedCrouch = Input.GetButton("Crouch");
}
/// <summary>
/// @brief 检测上方是否有墙壁
/// </summary>
private void CheckTopHasWall()
{
float yOffset = -0.2f; // 检测上方是否有墙壁的偏移量
// 射线起点
Vector2 leftStart2 = new Vector2(transform.position.x + m_CheckTopHasWall_Left_XOffset, transform.position.y + yOffset);
Vector2 rightStart2 = new Vector2(transform.position.x + m_CheckTopHasWall_Right_XOffset, transform.position.y + yOffset);
// 射线方向
Vector2 direction2 = Vector2.up;
#if DEBUG // 调试用变量
Vector3 leftStart3 = new Vector3(transform.position.x + m_CheckTopHasWall_Left_XOffset, transform.position.y + yOffset, 0.0f);
Vector3 rightStart3 = new Vector3(transform.position.x + m_CheckTopHasWall_Right_XOffset, transform.position.y + yOffset, 0.0f);
Vector3 direction3 = Vector3.up;
#endif
// 射线持续时间(Debug)
float durationTime = 0.0f;
RaycastHit2D leftHitResult = Physics2D.Raycast(leftStart2, direction2, m_CheckTopHasWall_Distance, m_LMGround);
RaycastHit2D rightHitResult = Physics2D.Raycast(rightStart2, direction2, m_CheckTopHasWall_Distance, m_LMGround);
if (leftHitResult || rightHitResult)
{
m_BTopHasWall = true;
// Debug
#if DEBUG
Debug.DrawLine(leftStart3, leftStart3 + direction3 * m_CheckTopHasWall_Distance, Color.green, durationTime); // 绿
Debug.DrawLine(rightStart3, rightStart3 + direction3 * m_CheckTopHasWall_Distance, Color.green, durationTime); // 绿
#endif
}
else
{
m_BTopHasWall = false;
// Debug
#if DEBUG
Debug.DrawLine(leftStart3, leftStart3 + direction3 * m_CheckTopHasWall_Distance, Color.red, durationTime); // 红
Debug.DrawLine(rightStart3, rightStart3 + direction3 * m_CheckTopHasWall_Distance, Color.red, durationTime); // 红
#endif
}
}
/// <summary>
/// @brief 检测是否在地面上
/// </summary>
private void CheckOnGround()
{
// 射线起点
Vector2 leftStart2 = new Vector2(transform.position.x + m_CheckGround_Left_XOffset, transform.position.y + m_CheckGround_YOffset);
Vector2 rightStart2 = new Vector2(transform.position.x + m_CheckGround_Right_XOffset, transform.position.y + m_CheckGround_YOffset);
Vector2 leftTurnDirectionStart2 = new Vector2(leftStart2.x + 0.18f, leftStart2.y);
Vector2 rightTurnDirectionStart2 = new Vector2(rightStart2.x + 0.22f, rightStart2.y);
#if DEBUG
Vector3 leftStart3 = new Vector3(leftStart2.x, leftStart2.y, 0.0f);
Vector3 rightStart3 = new Vector3(rightStart2.x, rightStart2.y, 0.0f);
Vector3 leftTurnDirectionStart3 = new Vector3(leftTurnDirectionStart2.x, leftTurnDirectionStart2.y, 0.0f);
Vector3 rightTurnDirectionStart3 = new Vector3(rightTurnDirectionStart2.x, rightTurnDirectionStart2.y, 0.0f);
#endif
// 射线方向
Vector2 direction2 = Vector2.down;
Vector3 direction3 = Vector3.down;
// 射线持续时间(Debug)
float durationTime = 0.0f;
// 射线结果,该结构体也有重写 bool operator,所以可以直接用来判断
RaycastHit2D leftHitResult;
RaycastHit2D rightHitResult;
if (m_BTurnDirection) // 如果转向了
{
leftHitResult = Physics2D.Raycast(leftTurnDirectionStart2, direction2, m_CheckGround_Distance, m_LMGround);
rightHitResult = Physics2D.Raycast(rightTurnDirectionStart2, direction2, m_CheckGround_Distance, m_LMGround);
}
else // 如果没转向
{
leftHitResult = Physics2D.Raycast(leftStart2, direction2, m_CheckGround_Distance, m_LMGround);
rightHitResult = Physics2D.Raycast(rightStart2, direction2, m_CheckGround_Distance, m_LMGround);
}
if (leftHitResult.collider || rightHitResult.collider)
{
// 在地面上
m_BOnGround = true;
// 重置空中跳跃次数
m_CurrentAllowAirJumpCount = m_AllowAirJumpCount;
// 调试,击中为绿色
#if DEBUG
if (m_BTurnDirection)
{
Debug.DrawLine(leftTurnDirectionStart3, leftTurnDirectionStart3 + direction3 * m_CheckGround_Distance, Color.green, durationTime);
Debug.DrawLine(rightTurnDirectionStart3, rightTurnDirectionStart3 + direction3 * m_CheckGround_Distance, Color.green, durationTime);
}
else
{
Debug.DrawLine(leftStart3, leftStart3 + direction3 * m_CheckGround_Distance, Color.green, durationTime);
Debug.DrawLine(rightStart3, rightStart3 + direction3 * m_CheckGround_Distance, Color.green, durationTime);
}
#endif
}
else
{
// 不在地面上
m_BOnGround = false;
// 调试
#if DEBUG
if (m_BTurnDirection)
{
Debug.DrawLine(leftTurnDirectionStart3, leftTurnDirectionStart3 + direction3 * m_CheckGround_Distance, Color.red, durationTime);
Debug.DrawLine(rightTurnDirectionStart3, rightTurnDirectionStart3 + direction3 * m_CheckGround_Distance, Color.red, durationTime);
}
else
{
Debug.DrawLine(leftStart3, leftStart3 + direction3 * m_CheckGround_Distance, Color.red, durationTime);
Debug.DrawLine(rightStart3, rightStart3 + direction3 * m_CheckGround_Distance, Color.red, durationTime);
}
#endif
}
}
/// <summary>
/// @brief 水平移动
/// </summary>
private void HorizontalMove()
{
// 获取水平输入 [-1, 1],松开按键时为 0
float horizontalValue = Input.GetAxis("Horizontal");
m_Rb.velocity = new Vector2(horizontalValue * m_HorizontalSpeedFactorPerSecond * Time.fixedDeltaTime, m_Rb.velocity.y);
// Player 转向
if (horizontalValue < 0.0f) // 默认是右边
{
m_BTurnDirection = true;
transform.rotation = new Quaternion(0.0f, 180.0f, 0.0f, 1.0f);
}
else if (horizontalValue > 0.0f)
{
m_BTurnDirection = false;
transform.rotation = new Quaternion(0.0f, 0.0f, 0.0f, 1.0f);
}
}
/// <summary>
/// @brief 跳跃
/// </summary>
private void Jump()
{
if (m_BOnGround && m_BDoJump)
{
m_BDoJump = false;
m_Rb.velocity = new Vector2(m_Rb.velocity.x, m_JumpSpeedPerSecond * Time.fixedDeltaTime);
}
if (!m_BOnGround && m_CurrentAllowAirJumpCount > 0 && m_BDoJump)
{
// 空中跳跃
m_BDoJump = false;
--m_CurrentAllowAirJumpCount;
m_Rb.velocity = new Vector2(m_Rb.velocity.x, m_JumpSpeedPerSecond * Time.fixedDeltaTime);
}
}
/// <summary>
/// @brief 下蹲
/// </summary>
private void Crouch()
{
if (m_BPressedCrouch)
{
m_CapsuleCollider2D.size = new Vector2(m_CapsuleCollider2D.size.x, 0.6f);
m_CapsuleCollider2D.offset = new Vector2(m_CapsuleCollider2D.offset.x, -0.545f);
}
else if (!m_BTopHasWall) // 上方如果检测到有墙不能起来
{
m_CapsuleCollider2D.size = m_CapsuleCollider2DSize;
m_CapsuleCollider2D.offset = m_CapsuleCollider2DOffset;
}
}
}
添加一个 Layer 命名为 Ground,不要忘记将 Tilemap_BaseMap
的图层 Layer 设置为 Ground
编辑器参数设置
如果脚本更改,一般需要刷新组件才能更新编辑器中的数值显示
这里应该只需要修改图层为 Ground
-
(3). 特别注意
常见的是跳跃手感问题,代码逻辑不当会让玩家感觉跳跃按键不灵。
简单说一下这里使用的一些手段,其他代码应该挺简单的,结合注释应该很好阅读
1.** 将检测部分放入 Update 中,将与刚体运动相关的放入 FixedUpdate 中。**
-
代码逻辑,参考如下(从上方摘取)【这里指出关键代码让大家体会】
检测部分放入 Update 中
[SerializeField] private bool m_BDoJump = false; // 是否执行跳跃相关 private void CheckInput() { // 检测跳跃键【GetButtonDown 这个函数,一直按下也只会算一次,需要松开再按才算下一次】 //Debug.LogWarning(Input.GetButtonDown("Jump")); if (Input.GetButtonDown("Jump") && m_CurrentAllowAirJumpCount > 0) { // 如果可以跳跃,执行跳跃相关 m_BDoJump = true; } // 检测下蹲键 m_BPressedCrouch = Input.GetButton("Crouch"); }
与刚体运动相关的放入 FixedUpdate 中
public float m_JumpSpeedPerSecond = 520.0f; // 跳跃速度 private void Jump() { if (m_BOnGround && m_BDoJump) { m_BDoJump = false; m_Rb.velocity = new Vector2(m_Rb.velocity.x, m_JumpSpeedPerSecond * Time.fixedDeltaTime); } if (!m_BOnGround && m_CurrentAllowAirJumpCount > 0 && m_BDoJump) { // 空中跳跃 m_BDoJump = false; --m_CurrentAllowAirJumpCount; m_Rb.velocity = new Vector2(m_Rb.velocity.x, m_JumpSpeedPerSecond * Time.fixedDeltaTime); } }
-
(4). Unity 函数生命周期参考
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DhUIlIkx-1670755939902)(https://img2023.cnblogs.com/blog/2805185/202212/2805185-20221211184019686-591164807.svg)]
[入门卷] 4. 角色添加动画逻辑,以及动画组件(包含动画状态机设置)
回到顶部
-
为 Player 添加 Animator 组件(或者先不添加),然后在 …/_Game/Animations/Player 中 右键 Create -> Animator Controller,这里命名为
AC_Player
并拖入 Hierarchy 面板上的 Player 对象中 或者 拖入 Player 的 Inspector -> Animator -> Controller 上。 -
在此目录下创建动画,在 Animation 窗口中,点击右下框的 Create,创建动画,这里命名为
Idling.anim
,创建一次后即可在 左边选择动画列表中(例如这边叫做 Player,左键点一下就会弹出下拉菜单),下方点击Create New Clip 创建动画
。暂时创建Idling
(刚刚创建了,不用创建了),Running
,Jumping
,Falling
,Crouching
5个动画片段。
注意:将素材设置为统一的格式,然后这里 Pixels Per Unit 设置的是 16 !
- 把相应的动画素材全选拖入 Animation 右边时间轴窗口。点击右边第二行三点,
Show Sample Rate
(我比较喜欢用),这个可以调节频率,按自己喜好调节 (我这里Idling,Running,Crouching 分别调节的是 10, 10, 7)Jumping 和 Falling 只有一张,我将 Samples 设置了 1,不知道是否能改善性能呢?,红色录制按钮右边还有播放按钮,非常方便。考虑本卷主要介绍基本使用,所有不弄太复杂,之后可能会单独出模块介绍。
- 设置 Animator。(也可以不使用 Unity 编辑器中的动画状态机,可以在代码操作,但是这次使用的是动画状态机)
右键单个动画片段,可以 Make Transition
然后关联(连线)动画片段。
动画控制器视图参考
✨ A. 动画控制器属性设置如下
【这里可以耐心配置一下,截图会占太多篇幅,所以可能需要大家稍微花点时间过一下,挺快的,我都打出来了,哈哈】
(1). 参数设置
Inspector 面板中,右下角有 Conditions List 条件列表,有 +
、-
可以操作 CRUD 列表
Idling -> Running
Conditions: HorizontalSpeedPerSecond > 0.1
Idling <- Running
Conditions: HorizontalSpeedPerSecond < 0.1
Running -> Jumping
Conditions: BJumping = true;
Jumping -> Falling
Conditions: BFalling = true;
Falling -> Jumping
Conditions: BJumping = true, BFalling = false;
Falling -> Idling
Conditions: BFalling = false, BIdling = true;
Idling -> Falling
Conditions: BFalling = true;
Idling -> Crouching
Conditions: BCrouching = true;
Crouching -> Idling
Conditions: BCrouching = false, BIdling = true;
Running -> Crouching
Conditions: BCrouching = true;
Crouching -> Running
Conditions: BCrouching = false, HorizontalSpeedPerSec > 0.1;
Crouching -> Jumping
Conditions: BCrouching = false, BJumping = true;
Falling -> Crouching
Conditions: BCrouching = true, BFalling = true;
Idling -> Jumping
Conditions: BJumping = true;
Jumping -> Idling
Conditions: BIdling = true, BFalling = false;
(2). 其他设置
把所有动画 Inspector 中,Has Exit Time 取消勾选,Settings -> Transition Duration(s) = 0。
但是 Idling -> Flling,Idling -> Jumping,Transition Duration(s) = 0.1,防止警告。
B. 添加脚本,加入动画控制逻辑,如下
- 添加组件
private Animator _Animator; // Animator
- 在按键检测注释下面添加
// 检测动画状态
[SerializeField] private bool m_BAnimIdling = true; // 闲置状态
[SerializeField] private bool m_BAnimRunning = false; // 跑动状态
[SerializeField] private bool m_BAnimJumping = false; // 跳跃状态
[SerializeField] private bool m_BAnimFalling = false; // 下落状态
[SerializeField] private bool m_BAnimCrouching = false; // 下蹲状态
- Start() 获取组件下面添加
m_Animator = GetComponent<Animator>();
- 添加方法
/// <summary>
/// @brief 动画控制
/// </summary>
public void AnimatorControl()
{
m_Animator.SetFloat("HorizontalSpeedPerSecond", Mathf.Abs(m_Rb.velocity.x));
m_Animator.SetBool("BIdling", m_BAnimIdling);
m_Animator.SetBool("BJumping", m_BAnimJumping);
m_Animator.SetBool("BFalling", m_BAnimFalling);
m_Animator.SetBool("BCrouching", m_BAnimCrouching);
}
/// <summary>
/// 检测状态
/// </summary>
private void CheckState()
{
// 跑动
m_BAnimRunning = (Mathf.Abs(m_Rb.velocity.x) > 0.1f && !m_BAnimCrouching && !m_BAnimJumping && !m_BAnimFalling);
// 下落
m_BAnimFalling = m_Rb.velocity.y < -0.1f;
// 跳跃
if ((m_BOnGround && m_BDoJump) || (!m_BOnGround && m_AllowAirJumpCount > 0 && m_BDoJump))
{
m_BAnimJumping = true;
}
else if (m_BAnimFalling)
{
m_BAnimJumping = false;
}
// 下蹲
m_BAnimCrouching = (m_BOnGround && ((m_BPressedCrouch && (!m_BAnimJumping || !m_BAnimFalling)) || m_BTopHasWall));
// 闲置
m_BAnimIdling = (m_BOnGround && !m_BAnimCrouching && !m_BAnimFalling);
}
AnimatorControl() 添加到 Update() 中,CheckState() 添加到 Check() 中
C. 添加动画逻辑后的完整代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
// --- Private ----------------------------------
private Rigidbody2D m_Rb; // Player 刚体组件
private CapsuleCollider2D m_CapsuleCollider2D; // 胶囊碰撞体组件
private Animator m_Animator; // 动画控制器
/** 检测相关 */
[Header("【只能看不能改】")]
[SerializeField] private bool m_BTurnDirection = false; // Player 是否转向
[SerializeField] private bool m_BDoJump = false; // 是否执行跳跃相关
[SerializeField] private bool m_BPressedCrouch = false; // 是否按下 下蹲键
[SerializeField] private bool m_BOnGround = false; // 是否在地面
[SerializeField] private int m_CurrentAllowAirJumpCount = 0;// 当前 允许跳空中跃次数
[SerializeField] private Vector2 m_CapsuleCollider2DSize; // 保存碰撞体 初始大小
[SerializeField] private Vector2 m_CapsuleCollider2DOffset; // 保存碰撞体 初始偏移
[SerializeField] private bool m_BTopHasWall = false; // 上方是否有墙
// 检测动画状态
[SerializeField] private bool m_BAnimIdling = true; // 闲置状态
[SerializeField] private bool m_BAnimRunning = false; // 跑动状态
[SerializeField] private bool m_BAnimJumping = false; // 跳跃状态
[SerializeField] private bool m_BAnimFalling = false; // 下落状态
[SerializeField] private bool m_BAnimCrouching = false; // 下蹲状态
// --- Public -----------------------------------
[Header("【移动参数】")]
public float m_HorizontalSpeedFactorPerSecond = 300.0f; // 水平移动速度
public float m_JumpSpeedPerSecond = 520.0f; // 跳跃速度
public int m_AllowAirJumpCount = 1; // 允许跳空中跃次数
[Header("【检测相关】")]
public LayerMask m_LMGround; // 地面图层蒙版
public float m_CheckGround_Left_XOffset = -0.4f; // 地面检测 射线 x 左偏移
public float m_CheckGround_Right_XOffset = 0.2f; // 地面检测 射线 x 右偏移
public float m_CheckGround_YOffset = -0.95f; // 地面检测 射线 y 偏移
public float m_CheckGround_Distance = 0.1f; // 地面检测 射线发射距离
public float m_CheckTopHasWall_Distance = 0.4f; // 检测上方是否右墙 射线 距离
public float m_CheckTopHasWall_Left_XOffset = -0.4f; // 地面检测 射线 x 左偏移
public float m_CheckTopHasWall_Right_XOffset = 0.35f; // 地面检测 射线 x 右偏移
// Start is called before the first frame update
private void Start()
{
// 获取组件
m_Rb = GetComponent<Rigidbody2D>();
m_CapsuleCollider2D = GetComponent<CapsuleCollider2D>();
m_Animator = GetComponent<Animator>();
// 设置参数
m_CurrentAllowAirJumpCount = m_AllowAirJumpCount;
m_CapsuleCollider2DSize = m_CapsuleCollider2D.size;
m_CapsuleCollider2DOffset = m_CapsuleCollider2D.offset;
}
private void FixedUpdate()
{
Move();
}
// Update is called once per frame
private void Update()
{
Check(); // 检测
AnimatorControl(); // 动画控制
}
// --- public -------------------------------------------
/// <summary>
/// @breif 移动
/// </summary>
public void Move()
{
HorizontalMove(); // 水平移动
Jump(); // 跳跃
Crouch(); // 下蹲
}
/// <summary>
/// @brief 检测部分
/// </summary>
public void Check()
{
CheckInput(); // 输入检测
CheckOnGround(); // 检测是否在地面
CheckTopHasWall(); // 检测头上是否有墙壁
CheckState();
}
/// <summary>
/// @brief 动画控制
/// </summary>
public void AnimatorControl()
{
m_Animator.SetFloat("HorizontalSpeedPerSecond", Mathf.Abs(m_Rb.velocity.x));
m_Animator.SetBool("BIdling", m_BAnimIdling);
m_Animator.SetBool("BJumping", m_BAnimJumping);
m_Animator.SetBool("BFalling", m_BAnimFalling);
m_Animator.SetBool("BCrouching", m_BAnimCrouching);
}
// --- private ------------------------------------------
/// <summary>
/// 检测状态
/// </summary>
private void CheckState()
{
// 跑动
m_BAnimRunning = (Mathf.Abs(m_Rb.velocity.x) > 0.1f && !m_BAnimCrouching && !m_BAnimJumping && !m_BAnimFalling);
// 下落
m_BAnimFalling = m_Rb.velocity.y < -0.1f;
// 跳跃
if ((m_BOnGround && m_BDoJump) || (!m_BOnGround && m_AllowAirJumpCount > 0 && m_BDoJump))
{
m_BAnimJumping = true;
}
else if (m_BAnimFalling)
{
m_BAnimJumping = false;
}
// 下蹲
m_BAnimCrouching = (m_BOnGround && ((m_BPressedCrouch && (!m_BAnimJumping || !m_BAnimFalling)) || m_BTopHasWall));
// 闲置
m_BAnimIdling = (m_BOnGround && !m_BAnimCrouching && !m_BAnimFalling);
}
/// <summary>
/// @brief 输入检测
/// </summary>
private void CheckInput()
{
// 检测跳跃键【GetButtonDown 这个函数,一直按下也只会算一次,需要松开再按才算下一次】
//Debug.LogWarning(Input.GetButtonDown("Jump"));
if (Input.GetButtonDown("Jump") && m_CurrentAllowAirJumpCount > 0)
{
// 如果可以跳跃,执行跳跃相关
m_BDoJump = true;
}
// 检测下蹲键
m_BPressedCrouch = Input.GetButton("Crouch");
}
/// <summary>
/// @brief 检测上方是否有墙壁
/// </summary>
private void CheckTopHasWall()
{
float yOffset = -0.2f; // 检测上方是否有墙壁的偏移量
// 射线起点
Vector2 leftStart2 = new Vector2(transform.position.x + m_CheckTopHasWall_Left_XOffset, transform.position.y + yOffset);
Vector2 rightStart2 = new Vector2(transform.position.x + m_CheckTopHasWall_Right_XOffset, transform.position.y + yOffset);
// 射线方向
Vector2 direction2 = Vector2.up;
#if DEBUG // 调试用变量
Vector3 leftStart3 = new Vector3(transform.position.x + m_CheckTopHasWall_Left_XOffset, transform.position.y + yOffset, 0.0f);
Vector3 rightStart3 = new Vector3(transform.position.x + m_CheckTopHasWall_Right_XOffset, transform.position.y + yOffset, 0.0f);
Vector3 direction3 = Vector3.up;
#endif
// 射线持续时间(Debug)
float durationTime = 0.0f;
RaycastHit2D leftHitResult = Physics2D.Raycast(leftStart2, direction2, m_CheckTopHasWall_Distance, m_LMGround);
RaycastHit2D rightHitResult = Physics2D.Raycast(rightStart2, direction2, m_CheckTopHasWall_Distance, m_LMGround);
if (leftHitResult || rightHitResult)
{
m_BTopHasWall = true;
// Debug
#if DEBUG
Debug.DrawLine(leftStart3, leftStart3 + direction3 * m_CheckTopHasWall_Distance, Color.green, durationTime); // 绿
Debug.DrawLine(rightStart3, rightStart3 + direction3 * m_CheckTopHasWall_Distance, Color.green, durationTime); // 绿
#endif
}
else
{
m_BTopHasWall = false;
// Debug
#if DEBUG
Debug.DrawLine(leftStart3, leftStart3 + direction3 * m_CheckTopHasWall_Distance, Color.red, durationTime); // 红
Debug.DrawLine(rightStart3, rightStart3 + direction3 * m_CheckTopHasWall_Distance, Color.red, durationTime); // 红
#endif
}
}
/// <summary>
/// @brief 检测是否在地面上
/// </summary>
private void CheckOnGround()
{
// 射线起点
Vector2 leftStart2 = new Vector2(transform.position.x + m_CheckGround_Left_XOffset, transform.position.y + m_CheckGround_YOffset);
Vector2 rightStart2 = new Vector2(transform.position.x + m_CheckGround_Right_XOffset, transform.position.y + m_CheckGround_YOffset);
Vector2 leftTurnDirectionStart2 = new Vector2(leftStart2.x + 0.18f, leftStart2.y);
Vector2 rightTurnDirectionStart2 = new Vector2(rightStart2.x + 0.22f, rightStart2.y);
#if DEBUG
Vector3 leftStart3 = new Vector3(leftStart2.x, leftStart2.y, 0.0f);
Vector3 rightStart3 = new Vector3(rightStart2.x, rightStart2.y, 0.0f);
Vector3 leftTurnDirectionStart3 = new Vector3(leftTurnDirectionStart2.x, leftTurnDirectionStart2.y, 0.0f);
Vector3 rightTurnDirectionStart3 = new Vector3(rightTurnDirectionStart2.x, rightTurnDirectionStart2.y, 0.0f);
#endif
// 射线方向
Vector2 direction2 = Vector2.down;
Vector3 direction3 = Vector3.down;
// 射线持续时间(Debug)
float durationTime = 0.0f;
// 射线结果,该结构体也有重写 bool operator,所以可以直接用来判断
RaycastHit2D leftHitResult;
RaycastHit2D rightHitResult;
if (m_BTurnDirection) // 如果转向了
{
leftHitResult = Physics2D.Raycast(leftTurnDirectionStart2, direction2, m_CheckGround_Distance, m_LMGround);
rightHitResult = Physics2D.Raycast(rightTurnDirectionStart2, direction2, m_CheckGround_Distance, m_LMGround);
}
else // 如果没转向
{
leftHitResult = Physics2D.Raycast(leftStart2, direction2, m_CheckGround_Distance, m_LMGround);
rightHitResult = Physics2D.Raycast(rightStart2, direction2, m_CheckGround_Distance, m_LMGround);
}
if (leftHitResult.collider || rightHitResult.collider)
{
// 在地面上
m_BOnGround = true;
// 重置空中跳跃次数
m_CurrentAllowAirJumpCount = m_AllowAirJumpCount;
// 调试,击中为绿色
#if DEBUG
if (m_BTurnDirection)
{
Debug.DrawLine(leftTurnDirectionStart3, leftTurnDirectionStart3 + direction3 * m_CheckGround_Distance, Color.green, durationTime);
Debug.DrawLine(rightTurnDirectionStart3, rightTurnDirectionStart3 + direction3 * m_CheckGround_Distance, Color.green, durationTime);
}
else
{
Debug.DrawLine(leftStart3, leftStart3 + direction3 * m_CheckGround_Distance, Color.green, durationTime);
Debug.DrawLine(rightStart3, rightStart3 + direction3 * m_CheckGround_Distance, Color.green, durationTime);
}
#endif
}
else
{
// 不在地面上
m_BOnGround = false;
// 调试
#if DEBUG
if (m_BTurnDirection)
{
Debug.DrawLine(leftTurnDirectionStart3, leftTurnDirectionStart3 + direction3 * m_CheckGround_Distance, Color.red, durationTime);
Debug.DrawLine(rightTurnDirectionStart3, rightTurnDirectionStart3 + direction3 * m_CheckGround_Distance, Color.red, durationTime);
}
else
{
Debug.DrawLine(leftStart3, leftStart3 + direction3 * m_CheckGround_Distance, Color.red, durationTime);
Debug.DrawLine(rightStart3, rightStart3 + direction3 * m_CheckGround_Distance, Color.red, durationTime);
}
#endif
}
}
/// <summary>
/// @brief 水平移动
/// </summary>
private void HorizontalMove()
{
// 获取水平输入 [-1, 1],松开按键时为 0
float horizontalValue = Input.GetAxis("Horizontal");
m_Rb.velocity = new Vector2(horizontalValue * m_HorizontalSpeedFactorPerSecond * Time.fixedDeltaTime, m_Rb.velocity.y);
// Player 转向
if (horizontalValue < 0.0f) // 默认是右边
{
m_BTurnDirection = true;
transform.rotation = new Quaternion(0.0f, 180.0f, 0.0f, 1.0f);
}
else if (horizontalValue > 0.0f)
{
m_BTurnDirection = false;
transform.rotation = new Quaternion(0.0f, 0.0f, 0.0f, 1.0f);
}
}
/// <summary>
/// @brief 跳跃
/// </summary>
private void Jump()
{
if (m_BOnGround && m_BDoJump)
{
m_BDoJump = false;
m_Rb.velocity = new Vector2(m_Rb.velocity.x, m_JumpSpeedPerSecond * Time.fixedDeltaTime);
}
if (!m_BOnGround && m_CurrentAllowAirJumpCount > 0 && m_BDoJump)
{
// 空中跳跃
m_BDoJump = false;
--m_CurrentAllowAirJumpCount;
m_Rb.velocity = new Vector2(m_Rb.velocity.x, m_JumpSpeedPerSecond * Time.fixedDeltaTime);
}
}
/// <summary>
/// @brief 下蹲
/// </summary>
private void Crouch()
{
if (m_BPressedCrouch)
{
m_CapsuleCollider2D.size = new Vector2(m_CapsuleCollider2D.size.x, 0.6f);
m_CapsuleCollider2D.offset = new Vector2(m_CapsuleCollider2D.offset.x, -0.545f);
}
else if (!m_BTopHasWall) // 上方如果检测到有墙不能起来
{
m_CapsuleCollider2D.size = m_CapsuleCollider2DSize;
m_CapsuleCollider2D.offset = m_CapsuleCollider2DOffset;
}
}
}
D. 瓦片裂缝问题?
✨参考链接
这里使用调节运行时分辨率的方式解决,之后可以优化
E. 小节演示
[入门卷] 5. 相机跟随 Player 移动(简单代码实现)
回到顶部
可以使用插件或代码或者两者结合使用,这里简单点使用纯代码实现,之后可能会单独出模块介绍
A. 实现简单相机跟随
-
给
Hierarchy
窗口中的Main Camera
添加 脚本,命名为CameraController
-
示例代码如下
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraController : MonoBehaviour
{
public GameObject m_Player;
void Start()
{
m_Player = GameObject.Find("Player");
}
void Update()
{
transform.position = new Vector3(m_Player.transform.position.x, m_Player.transform.position.y, transform.position.z);
}
}
注意 Z轴坐标的调节 transform.position.z
附加:也可以用 Unity 中的插件
Cinemachine
实现,这里入门篇不做介绍
B. 小结演示
[入门篇] 6. 游戏场景切换
回到顶部
- 新建场景。Ctrl + N 或者在 Project xxx文件夹下 右键 Create -> Scene 或者 File -> New Scene -> Basic 2D (Build-in)
保存在 _Game/Scenes 文件夹下,这里命名为 Scene2
- 在场景中添加一个触发物品,这里用 Sunnyland 中自带的 Prop -> door 门。
- 右键
UI -> Panel
,在 Panel 上,右键UI -> Legacy -> Text
。
这里我将画板 Panel 改了名字,Panel_EnterDoor
-
这里点击 EnterDoor,在 Animation 窗口点击 Create 可以创建空动画,这里点击录制按钮进行录制,比较简单,这里不做详细介绍。Text 文本可以按喜好输入文字。
-
这里用到了 “预制体”,在 …/_Game/Prefabs 文件夹,例如,将 Player 拖入到此文件夹中,或者在 文件夹中 右键 Create -> Prefab,这里使用前者。更改文件夹中的预制体可以更改所有预制体实例,或者通过 Hierarchy 窗口中的 预制体实例右边的 小箭头进入也可以修改全部,但是仅仅修改 预制体实例,是只影响单个的。这里只是简单介绍 Prefab 的使用。
-
第二个场景如下
同样要 Tilemap,更之前创建方法一样,这里不过多介绍,相机脚本要重新赋值一下
有个问题说一下,还是网格裂缝问题,这里最好改一下网格大小,改成 0.99,改分辨率没啥用
先切换到第一个场景,这里可以改个名字,MainScene
A. 代码部分
- 给 EnterDoor 添加脚本组件,命名为 DoorController,实例代码如下
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class DoorController : MonoBehaviour
{
[SerializeField] private bool m_BAtDoorPosition = false;
[Header("UI 参数")]
public GameObject m_UIEnterDoor;
private void Start()
{
m_UIEnterDoor.SetActive(false);
}
private void Update()
{
if (m_BAtDoorPosition && Input.GetKeyDown(KeyCode.E))
{
// using UnityEngine.SceneManagement
// 加载当前激活场景的下一个场景
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1);
}
}
/// <summary>
/// @biref 进入碰撞体开关
/// </summary>
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision && collision.tag == "Player")
{
m_BAtDoorPosition = true;
m_UIEnterDoor.SetActive(true);
}
}
/// <summary>
/// @biref 退出碰撞体开关
/// </summary>
/// <param name="collision"></param>
private void OnTriggerExit2D(Collider2D collision)
{
if (collision && collision.tag == "Player")
{
m_BAtDoorPosition = true;
m_UIEnterDoor.SetActive(false);
}
}
}
这里用到了 Inspector 中的 Tag,将 Player 的 Tag 设置为 Player,EnterDoor 可以设置默认禁用(我们在代码中也设置了,不影响)
组件启用是 enable,GameObject 激活是 SetActive。
这里还是说一下更改预制体的操作
将脚本挂到 door 上
B. 美化文字和画布
双击 Text 文字编辑
双击 Panel_EnterDoor,即可进入编辑模式
这里还可以做动画,例如增加渐入渐出的效果,使用录制功能,这里暂时不做介绍,我想放在单独的模块介绍
C. Build Settings 设置场景顺序
在菜单栏 File -> Build Settings
代码中我们用到的是场景的序号,然后这里需要设置一下场景
直接拖拽即可,或者点击添加打开的场景 Add Open Scenes
Ok,基本完成了,现在你可以运行你的项目
D. 小节演示
二、杂项篇
回到顶部
[杂项篇] 1. Visual Studio Code 配置 Unity
回到顶部
安装 C#,Uniy Code Snippets 2个插即可件
-
C# 支持 C#;
-
Uniy Code Snippets 提供 Awake(),Update() 等方法提示;
-
在 Unity 编辑器中 Edit -> Preferences -> External Tools -> External Script Editor -> 找到 Visual Studio Code,没有的话可以手动 Brower 到安装目录下找到 Code.exe;
-
我这里有下载 Visual Studio 2019,有安装 “使用 Unity 的游戏开发” 组件,如果没有,需要单独安装开发包,可以去 Vscode 官网看看怎么配置 Unity,简单一点就装一下 VS Unity 组件。(装了还没有提示,在 Vscode 设置中 开一下 Omnisharp Auto Start 试一下)
[杂项篇] 2. 瓦片裂缝问题?
回到顶部
有两种简单的方法这里先介绍一下,可以调节运行时的分辨率,或者改变网格大小,例如默认是 1,1,1 可以改成 0.99, 0.99, 0.99
更新补充
2022/12/12
人物转向时可能有 Bug,所以后面改用下面这种
/// <summary>
/// @brief 水平移动
/// </summary>
private void HorizontalMove()
{
// 获取水平输入 [-1, 1],松开按键时为 0
float horizontalValue = Input.GetAxis("Horizontal");
m_Rb.velocity = new Vector2(horizontalValue * m_HorizontalSpeedFactorPerSecond * Time.fixedDeltaTime, m_Rb.velocity.y);
// Player 转向
if (horizontalValue < 0.0f) // 默认是右边
{
m_BTurnDirection = true;
transform.rotation = Quaternion.Euler(0.0f, 180.0f, 0.0f);
}
else if (horizontalValue > 0.0f)
{
m_BTurnDirection = false;
transform.rotation = Quaternion.Euler(0.0f, 0.0f, 0.0f);
}
}
三、最后
第一次写这么长的篇幅,不知道大家习不习惯,可以在评论区评论或者给我留言
入门卷还未完结,之后还会介绍敌人、光照等等
那么,本卷的内容就到这里了,下卷会继续分享 Unity2D 游戏开发入门相关知识文章来源:https://uudwc.com/A/yk6Xy
The End.文章来源地址https://uudwc.com/A/yk6Xy