Unity2d速通
1 概述
关于Unity正文前,先大致整理一些预备内容和框架。
-
Unity是一个平台/引擎,有很多库文件,开发者需要做两件事:利用Unity自带的图形化界面(Inspector面板)挂载调整;利用自己写的C#(Script脚本)调用库文件实现逻辑。
-
一个Unity工程有多个场景(Scene),每个场景下有多个对象(GameObject,也称物件),一个对象就是一个容器(Container),容器中有很多组件(Component),一个组件有很多属性(Attribute)。
-
通常一个Unity2d项目的对象包括:基础(Camera、EventSystem、Light等)、环境(Background、Midground、Foreground等)、游戏(Player、Enemy等)、用户界面(Canvas、UI Elements等)。
-
Unity的xyz坐标系是左手系。对于在3D场景中z坐标更小(朝外)但图层更低的对象,在2D中会被z坐标更大(朝里)的对象遮住。
-
做好的复用对象模版保存在预制体(Prefab)中。整体Assets文件夹通常包括Art、Animation、Prefab、Scene、Script等。
-
如果需要共享工程,只需要打包Assets、Packages、ProjectSettings三个文件夹。
2 C#
好了,让我们学习代码基础。
2.1 让变量显示在Inspector面板
两种方式:SerializeField(序列化字段)或Public(公有)。
[SerializeField] private float walkSpeed = 1;
public float jumpForce = 45f;
区别在于SerializeField仍保持封装不被外部脚本修改(推荐),Public的变量则是完全公开修改的。
通常为了易读性,这样组织变量定义:
[Header("Ground Check Settings:")]
[SerializeField] private Transform groundCheckPoint; //point at which ground check happens
[SerializeField] private float groundCheckY = 0.2f; //how far down from ground chekc point is Grounded() checked
[SerializeField] private float groundCheckX = 0.5f; //how far horizontally from ground chekc point to the edge of the player is
[SerializeField] private LayerMask whatIsGround; //sets the ground layer
[Space(5)]
可以在定义时赋值或不赋值。
2.2 生命周期函数
-
Awake():初始化,最先调用
-
OnEnable():对象被启用时调用
-
Start():在Awake()和OnEnable()之后调用
-
FixedUpdate():按固定间隔,多帧调用
-
Update():每帧调用
-
LateUpdate():在Update()之后,每帧调用
-
OnDisable():对象被禁用时调用
-
OnDestroy():对象被销毁时调用
单例模式(Singleton)的类在同一时刻只存在一个实例(比如玩家角色就是单例),并且提供全局访问点,是典型的Awake()函数实现:
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
}
else
{
Instance = this;
}
DontDestroyOnLoad(gameObject);
}
Start()通常用于获取组件和属性,为了避免后面每一帧Update()频繁调用,例如:
void Start()
{
pState = GetComponent<PlayerStateList>();
rb = GetComponent<Rigidbody2D>();
sr = GetComponent<SpriteRenderer>();
anim = GetComponent<Animator>();
gravity = rb.gravityScale;
Health = maxHealth;
Mana = mana;
manaStorage.fillAmount = Mana;
}
Update()处理每一帧动画,以下是一个完整的2D角色单帧处理流程:
void Update()
{
if (pState.cutscene) return;
GetInputs();
UpdateJumpVariables();
if (pState.dashing) return;
RestoreTimeScale();
FlashWhileInvincible();
Move();
Heal();
CastSpell();
if (pState.healing) return;
Flip();
Jump();
StartDash();
Attack();
}
FixedUpdate()用于处理多帧动作:
private void FixedUpdate()
{
if (pState.dashing || pState.healing || pState.cutscene) return;
Recoil();
}
2.3 获取Input输入
这些可以在Input Manager调整,代码如:
void GetInputs()
{
xAxis = Input.GetAxisRaw("Horizontal");
yAxis = Input.GetAxisRaw("Vertical");
attack = Input.GetButtonDown("Attack");
}
2.4 地面检测
这个功能通常需要完成两个步骤:将地面设置为专用图层,然后给对象添加地面检查点。
图层设置可以在Inspector中完成(指派Ground图层为whatIsGround),地面检查点作为子对象附加到父对象(groundCheck宽度小于父对象的Box Collider)。
代码可以这样实现:
public bool Grounded()
{
if (Physics2D.Raycast(groundCheckPoint.position, Vector2.down, groundCheckY, whatIsGround)
|| Physics2D.Raycast(groundCheckPoint.position + new Vector3(groundCheckX, 0, 0), Vector2.down, groundCheckY, whatIsGround)
|| Physics2D.Raycast(groundCheckPoint.position + new Vector3(-groundCheckX, 0, 0), Vector2.down, groundCheckY, whatIsGround))
{
return true;
}
else
{
return false;
}
}
这里Raycast(射线检测)指的是从起点沿着指定的方向发射一条射线,检测是否与任何物体发生碰撞。Vector2和Vector3分别代表二维向量与三维向量,Vector2.down是正下方,groundCheckX和groundCheckY是检测范围长度。
2.5 左右翻转
这可以通过一个状态bool变量实现。
public bool lookingRight;
然后编写代码:
void Flip()
{
if (xAxis < 0)
{
transform.localScale = new Vector2(-Mathf.Abs(transform.localScale.x), transform.localScale.y);
pState.lookingRight = false;
}
else if (xAxis > 0)
{
transform.localScale = new Vector2(Mathf.Abs(transform.localScale.x), transform.localScale.y);
pState.lookingRight = true;
}
}
Transform是少数不需要手动GetComponent()的组件,其三个属性分别是position、rotation、localscale。
另外移动的代码如下:
private void Move()
{
if (pState.healing) rb.velocity = new Vector2(0, 0);
rb.velocity = new Vector2(walkSpeed * xAxis, rb.velocity.y);
anim.SetBool("Walking", rb.velocity.x != 0 && Grounded());
}
2.6 帧间时间deltatime
为了避免帧率对时间造成影响,采用deltatime(每帧的时间流逝量)如:
void Attack()
{
timeSinceAttack += Time.deltaTime;
if (attack && timeSinceAttack >= timeBetweenAttack)
{
timeSinceAttack = 0;
anim.SetTrigger("Attacking");
if (yAxis == 0 || yAxis < 0 && Grounded())
{
Hit(SideAttackTransform, SideAttackArea, ref pState.recoilingX, recoilXSpeed);
Instantiate(slashEffect, SideAttackTransform);
}
else if (yAxis > 0)
{
Hit(UpAttackTransform, UpAttackArea, ref pState.recoilingY, recoilYSpeed);
SlashEffectAtAngle(slashEffect, 80, UpAttackTransform);
}
else if (yAxis < 0 && !Grounded())
{
Hit(DownAttackTransform, DownAttackArea, ref pState.recoilingY, recoilYSpeed);
SlashEffectAtAngle(slashEffect, -90, DownAttackTransform);
}
}
这里也用到了自定义的计时器,用于控制时间间隔。如果涉及Time.timeScale,那么Time.unscaledDeltaTime则是不受缩放影响的真实时间,Time.deltaTime会受到更改。
2.7 可视化调试OnDrawGizmos
这种方法只在编辑模式和游戏模式下的场景视图中起作用,不会在实际游戏运行时的屏幕上显示。
private void OnDrawGizmos()
{
Gizmos.color = Color.red;
Gizmos.DrawWireCube(SideAttackTransform.position, SideAttackArea);
Gizmos.DrawWireCube(UpAttackTransform.position, UpAttackArea);
Gizmos.DrawWireCube(DownAttackTransform.position, DownAttackArea);
}
2.8 协程Coroutine
协程用于处理需要一定时间完成的动作。以冲刺为例:
IEnumerator Dash()
{
canDash = false;
pState.dashing = true;
anim.SetTrigger("Dashing");
rb.gravityScale = 0;
int _dir = pState.lookingRight ? 1 : -1;
rb.velocity = new Vector2(_dir * dashSpeed, 0);
if (Grounded()) Instantiate(dashEffect, transform);
yield return new WaitForSeconds(dashTime);
rb.gravityScale = gravity;
pState.dashing = false;
yield return new WaitForSeconds(dashCooldown);
canDash = true;
}
3 面板
3.1 图像
PPU(Pixels Per Unit)越大,导入图片越小,画面越清晰。
Max Size是导入的图片在缩放下不超过的最大尺寸,如果过小会使得画面模糊。
Filter Mode是过滤模式,通常多线性的纹理效果优于点线性。
Wrap Mode是环绕模式,Repeat会在超出范围时重复显示,Clamp是钳制。
3.2 刚体
Rigidbody处理物体的物理运动,包括重力、速度、力。
注意如果子对象不是刚体,那么父对象的Collider是自己和子对象的Collider之和。此时子对象可以触发父对象的OnTriggerEnter2D()。
3.3 SortingLayer
Unity有两种自带的图层:Layer和Sorting Layer。前者如检测碰撞用于图层逻辑(逻辑分层),后者用于显示渲染覆盖(排序分层)。
Sorting Layer在制作环境、背景时非常有用,可以制作景深、雾气、特殊的前后遮挡效果等等。
如果UI物件和场景物体相互遮盖,可以在Layer中隐藏。
3.4 URP Lighting
2D Light在新版Unity中被放进URP(Universal Render Pipeline)Package中。
为了添加Lighting,需要把所有被Light的对象材质换成URP材质。
选择需要的Sorting Layer添加2D Light,适当调整颜色和强度以匹配环境。
4 动画
4.1 K帧
K帧即关键帧动画,通过在关键帧之间插值实现平滑过渡。
Unity的K帧动画在Animation窗口,朴素的方法是把预制的关键帧Sprite添加进去保存,会自动在Animator窗口生成状态机,然后可以在代码中调用如:
anim.SetBool("Walking", rb.velocity.x != 0 && Grounded());
这样可以控制Animator的标记变量实现条件状态转移。
4.2 骨骼动画
朴素的K帧消耗大量时间,这时可以用Unity的skinning editor处理分层的单张图片(推荐psb/psd,也可以是png;Unity可以自动切割图片,注意背景透明)。
通常先绑定(Rigging)骨骼(如要细致动画可在头发或衣服上添加分叉的骨骼),并在Visualization分配骨骼排序。然后使用Auto Geometry生成权重,应用后在需要动画的对象添加Sprite Skin组件创建骨骼。
如果需要为同一个物件准备多个Sprite,那么都对其作绑定,后续动画采用Sprite Resolver或Sprite Swap等组件设置同一个物件不同Sprite的可见性。
武器装备之类应当和角色骨骼层级相同,不应低于角色。
4.3 IK反向动力学
IK Manager可以实现子骨骼连续带动父骨骼转动的效果。
这通常是把手、脚等边缘骨骼挂载到IK Manager的List中,然后创建Target,并对翻转、约束作调整。
4.4 粒子系统ParticleSystem
粒子系统可以制作动画特效。
首先需要粒子的Material,然后在层次面板创建粒子系统,在Inspector中配置。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示