构建对话系统
构建对话UI的canvas,并完成内容显示
创建过程中需要注意如果要保证对话框内容主要是通过
以下三个组件来控制
创建对话数据实现对话逻辑
该部分的主要实现方式是构建一个专门记录对话信息的数据结构,然后可以用SO文件的方式来存储,也可以直接挂载到某个人物身上。当与人物触发对话的时候,就通过事件系统,不断将对话数据传递到对话的Canvas
中,显示对应数据。
可以在每一个对话部分都构建一个事件,当对话完成后,可以直接调用事件,创造更多的内容
1
2
3
4
5
6
7
8
9
10
11
|
[System.Serializable]
public class DialoguePiece
{
[Header("对话详情")] public Sprite faceImage;
public bool onLeft; /*人物头像的位置*/
public string characterName;
[TextArea] public string dialogueText;
public bool hasToPause; /* 是否需要停止,即还有下一句话 */
[HideInInspector]public bool isDone; /* 对话是否完成 */
// public UnityEvent afterDialogueEvent; /*对话完成后的事件*/
}
|
然后为每个NPC挂载操作对话数据的内容,然后使用栈来控制对话数据,如果数据需要被展示,则通过事件通知的方法,告诉DialogueUI
显示对话内容
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
|
[RequireComponent(typeof(BoxCollider2D))]
[RequireComponent(typeof(NpcMovement))]
public class DialogueController : MonoBehaviour
{
public List<DialoguePiece> dialoguePieces = new List<DialoguePiece>();
private NpcMovement NpcMovement => GetComponent<NpcMovement>();
public UnityEvent onFinishEvent;
private Stack<DialoguePiece> _dialoguePieceStack = new Stack<DialoguePiece>();
private bool _canTalk;
private bool _isTalking; /* 防止生成过多协程 */
private GameObject _uiSign;
private void Awake()
{
_uiSign = transform.GetChild(1).gameObject;
FillDialogueStack();
}
private void OnTriggerEnter2D(Collider2D other)
{
if (other.CompareTag("Player"))
{
_canTalk = !NpcMovement.isMoving && NpcMovement.interactable;
}
}
private void OnTriggerExit2D(Collider2D other)
{
if (other.CompareTag("Player"))
{
_canTalk = false;
}
}
private void Update()
{
_uiSign.SetActive(_canTalk);
if (Input.GetKeyDown(KeyCode.Space) && _canTalk && !_isTalking)
{
StartCoroutine(DialogueRoutine());
}
}
/// <summary>
/// 填充对话
/// </summary>
private void FillDialogueStack()
{
/* 由于是入栈,因此采用倒序的方式 */
for (int i = dialoguePieces.Count - 1; i >= 0; i--)
{
dialoguePieces[i].isDone = false;
_dialoguePieceStack.Push(dialoguePieces[i]);
}
}
private IEnumerator DialogueRoutine()
{
_isTalking = true;
if (_dialoguePieceStack != null && _dialoguePieceStack.Count > 0)
{
if (_dialoguePieceStack.TryPop(out DialoguePiece result))
{
/* 将结果发送到UI界面更新UI */
EventHandler.CallShowDialogueEvent(result);
yield return new WaitUntil(() => result.isDone); /* 直到点击空格,才能显示后续内容 */
_isTalking = false;
}
}
else
{
EventHandler.CallShowDialogueEvent(null);
FillDialogueStack();
_isTalking = false;
onFinishEvent?.Invoke();
}
}
}
|
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
|
public class DialogueUI : MonoBehaviour
{
public GameObject dialogueBox;
public Text dialogueText;
public Image faceRight, faceLeft;
public TextMeshProUGUI nameRight, nameLeft;
public GameObject continueBox;
private void Awake()
{
dialogueBox.SetActive(false);
continueBox.SetActive(false);
}
private void OnEnable()
{
EventHandler.ShowDialogueEvent += OnShowDialogueEvent;
}
private void OnDisable()
{
EventHandler.ShowDialogueEvent -= OnShowDialogueEvent;
}
#region 注册事件
private void OnShowDialogueEvent(DialoguePiece piece)
{
StartCoroutine(ShowDialogue(piece));
}
#endregion
#region 显示对话窗口
/* 由于要文字一点一点打出来,因此需要使用协程方法 */
private IEnumerator ShowDialogue(DialoguePiece piece)
{
if (piece is not null)
{
piece.isDone = false;
dialogueBox.SetActive(true);
continueBox.SetActive(false);
dialogueText.text = String.Empty;
if (piece.characterName != String.Empty)
{
faceLeft.gameObject.SetActive(piece.onLeft);
faceRight.gameObject.SetActive(!piece.onLeft);
if (piece.onLeft)
{
faceLeft.sprite = piece.faceImage;
nameLeft.text = piece.characterName;
}
else
{
faceRight.sprite = piece.faceImage;
nameRight.text = piece.characterName;
}
}
else
{
faceLeft.gameObject.SetActive(false);
faceRight.gameObject.SetActive(false);
nameLeft.gameObject.SetActive(false);
nameRight.gameObject.SetActive(false);
}
/*等当前语句执行结束才能继续下一步*/
yield return dialogueText.DOText(piece.dialogueText, 1f).WaitForCompletion();
piece.isDone = true;
if (piece.hasToPause && piece.isDone)
continueBox.SetActive(true);
else
continueBox.SetActive(true);
}
else
{
dialogueBox.SetActive(false);
yield break;
}
}
#endregion
}
|
实现物品交易
以角色背包为基础,设计一个通用背包的UI,该UI以内容完全自适应为基础,即背包的大小会随着内容的大小而改变,因此每次都需要重新生成物品内容的slot.
如果父物体设置了Vertical Layout Group,但是子物体向摆脱父物体的自适应,则需要为子物体设置LayoutElement
,然后选择忽略布局。
然后再InventoryUI
中设置需要的变量,为通用背包赋值
1
2
3
|
[Header("通用背包")] [SerializeField] private GameObject baseBag;
public GameObject shopSlotPrefab;
[SerializeField] private List<SlotUI> baseBagSlots;
|
设置对话结束事件,为用户打开商店做准备,由于通知NPC打开背包是再NPC身上调用的,因此还是需要通过事件的方式来触发打开背包,然后让Inventory来注册背包打开的函数。最后在完成对话后直接调用该事件即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public class NpcFunction : MonoBehaviour
{
public InventoryBag_SO shopData;
private bool _isOpen;
private void Update()
{
if (Input.GetKeyDown(KeyCode.Escape) && _isOpen)
{
_isOpen = false;
// 关闭背包
}
}
public void OpenShop()
{
_isOpen = true;
EventHandler.CallBaseBagOpenEvent(SlotType.Shop, shopData);
}
}
|
-
inventoryUI
打开商店事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
private void OnBaseBagOpenEvent(SlotType slotType, InventoryBag_SO bagSo)
{
GameObject slotPrefab = slotType switch
{
SlotType.Shop => shopSlotPrefab,
_ => null
};
baseBag.SetActive(true);
for (int i = 0; i < bagSo.inventoryItemList.Count; i++)
{
SlotUI slot = Instantiate(slotPrefab, baseBag.transform.GetChild(1)).GetComponent<SlotUI>();
slot.slotIndex = i;
baseBagSlots.Add(slot);
}
/* 强制更新界面 */
LayoutRebuilder.ForceRebuildLayoutImmediate(baseBag.GetComponent<RectTransform>());
/* 更新格子 */
OnUpdateInventoryUI(InventoryLocation.Shop, bagSo.inventoryItemList);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// 更新界面的方法
case InventoryLocation.Shop:
for (int i = 0; i < items.Count; i++)
{
if (items[i].itemAmount > 0)
{
// 获得需要更新的物体的具体信息
var itemDetails = InventoryManager.Instance.GetItemDetails(items[i].itemID);
baseBagSlots[i].UpdateSlot(itemDetails, items[i].itemAmount);
}
else
{
baseBagSlots[i].UpdateEmptySlot();
}
}
break;
|
强制更新UI界面需要使用
1
|
LayoutRebuilder.ForceRebuildLayoutImmediate(baseBag.GetComponent<RectTransform>());
|
实现物品交易结束以及拖拽交易
注册交易结束事件,然后点击关闭的时候调用
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// inventory中注册的关闭事件的方法
private void OnBaseBagCloseEvent(SlotType slotType, InventoryBag_SO bagSo)
{
baseBag.SetActive(false);
itemTooltip.gameObject.SetActive(false);
UpdateSlotHighlight(-1);
foreach (var slot in baseBagSlots)
{
Destroy(slot.gameObject);
}
baseBagSlots.Clear();
}
|
- 使用不同的状态来表示游戏的运行状态(
GamePlay
和Pause
)
1
2
3
4
5
6
|
public static event Action<GameState> UpdateGameStateEvent;
public static void CallUpdateGameStateEvent(GameState gameState)
{
UpdateGameStateEvent?.Invoke(gameState);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// player.cs中注册人物移动
private void OnUpdateGameStateEvent(GameState gameState)
{
switch (gameState)
{
case GameState.Gameplay:
_inputDisable = false;
break;
case GameState.Pause:
_inputDisable = true;
break;
}
}
|
然后在不同的需要控制玩家点击和不能移动的地方调用该事件
保证打开人物背包的时候修改背包的锚点位置即可
1
2
3
4
5
6
7
|
/* Inventory.cs 中OnBaseBagOpenEvent()打开人物背包 */
if (slotType == SlotType.Shop)
{
bagUI.GetComponent<RectTransform>().pivot = new(-1, 0.5f);
bagUI.SetActive(true);
bagOpened = true;
}
|
在slotUI
拖拽的位置设置对应的处理事件,然后触发交易UI,然后根据交易UI的具体内容,和是否贩卖的标志来完成具体的交易内容
1
2
3
4
5
6
7
8
9
10
11
|
// SlotUI.cs
// OnEndDrag()方法中
/* 物品买卖 */
else if (slotType == SlotType.Shop && targetSlot.slotType == SlotType.Bag)
{
EventHandler.CallShowTradeUIEvent(slotItemDetails, false);
}
else if (slotType == SlotType.Bag && targetSlot.slotType == SlotType.Shop)
{
EventHandler.CallShowTradeUIEvent(slotItemDetails, true);
}
|
然后构造交易UI
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
|
public class TradeUI : MonoBehaviour
{
public Image itemIcon;
public Text itemName;
public InputField tradeAmount;
public Button submitBtn;
public Button cancelBtn;
private ItemDetails _itemDetails;
private bool _isSellTrade;
private void Awake()
{
cancelBtn.onClick.AddListener(CancelTrade);
}
public void SetTradeUI(ItemDetails item, bool isSell)
{
this._itemDetails = item;
itemIcon.sprite = item.itemIcon;
itemName.text = item.itemName;
_isSellTrade = isSell;
tradeAmount.text = string.Empty;
}
public void CancelTrade()
{
this.gameObject.SetActive(false);
}
}
|
注意在此过程中需要随时根据具体情况修改游戏运行状态
在InventoryManager
中实现交易的具体过程,即对玩家的金钱和物品内容的处理,然后更新UI,然后在UI界面注册对该方法的执行。
具体的交易执行方法
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
|
public void TradeItem(ItemDetails item, bool isSell, int tradeAmount)
{
/* 计算金额 */
int totalCost = item.itemPrice * tradeAmount;
int realCost = 0;
/* 获得背包位置,检测背包是否有当前物品 */
int itemIndexId = GetItemIndexInBag(item.itemID); // 如果没有返回-1
if (isSell)
{
// 有足够的物品才卖出
if (itemIndexId != -1 && playerBag.inventoryItemList[itemIndexId].itemAmount >= tradeAmount)
{
RemoveItem(item.itemID, tradeAmount);
realCost = (int)(totalCost * item.sellPercentage);
playerCash += realCost;
}
}
else
{
if (playerCash >= totalCost && CheckBagCapacity() != -1)
{
playerCash -= totalCost;
AddItemInIndex(item.itemID, itemIndexId , tradeAmount);
}
}
EventHandler.CallUpdateInventoryUI(InventoryLocation.Player, playerBag.inventoryItemList);
}
|
然后在TradeUI.cs
中实现对上述执行方法的调用
1
2
3
4
5
6
7
|
private void TradeItem()
{
/* 将文本转化为具体需要执行的数值 */
var amount = Convert.ToInt32(tradeAmount.text);
InventoryManager.Instance.TradeItem(_itemDetails, _isSellTrade, amount);
}
|
该方法需要通过对btn
绑定监听器
1
|
submitBtn.onClick.AddListener(TradeItem);
|
最后注意更新玩家的金钱即可。