创建开始菜单面板和设置面板
- 添加 MenuForm 与 SettingForm - 为带左右按钮的水平选择界面添加HorizonSelectGroup预制件。 - 添加游戏核心可配置项
This commit is contained in:
@@ -0,0 +1,280 @@
|
||||
using DG.Tweening;
|
||||
using Definition.Enum;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
public class BottomDialogForm : DialogFormBase
|
||||
{
|
||||
public override DialogFormMode UIMode => DialogFormMode.BottomBox;
|
||||
|
||||
[SerializeField] private GameObject _speakerArea;
|
||||
|
||||
[SerializeField] private TMP_Text _speakerNameText;
|
||||
|
||||
[SerializeField] private TMP_Text _contentText;
|
||||
|
||||
[SerializeField] private Image _leftSprite;
|
||||
|
||||
[SerializeField] private Image _rightSprite;
|
||||
|
||||
[SerializeField] private int _leftSpritePosition = 450;
|
||||
|
||||
[SerializeField] private int _rightSpritePosition = -450;
|
||||
|
||||
[SerializeField] private float _moveDuration = 0.25f;
|
||||
|
||||
[SerializeField] private Ease _moveEase = Ease.OutCubic;
|
||||
|
||||
private readonly int _singleSpeakerCenterPosition = Screen.width / 2;
|
||||
|
||||
private string _leftSpeakerToken = string.Empty;
|
||||
private string _rightSpeakerToken = string.Empty;
|
||||
private Sequence _layoutSequence;
|
||||
|
||||
public override void StartDialog(DialogFormContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
Log.Warning("BottomDialogForm start failed. context is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
_context = context;
|
||||
|
||||
string speakerName = _context.SpeakerName;
|
||||
|
||||
if (_speakerArea != null)
|
||||
{
|
||||
_speakerArea.SetActive(!string.IsNullOrEmpty(speakerName));
|
||||
}
|
||||
|
||||
if (_speakerNameText != null)
|
||||
{
|
||||
_speakerNameText.text = speakerName;
|
||||
}
|
||||
|
||||
PlayTypewriter(_contentText, _context.Text, _context.PlayingSpeed);
|
||||
|
||||
if (string.IsNullOrEmpty(speakerName))
|
||||
{
|
||||
ClearSpeakerState();
|
||||
ApplySpeakerLayout(false, false, true);
|
||||
return;
|
||||
}
|
||||
|
||||
bool isRightSpeaker = context.Direction > 0;
|
||||
if (isRightSpeaker)
|
||||
{
|
||||
_rightSpeakerToken = speakerName;
|
||||
}
|
||||
else
|
||||
{
|
||||
_leftSpeakerToken = speakerName;
|
||||
}
|
||||
|
||||
bool hasLeftSpeaker = !string.IsNullOrEmpty(_leftSpeakerToken);
|
||||
bool hasRightSpeaker = !string.IsNullOrEmpty(_rightSpeakerToken);
|
||||
ApplySpeakerLayout(hasLeftSpeaker, hasRightSpeaker, false);
|
||||
}
|
||||
|
||||
protected override void OnClose(bool isShutdown, object userData)
|
||||
{
|
||||
ClearSpeakerState();
|
||||
KillLayoutTween();
|
||||
ApplySpeakerLayout(false, false, true);
|
||||
base.OnClose(isShutdown, userData);
|
||||
}
|
||||
|
||||
private void ClearSpeakerState()
|
||||
{
|
||||
_leftSpeakerToken = string.Empty;
|
||||
_rightSpeakerToken = string.Empty;
|
||||
}
|
||||
|
||||
private void KillLayoutTween()
|
||||
{
|
||||
if (_layoutSequence != null)
|
||||
{
|
||||
_layoutSequence.Kill();
|
||||
_layoutSequence = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplySpeakerLayout(bool hasLeftSpeaker, bool hasRightSpeaker, bool instant)
|
||||
{
|
||||
if (_leftSprite == null || _rightSprite == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool leftCurrentlyVisible = _leftSprite.gameObject.activeSelf;
|
||||
bool rightCurrentlyVisible = _rightSprite.gameObject.activeSelf;
|
||||
|
||||
bool leftTargetVisible = hasLeftSpeaker;
|
||||
bool rightTargetVisible = hasRightSpeaker;
|
||||
|
||||
float leftTargetX = GetTargetX(true, hasLeftSpeaker, hasRightSpeaker);
|
||||
float rightTargetX = GetTargetX(false, hasLeftSpeaker, hasRightSpeaker);
|
||||
|
||||
KillLayoutTween();
|
||||
|
||||
PrepareStartState(
|
||||
leftCurrentlyVisible,
|
||||
rightCurrentlyVisible,
|
||||
leftTargetVisible,
|
||||
rightTargetVisible,
|
||||
hasLeftSpeaker,
|
||||
hasRightSpeaker);
|
||||
|
||||
if (instant || _moveDuration <= 0f)
|
||||
{
|
||||
SetSpritePosition(_leftSprite.rectTransform, leftTargetX);
|
||||
SetSpritePosition(_rightSprite.rectTransform, rightTargetX);
|
||||
SetSpriteVisible(_leftSprite, leftTargetVisible);
|
||||
SetSpriteVisible(_rightSprite, rightTargetVisible);
|
||||
return;
|
||||
}
|
||||
|
||||
_layoutSequence = DOTween.Sequence();
|
||||
|
||||
Tween leftTween = CreateMoveTween(_leftSprite.rectTransform, leftTargetX);
|
||||
if (leftTween != null)
|
||||
{
|
||||
_layoutSequence.Join(leftTween);
|
||||
}
|
||||
|
||||
Tween rightTween = CreateMoveTween(_rightSprite.rectTransform, rightTargetX);
|
||||
if (rightTween != null)
|
||||
{
|
||||
_layoutSequence.Join(rightTween);
|
||||
}
|
||||
|
||||
if (_layoutSequence.active && _layoutSequence.Duration(false) > 0f)
|
||||
{
|
||||
_layoutSequence.OnComplete(() =>
|
||||
{
|
||||
SetSpriteVisible(_leftSprite, leftTargetVisible);
|
||||
SetSpriteVisible(_rightSprite, rightTargetVisible);
|
||||
_layoutSequence = null;
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
SetSpritePosition(_leftSprite.rectTransform, leftTargetX);
|
||||
SetSpritePosition(_rightSprite.rectTransform, rightTargetX);
|
||||
SetSpriteVisible(_leftSprite, leftTargetVisible);
|
||||
SetSpriteVisible(_rightSprite, rightTargetVisible);
|
||||
_layoutSequence.Kill();
|
||||
_layoutSequence = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void PrepareStartState(
|
||||
bool leftCurrentlyVisible,
|
||||
bool rightCurrentlyVisible,
|
||||
bool leftTargetVisible,
|
||||
bool rightTargetVisible,
|
||||
bool hasLeftSpeaker,
|
||||
bool hasRightSpeaker)
|
||||
{
|
||||
if (leftTargetVisible && !leftCurrentlyVisible)
|
||||
{
|
||||
float leftStartX = GetAppearStartX(true, rightCurrentlyVisible, hasLeftSpeaker, hasRightSpeaker);
|
||||
SetSpritePosition(_leftSprite.rectTransform, leftStartX);
|
||||
SetSpriteVisible(_leftSprite, true);
|
||||
}
|
||||
|
||||
if (rightTargetVisible && !rightCurrentlyVisible)
|
||||
{
|
||||
float rightStartX = GetAppearStartX(false, leftCurrentlyVisible, hasLeftSpeaker, hasRightSpeaker);
|
||||
SetSpritePosition(_rightSprite.rectTransform, rightStartX);
|
||||
SetSpriteVisible(_rightSprite, true);
|
||||
}
|
||||
|
||||
if (leftCurrentlyVisible && !leftTargetVisible)
|
||||
{
|
||||
SetSpriteVisible(_leftSprite, true);
|
||||
}
|
||||
|
||||
if (rightCurrentlyVisible && !rightTargetVisible)
|
||||
{
|
||||
SetSpriteVisible(_rightSprite, true);
|
||||
}
|
||||
}
|
||||
|
||||
private float GetAppearStartX(bool isLeft, bool otherCurrentlyVisible, bool hasLeftSpeaker,
|
||||
bool hasRightSpeaker)
|
||||
{
|
||||
if (hasLeftSpeaker && hasRightSpeaker)
|
||||
{
|
||||
// single -> multi: hidden side starts from center, then both move to side positions.
|
||||
if (otherCurrentlyVisible)
|
||||
{
|
||||
return _singleSpeakerCenterPosition;
|
||||
}
|
||||
|
||||
return isLeft ? _leftSpritePosition : _rightSpritePosition;
|
||||
}
|
||||
|
||||
// single appears: active side starts from its side and moves to center.
|
||||
return isLeft ? _leftSpritePosition : _rightSpritePosition;
|
||||
}
|
||||
|
||||
private float GetTargetX(bool isLeft, bool hasLeftSpeaker, bool hasRightSpeaker)
|
||||
{
|
||||
if (hasLeftSpeaker && hasRightSpeaker)
|
||||
{
|
||||
return isLeft ? _leftSpritePosition : _rightSpritePosition;
|
||||
}
|
||||
|
||||
if (hasLeftSpeaker || hasRightSpeaker)
|
||||
{
|
||||
// multi -> single: both move to center first, then inactive side hides.
|
||||
return _singleSpeakerCenterPosition;
|
||||
}
|
||||
|
||||
return isLeft ? _leftSpritePosition : _rightSpritePosition;
|
||||
}
|
||||
|
||||
private Tween CreateMoveTween(RectTransform rectTransform, float targetX)
|
||||
{
|
||||
if (rectTransform == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Mathf.Abs(rectTransform.anchoredPosition.x - targetX) < 0.01f)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return rectTransform.DOAnchorPosX(targetX, _moveDuration).SetEase(_moveEase);
|
||||
}
|
||||
|
||||
private static void SetSpriteVisible(Image spriteImage, bool visible)
|
||||
{
|
||||
if (spriteImage == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
spriteImage.gameObject.SetActive(visible);
|
||||
}
|
||||
|
||||
private static void SetSpritePosition(RectTransform rectTransform, float xPosition)
|
||||
{
|
||||
if (rectTransform == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 anchoredPosition = rectTransform.anchoredPosition;
|
||||
anchoredPosition.x = xPosition;
|
||||
rectTransform.anchoredPosition = anchoredPosition;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ea6f55294a4cd2946a83649e7be82314
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,391 @@
|
||||
using CustomComponent;
|
||||
using DG.Tweening;
|
||||
using Definition.Enum;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 可拖拽拼装部件:负责拖拽交互、出生点回退与放置到槽位。
|
||||
/// </summary>
|
||||
[RequireComponent(typeof(RectTransform))]
|
||||
[DisallowMultipleComponent]
|
||||
public class CombineDraggablePart : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IPointerEnterHandler, IPointerExitHandler
|
||||
{
|
||||
#region Inspector Config
|
||||
|
||||
[SerializeField] private CombinePartType _partType = CombinePartType.Dou;
|
||||
|
||||
[SerializeField] private string _partDisplayName = "Dou";
|
||||
|
||||
[SerializeField] [TextArea(2, 4)] private string _mechanicsExplanation = string.Empty;
|
||||
|
||||
[SerializeField] private bool _lockAfterPlaced = true;
|
||||
|
||||
[SerializeField] private RectTransform _rectTransform;
|
||||
|
||||
[SerializeField] private CanvasGroup _canvasGroup;
|
||||
|
||||
[SerializeField] private float _defaultScaleMultiplier = 0.8f;
|
||||
|
||||
[SerializeField] private float _hoverScaleMultiplier = 1f;
|
||||
|
||||
[SerializeField] private float _hoverTweenDuration = 0.15f;
|
||||
|
||||
[SerializeField] private Ease _hoverTweenEase = Ease.OutQuad;
|
||||
|
||||
[SerializeField] private float _returnToSpawnDuration = 0.4f;
|
||||
|
||||
[SerializeField] private Ease _returnToSpawnEase = Ease.OutCubic;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Spawn State
|
||||
|
||||
private bool _spawnStateCached;
|
||||
|
||||
private Transform _spawnParent;
|
||||
|
||||
private Vector3 _spawnWorldPosition = Vector3.zero;
|
||||
|
||||
private Quaternion _spawnWorldRotation = Quaternion.identity;
|
||||
|
||||
private Vector3 _spawnWorldScale = Vector3.one;
|
||||
|
||||
private int _spawnSiblingIndex;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Runtime State
|
||||
|
||||
private CombineComponent _controller;
|
||||
|
||||
private CombineSlot _currentSlot;
|
||||
|
||||
private bool _isPlaced;
|
||||
|
||||
private bool _isLocked;
|
||||
|
||||
private bool _isDragging;
|
||||
|
||||
private Vector3 _originalLocalScale = Vector3.one;
|
||||
|
||||
private Tween _scaleTween;
|
||||
|
||||
private Tween _returnTween;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Query
|
||||
|
||||
public CombinePartType PartType => _partType;
|
||||
|
||||
public string MechanicsExplanation => _mechanicsExplanation;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_rectTransform == null)
|
||||
{
|
||||
_rectTransform = transform as RectTransform;
|
||||
}
|
||||
|
||||
if (_canvasGroup == null)
|
||||
{
|
||||
_canvasGroup = gameObject.GetOrAddComponent<CanvasGroup>();
|
||||
}
|
||||
|
||||
_originalLocalScale = _rectTransform.localScale;
|
||||
ApplyScaleImmediate(_defaultScaleMultiplier);
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
KillScaleTween();
|
||||
KillReturnTween();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Setup
|
||||
|
||||
/// <summary>
|
||||
/// 绑定玩法控制器。
|
||||
/// </summary>
|
||||
public void BindController(CombineComponent controller)
|
||||
{
|
||||
_controller = controller;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用运行时数据初始化部件配置。
|
||||
/// </summary>
|
||||
public void Initialize(CombinePartContext data)
|
||||
{
|
||||
_partType = data.PartType;
|
||||
_partDisplayName = string.IsNullOrEmpty(data.PartDisplayName)
|
||||
? data.PartType.ToString()
|
||||
: data.PartDisplayName;
|
||||
_mechanicsExplanation = data.MechanicsExplanation ?? string.Empty;
|
||||
_lockAfterPlaced = data.LockAfterPlaced;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 缓存出生点状态(仅首次缓存)。
|
||||
/// </summary>
|
||||
public void CacheSpawnState()
|
||||
{
|
||||
if (_spawnStateCached)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_spawnStateCached = true;
|
||||
_spawnParent = _rectTransform.parent;
|
||||
_spawnWorldPosition = _rectTransform.position;
|
||||
_spawnWorldRotation = _rectTransform.rotation;
|
||||
_spawnWorldScale = _rectTransform.localScale;
|
||||
_spawnSiblingIndex = _rectTransform.GetSiblingIndex();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置部件到出生点,并清理槽位占用状态。
|
||||
/// </summary>
|
||||
public void ResetToSpawn()
|
||||
{
|
||||
ClearSlotOccupancy();
|
||||
_isDragging = false;
|
||||
_isPlaced = false;
|
||||
_isLocked = false;
|
||||
ReturnToSpawn();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Drag Flow
|
||||
|
||||
/// <summary>
|
||||
/// 开始拖拽:关闭射线阻挡并切到拖拽层级。
|
||||
/// </summary>
|
||||
public void OnBeginDrag(PointerEventData eventData)
|
||||
{
|
||||
if (!CanStartDrag())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
KillReturnTween();
|
||||
_isDragging = true;
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
MoveToDragRoot();
|
||||
_rectTransform.SetAsLastSibling();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 拖拽中:按指针增量更新锚点位置。
|
||||
/// </summary>
|
||||
public void OnDrag(PointerEventData eventData)
|
||||
{
|
||||
if (!CanDrag(eventData))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_rectTransform.anchoredPosition += eventData.delta;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 结束拖拽:恢复射线阻挡;未成功放置则回到出生点。
|
||||
/// </summary>
|
||||
public void OnEndDrag(PointerEventData eventData)
|
||||
{
|
||||
_isDragging = false;
|
||||
|
||||
if (_isLocked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_canvasGroup.blocksRaycasts = true;
|
||||
|
||||
if (!_isPlaced)
|
||||
{
|
||||
ReturnToSpawnAnimated();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnPointerEnter(PointerEventData eventData)
|
||||
{
|
||||
if (_isLocked || _isPlaced || _isDragging)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PlayScaleTween(_hoverScaleMultiplier);
|
||||
}
|
||||
|
||||
public void OnPointerExit(PointerEventData eventData)
|
||||
{
|
||||
if (_isLocked || _isPlaced || _isDragging)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
PlayScaleTween(_defaultScaleMultiplier);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验是否允许开始拖拽。
|
||||
/// </summary>
|
||||
private bool CanStartDrag()
|
||||
{
|
||||
return !_isLocked && !_isPlaced;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验拖拽中是否可更新位置。
|
||||
/// </summary>
|
||||
private bool CanDrag(PointerEventData eventData)
|
||||
{
|
||||
return !_isLocked && !_isPlaced && eventData != null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将部件切到控制器提供的拖拽根节点。
|
||||
/// </summary>
|
||||
private void MoveToDragRoot()
|
||||
{
|
||||
if (_controller == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Transform dragRoot = _controller.GetDragRoot();
|
||||
if (dragRoot != null)
|
||||
{
|
||||
_rectTransform.SetParent(dragRoot, true);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Placement
|
||||
|
||||
/// <summary>
|
||||
/// 将部件恢复到出生点状态。
|
||||
/// </summary>
|
||||
public void ReturnToSpawn()
|
||||
{
|
||||
KillReturnTween();
|
||||
|
||||
if (_spawnParent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_rectTransform.SetParent(_spawnParent, true);
|
||||
_rectTransform.position = _spawnWorldPosition;
|
||||
_rectTransform.rotation = _spawnWorldRotation;
|
||||
_rectTransform.localScale = _spawnWorldScale;
|
||||
_rectTransform.SetSiblingIndex(_spawnSiblingIndex);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用成功放置到槽位后的表现与状态。
|
||||
/// </summary>
|
||||
internal void PlaceToSlot(CombineSlot slot)
|
||||
{
|
||||
KillScaleTween();
|
||||
KillReturnTween();
|
||||
_isDragging = false;
|
||||
_currentSlot = slot;
|
||||
_isPlaced = true;
|
||||
_isLocked = _lockAfterPlaced;
|
||||
|
||||
RectTransform snapPoint = slot.SnapPoint;
|
||||
_rectTransform.SetParent(snapPoint != null ? snapPoint : slot.transform, false);
|
||||
_rectTransform.anchoredPosition = Vector2.zero;
|
||||
_rectTransform.localRotation = Quaternion.identity;
|
||||
_rectTransform.localScale = Vector3.one;
|
||||
_canvasGroup.blocksRaycasts = !_isLocked;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理当前槽位占用记录。
|
||||
/// </summary>
|
||||
private void ClearSlotOccupancy()
|
||||
{
|
||||
if (_currentSlot != null)
|
||||
{
|
||||
_currentSlot.ClearOccupiedPart(this);
|
||||
_currentSlot = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyScaleImmediate(float multiplier)
|
||||
{
|
||||
_rectTransform.localScale = _originalLocalScale * Mathf.Max(0f, multiplier);
|
||||
}
|
||||
|
||||
private void PlayScaleTween(float targetMultiplier)
|
||||
{
|
||||
KillScaleTween();
|
||||
_scaleTween = _rectTransform
|
||||
.DOScale(_originalLocalScale * Mathf.Max(0f, targetMultiplier), _hoverTweenDuration)
|
||||
.SetEase(_hoverTweenEase)
|
||||
.SetUpdate(true);
|
||||
}
|
||||
|
||||
public void ReturnToSpawnAnimated()
|
||||
{
|
||||
if (_spawnParent == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_returnToSpawnDuration <= 0f)
|
||||
{
|
||||
ReturnToSpawn();
|
||||
return;
|
||||
}
|
||||
|
||||
KillReturnTween();
|
||||
_rectTransform.SetParent(_spawnParent, true);
|
||||
_rectTransform.SetSiblingIndex(_spawnSiblingIndex);
|
||||
|
||||
_returnTween = DOTween.Sequence()
|
||||
.Append(_rectTransform.DOMove(_spawnWorldPosition, _returnToSpawnDuration))
|
||||
.Join(_rectTransform.DORotateQuaternion(_spawnWorldRotation, _returnToSpawnDuration))
|
||||
.Join(_rectTransform.DOScale(_spawnWorldScale, _returnToSpawnDuration))
|
||||
.SetEase(_returnToSpawnEase)
|
||||
.SetUpdate(true)
|
||||
.OnKill(() => _returnTween = null);
|
||||
}
|
||||
|
||||
private void KillScaleTween()
|
||||
{
|
||||
if (_scaleTween != null && _scaleTween.IsActive())
|
||||
{
|
||||
_scaleTween.Kill();
|
||||
}
|
||||
|
||||
_scaleTween = null;
|
||||
}
|
||||
|
||||
private void KillReturnTween()
|
||||
{
|
||||
if (_returnTween != null && _returnTween.IsActive())
|
||||
{
|
||||
_returnTween.Kill();
|
||||
}
|
||||
|
||||
_returnTween = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c019085f31e649a88e99eb655ed94b15
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,306 @@
|
||||
using System.Collections.Generic;
|
||||
using CustomComponent;
|
||||
using Event;
|
||||
using GameFramework.Event;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
/// <summary>
|
||||
/// MVC UI form for GameplayA. It builds slots and draggable parts from external data.
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class CombineForm : UGuiForm
|
||||
{
|
||||
#region Property
|
||||
|
||||
[SerializeField] private CombineComponent _controller;
|
||||
|
||||
[SerializeField] private RectTransform _slotRoot;
|
||||
|
||||
[SerializeField] private RectTransform _partRoot;
|
||||
|
||||
[SerializeField] private CombineSlot _slotPrefab;
|
||||
|
||||
[SerializeField] private CombineDraggablePart _partPrefab;
|
||||
|
||||
[SerializeField] private TMP_Text _guideText;
|
||||
|
||||
[SerializeField] private TMP_Text _explanationText;
|
||||
|
||||
[SerializeField] private TMP_Text _progressText;
|
||||
|
||||
[SerializeField] private string _progressFormat = "{0}/{1}";
|
||||
|
||||
[SerializeField] private Vector2 _partStartAnchoredPosition = new(0f, -40f);
|
||||
|
||||
[SerializeField] private float _partVerticalSpacing = 120f;
|
||||
|
||||
[SerializeField] [Range(0f, 1f)] private float _partSpawnMinXNormalized = 0.55f;
|
||||
|
||||
[SerializeField] [Range(0f, 1f)] private float _partSpawnMaxXNormalized = 0.95f;
|
||||
|
||||
[SerializeField] [Range(0f, 1f)] private float _partSpawnMinYNormalized = 0.1f;
|
||||
|
||||
[SerializeField] [Range(0f, 1f)] private float _partSpawnMaxYNormalized = 0.9f;
|
||||
|
||||
private readonly List<GameObject> _runtimeNodes = new();
|
||||
|
||||
#endregion
|
||||
|
||||
#region FSM
|
||||
|
||||
protected override void OnInit(object userData)
|
||||
{
|
||||
base.OnInit(userData);
|
||||
|
||||
if (_controller == null)
|
||||
{
|
||||
_controller = GameEntry.Combine;
|
||||
}
|
||||
|
||||
ResetTexts();
|
||||
}
|
||||
|
||||
protected override void OnOpen(object userData)
|
||||
{
|
||||
base.OnOpen(userData);
|
||||
|
||||
if (_controller == null)
|
||||
{
|
||||
_controller = GameEntry.Combine;
|
||||
}
|
||||
|
||||
if (userData is CombineFormContext context)
|
||||
{
|
||||
if (context.Slots.Count == 0)
|
||||
{
|
||||
Log.Warning("CombineMvcForm open failed. Slot model is empty.");
|
||||
return;
|
||||
}
|
||||
|
||||
Build(context);
|
||||
_controller.BindRuntimeContext(_slotRoot, _partRoot);
|
||||
|
||||
GameEntry.Event.Subscribe(CombineGuideMessageEventArgs.EventId, OnGuideMessageChanged);
|
||||
GameEntry.Event.Subscribe(CombinePartMessageEventArgs.EventId, OnPartExplained);
|
||||
GameEntry.Event.Subscribe(CombineProgressEventArgs.EventId, OnProgressChanged);
|
||||
GameEntry.Event.Subscribe(CombineCompletedEventArgs.EventId, OnPuzzleCompleted);
|
||||
|
||||
if (context.AutoStart)
|
||||
{
|
||||
_controller.StartPuzzle();
|
||||
}
|
||||
else
|
||||
{
|
||||
GameEntry.Event.Fire(this,
|
||||
CombineProgressEventArgs.Create(_controller.CurrentStep, _controller.TotalStep));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error("CombineMvcForm open failed. userData is invalid.");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnClose(bool isShutdown, object userData)
|
||||
{
|
||||
GameEntry.Event.Unsubscribe(CombineGuideMessageEventArgs.EventId, OnGuideMessageChanged);
|
||||
GameEntry.Event.Unsubscribe(CombinePartMessageEventArgs.EventId, OnPartExplained);
|
||||
GameEntry.Event.Unsubscribe(CombineProgressEventArgs.EventId, OnProgressChanged);
|
||||
GameEntry.Event.Unsubscribe(CombineCompletedEventArgs.EventId, OnPuzzleCompleted);
|
||||
|
||||
if (_controller != null)
|
||||
{
|
||||
_controller.ClearRuntimeContext();
|
||||
}
|
||||
|
||||
ClearRuntimeNodes();
|
||||
|
||||
ResetTexts();
|
||||
|
||||
base.OnClose(isShutdown, userData);
|
||||
}
|
||||
|
||||
protected override void OnPause()
|
||||
{
|
||||
base.OnPause();
|
||||
|
||||
if (_controller != null)
|
||||
{
|
||||
_controller.PausePuzzle();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnResume()
|
||||
{
|
||||
base.OnResume();
|
||||
|
||||
if (_controller != null)
|
||||
{
|
||||
_controller.ResumePuzzle();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnCover()
|
||||
{
|
||||
base.OnCover();
|
||||
|
||||
if (_controller != null)
|
||||
{
|
||||
_controller.PausePuzzle();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnReveal()
|
||||
{
|
||||
base.OnReveal();
|
||||
|
||||
if (_controller != null)
|
||||
{
|
||||
_controller.ResumePuzzle();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Other Methods
|
||||
|
||||
private void ClearRuntimeNodes()
|
||||
{
|
||||
for (int i = _runtimeNodes.Count - 1; i >= 0; i--)
|
||||
{
|
||||
GameObject node = _runtimeNodes[i];
|
||||
if (node != null)
|
||||
{
|
||||
GameObject.Destroy(node);
|
||||
}
|
||||
}
|
||||
|
||||
_runtimeNodes.Clear();
|
||||
}
|
||||
|
||||
private void ResetTexts()
|
||||
{
|
||||
GameEntry.Event.Fire(this, CombineGuideMessageEventArgs.Create(string.Empty));
|
||||
GameEntry.Event.Fire(this, CombinePartMessageEventArgs.Create(string.Empty));
|
||||
GameEntry.Event.Fire(this, CombineProgressEventArgs.Create(0, 0));
|
||||
}
|
||||
|
||||
private void Build(CombineFormContext context)
|
||||
{
|
||||
ClearRuntimeNodes();
|
||||
|
||||
foreach (var slotData in context.Slots)
|
||||
{
|
||||
CombineSlot slot = Instantiate(_slotPrefab, _slotRoot, false);
|
||||
slot.Initialize(slotData);
|
||||
slot.SetSnapPoint(slot.transform as RectTransform);
|
||||
_runtimeNodes.Add(slot.gameObject);
|
||||
}
|
||||
|
||||
List<CombinePartContext> partContexts = BuildPartContextList(context);
|
||||
for (int i = 0; i < partContexts.Count; i++)
|
||||
{
|
||||
CombinePartContext partData = partContexts[i];
|
||||
CombineDraggablePart part = Instantiate(_partPrefab, _partRoot, false);
|
||||
part.Initialize(partData);
|
||||
SetPartSpawnPosition(part, i);
|
||||
_runtimeNodes.Add(part.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private List<CombinePartContext> BuildPartContextList(CombineFormContext context)
|
||||
{
|
||||
if (context.Parts != null && context.Parts.Count > 0)
|
||||
{
|
||||
return context.Parts;
|
||||
}
|
||||
|
||||
List<CombinePartContext> fallbackParts = new List<CombinePartContext>(context.Slots.Count);
|
||||
for (int i = 0; i < context.Slots.Count; i++)
|
||||
{
|
||||
CombineSlotContext slot = context.Slots[i];
|
||||
fallbackParts.Add(new CombinePartContext
|
||||
{
|
||||
PartType = slot.RequiredPartType,
|
||||
PartDisplayName = slot.RequiredPartType.ToString(),
|
||||
MechanicsExplanation = slot.MechanicsExplanation,
|
||||
LockAfterPlaced = true
|
||||
});
|
||||
}
|
||||
|
||||
return fallbackParts;
|
||||
}
|
||||
|
||||
private void SetPartSpawnPosition(CombineDraggablePart part, int index)
|
||||
{
|
||||
RectTransform partRect = part.transform as RectTransform;
|
||||
if (partRect == null || _partRoot == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Rect rootRect = _partRoot.rect;
|
||||
if (rootRect.width <= 0f || rootRect.height <= 0f)
|
||||
{
|
||||
partRect.anchoredPosition = _partStartAnchoredPosition + Vector2.down * (_partVerticalSpacing * index);
|
||||
return;
|
||||
}
|
||||
|
||||
float minX = Mathf.Lerp(rootRect.xMin, rootRect.xMax, Mathf.Min(_partSpawnMinXNormalized, _partSpawnMaxXNormalized));
|
||||
float maxX = Mathf.Lerp(rootRect.xMin, rootRect.xMax, Mathf.Max(_partSpawnMinXNormalized, _partSpawnMaxXNormalized));
|
||||
float minY = Mathf.Lerp(rootRect.yMin, rootRect.yMax, Mathf.Min(_partSpawnMinYNormalized, _partSpawnMaxYNormalized));
|
||||
float maxY = Mathf.Lerp(rootRect.yMin, rootRect.yMax, Mathf.Max(_partSpawnMinYNormalized, _partSpawnMaxYNormalized));
|
||||
|
||||
partRect.anchoredPosition = new Vector2(
|
||||
Random.Range(minX, maxX),
|
||||
Random.Range(minY, maxY));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Event Handlers
|
||||
|
||||
private void OnGuideMessageChanged(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is CombineGuideMessageEventArgs args)) return;
|
||||
if (_guideText != null)
|
||||
{
|
||||
_guideText.text = args.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPartExplained(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is CombinePartMessageEventArgs args)) return;
|
||||
if (_explanationText != null)
|
||||
{
|
||||
_explanationText.text = args.Message;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnProgressChanged(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is CombineProgressEventArgs args)) return;
|
||||
if (_progressText != null)
|
||||
{
|
||||
_progressText.text = string.Format(_progressFormat, args.CurrentStep, args.TotalSteps);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPuzzleCompleted(object sender, GameEventArgs e)
|
||||
{
|
||||
if (!(e is CombineCompletedEventArgs args)) return;
|
||||
if (_progressText != null)
|
||||
{
|
||||
_progressText.text = string.Format(_progressFormat, _controller.TotalStep, _controller.TotalStep);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 004eb70514fd9064ca990769ebe97043
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,202 @@
|
||||
using CustomComponent;
|
||||
using Definition.Enum;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
/// <summary>
|
||||
/// 拼装槽位:接收拖拽部件并转发给玩法控制器做规则校验。
|
||||
/// </summary>
|
||||
[DisallowMultipleComponent]
|
||||
public class CombineSlot : MonoBehaviour, IDropHandler
|
||||
{
|
||||
#region Inspector Config
|
||||
|
||||
/// <summary>
|
||||
/// 槽位要求的部件类型。
|
||||
/// </summary>
|
||||
[SerializeField] private CombinePartType _requiredPartType = CombinePartType.Dou;
|
||||
|
||||
/// <summary>
|
||||
/// 槽位在拼装流程中的顺序值。
|
||||
/// </summary>
|
||||
[SerializeField] private int _buildOrder;
|
||||
|
||||
/// <summary>
|
||||
/// 是否要求严格按顺序放置。
|
||||
/// </summary>
|
||||
[SerializeField] private bool _requireStrictOrder = true;
|
||||
|
||||
/// <summary>
|
||||
/// 部件吸附点;为空时使用自身 RectTransform。
|
||||
/// </summary>
|
||||
[SerializeField] private RectTransform _snapPoint;
|
||||
|
||||
/// <summary>
|
||||
/// 放置成功后的力学说明文案。
|
||||
/// </summary>
|
||||
[SerializeField] [TextArea(2, 4)] private string _mechanicsExplanation = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 部件不匹配时的自定义提示文案。
|
||||
/// </summary>
|
||||
[SerializeField] [TextArea(2, 4)] private string _mismatchHint = string.Empty;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Runtime State
|
||||
|
||||
/// <summary>
|
||||
/// 玩法控制器引用。
|
||||
/// </summary>
|
||||
private CombineComponent _controller;
|
||||
|
||||
/// <summary>
|
||||
/// 当前占用该槽位的部件。
|
||||
/// </summary>
|
||||
private CombineDraggablePart _occupiedPart;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Public Query
|
||||
|
||||
/// <summary>
|
||||
/// 获取槽位要求的部件类型。
|
||||
/// </summary>
|
||||
public CombinePartType RequiredPartType => _requiredPartType;
|
||||
|
||||
/// <summary>
|
||||
/// 获取槽位顺序值。
|
||||
/// </summary>
|
||||
public int BuildOrder => _buildOrder;
|
||||
|
||||
/// <summary>
|
||||
/// 获取槽位是否要求严格顺序。
|
||||
/// </summary>
|
||||
public bool RequireStrictOrder => _requireStrictOrder;
|
||||
|
||||
/// <summary>
|
||||
/// 获取槽位是否已被占用。
|
||||
/// </summary>
|
||||
public bool IsOccupied => _occupiedPart != null;
|
||||
|
||||
/// <summary>
|
||||
/// 获取槽位的力学说明文案。
|
||||
/// </summary>
|
||||
public string MechanicsExplanation => _mechanicsExplanation;
|
||||
|
||||
/// <summary>
|
||||
/// 获取槽位不匹配提示文案。
|
||||
/// </summary>
|
||||
public string MismatchHint => _mismatchHint;
|
||||
|
||||
/// <summary>
|
||||
/// 获取槽位吸附点。
|
||||
/// </summary>
|
||||
public RectTransform SnapPoint => _snapPoint != null ? _snapPoint : transform as RectTransform;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Setup
|
||||
|
||||
/// <summary>
|
||||
/// 绑定玩法控制器。
|
||||
/// </summary>
|
||||
public void BindController(CombineComponent controller)
|
||||
{
|
||||
_controller = controller;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用运行时数据初始化槽位配置。
|
||||
/// </summary>
|
||||
public void Initialize(CombineSlotContext data)
|
||||
{
|
||||
_requiredPartType = data.RequiredPartType;
|
||||
_buildOrder = data.BuildOrder;
|
||||
_requireStrictOrder = data.RequireStrictOrder;
|
||||
_mechanicsExplanation = data.MechanicsExplanation ?? string.Empty;
|
||||
_mismatchHint = data.MismatchHint ?? string.Empty;
|
||||
|
||||
RectTransform rectTransform = transform as RectTransform;
|
||||
if (rectTransform != null)
|
||||
{
|
||||
rectTransform.anchoredPosition = data.AnchoredPosition;
|
||||
rectTransform.sizeDelta = data.SizeDelta;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置槽位吸附点,为空时回退到槽位自身。
|
||||
/// </summary>
|
||||
public void SetSnapPoint(RectTransform snapPoint)
|
||||
{
|
||||
_snapPoint = snapPoint;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重置槽位占用状态。
|
||||
/// </summary>
|
||||
public void ResetSlot()
|
||||
{
|
||||
_occupiedPart = null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Drop Handling
|
||||
|
||||
/// <summary>
|
||||
/// 拖拽放下回调:识别部件并交给控制器校验放置。
|
||||
/// </summary>
|
||||
public void OnDrop(PointerEventData eventData)
|
||||
{
|
||||
if (!CanHandleDrop(eventData))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CombineDraggablePart part = eventData.pointerDrag.GetComponent<CombineDraggablePart>();
|
||||
if (part == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_controller.TryPlacePart(part, this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 校验是否满足处理放下事件的前置条件。
|
||||
/// </summary>
|
||||
private bool CanHandleDrop(PointerEventData eventData)
|
||||
{
|
||||
return _controller != null && eventData != null && eventData.pointerDrag != null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Occupancy
|
||||
|
||||
/// <summary>
|
||||
/// 标记当前槽位被指定部件占用。
|
||||
/// </summary>
|
||||
internal void SetOccupiedPart(CombineDraggablePart part)
|
||||
{
|
||||
_occupiedPart = part;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理槽位占用,仅在占用者与传入部件一致时生效。
|
||||
/// </summary>
|
||||
internal void ClearOccupiedPart(CombineDraggablePart part)
|
||||
{
|
||||
if (_occupiedPart == part)
|
||||
{
|
||||
_occupiedPart = null;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5986fde954b3467b96ccc5d2d741297a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,146 @@
|
||||
using System.Collections;
|
||||
using Definition.Enum;
|
||||
using Event;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
public abstract class DialogFormBase : UGuiForm
|
||||
{
|
||||
protected DialogFormContext _context;
|
||||
private Coroutine _typingCoroutine;
|
||||
private TMP_Text _typingTargetText;
|
||||
private bool _isTypewriting;
|
||||
|
||||
public abstract DialogFormMode UIMode { get; }
|
||||
|
||||
public abstract void StartDialog(DialogFormContext context);
|
||||
|
||||
protected override void OnOpen(object userData)
|
||||
{
|
||||
base.OnOpen(userData);
|
||||
|
||||
if (!(userData is DialogFormContext context))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_context = context;
|
||||
StartDialog(context);
|
||||
}
|
||||
|
||||
protected override void OnClose(bool isShutdown, object userData)
|
||||
{
|
||||
StopTypewriter();
|
||||
_context = null;
|
||||
base.OnClose(isShutdown, userData);
|
||||
}
|
||||
|
||||
public void OnClickNextLine()
|
||||
{
|
||||
if (CompleteTypewriterIfRunning())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GameEntry.Event.Fire(this, DialogNextLineRequestEventArgs.Create());
|
||||
}
|
||||
|
||||
public void OnClickSkipDialog()
|
||||
{
|
||||
GameEntry.Event.Fire(this, DialogSkipRequestEventArgs.Create());
|
||||
}
|
||||
|
||||
public void OnClickStopDialog()
|
||||
{
|
||||
GameEntry.Event.Fire(this, DialogStopRequestEventArgs.Create());
|
||||
}
|
||||
|
||||
protected void PlayTypewriter(TMP_Text targetText, string text, float charsPerSecond)
|
||||
{
|
||||
StopTypewriter();
|
||||
|
||||
if (targetText == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string finalText = text ?? string.Empty;
|
||||
_typingTargetText = targetText;
|
||||
|
||||
if (charsPerSecond <= 0f || string.IsNullOrEmpty(finalText))
|
||||
{
|
||||
targetText.text = finalText;
|
||||
targetText.maxVisibleCharacters = int.MaxValue;
|
||||
_isTypewriting = false;
|
||||
return;
|
||||
}
|
||||
|
||||
_isTypewriting = true;
|
||||
_typingCoroutine = StartCoroutine(TypewriterRoutine(targetText, finalText, charsPerSecond));
|
||||
}
|
||||
|
||||
protected void StopTypewriter()
|
||||
{
|
||||
if (_typingCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_typingCoroutine);
|
||||
_typingCoroutine = null;
|
||||
}
|
||||
|
||||
_typingTargetText = null;
|
||||
_isTypewriting = false;
|
||||
}
|
||||
|
||||
private bool CompleteTypewriterIfRunning()
|
||||
{
|
||||
if (!_isTypewriting || _typingTargetText == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_typingTargetText.maxVisibleCharacters = int.MaxValue;
|
||||
StopTypewriter();
|
||||
return true;
|
||||
}
|
||||
|
||||
private IEnumerator TypewriterRoutine(TMP_Text targetText, string finalText, float charsPerSecond)
|
||||
{
|
||||
targetText.text = finalText;
|
||||
targetText.ForceMeshUpdate();
|
||||
|
||||
int totalCharacters = targetText.textInfo.characterCount;
|
||||
if (totalCharacters <= 0)
|
||||
{
|
||||
targetText.maxVisibleCharacters = int.MaxValue;
|
||||
_typingCoroutine = null;
|
||||
_typingTargetText = null;
|
||||
_isTypewriting = false;
|
||||
yield break;
|
||||
}
|
||||
|
||||
targetText.maxVisibleCharacters = 0;
|
||||
float elapsed = 0f;
|
||||
int visibleCharacters = 0;
|
||||
|
||||
while (visibleCharacters < totalCharacters)
|
||||
{
|
||||
elapsed += Time.unscaledDeltaTime;
|
||||
int nextVisible = Mathf.Min(totalCharacters, Mathf.FloorToInt(elapsed * charsPerSecond));
|
||||
if (nextVisible != visibleCharacters)
|
||||
{
|
||||
visibleCharacters = nextVisible;
|
||||
targetText.maxVisibleCharacters = visibleCharacters;
|
||||
}
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
targetText.maxVisibleCharacters = int.MaxValue;
|
||||
_typingCoroutine = null;
|
||||
_typingTargetText = null;
|
||||
_isTypewriting = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: aafc42873758b514282b3a75e3ba4665
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,82 @@
|
||||
using System;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
public class HorizonSelectGroup : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private TMP_Text _showText;
|
||||
|
||||
[SerializeField] private GameObject _leftButton;
|
||||
|
||||
[SerializeField] private GameObject _rightButton;
|
||||
|
||||
[SerializeField] private int _currentValue;
|
||||
|
||||
[SerializeField] private string[] _showTexts;
|
||||
|
||||
private int LeftBound = 0;
|
||||
private int RightBound => _showTexts.Length - 1;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
SetValue(_currentValue);
|
||||
_leftButton.SetActive(false);
|
||||
_rightButton.SetActive(false);
|
||||
}
|
||||
|
||||
public void OnLeftClick()
|
||||
{
|
||||
_currentValue--;
|
||||
_showText.text = _showTexts[_currentValue];
|
||||
|
||||
UpdateButtonState();
|
||||
}
|
||||
|
||||
public void OnRightClick()
|
||||
{
|
||||
_currentValue++;
|
||||
_showText.text = _showTexts[_currentValue];
|
||||
|
||||
UpdateButtonState();
|
||||
}
|
||||
|
||||
public void SetValue(int value)
|
||||
{
|
||||
if (value < 0 || value >= _showTexts.Length)
|
||||
{
|
||||
throw new IndexOutOfRangeException();
|
||||
}
|
||||
|
||||
_currentValue = value;
|
||||
_showText.text = _showTexts[_currentValue];
|
||||
|
||||
UpdateButtonState();
|
||||
}
|
||||
|
||||
public void SetValue(bool value)
|
||||
{
|
||||
_currentValue = value ? 1 : 0;
|
||||
_showText.text = _showTexts[_currentValue];
|
||||
|
||||
UpdateButtonState();
|
||||
}
|
||||
|
||||
public void UpdateButtonState()
|
||||
{
|
||||
_leftButton.SetActive(_currentValue != LeftBound);
|
||||
_rightButton.SetActive(_currentValue != RightBound);
|
||||
}
|
||||
|
||||
public void HideButtons()
|
||||
{
|
||||
_leftButton.SetActive(false);
|
||||
_rightButton.SetActive(false);
|
||||
}
|
||||
|
||||
public int GetIntValue() => _currentValue;
|
||||
|
||||
public bool GetBoolValue() => _currentValue == 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e3ad474ed1648574f984d626f3d9726a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,35 @@
|
||||
using Definition.Enum;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
public class MaskDialogForm : DialogFormBase
|
||||
{
|
||||
public override DialogFormMode UIMode => DialogFormMode.Mask;
|
||||
|
||||
[SerializeField] private Image _maskImage;
|
||||
|
||||
[SerializeField] private TMP_Text _text;
|
||||
|
||||
public override void StartDialog(DialogFormContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
Log.Warning("MaskDialogForm start failed. context is null.");
|
||||
return;
|
||||
}
|
||||
|
||||
_context = context;
|
||||
|
||||
if (_maskImage != null)
|
||||
{
|
||||
_maskImage.gameObject.SetActive(true);
|
||||
}
|
||||
|
||||
PlayTypewriter(_text, context.Text, context.PlayingSpeed);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5a63462640e70af40972b9841beafe6c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,49 @@
|
||||
using Event;
|
||||
using UnityEngine;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
public class MenuForm : UGuiForm
|
||||
{
|
||||
[SerializeField] private GameObject _continueButton;
|
||||
|
||||
public void RefreshUI(MenuFormContext context)
|
||||
{
|
||||
_continueButton.SetActive(context.HasGameData);
|
||||
}
|
||||
|
||||
protected override void OnOpen(object userData)
|
||||
{
|
||||
base.OnOpen(userData);
|
||||
|
||||
if (!(userData is MenuFormContext context))
|
||||
{
|
||||
Log.Error("MenuFormContext is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
RefreshUI(context);
|
||||
}
|
||||
|
||||
public void OnContinueButtonClick()
|
||||
{
|
||||
GameEntry.Event.Fire(this, MenuContinueEventArgs.Create());
|
||||
}
|
||||
|
||||
public void OnStartButtonClick()
|
||||
{
|
||||
GameEntry.Event.Fire(this, MenuStartEventArgs.Create());
|
||||
}
|
||||
|
||||
public void OnSettingButtonClick()
|
||||
{
|
||||
GameEntry.Event.Fire(this, MenuSettingEventArgs.Create());
|
||||
}
|
||||
|
||||
public void OnExitButtonClick()
|
||||
{
|
||||
GameEntry.Event.Fire(this, MenuExitEventArgs.Create());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 81cd82e6a7856b148bcffd790e5e5ed8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,37 @@
|
||||
using DG.Tweening;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
public class SelectableItem : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
|
||||
{
|
||||
[SerializeField] private Image _bgImage;
|
||||
|
||||
[SerializeField] private float _fadeDuration;
|
||||
|
||||
private Sequence _fadeSequence;
|
||||
|
||||
public UnityEvent _onSelect;
|
||||
|
||||
public UnityEvent _onDeselect;
|
||||
|
||||
public void OnPointerEnter(PointerEventData eventData)
|
||||
{
|
||||
_fadeSequence.Kill();
|
||||
_fadeSequence = DOTween.Sequence();
|
||||
_fadeSequence.Append(_bgImage.DOFade(1, _fadeDuration));
|
||||
_onSelect.Invoke();
|
||||
}
|
||||
|
||||
public void OnPointerExit(PointerEventData eventData)
|
||||
{
|
||||
_fadeSequence.Kill();
|
||||
_fadeSequence = DOTween.Sequence();
|
||||
_fadeSequence.Append(_bgImage.DOFade(0, _fadeDuration));
|
||||
_onDeselect.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 74ad2e39e0d4fc546ab11cf446143f44
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,134 @@
|
||||
using Definition.DataStruct;
|
||||
using Definition.Enum;
|
||||
using Event;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using UnityGameFramework.Runtime;
|
||||
|
||||
namespace UI
|
||||
{
|
||||
public class SettingForm : UGuiForm
|
||||
{
|
||||
[SerializeField] private Image[] _navigateButtonImages;
|
||||
|
||||
[SerializeField] private TMP_Text[] _navigateButtonTexts;
|
||||
|
||||
[SerializeField] private Color _darkColor;
|
||||
|
||||
[SerializeField] private Color _brightColor;
|
||||
|
||||
[SerializeField] private Slider _bgmVolumeSlider;
|
||||
|
||||
[SerializeField] private Slider _seVolumeSlider;
|
||||
|
||||
[SerializeField] private HorizonSelectGroup _allowShakeGroup;
|
||||
|
||||
[SerializeField] private HorizonSelectGroup _allowBlinkGroup;
|
||||
|
||||
[SerializeField] private HorizonSelectGroup _dialogWindowAlpha;
|
||||
|
||||
[SerializeField] private HorizonSelectGroup _playingSpeed;
|
||||
|
||||
[SerializeField] private HorizonSelectGroup _screenSolution;
|
||||
|
||||
[SerializeField] private HorizonSelectGroup _screenWindow;
|
||||
|
||||
[SerializeField] private HorizonSelectGroup _vSyncGroup;
|
||||
|
||||
[SerializeField] private HorizonSelectGroup _antiAliasingGroup;
|
||||
|
||||
private SettingFormController _controller;
|
||||
|
||||
public void RefreshUI(SettingFormContext context)
|
||||
{
|
||||
_controller = context.Controller;
|
||||
|
||||
var setting = context.Setting;
|
||||
|
||||
_bgmVolumeSlider.value = setting.BGMVolume;
|
||||
_seVolumeSlider.value = setting.SEVolume;
|
||||
|
||||
_allowBlinkGroup.SetValue(setting.AllowBlink);
|
||||
_allowShakeGroup.SetValue(setting.AllowShake);
|
||||
_dialogWindowAlpha.SetValue((int)setting.DialogWindowAlpha);
|
||||
_playingSpeed.SetValue((int)setting.PlayingSpeed);
|
||||
|
||||
_screenSolution.SetValue((int)setting.ScreenSolution);
|
||||
_screenWindow.SetValue((int)setting.ScreenWindow);
|
||||
_vSyncGroup.SetValue(setting.VSync);
|
||||
_antiAliasingGroup.SetValue(setting.AntiAliasing);
|
||||
}
|
||||
|
||||
protected override void OnOpen(object userData)
|
||||
{
|
||||
base.OnOpen(userData);
|
||||
|
||||
if (!(userData is SettingFormContext context))
|
||||
{
|
||||
Log.Error("SettingFormContext is invalid.");
|
||||
return;
|
||||
}
|
||||
|
||||
RefreshUI(context);
|
||||
}
|
||||
|
||||
private GameSetting CollectSetting()
|
||||
{
|
||||
var setting = new GameSetting
|
||||
{
|
||||
BGMVolume = _bgmVolumeSlider.value,
|
||||
SEVolume = _seVolumeSlider.value,
|
||||
|
||||
AllowShake = _allowShakeGroup.GetBoolValue(),
|
||||
AllowBlink = _allowBlinkGroup.GetBoolValue(),
|
||||
DialogWindowAlpha = (DialogWindowAlpha)_dialogWindowAlpha.GetIntValue(),
|
||||
PlayingSpeed = (DialogPlayingSpeed)_playingSpeed.GetIntValue(),
|
||||
|
||||
ScreenSolution = (ScreenSolutionType)_screenSolution.GetIntValue(),
|
||||
ScreenWindow = (ScreenWindowType)_screenWindow.GetIntValue(),
|
||||
VSync = _vSyncGroup.GetBoolValue(),
|
||||
AntiAliasing = _antiAliasingGroup.GetBoolValue()
|
||||
};
|
||||
|
||||
return setting;
|
||||
}
|
||||
|
||||
public void OnNavigateButtonClick(int index)
|
||||
{
|
||||
if (index < 0 || index >= _navigateButtonImages.Length)
|
||||
{
|
||||
Log.Error("NavigateButtonClick index is out of range.");
|
||||
}
|
||||
|
||||
for (int i = 0; i < _navigateButtonImages.Length; i++)
|
||||
{
|
||||
_navigateButtonImages[i].color = i != index ? _brightColor : _darkColor;
|
||||
_navigateButtonTexts[i].color = i != index ? _darkColor : _brightColor;
|
||||
}
|
||||
}
|
||||
|
||||
public void OnReturnButtonClick()
|
||||
{
|
||||
var dialogParams = new DialogParams
|
||||
{
|
||||
Title = "确认是否返回",
|
||||
Message = "是否保存更改?",
|
||||
ConfirmText = "保存并返回",
|
||||
CancelText = "直接返回",
|
||||
OtherText = "取消",
|
||||
Mode = 3,
|
||||
};
|
||||
dialogParams.OnClickConfirm += SaveSettingAndReturn;
|
||||
dialogParams.OnClickCancel += _ => _controller.CloseUI();
|
||||
|
||||
GameEntry.UI.OpenUIForm(UIFormId.DialogForm, dialogParams);
|
||||
}
|
||||
|
||||
private void SaveSettingAndReturn(object userData)
|
||||
{
|
||||
var setting = CollectSetting();
|
||||
GameEntry.Event.Fire(this, SettingSaveEventArgs.Create(setting));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cbbc5660f5b30714183f047355830159
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user