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);
}
}
這裡也用到了自定義的計時器,用於控制時間間隔。
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在製作環境、背景時非常有用,可以製作景深、霧氣、特殊的前後遮擋效果等等。
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中配置。