376 lines
13 KiB
C#
376 lines
13 KiB
C#
using UnityEngine;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using DG.Tweening;
|
|
|
|
namespace AibisDream.MiniGame.HuoShan
|
|
{
|
|
/// <summary>
|
|
/// 造句系统 - 处理句子生成流程:选定文字、候选文字、概率变化、连线
|
|
/// </summary>
|
|
public class SentenceBuilder
|
|
{
|
|
private BackgroundTextLayer _backgroundLayer;
|
|
private CircularViewport _viewport;
|
|
private List<TextNode> _sentenceNodes;
|
|
private string _targetSentence;
|
|
private int _currentIndex = 0;
|
|
private bool _isBuilding = false;
|
|
private int _stuckIndex = -1; // 卡壳位置,-1表示不卡壳
|
|
|
|
// 候选文字相关
|
|
private List<TextNode> _candidateNodes;
|
|
private float _probabilityChangeSpeed = 0.5f;
|
|
private Coroutine _buildingCoroutine;
|
|
private MonoBehaviour _coroutineRunner;
|
|
|
|
// 句子连线设置
|
|
private float _sentenceLineSpacing = 1.5f;
|
|
private Vector2 _sentenceDirection = Vector2.right;
|
|
|
|
public SentenceBuilder(BackgroundTextLayer backgroundLayer, CircularViewport viewport, MonoBehaviour coroutineRunner)
|
|
{
|
|
_backgroundLayer = backgroundLayer;
|
|
_viewport = viewport;
|
|
_coroutineRunner = coroutineRunner;
|
|
_sentenceNodes = new List<TextNode>();
|
|
_candidateNodes = new List<TextNode>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 开始构建句子
|
|
/// </summary>
|
|
public void StartBuildingSentence(string sentence, int stuckIndex = -1)
|
|
{
|
|
if (_isBuilding)
|
|
{
|
|
StopBuilding();
|
|
}
|
|
|
|
_targetSentence = sentence;
|
|
_currentIndex = 0;
|
|
_stuckIndex = stuckIndex;
|
|
_isBuilding = true;
|
|
|
|
// 清理之前的句子节点
|
|
ClearSentence();
|
|
|
|
// 开始构建协程
|
|
_buildingCoroutine = _coroutineRunner.StartCoroutine(BuildingCoroutine());
|
|
}
|
|
|
|
private IEnumerator BuildingCoroutine()
|
|
{
|
|
// 等待一帧确保背景层已初始化
|
|
yield return null;
|
|
|
|
while (_currentIndex < _targetSentence.Length && _isBuilding)
|
|
{
|
|
char targetChar = _targetSentence[_currentIndex];
|
|
|
|
// 检查是否到达卡壳位置
|
|
if (_currentIndex == _stuckIndex)
|
|
{
|
|
yield return _coroutineRunner.StartCoroutine(HandleStuck());
|
|
continue;
|
|
}
|
|
|
|
// 在当前可视范围内找到或创建目标文字
|
|
TextNode targetNode = FindOrCreateNodeInViewport(targetChar.ToString());
|
|
|
|
if (targetNode != null)
|
|
{
|
|
// 设置为选定状态
|
|
SetNodeAsSelected(targetNode, _currentIndex);
|
|
_sentenceNodes.Add(targetNode);
|
|
|
|
// 移动到句子位置
|
|
yield return MoveNodeToSentencePosition(targetNode, _currentIndex);
|
|
|
|
// 如果不是最后一个字,准备下一个字的候选
|
|
if (_currentIndex < _targetSentence.Length - 1)
|
|
{
|
|
yield return _coroutineRunner.StartCoroutine(PrepareNextCandidate());
|
|
}
|
|
|
|
_currentIndex++;
|
|
}
|
|
|
|
yield return new WaitForSeconds(0.5f);
|
|
}
|
|
|
|
_isBuilding = false;
|
|
}
|
|
|
|
private TextNode FindOrCreateNodeInViewport(string text)
|
|
{
|
|
// 首先尝试在可视范围内找到匹配的文字
|
|
var nodesInViewport = _backgroundLayer.GetNodesInRange(_viewport.ViewportCenter, _viewport.ViewportRadius);
|
|
var matchingNode = nodesInViewport.FirstOrDefault(n => n.Text == text && n.State == TextNodeState.Random);
|
|
|
|
if (matchingNode != null)
|
|
{
|
|
return matchingNode;
|
|
}
|
|
|
|
// 如果找不到,在可视范围内随机选择一个节点并改变其文字
|
|
if (nodesInViewport.Count > 0)
|
|
{
|
|
var randomNode = nodesInViewport[Random.Range(0, nodesInViewport.Count)];
|
|
randomNode.Text = text;
|
|
randomNode.UpdateVisual();
|
|
return randomNode;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private void SetNodeAsSelected(TextNode node, int index)
|
|
{
|
|
node.State = TextNodeState.Selected;
|
|
node.SentenceIndex = index;
|
|
node.Scale = 1.5f;
|
|
node.Brightness = 1f;
|
|
node.Color = Color.yellow;
|
|
node.UpdateVisual();
|
|
|
|
// 放大动画
|
|
node.GameObject.transform.DOScale(1.5f, 0.3f).SetEase(Ease.OutBack);
|
|
}
|
|
|
|
private IEnumerator MoveNodeToSentencePosition(TextNode node, int index)
|
|
{
|
|
Vector2 targetPos = GetSentencePosition(index);
|
|
|
|
// 使用DOTween移动
|
|
yield return node.GameObject.transform.DOMove(
|
|
new Vector3(targetPos.x, targetPos.y, 0),
|
|
0.5f
|
|
).SetEase(Ease.OutQuad).WaitForCompletion();
|
|
|
|
node.Position = targetPos;
|
|
}
|
|
|
|
private Vector2 GetSentencePosition(int index)
|
|
{
|
|
// 以视口中心为起点,沿方向排列
|
|
Vector2 startPos = _viewport.ViewportCenter;
|
|
Vector2 offset = _sentenceDirection * index * _sentenceLineSpacing;
|
|
return startPos + offset;
|
|
}
|
|
|
|
private IEnumerator PrepareNextCandidate()
|
|
{
|
|
if (_currentIndex >= _targetSentence.Length - 1) yield break;
|
|
|
|
char nextChar = _targetSentence[_currentIndex + 1];
|
|
|
|
// 清除之前的候选
|
|
ClearCandidates();
|
|
|
|
// 在当前选定节点周围找到几个节点作为候选
|
|
TextNode selectedNode = _sentenceNodes[_currentIndex];
|
|
var nearbyNodes = _backgroundLayer.GetNodesInRange(selectedNode.Position, 3f)
|
|
.Where(n => n.State == TextNodeState.Random)
|
|
.Take(5)
|
|
.ToList();
|
|
|
|
// 确保目标字符在候选列表中
|
|
bool hasTargetChar = false;
|
|
foreach (var node in nearbyNodes)
|
|
{
|
|
if (node.Text == nextChar.ToString())
|
|
{
|
|
hasTargetChar = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!hasTargetChar && nearbyNodes.Count > 0)
|
|
{
|
|
nearbyNodes[0].Text = nextChar.ToString();
|
|
nearbyNodes[0].UpdateVisual();
|
|
}
|
|
|
|
// 设置候选状态
|
|
_candidateNodes.Clear();
|
|
foreach (var node in nearbyNodes)
|
|
{
|
|
node.State = TextNodeState.Candidate;
|
|
node.Color = Color.cyan;
|
|
node.Probability = Random.Range(0f, 0.5f);
|
|
node.AddConnection(selectedNode);
|
|
selectedNode.AddConnection(node);
|
|
node.UpdateVisual();
|
|
_candidateNodes.Add(node);
|
|
}
|
|
|
|
// 概率变化动画
|
|
yield return _coroutineRunner.StartCoroutine(AnimateCandidateProbabilities(nextChar.ToString()));
|
|
}
|
|
|
|
private IEnumerator AnimateCandidateProbabilities(string targetText)
|
|
{
|
|
float duration = 2f;
|
|
float elapsed = 0f;
|
|
|
|
while (elapsed < duration)
|
|
{
|
|
elapsed += Time.deltaTime;
|
|
float t = elapsed / duration;
|
|
|
|
foreach (var node in _candidateNodes)
|
|
{
|
|
if (node.Text == targetText)
|
|
{
|
|
// 目标字符概率逐渐增加到1
|
|
node.Probability = Mathf.Lerp(node.Probability, 1f, t * 0.1f);
|
|
}
|
|
else
|
|
{
|
|
// 其他字符概率逐渐降低
|
|
node.Probability = Mathf.Lerp(node.Probability, 0f, t * 0.1f);
|
|
}
|
|
|
|
// 根据概率更新亮度
|
|
node.Brightness = 0.5f + node.Probability * 0.5f;
|
|
node.UpdateVisual();
|
|
}
|
|
|
|
yield return null;
|
|
}
|
|
|
|
// 找到概率最高的节点(应该是目标字符)
|
|
var selectedCandidate = _candidateNodes.OrderByDescending(n => n.Probability).FirstOrDefault();
|
|
if (selectedCandidate != null && selectedCandidate.Text == targetText)
|
|
{
|
|
// 清除候选状态,恢复随机
|
|
ClearCandidates();
|
|
selectedCandidate.State = TextNodeState.Random;
|
|
}
|
|
}
|
|
|
|
private IEnumerator HandleStuck()
|
|
{
|
|
// 生成卡壳的错误文字
|
|
string stuckText = "我是个傻逼";
|
|
|
|
// 在当前位置创建卡壳文字
|
|
List<TextNode> stuckNodes = new List<TextNode>();
|
|
Vector2 currentPos = GetSentencePosition(_currentIndex);
|
|
|
|
for (int i = 0; i < stuckText.Length; i++)
|
|
{
|
|
TextNode stuckNode = FindOrCreateNodeInViewport(stuckText[i].ToString());
|
|
if (stuckNode != null)
|
|
{
|
|
stuckNode.State = TextNodeState.Stuck;
|
|
stuckNode.IsStuck = true;
|
|
stuckNode.SentenceIndex = _currentIndex + i;
|
|
stuckNode.Color = Color.red;
|
|
stuckNode.Probability = 1f;
|
|
stuckNode.Scale = 1.2f;
|
|
stuckNode.Position = currentPos + Vector2.right * i * _sentenceLineSpacing;
|
|
stuckNode.UpdateVisual();
|
|
stuckNodes.Add(stuckNode);
|
|
}
|
|
}
|
|
|
|
// 等待玩家点击移除所有卡壳文字
|
|
while (stuckNodes.Count > 0)
|
|
{
|
|
yield return null;
|
|
|
|
// 检查点击
|
|
if (Input.GetMouseButtonDown(0))
|
|
{
|
|
Vector2 mousePos = GetMouseWorldPosition();
|
|
for (int i = stuckNodes.Count - 1; i >= 0; i--)
|
|
{
|
|
var node = stuckNodes[i];
|
|
float distance = Vector2.Distance(mousePos, node.Position);
|
|
if (distance < 1f) // 点击范围
|
|
{
|
|
// 移除节点
|
|
node.State = TextNodeState.Random;
|
|
node.IsStuck = false;
|
|
node.Color = Color.white;
|
|
node.UpdateVisual();
|
|
stuckNodes.RemoveAt(i);
|
|
|
|
// 销毁GameObject
|
|
if (node.GameObject != null)
|
|
{
|
|
Object.Destroy(node.GameObject);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 卡壳处理完成,继续构建
|
|
}
|
|
|
|
private Vector2 GetMouseWorldPosition()
|
|
{
|
|
Camera cam = Camera.main;
|
|
if (cam == null) return Vector2.zero;
|
|
|
|
Vector3 mousePos = Input.mousePosition;
|
|
mousePos.z = cam.nearClipPlane + 1f;
|
|
Vector3 worldPos = cam.ScreenToWorldPoint(mousePos);
|
|
return new Vector2(worldPos.x, worldPos.y);
|
|
}
|
|
|
|
private void ClearCandidates()
|
|
{
|
|
foreach (var node in _candidateNodes)
|
|
{
|
|
if (node.State == TextNodeState.Candidate)
|
|
{
|
|
node.State = TextNodeState.Random;
|
|
node.Color = Color.white;
|
|
node.Probability = 0f;
|
|
node.Brightness = 1f;
|
|
node.UpdateVisual();
|
|
}
|
|
}
|
|
_candidateNodes.Clear();
|
|
}
|
|
|
|
private void ClearSentence()
|
|
{
|
|
foreach (var node in _sentenceNodes)
|
|
{
|
|
if (node.State == TextNodeState.Selected)
|
|
{
|
|
node.State = TextNodeState.Random;
|
|
node.SentenceIndex = -1;
|
|
node.Scale = 1f;
|
|
node.Color = Color.white;
|
|
node.UpdateVisual();
|
|
}
|
|
}
|
|
_sentenceNodes.Clear();
|
|
}
|
|
|
|
public void StopBuilding()
|
|
{
|
|
_isBuilding = false;
|
|
if (_buildingCoroutine != null)
|
|
{
|
|
_coroutineRunner.StopCoroutine(_buildingCoroutine);
|
|
_buildingCoroutine = null;
|
|
}
|
|
ClearCandidates();
|
|
}
|
|
|
|
public List<TextNode> GetSentenceNodes()
|
|
{
|
|
return _sentenceNodes;
|
|
}
|
|
}
|
|
}
|
|
|