Unity Sunny Land開發流程(二)
文章目錄
0.開發流程一
詳見https://blog.csdn.net/xiji333/article/details/109621328。
1.Animation Events
首先來修改之前程式碼中的一個問題,你可能已經發現了,就是當人物從高處落下(不是跳躍後落下)時,動畫並沒有切換到 f a l l i n g falling falling,依然是 i d l e idle idle狀態,而且此時落到敵人頭上並不會消滅敵人,因為動畫不是 f a l l i n g falling falling狀態,現在來解決這個問題吧:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class PlayerController : MonoBehaviour
{
void SwitchAnimation()
{
//animator.SetBool("idle", false);
if (rb2d.velocity.y < 0 && !boxCollider2D.IsTouchingLayers(ground)) //y軸速度<0且沒有接觸到地面時
{
animator.SetBool("falling", true);
}
}
}
記得也要修改
p
l
a
y
e
r
player
player的狀態機。
接下來製作敵人的動畫:
設定狀態機:
在修改程式碼之前,我們先來捋一下思路。我們要實現的是青蛙的移動,但是青蛙不能一直在跳躍呀,他需要適時的回到
i
d
l
e
idle
idle狀態,能不能找到一種方法,讓青蛙在
i
d
l
e
idle
idle動畫播放完畢後自動跳躍一次呢?可以,通過
A
n
i
m
a
t
i
o
n
E
v
e
n
t
s
Animation\ Events
Animation Events我們可以在某個動畫的某一幀設定一個事件,讓他呼叫某個函式。那麼我們可以在
i
d
l
e
idle
idle結束的時候設定一個
e
v
e
n
t
s
events
events,呼叫函式使青蛙進入跳躍狀態,同時我們還需要在
u
p
d
a
t
e
update
update內修改播放的動畫,而且青蛙移動的邏輯也需要修改了,如果青蛙在跳躍過程中轉向的話肯定會非常奇怪吧?所以我,我們需要提前判斷落點位置是否在地面上,如果不在的話就需要提前進行轉向啦。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy_Frog : MonoBehaviour
{
private Rigidbody2D rb2d;
private Animator animator;
private CircleCollider2D circleCollider2D;
public LayerMask ground;
//為了增加遊戲的多樣性 我們可以設定minSpeed和maxSpeed 隨機選擇中間的某個值作為青蛙的移動速度
public float minSpeed = 2.5f;
public float maxSpeed = 4f;
public float jumpForce = 4.5f;
[SerializeField]
private bool faceLeft = true;
//記錄初始時的y座標
private float initPositionY;
// Start is called before the first frame update
void Start()
{
rb2d = GetComponent<Rigidbody2D>();
animator = GetComponent<Animator>();
circleCollider2D = GetComponent<CircleCollider2D>();
initPositionY = transform.position.y;
}
// Update is called once per frame
void Update()
{
SwitchAnimation();
}
//這個函式將通過animation event呼叫!
void Movement()
{
Vector2 frontPosition = transform.position;
frontPosition.y = initPositionY;
float speed = Random.Range(minSpeed, maxSpeed);
//預測落地 這裡並沒有精確計算 不過和你的jumpForce有關係
if (faceLeft)
frontPosition += Vector2.left * speed;
else
frontPosition += Vector2.right * speed;
if (!Physics2D.Linecast(frontPosition + Vector2.down, frontPosition, ground) || Physics2D.Linecast(frontPosition, frontPosition + Vector2.up * jumpForce, ground)) //沒有檢測到地面 或者頭上有障礙物
{
faceLeft = !faceLeft;
transform.localScale = new Vector3(faceLeft ? 1 : -1, 1, 1); //角色反向
//注意此時應該結束這個函式 不然frog反向後依然會跳出去 然而你並不能確定這次跳躍的有效性
animator.Play("Frog_idle", 0, 0f);
return;
}
rb2d.velocity = new Vector2(faceLeft ? -speed : speed, jumpForce);
animator.SetBool("jumping", true);
}
void SwitchAnimation()
{
if (animator.GetBool("jumping")) //跳躍狀態
{
if (rb2d.velocity.y < 0)
{
animator.SetBool("jumping", false);
animator.SetBool("falling", true);
}
}
else if (animator.GetBool("falling")) //下落狀態
{
if (circleCollider2D.IsTouchingLayers(ground))
{
animator.SetBool("falling", false);
}
}
}
private void OnDrawGizmosSelected()
{
Vector2 frontPosition = new Vector2(transform.position.x, initPositionY) + Vector2.left * minSpeed * (faceLeft ? 1 : -1);
Gizmos.DrawLine(frontPosition + Vector2.down, frontPosition + Vector2.up * jumpForce);
frontPosition = new Vector2(transform.position.x, initPositionY) + Vector2.left * maxSpeed * (faceLeft ? 1 : -1);
Gizmos.DrawLine(frontPosition + Vector2.down, frontPosition + Vector2.up * jumpForce);
}
}
先扯一下
d
e
b
u
g
debug
debug的辛酸歷程,首先是青蛙的移動問題,程式碼中只判斷了落點的有效性,然而這並不能證明當前位置和落點之間都是地面,所以青蛙還是有機率會跳出地圖外,這取決於你地圖的搭建以及
m
i
n
S
p
e
e
d
、
m
a
x
S
p
e
e
d
、
j
u
m
p
F
o
r
c
e
minSpeed、maxSpeed、jumpForce
minSpeed、maxSpeed、jumpForce這三個數的值,不過對於我的地圖而言,上面的這種做法夠用了;其次是如何拿到落點的有效位置,我這裡只能做近似估計,而且要記錄青蛙初始的
y
y
y座標,否則基於當前座標再判斷落點的話是比較麻煩的;最後是一種比較尷尬的情況,因為
s
p
e
e
d
speed
speed是隨機的,那麼有可能出現青蛙往左跳往右跳都不行的情況,我的做法是讓青蛙原地不動,再次播放
i
d
l
e
idle
idle動畫,順便提一下,我的動畫的
l
o
o
p
loop
loop選項被關閉了,所以通過程式碼再次播放,如果你的動畫是重複播放的,那麼就無需通過程式碼控制。綜上,這種做法並不完美,但是我目前並不打算修改orz。
2.Class呼叫&死亡動畫&繼承多型
首先來製作老鷹:
然後製作老鷹的動畫:
接下來開始寫控制老鷹移動的程式碼,這裡用的邏輯和青蛙不一樣哦,我打算指定一個上下邊界,讓老鷹在這個邊界內上下移動:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy_Eagle : MonoBehaviour
{
private Rigidbody2D rb2d;
private float minPositionY, maxPositionY;
[SerializeField]
private bool isUp = false;
public float moveLength = 2.5f;
public float speed = 3.5f;
// Start is called before the first frame update
void Start()
{
rb2d = GetComponent<Rigidbody2D>();
minPositionY = transform.position.y - moveLength;
maxPositionY = transform.position.y + moveLength;
}
// Update is called once per frame
void Update()
{
Movement();
}
void Movement()
{
if (transform.position.y > maxPositionY)
isUp = false;
else if (transform.position.y < minPositionY)
isUp = true;
rb2d.velocity = new Vector2(0, isUp ? speed : -speed);
}
private void OnDrawGizmosSelected()
{
Gizmos.DrawLine(new Vector2(transform.position.x, minPositionY), new Vector2(transform.position.x, maxPositionY));
}
}
然後製作消滅青蛙的動畫:
現在來捋一下思路,我們之前是怎麼消滅青蛙的?是在人物的
O
n
C
o
l
l
i
s
i
o
n
E
n
t
e
r
2
D
OnCollisionEnter2D
OnCollisionEnter2D裡面直接銷燬了對應的遊戲物件。當遊戲比較簡單時這麼做無可厚非,但是現在青蛙死亡要播放一段動畫,如果把這段邏輯也交給人物來控制的話,是不是不太合理?我們希望青蛙自己控制這段邏輯,並向外提供一個介面供人呼叫即可。
o
k
ok
ok,那麼還有一個問題,青蛙要先播放完動畫再消滅自己,這個怎麼控制呢?當然是利用我們上一節剛學過的
e
v
e
n
t
s
events
events辣:
E
n
e
m
y
F
r
o
g
.
c
s
:
Enemy_Frog.cs:
EnemyFrog.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy_Frog : MonoBehaviour
{
private void Death()
{
Destroy(gameObject);
}
//供player呼叫
public void JumpOn()
{
animator.SetTrigger("death");
}
}
P
l
a
y
e
r
C
o
n
t
r
o
l
l
e
r
.
c
s
:
PlayerController.cs:
PlayerController.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class PlayerController : MonoBehaviour
{
……
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.tag == "Enemy")
{
if (animator.GetBool("falling")) //掉落狀態
{
collision.gameObject.GetComponent<Enemy_Frog>().JumpOn();
……
}
……
}
}
}
至此青蛙的死亡動畫已經做好了,但是先別激動,你還有老鷹的沒做23333。不過在動手之前,我想請你認真思考一下,如果再從頭實現老鷹的死亡動畫的話,是不是大部分程式碼都是重複的?如果以後還要增加新的敵人,那豈不是每個都要重寫,而且在人物的程式碼中還要具體區分每一個敵人。想想就頭痛,那有沒有更好的辦法呢?當然有,我們可以使用物件導向的思維,給所有的敵人寫一個基類,讓敵人繼承這個基類。這樣不僅可以提高程式碼的複用,還可以讓程式更加靈活。接下來將涉及到繼承多型等知識,大家可以先學習一波。
E
n
e
m
y
.
c
s
:
Enemy.cs:
Enemy.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy : MonoBehaviour
{
// Start is called before the first frame update
protected Animator animator;
protected virtual void Start()
{
animator = GetComponent<Animator>();
}
// Update is called once per frame
void Update()
{
}
private void Death()
{
Destroy(gameObject);
}
//供player呼叫
public void JumpOn()
{
animator.SetTrigger("death");
}
}
老鷹和青蛙需要繼承這個基類,同時它們不需要自己定義 a n i m a t o r animator animator變數了, D e a t h Death Death和 J u m p O n JumpOn JumpOn兩個函式也可以省去。
E n e m y F r o g . c s : Enemy_Frog.cs: EnemyFrog.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy_Frog : Enemy
{
protected override void Start()
{
//呼叫基類的start
base.Start();
}
P l a y e r C o n t r o l l e r . c s : PlayerController.cs: PlayerController.cs:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class PlayerController : MonoBehaviour
{
……
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.tag == "Enemy")
{
if (animator.GetBool("falling")) //掉落狀態
{
//注意這裡得到的元件是 所有敵人的基類 Enemy
collision.gameObject.GetComponent<Enemy>().JumpOn();
……
}
……
}
}
}
3.音效Audio
給
P
l
a
y
e
r
Player
Player新增背景音樂:
給兩個敵人新增爆炸的音效並修改程式碼:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy : MonoBehaviour
{
// Start is called before the first frame update
protected Animator animator;
protected AudioSource audioSource;
protected virtual void Start()
{
animator = GetComponent<Animator>();
audioSource = GetComponent<AudioSource>();
}
// Update is called once per frame
void Update()
{
}
private void Death()
{
Destroy(gameObject);
}
//供player呼叫
public void JumpOn()
{
animator.SetTrigger("death");
audioSource.Play();
}
}
人物新增跳躍、受傷、拾取櫻桃的音效並通過程式碼控制:
4.對話方塊Dialog&淡入動畫效果
這一節我們將建立一個對話方塊,用來在恰當的時機提示使用者如何進入下一個關卡,那麼就要用到
U
I
UI
UI了:
接下來設定它的位置,注意要修改錨點:
接下來為它新增一個子物體
T
e
x
t
Text
Text,並設定相關的屬性:
我想把房子的門當作下一關的入口,所以我需要一個碰撞體,並把它當作觸發器使用,同時建立一個指令碼用來控制對話方塊的顯示與關閉。如果你需要多個這樣的入口,那麼你可以自己實現一個預製體——帶有控制指令碼和碰撞體的空遊戲物件,這樣可以不用重新寫程式碼,只需要把它放置在合適的位置即可。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnterDialog : MonoBehaviour
{
// Start is called before the first frame update
public GameObject dialog;
void Start()
{
}
// Update is called once per frame
void Update()
{
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.tag == "Player")
{
dialog.SetActive(true);
}
}
private void OnTriggerExit2D(Collider2D collision)
{
if (collision.tag == "Player")
{
dialog.SetActive(false);
}
}
}
接下來給我們的對話方塊製作一段簡單的動畫效果吧~注意這次的動畫和之前的不太一樣,之前是通過多張圖片來製作的,這次要錄製動畫。點選下圖所示的紅色圓圈,就可以開始錄製了。
這個時候你可以修改對應物體(動畫所對應的物體)的顏色、透明度、位置、旋轉等等屬性,這些修改會以關鍵幀的形式表現出來:
以我製作的為例,第一幀對話方塊的透明度為
0
0
0,第二幀背景的透明度回到之前設定的值,文字部分保持不變,第三幀文字部分的透明度回到之前設定的值。這時候點選播放就可以看到淡入效果啦。
在開始下一章的內容之前,你需要自己搭建第二關的地形:
5.場景控制SceneManager&Invoke&BuildIndex
首先給我們的場景底部加一條
D
e
a
d
L
i
n
e
Dead\ Line
Dead Line,當玩家掉落到這條線以下時認為遊戲失敗,並重新載入第一個場景,我們希望這個載入可以滯後兩秒執行(
I
n
v
o
k
e
Invoke
Invoke),同時停止播放背景音樂:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class PlayerController : MonoBehaviour
{
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.tag == "Collection")
{
pickCherryAudio.Play();
Destroy(collision.gameObject);
++cherry;
cherryText.text = cherry.ToString();
}
else if (collision.tag == "DeadLine")
{
//注意這個只會停止播放第一個
GetComponent<AudioSource>().Stop();
//延遲2s後執行
Invoke(nameof(Restart), 2f);
}
}
void Restart()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
接下來製作第一關到第二關的跳轉。還記得我們之前製作的 d i a l o g dialog dialog嗎,它是用來提示玩家進入下一關卡的,那麼我們可以寫一個指令碼並將其掛在 d i a l o g dialog dialog上,這樣只有當 d i a l o g dialog dialog為活躍狀態時才能響應玩家的按鍵:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class EnterHouse : MonoBehaviour
{
// Update is called once per frame
void Update()
{
if (Input.GetKeyDown(KeyCode.E))
{
SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1);
}
}
}
上面函式的引數是場景的
b
u
i
l
d
I
n
d
e
x
buildIndex
buildIndex,在哪可以看到呢?
6.趴下效果Crouch&Input設定
在開始之前,我們需要設定一下趴下的按鍵:
然後製作趴下的動畫並設定狀態機:
控制動畫的程式碼很好寫,不多贅述。但是當人物下蹲時,我們也應該修改它的碰撞體的位置和大小,或者使用一個新的碰撞體,更進一步,我們不能無條件的信任玩家,如果他們下蹲進入了一個障礙物,然後站立起來,此時我們依然認為人物回到了
i
d
l
e
idle
idle狀態的話就會有問題,比如卡在障礙物裡面、播放錯誤的動畫等等。所以我們需要判斷玩家的頭上有沒有障礙物。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
public class PlayerController : MonoBehaviour
{
private Rigidbody2D rb2d;
private Animator animator;
private BoxCollider2D boxCollider2D;
[SerializeField]
private int cherry = 0;
[SerializeField]
private bool isHurt = false;
private Vector2 initBoxCollider2DOffset;
private Vector2 initBoxCollider2DSize;
private Vector2 crouchBoxCollider2DOffset = new Vector2(0, -0.57f);
private Vector2 crouchBoxCollider2DSize = new Vector2(0.92f, 0.85f);
public Text cherryText;
public float speed = 7f;
public float jumpForce = 7f;
public LayerMask ground;
public AudioSource jumpAudio;
public AudioSource hurtAudio;
public AudioSource pickCherryAudio;
// Start is called before the first frame update
void Start()
{
rb2d = GetComponent<Rigidbody2D>();
animator = GetComponent<Animator>();
boxCollider2D = GetComponent<BoxCollider2D>();
initBoxCollider2DOffset = boxCollider2D.offset;
initBoxCollider2DSize = boxCollider2D.size;
}
// Update is called once per frame
void Update()
{
if (!isHurt)//非受傷狀態
{
Movement();
}
SwitchAnimation();
}
void Movement()
{
float horizontalMove = Input.GetAxis("Horizontal");
int faceDirection = (int)Input.GetAxisRaw("Horizontal");
rb2d.velocity = new Vector2(horizontalMove * speed, rb2d.velocity.y);
animator.SetFloat("running", Mathf.Abs(horizontalMove));
if (faceDirection != 0)
{
transform.localScale = new Vector3(faceDirection, 1, 1);
}
if (Input.GetButtonDown("Jump") && boxCollider2D.IsTouchingLayers(ground))
{
rb2d.velocity = new Vector2(rb2d.velocity.x, jumpForce);
animator.SetBool("jumping", true);
jumpAudio.Play();
}
Crouch();
}
void Crouch()
{
//頭頂上沒有障礙物
if (!Physics2D.OverlapCircle(transform.position, 0.4f, ground))
{
//持續按下
if (Input.GetButton("Crouch"))
{
animator.SetBool("crouching", true);
boxCollider2D.offset = crouchBoxCollider2DOffset;
boxCollider2D.size = crouchBoxCollider2DSize;
}
else
{
animator.SetBool("crouching", false);
boxCollider2D.offset = initBoxCollider2DOffset;
boxCollider2D.size = initBoxCollider2DSize;
}
}
}
void SwitchAnimation()
{
//animator.SetBool("idle", false);
if (rb2d.velocity.y < 0 && !boxCollider2D.IsTouchingLayers(ground)) //y軸速度<0且沒有接觸到地面時
{
animator.SetBool("falling", true);
}
if (animator.GetBool("jumping")) //跳躍狀態
{
if (rb2d.velocity.y < 0)
{
animator.SetBool("jumping", false);
animator.SetBool("falling", true);
}
}
else if (animator.GetBool("falling")) //下落狀態
{
if (boxCollider2D.IsTouchingLayers(ground))
{
animator.SetBool("falling", false);
animator.SetBool("idle", true);
}
}
else if (isHurt) //受傷狀態
{
animator.SetBool("hurt", true);
int sign = rb2d.velocity.x < 0 ? -1 : 1;
rb2d.velocity += new Vector2(speed * Time.deltaTime, 0f) * -sign;
if (Mathf.Abs(rb2d.velocity.x) < 0.1f)
{
isHurt = false;
animator.SetBool("hurt", false);
}
}
}
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.tag == "Collection")
{
pickCherryAudio.Play();
Destroy(collision.gameObject);
++cherry;
cherryText.text = cherry.ToString();
}
else if (collision.tag == "DeadLine")
{
//注意這個只會停止播放第一個
GetComponent<AudioSource>().Stop();
//延遲2s後執行
Invoke(nameof(Restart), 2f);
}
}
void Restart()
{
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.tag == "Enemy")
{
if (animator.GetBool("falling")) //掉落狀態
{
//注意這裡得到的元件是 所有敵人的基類 Enemy
collision.gameObject.GetComponent<Enemy>().JumpOn();
rb2d.velocity = new Vector2(rb2d.velocity.x, jumpForce); //小跳效果
animator.SetBool("jumping", true);
}
else if (transform.position.x < collision.gameObject.transform.position.x)//左側
{
hurtAudio.Play();
isHurt = true;
rb2d.velocity = new Vector2(-7f, rb2d.velocity.y);
}
else if(transform.position.x > collision.gameObject.transform.position.x)//右側
{
hurtAudio.Play();
isHurt = true;
rb2d.velocity = new Vector2(7f, rb2d.velocity.y);
}
}
}
}
7.2D光效
這一節我們來做一些簡單的
2
D
2D
2D光效。我希望第二個場景總體是暗的,只有壁火和人物身上有光源。首先修改
t
i
l
e
m
a
p
tilemap
tilemap的渲染材質:
自己建立一個
m
a
t
e
r
i
a
l
material
material,把它新增到人物和門上面:
然後在合適的位置新增點光源吧!記得修改光源的
z
z
z軸:
8.優化程式碼Fix code
這一節用來優化之前的程式碼或者實現。首先可以清除 A n i m a t o r Animator Animator中一些沒用的條件,比如人物中的那個 i d l e idle idle變數。視訊中提到的其他問題我暫時沒有遇到(因為我的人物只有 1 1 1個碰撞體),所以不打算修改了,至於移動和跳躍的手感問題我打算放到最後說。
相關文章
- unity 開發2D坦克大戰教程 系列二Unity
- Unity3D開發入門教程(二)—— Lua入門Unity3D
- 研發流程(二)(轉)
- 【Unity3D開發小遊戲】《戰棋小遊戲》Unity開發教程Unity3D遊戲
- Unity匯入xLua流程Unity
- 開發流程
- Unity 消消樂開發思路Unity
- Django開發流程Django
- JNI開發流程
- Unity開發 資源準備Unity
- Android技術分享| 視訊通話開發流程(二)Android
- APP開啟(二)—標準流程APP
- npm元件開發流程NPM元件
- git合作開發流程Git
- MyBatis——MyBatis開發流程MyBatis
- Flutter Plugin開發流程FlutterPlugin
- ThinkPHP的開發流程PHP
- 軟體開發流程
- 遊戲開發流程遊戲開發
- 系統學習 TypeScript(二)——開發流程和語法規則TypeScript
- Unity3D獨立遊戲開發日記(二):擺放建築物Unity3D遊戲開發
- C#開發Unity遊戲教程之Unity中方法的引數C#Unity遊戲
- Unity——技能系統(二)Unity
- Android/iOS內嵌Unity開發示例AndroidiOSUnity
- Android Unity Plugin 開發指南(轉載)AndroidUnityPlugin
- 讀《Unity 3D遊戲開發》Unity3D遊戲開發
- sunny 攔截不成功解決
- APP開發具體流程APP
- 機器學習開發流程基礎機器學習
- app開發流程規範APP
- git團隊開發流程Git
- angular based app開發流程AngularAPP
- web專案開發流程Web
- 從零開始的Unity個人學習日記(二)Unity
- Lua遊戲開發(二)---cocos2d VS libgdx VS unity3d遊戲開發Unity3D
- 開發unity外掛——一次搞定unity編輯器常用功能Unity
- 【小松教你手遊開發】【unity實用技能】unity 記憶體除錯方法Unity記憶體除錯
- VR開發教程:Unity開發VR眼鏡中文入門教程四VRUnity