【Archive】背包,卡肉,镜头抖动,有限状态机

背包系统

  背包 GUI 的实现、ScriptableObject、数据的存储与读取。

层级结构

  • Bag 挂载 BagOnDrag 脚本
  • Grid 挂载 Grid Layout Group 组件,使其子物体(Slot)整齐地排放
  • Slot 挂载 Slot 脚本,更改其子物体(Item)以实现拖拽
  • Item 挂载 ItemOnDrag 脚本和 CanvasGroup 组件,以及作为 buttom 用于触发 Slot 中的 OnItemClicked()
  • Image 和 Count 负责物品图片和数量的显示

Grid Layout Group

后端

物品数据

以 ScriptableObject 的方式存储每一种物品的数据。

1
2
3
4
5
6
7
8
9
[CreateAssetMenu(fileName = "NewItem",menuName = "Inventory/New Item")]
public class InventoryItem : ScriptableObject
{
public string itemName;
public Sprite itemSprite;
public int itemCount = 1;
[TextArea]
public string itemInfo;
}

背包数据

以 ScriptableObject 的方式存储每个背包的数据,每个背包类内维护一个 List 用于存储多个物品。

1
2
3
4
5
[CreateAssetMenu(fileName = "NewBag", menuName = "Inventory/New Bag")]
public class InventoryBag : ScriptableObject
{
public List<InventoryItem> itemList = new List<InventoryItem>();
}

捡起物品

ItemOnWorld 挂载在物品对象上,随时更新两个 SO 的数据。

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
public class ItemOnWorld : MonoBehaviour
{
public InventoryItem inventoryItem;
public InventoryBag inventoryBag;

private void OnTriggerEnter2D(Collider2D collision) {
if (collision.CompareTag("Player")) {
AddItemToBag();
Destroy(gameObject);
}
}
void AddItemToBag() {
if (inventoryBag.itemList.Contains(inventoryItem)) {
inventoryItem.itemCount++;
}
else {
// 背包内不存在此物品
for (int i = 0; i < inventoryBag.itemList.Count; i++) {
// 顺序找到背包内第一个空余位置
if (inventoryBag.itemList[i] == null) {
inventoryBag.itemList[i] = inventoryItem;
break;
}
Debug.Log("Bag Over Flow!");
}
}
InventoryManager.UpdateGUI();
}
}

前端

Manager

单例模式的 InventoryManager,主要作用是提供更新 GUI 的接口 InventoryManager.UpdateGUI()

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
public class InventoryManager : MonoBehaviour
{
static InventoryManager instance;

public InventoryBag bag;
public GameObject grid;
public GameObject emptySlot;// 预制体Slot
public Text itemInfo;// 显示在背包左下角的UI
public List<GameObject> slotList = new List<GameObject>();

private void Awake() {
if (instance != null) {
Destroy(gameObject);
}
instance = this;
UpdateGUI();
}
public static void UpdateItemInfo(string info) {
instance.itemInfo.text = info;
}
public static void UpdateGUI() {
// 先清空Grid的子物体与slotList[]
instance.slotList.Clear();
for (int i = 0; i < instance.grid.transform.childCount; i++) {
Destroy(instance.grid.transform.GetChild(i).gameObject);
}

for (int i = 0; i < instance.bag.itemList.Count; i++) {
// 将emptySlot生成为grid的子物体,然后加入slotList[]
instance.slotList.Add(Instantiate(
instance.emptySlot, instance.grid.transform, false));

// 用itemList[]刷新slotList[]
instance.slotList[i].GetComponent<Slot>()
.InitSlot(instance.bag.itemList[i]);
}
}
}

Slot

负责 UpdateGUI() 中针对具体 Slot 中 Item 数据的更新,以及负责 bag 中物品描述的更新。

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
public class Slot : MonoBehaviour
{
public GameObject slotItem;
public Image slotImage;
public Text slotCount;
public string slotInfo;

private void OnEnable() {
InventoryManager.UpdateItemInfo("");
}
public void OnItemClicked() {
// 在作Item中以OnClick的形式调用
InventoryManager.UpdateItemInfo(slotInfo);
}
public void InitSlot(InventoryItem inventoryItem) {
// 将传入的Itme信息写入该Slot的子物体
if (inventoryItem == null) {
slotItem.SetActive(false);
return;
}
else {
slotImage.sprite = inventoryItem.itemSprite;
slotCount.text = inventoryItem.itemCount.ToString();
slotInfo = inventoryItem.itemInfo;
}
}
}

背包内物品的拖拽

实现 EventSystems 中三个接口以响应鼠标的交互。
更改 Item 的父级和 Item 的位置以实现拖拽效果。

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
using UnityEngine.EventSystems;
public class ItemOnDrag
: MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
public InventoryBag bag;

private Transform originalParent;
private int firstIndex, secondIndex;
public void OnBeginDrag(PointerEventData eventData) {
firstIndex = GetIndex(transform.parent);
originalParent = transform.parent;
// 将Item移至Canvas子集以不至于被其他UI遮挡
transform.SetParent(transform.parent.parent.parent.parent);
transform.position = eventData.position;
// 令所拖拽的Item本身不会遮挡鼠标射线
GetComponent<CanvasGroup>().blocksRaycasts = false;
}
public void OnDrag(PointerEventData eventData) {
transform.position = eventData.position;
// Debug.Log(eventData.pointerCurrentRaycast.gameObject.name);
}
public void OnEndDrag(PointerEventData eventData) {
GameObject crtObject = eventData.pointerCurrentRaycast.gameObject;
if (crtObject == null) {
// 拖至UI界面之外,Item复位
transform.SetParent(originalParent);
transform.position = originalParent.transform.position;
GetComponent<CanvasGroup>().blocksRaycasts = true;
return;
}
if (crtObject.name == "Image" || crtObject.name == "Count") {
// 拖至另一Item上,交换两者
Transform crtItem = crtObject.transform.parent;
Transform crtSlot = crtItem.parent;
secondIndex = GetIndex(crtSlot);

transform.SetParent(crtSlot);
transform.position = crtSlot.position;

crtItem.SetParent(originalParent);
crtItem.position = originalParent.position;
}
else if(crtObject.name == "Slot(Clone)") {
// 拖至空Slot
secondIndex = GetIndex(crtObject.transform);
transform.SetParent(crtObject.transform);
transform.position = crtObject.transform.position;
}
else {
// 拖至其他UI上,Item复位
transform.SetParent(originalParent);
transform.position = originalParent.transform.position;
GetComponent<CanvasGroup>().blocksRaycasts = true;
return;
}
Debug.Log("firstIndex: " + firstIndex + " " + "secondIndex: " + secondIndex);

// 更新itemList
InventoryItem tmp = bag.itemList[firstIndex];
bag.itemList[firstIndex] = bag.itemList[secondIndex];
bag.itemList[secondIndex] = tmp;

GetComponent<CanvasGroup>().blocksRaycasts = true;
}
private int GetIndex(Transform currentSlot) {
// 返回slot在grid中的顺序
Transform grid = currentSlot.parent;
for(int i=0;i< grid.childCount; i++) {
if (grid.GetChild(i) == currentSlot) {
return i;
}
}
return 1;
}
}

背包的拖拽

1
2
3
4
5
6
7
8
9
10
public class BagOnDrag : MonoBehaviour, IDragHandler
{
private RectTransform rectTransform;
private void Awake() {
rectTransform = GetComponent<RectTransform>();
}
public void OnDrag(PointerEventData eventData) {
rectTransform.anchoredPosition += eventData.delta;
}
}

存储 Scriptable Object

存储时
Scriptable Object 通过 JsonUtility.ToJson(so) 转换为 json。
json 通过 formatter.Serialize(file, json) 写入文件。

读取时
文件通过 formatter.Deserialize(file) 转换为 json。
json 通过 JsonUtility.FromJsonOverwrite(json, so) 写入 Scriptable Object。

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
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;

public class SaveLoadManager : MonoBehaviour
{
public InventoryBag inventoryBag;
public InventoryItem item1;
public InventoryItem item2;

private string PATH;
private void Awake() {
PATH = Application.persistentDataPath;
}
public void SaveGame() {
// 在bottom中被调用
Debug.Log(PATH);
if (!Directory.Exists(PATH + "/SaveData")) {
Directory.CreateDirectory(PATH + "/SaveData");
}

SaveData(inventoryBag, nameof(inventoryBag));
SaveData(item1, nameof(item1));
SaveData(item2, nameof(item2));
}
private void SaveData<T>(T so, string name) {
BinaryFormatter formatter = new BinaryFormatter();
FileStream file = File.Create(PATH + "/SaveData/" + name + ".bin");
string json = JsonUtility.ToJson(so);
formatter.Serialize(file, json);
file.Close();
}
public void LoadGame() {
// 在bottom中被调用
LodData(inventoryBag, nameof(inventoryBag));
LodData(item1, nameof(item1));
LodData(item2, nameof(item2));

InventoryManager.UpdateGUI();
}
private void LodData<T>(T so, string name) {
BinaryFormatter formatter = new BinaryFormatter();
if (File.Exists(PATH + "/SaveData/" + name + ".bin")) {
FileStream file =
File.Open(PATH + "/SaveData/" + name + ".bin", FileMode.Open);
string json = (string)formatter.Deserialize(file);
JsonUtility.FromJsonOverwrite(json, so);
file.Close();
}
}
}

相关链接

Scriptable Object
Create Asset Menu Attribute
Instantiate
Supported Events
Rect Transform
persistentDataPath
Directory
JsonUtility
BinaryFormatter

打击感

  一般来说,打击感由帧冻结、镜头抖动与运动、敌人的受击反馈(动画、音效、击退等)、特效、环境的反馈等组成。
动画器

连击

  攻击结束方法可以不在攻击动画的最后一帧调用,而是提前几帧。这是因为连击往往存在预输入,这样做可以提高连贯性,提升手感。

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
private Coroutine tmpCoroutine = null;// 仅在 Attack 中使用
private void Attack() {
if (!animator.GetBool(isAttackID) && !isHurt && !isDash && !isParryStance) {
// 可攻击的状态
if (uponGround) {
// 地面攻击

if (Input.GetMouseButton(0)) {
// 左键轻攻击
if (tmpCoroutine != null) {
StopCoroutine(tmpCoroutine);
}
/* 关闭协程确保这段时间内 isAttack 不会被设为 false
启动协程倒计时使 isAttack 为 false */
tmpCoroutine = StartCoroutine(WaitForAttackOver(4f / 14f));

body.velocity = Vector2.zero;
isAttack = true;
// 每次轻攻击时计时器重置为最大值
timer = interval;
combo++;
if (combo > finalMaxCombo) {
// combo 为 1 时播放一段攻击动画,为 2 时播放二段攻击动画
combo = 1;
}

animator.SetBool(isAttackID, true);
animator.SetTrigger(lightAttackID);
body.MovePosition(frontPoint.position);
animator.SetInteger(comboID, combo);
attackName = "Light";
}
if (Input.GetMouseButton(1)) {
// 右键重攻击
if (tmpCoroutine != null) {
StopCoroutine(tmpCoroutine);
}
tmpCoroutine = StartCoroutine(WaitForAttackOver(6f / 14f));

body.velocity = Vector2.zero;
isAttack = true;
animator.SetBool(isAttackID, true);
animator.SetTrigger(haveyAttackID);
attackName = "Heavy";
}
}
else {
// 空中攻击

if (Input.GetMouseButton(0)) {
// 空中普通攻击
animator.SetBool(isAttackID, true);
animator.SetTrigger(lightAttackID);
attackName = "Light";
}
if (Input.GetMouseButton(1)) {
// 下落攻击
if (tmpCoroutine != null) {
StopCoroutine(tmpCoroutine);
}
tmpCoroutine = StartCoroutine(WaitForAttackOver(6f / 14f));

body.velocity = Vector2.zero;
isAttack = true;
isSlam = true;
animator.SetBool(isAttackID, true);
animator.SetTrigger(haveyAttackID);
attackName = "Slam";
}
}
}
if (timer >= 0) {
// 倒计时结束后重置 combo 数
timer -= Time.deltaTime;
if (timer <= 0) {
combo = 0;
}
}
}

// 在Attack()中被调用
private IEnumerator WaitForAttackOver(float time) {
yield return new WaitForSeconds(time);
isAttack = false;
isSlam = false;
// 确保即使攻击动画被打断,animator 中的 isAttack 也会正确地被重置
animator.SetBool(isAttackID, false);
}

// 在每个攻击动画快结束时调用
public void AttackOver() {
animator.SetBool(isAttackID, false);
}

帧冻结

帧冻结结束后可以适当提升3-5倍的动画速度以补偿缺失的时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class AttackSense : MonoBehaviour
{
public static AttackSense instance;

private bool isShake;
private void Awake() {
instance = this;
}
public void FrameFreeze(int pauseFrame) {
StartCoroutine(PauseOnAttack(pauseFrame));
}
IEnumerator PauseOnAttack(int pauseFrame) {
float pauseTime = pauseFrame / 60f;
Time.timeScale = 0;
yield return new WaitForSecondsRealtime(pauseTime);
Time.timeScale = 1;
}
}

镜头抖动

为相机添加

为玩家的攻击碰撞体添加,并调整对应的参数

反馈

敌人的反馈会在有限状态机中进行具体的实现。

有限状态机

  有限状态机是一个具有有限数量状态的模型,并且同时只能处于一种状态,可以通过外部输入等方式触发状态之间的切换。这里实现一个简易的敌人 AI。

接口

所有的状态类都继承这个接口。

1
2
3
4
5
6
7
public interface IState
{
public void OnEnter();
public void OnUpdate();
public void OnFixedUpdate();
public void OnExit();
}

FSM

挂载在敌人身上,这里省略一些组件的定义与获取。一些对所有状态通用的方法也可以写在这里。

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
// 先创建一个枚举类存储所有状态,作为字典中的键
public enum StateType {
Idle, Run, Attack, Hurt, Throw, SpecialAttack
}
public class FSM : MonoBehaviour
{
private IState crtState = null;// 当前状态
private Dictionary<StateType, IState> stateLise =
new Dictionary<StateType, IState>();

void Awake() {
// 注册状态并将 FSM 自身传入状态
stateLise.Add(StateType.Idle, new IdleState(this));
stateLise.Add(StateType.Run, new RunState(this));
stateLise.Add(StateType.Attack, new AttackState(this));
stateLise.Add(StateType.Hurt, new HurtState(this));
stateLise.Add(StateType.Throw, new ThrowState(this));
stateLise.Add(StateType.SpecialAttack, new SpecialAttackState(this));

ChangeState(StateType.Idle);
}
private void Update() {
crtState.OnUpdate();
}
private void FixedUpdate() {
crtState.OnFixedUpdate();
}

public void ChangeState(StateType type) {
if (crtState != null) {
crtState.OnExit();
}
crtState = stateLise[type];
crtState.OnEnter();
}
}

状态类

以普通攻击状态为例。
动画在具体的状态类中进行控制,动画中包含了录制好的攻击碰撞器(Trigger)的启用与变形。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class AttackState : IState {
private FSM fsm;
private AnimatorStateInfo animeInfo;
// 在构造函数中获取 fsm
public AttackState(FSM fsmIn) {
this.fsm = fsmIn;
}
public void OnEnter() {
fsm.body.velocity = Vector2.zero;
fsm.animator.Play("Rogue_Attack");
}
public void OnUpdate() {
animeInfo = fsm.animator.GetCurrentAnimatorStateInfo(0);
if (animeInfo.normalizedTime >= 0.99f) {
fsm.ChangeState(StateType.Idle);
}
}
public void OnFixedUpdate() {

}
public void OnExit() {
fsm.idleStartTime = Time.time;
}
}

对象池

  对于需要大量或者多次反复实例化,且需要销毁的物体(比如子弹、残影等),频繁地创建与销毁物体对于性能的开销过大。

  故使用对象池,提前生成一定数量的空物体,在需要时取一进行启用与赋值,一段时间后进行回收,一定程度上减少了动态加载性能的消耗。

  这里实现一个角色冲锋产生残影的效果。

预制体

空物体预制体

对象池实现

由一单例控制空物体的预生成、启用和回收。

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
public class AfterimagePool : MonoBehaviour
{
public static AfterimagePool instance;
public GameObject imagePrefab;

private Queue<GameObject> pool = new Queue<GameObject>();
private void Awake() {
if(instance == null) {
instance = this;
}
FillPoll(10);
}

// 在对象自身脚本中调用
public void ReturnToPool(GameObject objectIn) {
objectIn.SetActive(false);
pool.Enqueue(objectIn);
}
public void FillPoll(int num) {
for (int i = 0; i < num; i++) {
GameObject newImage = Instantiate(imagePrefab);
newImage.transform.SetParent(transform);
ReturnToPool(newImage);
}
}

// Player 为冲刺状态时会在 FixedUpdate 中反复调用
public void TakeFromPool() {
if (pool.Count <= 0) {
// 对象池不够用时多生成几个预制件
FillPoll(5);
}
GameObject crtImage = pool.Dequeue();
crtImage.SetActive(true);
}
}

启用物体

在 OnEnable() 中将 Player 此刻的 Sprite、位置等信息赋给空物体,然后不断减小其 Alpha 值。

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
public class Afterimage : MonoBehaviour
{
public float activeTime;
[Range(0f, 1f)]
public float startAlpha;

private Transform player;
private SpriteRenderer thisSR, playerSR;
private float alpha, startTime;

private void OnEnable() {
player = GameObject.FindGameObjectWithTag("Player").transform;
thisSR = GetComponent<SpriteRenderer>();
playerSR = player.GetComponent<SpriteRenderer>();

alpha = startAlpha;
startTime = Time.time;

thisSR.sprite = playerSR.sprite;
transform.position = player.position;
transform.localScale = player.localScale;
transform.rotation = player.rotation;
StartCoroutine(DecreaseAlpha());
}
void Update() {
thisSR.color = new Color(0.5f, 0.5f, 1, alpha);
if (Time.time - startTime >= activeTime) {
AfterimagePool.instance.ReturnToPool(gameObject);
}
}
private IEnumerator DecreaseAlpha() {
while (alpha > 0) {
// alpha 需要在 activeTime 秒内从 startAlpha 减为 0
alpha -= (startAlpha / (activeTime * 50));
yield return new WaitForFixedUpdate();
}
}
}

单例模式

单例有且仅有一个静态的实例,可以直接通过这个实例调用方法,而无需实例化该类的对象。

简单实现

1
2
3
4
5
6
7
8
9
10
11
12
public class ExampleManager : MonoBehaviour
{
public static ExampleManager instance;
private void Awake(){
if (instance == null) {
instance = this;
}
}
public void ExampleFun(){
DO SOMETHING;
}
}

外部调用时只需写作

1
ExampleManager.instance.ExampleFun();

另一种做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ExampleManager : MonoBehaviour
{
private static ExampleManager instance;
public static ExampleManager Instance {
get {
if (instance == null) {
instance = FindObjectOfType<ExampleManager>();
}
return instance;
}
}
public void ExampleFun(){
DO SOMETHING;
}
}

外部调用时写作

1
ExampleManager.Instance.ExampleFun();

更优雅的做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ExampleManager<T> : MonoBehaviour where T : ExampleManager<T>
{
private static T instance;
public static ExampleManager Instance {
get {
return instance;
}
}
protected virtual void Awake(){
id(instance == null){
instance = (T)this;
}
}
public static bool IsInit{
get{
return (instance != null);
}
}
protected virtual void OnDestory(){
if(instance == this){
instance = null;
}
}
}

以后任何新的单例类只用继承这个泛型类即可。

我的 SoundManager

音效

  • 首先需要一个挂载 Manager 的 Object
  • 上一个场景的 Manager 实例需要带到下一个场景中去,故使用 DontDestroyOnLoad(gameObject)
  • 调试时若不从第一个场景进入则不存在该 Object,十分不便
  • 若每个场景都放置该 bject,大量不会销毁的 Object 会带着其上挂载的的 Manager 堆积起来
  • 故在 Awake 中做判断销毁任何新加载的 Object
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
public class SFXMananger : MonoBehaviour
{
public static SFXMananger instance;

[SerializeField]
private AudioClip hurtAudio, collectAudio, enemyDestoryAudio;
private AudioSource audioSource;
private void Awake() {
audioSource = GetComponent<AudioSource>();
if (instance == null) {
instance = this;
}
else if (instance != this) {
Destroy(gameObject);
}
DontDestroyOnLoad(gameObject);
}
public void HurtAudio() {
audioSource.clip = hurtAudio;
audioSource.Play();
}
public void CollectAudio() {
audioSource.clip = collectAudio;
audioSource.Play();
}
public void EnemyDestoryAudio() {
audioSource.clip = enemyDestoryAudio;
audioSource.Play();
}
}

bgm

在满足与音效相同的条件之上,反复加载同一场景时,我不希望 bgm 从头开始播放。

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
public class BGMManager : MonoBehaviour
{
public static BGMManager instance;
public static int sceneIndex;

[SerializeField]
private AudioClip openingBGM, scene1BGM, scene2BGM, scene3BGM, endingBGM, defautBGM;
private AudioSource audioSource;
private void Awake () {
audioSource = GetComponent<AudioSource>();
int crtIndex = SceneManager.GetActiveScene().buildIndex;
if (instance == null) {
// 只有首个 Manager 加载时会进入这个分支
instance = this;
sceneIndex = crtIndex;
}
else if (instance != this) {
Destroy(gameObject);
if (sceneIndex != crtIndex) {
// 根据关卡编号切换 bgm,重新加载同一关时则不需要切换
instance.SwitchBGM(crtIndex);
sceneIndex = crtIndex;
}
}
DontDestroyOnLoad(gameObject);
}
public void SwitchBGM(int i) {
switch (i) {
case 0:
audioSource.clip = openingBGM;
audioSource.Play();
break;
case 1:
audioSource.clip = scene1BGM;
audioSource.Play();
break;
case 2:
audioSource.clip = scene2BGM;
audioSource.Play();
break;
case 3:
audioSource.clip = scene3BGM;
audioSource.Play();
break;
case 4:
audioSource.clip = endingBGM;
audioSource.Play();
break;
default:
audioSource.clip = defautBGM;
audioSource.Play();
break;
}
}
}

协程

  协程就像一个函数,能够暂停执行并将控制权返还给 Unity 一段时间,可以是等待一帧,可以是等待几秒,也可以是等待另一个协程。当需要一个函数的内容(如一个循环)不需要在单帧内执行完成时往往会使用协程。

使用

1
2
3
4
5
6
7
8
IEnumerator Fun(){
DO SOMETHING;
yield return null;// 暂停执行并在下一帧恢复的点
yield return new WaitForSeconds(.1f);// 暂停执行并等待 0.1s 后恢复的点
yield return StartCoroutine(Fun2());// 暂停执行并等待 Fun2() 返还控制权
}

StartCoroutine(Fun());

我的主菜单

间隔dt秒依次激活所有 UI。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MainMenu : MonoBehaviour
{
public GameObject titleUI, startUI, endUI;
void Start() {
StartCoroutine(Delayed(0.65f, titleUI, startUI, endUI));
}
IEnumerator Delayed(float dt, params GameObject[] list) {
foreach (GameObject obj in list) {
yield return new WaitForSeconds(dt);
obj.SetActive(true);
}
}
}

相关链接

Bounds
MovePosition
Animator
AnimatorStateInfo
SpriteRenderer
SceneManager
Scene
DontDestroyOnLoad
Coroutines
YieldInstruction