Files
ft/Client/Assets/Scripts/EventPartnerGather/Mining/EventPartnerTrainController.cs
2026-06-29 21:18:33 +08:00

568 lines
20 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections;
using System.Linq;
using System.Threading.Tasks;
using asap.core;
using DataCenter;
using DG.Tweening;
using GameCore;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.UI;
using Object = UnityEngine.Object;
namespace EventPartnerGather.Mining
{
public static class LegacyAnimExtensions
{
public static void GoToFrame(this Animation anim, int targetFrame)
{
var clipName = anim.clip.name;
// 获取动画状态
var state = anim[clipName];
float frameRate = state.clip.frameRate;
// 计算动画的总帧数(假设动画时长正确且帧率已知)
// 如果你知道确切的帧率最好直接使用帧率例如float frameRate = 30f;
// totalFrames = state.length * frameRate;
// 这里是一种估算方式,实际帧数取决于制作时的帧率
var approximateFrames = state.length * 60f; // 假设60FPS制作
// 确保目标帧在有效范围内
targetFrame = Mathf.Clamp(targetFrame, 0, (int)approximateFrames);
// 计算目标时间 (时间 = 帧数 / 帧率)
// 使用相同的假设帧率进行计算
float targetTime = targetFrame / 60f;
// 设置动画时间
state.time = targetTime;
// 播放动画并立即采样到该位置
anim.Play(clipName);
anim.Sample(); // 强制立即更新到当前time指定的帧
anim.Stop(); // 停止,如果只想定位而不播放
}
public static Task PlayAnimFromToAsync(this Animation anim, MonoBehaviour runner, string clipName, int startFrame, int endFrame)
{
var tcs = new TaskCompletionSource<bool>();
runner.StartCoroutine(PlayAnimFromToCoroutine(anim, clipName, startFrame, endFrame, () =>
{
tcs.TrySetResult(true);
}));
return tcs.Task;
}
private static IEnumerator PlayAnimFromToCoroutine(Animation anim, string clipName, int startFrame, int endFrame, Action onComplete)
{
ELog($"PlayAnimFromToCoroutine startFrame={startFrame} endFrame={endFrame}");
AnimationState state = anim[clipName];
float frameRate = state.clip.frameRate;
float startTime = startFrame / frameRate;
float endTime = endFrame / frameRate;
// 设置起始时间并播放
// anim.Stop();
state.time = startTime;
anim.Play(clipName);
// 等待到目标时间,留浮点安全误差
while (state.time < endTime && anim.isPlaying)
{
yield return null;
}
// 强制采样到结束帧,避免漏采
state.time = endTime;
anim.Sample();
//state.speed = 0;
anim.Stop();
onComplete?.Invoke();
}
private static void ELog(string message)
{
#if UNITY_EDITOR
Debug.Log($"<color=cyan>LegacyAnimExtensions -> {message}</color>");
#endif
}
}
[Serializable]
public class TrainCar
{
public Transform carTransform;
public Transform oreContainer; // 矿石收集容器
// [HideInInspector]
public Transform collectPoint; // 收集点
[HideInInspector]
public Image bar;
[HideInInspector]
public Transform reward;
// [HideInInspector]
public int pointsRange; // 对应的点数范围 (如 0-10, 10-20, 20-30, 30-40, 40+)
// [HideInInspector]
public int maxCapacity = 10;
//
private int _collectPoint = 0;
// 检测获得点数是否可以收集
public bool CanCollect(int points)
{
ELog($"CanCollect points={points} maxCapacity={maxCapacity}");
return points <= maxCapacity;
}
//
// public int GetCollectedOresCount => _collectedOres.Count;
public void AddOre(GameObject ore)
{
// 消除 Ore
Object.Destroy(ore);
_collectPoint++;
// // Log("AddOre-> ");
// if (oreContainer && ore)
// {
// ore.transform.SetParent(oreContainer);
// // 简单的堆叠布局
// var count = _collectedOres.Count;
// ore.transform.localPosition = new Vector3(
// (count % 3 - 1) * 0.5f,
// (count / 3) * 0.3f,
// 0f
// );
// ore.transform.localScale = Vector3.one * 0.8f;
// _collectedOres.Add(ore);
// }
}
private static void ELog(string t)
{
#if UNITY_EDITOR
Debug.Log($"<color=cyan>TrainCar-> {t} </color>");
#endif
}
public void ClearOres()
{
// foreach (var ore in _collectedOres)
// {
// if (ore != null)
// {
// Object.DestroyImmediate(ore);
// }
// }
// _collectedOres.Clear();
}
}
/// <summary>
/// 事件合作伙伴火车控制器
/// 管理火车车厢系统,包括矿石收集、车厢管理、矿石飞行动画等
/// 建议挂载在Train容器对象上确保包含所有车厢的Transform
///
/// 场景结构建议:
/// Train (挂载此脚本)
/// ├── TrainCar1 (第一节车厢)
/// │ └── OreContainer (矿石容器)
/// ├── TrainCar2 (第二节车厢)
/// │ └── OreContainer (矿石容器)
/// └── ... (更多车厢)
/// </summary>
public class EventPartnerTrainController : MonoBehaviour
{
[Header("火车车厢配置")]
public TrainCar[] trainCars = new TrainCar[5]; // 5节车厢
[Header("动画配置")]
// public float oreFlightDuration = 1f;
public AnimationCurve oreFlightCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
[Header("飞行偏移")]
public float flyOreOffsetX = 2f;
public float flyOreOffsetY = 2f;
[Header("移动目标")]
public Transform target; // 目标点(可选)
public Vector3 targetWorldPos; // 备用的世界坐标目标点(当 target 为空时使用)
public bool useTargetTransform = true;
[Header("移动参数")]
[Min(0.01f)]
public float duration = 1f; // 总时长(秒)
[Tooltip("位移归一化曲线X=时间(0~1), Y=位移进度(0~1)。建议是S形起点0终点1中间变快末尾变慢。")]
public AnimationCurve positionCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
[Header("调试/外观")]
public Color gizmoColor = new Color(0.2f, 0.8f, 1f, 0.8f);
// 声音
private Sound soundFly;
private AudioClip audioFlyClip;
// 运行时
private Vector3 _startPos;
private Vector3 _endPos;
private float _t; // 0~1
private bool _isMoving;
// 周期
public float OreFlightDuration { get; set; } = 1.0f;
//
private EventPartnerGatherManager _manager;
//
private PlayerItemData _playerItemData;
private void Start()
{
//
_playerItemData = GContext.container.Resolve<PlayerItemData>();
InitializeTrainCars();
// 设一个合理的默认曲线Slow -> Fast -> Stop确保末尾Y=1停住
if (positionCurve == null || positionCurve.length < 2)
{
positionCurve = new AnimationCurve(
new Keyframe(0f, 0f, 0f, 2f), // 起步慢
new Keyframe(0.5f, 0.8f, 1.5f, 1.5f), // 中段加速
new Keyframe(1f, 1f, 0f, 0f) // 末尾速度收敛到0
);
}
LoadAudio();
}
private void Update()
{
if (!_isMoving) return;
_t += Time.deltaTime / Mathf.Max(0.0001f, duration);
float t01 = Mathf.Clamp01(_t);
float p = positionCurve.Evaluate(t01);
transform.position = Vector3.LerpUnclamped(_startPos, _endPos, p);
if (_t >= 1f)
{
// 确保精确落点
transform.position = _endPos;
_isMoving = false;
}
}
private void OnDisable()
{
StopFlyAudio();
}
private void OnDestroy()
{
StopFlyAudio();
AudioRelease();
}
// 开始从当前位置移动到目标世界坐标
public void StartMoveTo(Vector3 worldTarget)
{
_startPos = transform.position;
_endPos = worldTarget;
_t = 0f;
_isMoving = true;
}
//
public void StartMoveByDelta(Vector3 delta)
{
StartMoveTo(transform.position + delta);
}
// 重新设置目标(例如在运行时切换目标物体后调用)
public void SetTarget(Transform newTarget)
{
target = newTarget;
useTargetTransform = true;
}
void OnDrawGizmosSelected()
{
Vector3 end = useTargetTransform && target != null ? target.position : targetWorldPos;
Gizmos.color = gizmoColor;
// 预览当前配置下的终点
Gizmos.DrawWireSphere(end, 0.15f);
// 路径线
Gizmos.DrawLine(Application.isPlaying ? _startPos : transform.position, end);
}
private void InitializeTrainCars()
{
ELog($"InitTrainCars count={trainCars.Length}");
foreach (var train in trainCars)
{
var carTransform = train.carTransform;
if (!train.oreContainer)
{
train.oreContainer = carTransform.Find("OreContainer");
}
if (!train.collectPoint)
{
train.collectPoint = carTransform.Find("CollectPoint");
}
train.bar = carTransform.Find("ui/bg_bar/bar").GetComponent<Image>();
train.bar.fillAmount = 0;
train.reward = carTransform.Find("ui/bg_bar/reward");
// var anim = train.oreContainer.GetComponent<Animation>();
// anim.GoToFrame(0);
// 默认设置未 全部隐藏
var childAnim = train.oreContainer.GetComponent<OreIncreaseController>();
childAnim.Reset();
}
}
public void SetTrainCarCollectPoints(int index, int endPoints = -1)
{
var targetCar = trainCars[index];
var maxNum = targetCar.maxCapacity;
if (endPoints == -1)
{
endPoints = maxNum;
}
var fillAmount = (float)endPoints/maxNum;
targetCar.bar.fillAmount = fillAmount;
// var anim = targetCar.oreContainer.GetComponent<Animation>();
// anim.GoToFrame(Mathf.CeilToInt(fillAmount * 100));
var animController = targetCar.oreContainer.GetComponent<OreIncreaseController>();
animController.JumpTo(fillAmount);
ELog($"SetTrainCarCollectPoints index={index} endPoints={endPoints} fillAmount={fillAmount:F3}");
}
public async Task CollectOre(int step, GameObject ore)
{
// Log($"CollectOre-> {step}");
if (!ore) return;
if (step > trainCars.Length - 1)
{
return;
}
var targetCar = trainCars[step];
if (targetCar == null)
return;
await AnimateOreFlight(ore, targetCar);
targetCar.AddOre(ore);
}
#region Audio
private async void LoadAudio()
{
audioFlyClip = await Addressables.LoadAssetAsync<AudioClip>("audio_ui_partnermining_resource_fly").Task;
}
void AudioRelease()
{
Addressables.Release(audioFlyClip);
}
private void StopFlyAudio()
{
if (soundFly)
{
soundFly.audioSource.Stop();
soundFly.ReturnPool();
soundFly = null;
}
}
private void PlayFlyAudio()
{
StopFlyAudio();
soundFly = GContext.container.Resolve<ISoundService>().GetNewUISound(audioFlyClip);
soundFly.audioSource.Play();
soundFly.audioSource.loop = true;
}
#endregion
// TrainCar 都在一辆上面
public async Task RunOreIncreaseAnimation(int index, int startPoints,int endPoints)
{
ELog($"RunOreIncreaseAnimation ->{index} {startPoints} {endPoints}");
// GContext.Publish(new EventUISound("audio_ui_partnermining_resource_fly"));
PlayFlyAudio();
var targetCar = trainCars[index];
var maxNum = targetCar.maxCapacity;
var startFrame = startPoints * 100 / maxNum ;
var endFrame = endPoints * 100 / maxNum;
targetCar.bar.fillAmount = (float)startPoints/maxNum;
targetCar.bar.DOFillAmount((float)endPoints/maxNum,0.4f);
// var anim = targetCar.oreContainer.GetComponent<Animation>();
// var clipName = anim.clip.name;
// await anim.PlayAnimFromToAsync(this,clipName,startFrame,endFrame);
var animController = targetCar.oreContainer.GetComponent<OreIncreaseController>();
await animController.AsyncPlayFromTo(startFrame, endFrame);
StopFlyAudio();
}
#region ,
private TrainCar SelectTargetCar(int points)
{
// 根据点数选择对应的车厢
foreach (var car in trainCars)
{
if (car != null && car.CanCollect(points))
{
// 简单的分配逻辑:按点数范围分配
if (points <= car.pointsRange)
{
return car;
}
}
}
// // 如果没有合适的车厢,返回第一个有空间的车厢
// foreach (var car in trainCars)
// {
// if (car != null && car.CanCollect(points))
// {
// return car;
// }
// }
return null;
}
private async Task _AnimateOreFlight(GameObject ore, TrainCar targetCar)
{
if (!ore || !targetCar?.oreContainer) return;
var startPos = ore.transform.position;
var endPos = targetCar.collectPoint.position;
// 添加抛物线轨迹
var midPos = (startPos + endPos) * 0.5f;
midPos.y += flyOreOffsetY; // 抛物线高度
midPos.x -= flyOreOffsetX; // 偏移值
float t = 0f;
while (t < OreFlightDuration)
{
t += Time.deltaTime / OreFlightDuration;
var curveT = oreFlightCurve.Evaluate(t);
// 贝塞尔曲线插值
var pos = CalculateBezierPoint(curveT, startPos, midPos, endPos);
ore.transform.position = pos;
// 旋转效果
ore.transform.Rotate(0, 0, 180 * Time.deltaTime / OreFlightDuration);
await Awaiters.NextFrame;
}
ore.transform.position = endPos;
}
#endregion
private async Task AnimateOreFlight(GameObject ore, TrainCar targetCar)
{
if (!ore || !targetCar?.oreContainer) return;
Vector3 startPos = ore.transform.position;
Vector3 endPos = targetCar.collectPoint.position;
// 中间抛物点
Vector3 midPos = (startPos + endPos) * 0.5f;
midPos.y += flyOreOffsetY;
midPos.x -= flyOreOffsetX;
// 清理之前的 tween如果有
ore.transform.DOKill();
// 路径点(贝塞尔可近似成分段点,这里用三个点实现抛物)
Vector3[] path = { startPos, midPos, endPos };
// // 位移动画
// ore.transform.DOPath(path, oreFlightDuration, PathType.CatmullRom)
// .SetEase(oreFlightCurve)
// .OnComplete(() =>
// {
// // 动画结束确保位置对齐
// ore.transform.position = endPos;
// });
// 先清除旧的 tween
// ore.transform.DOKill();
var t = 0f;
Tween tween = DOTween.To(() => t, x =>
{
t = x;
float curveT = oreFlightCurve.Evaluate(t);
Vector3 pos = CalculateBezierPoint(curveT, startPos, midPos, endPos);
ore.transform.position = pos;
// ore.transform.Rotate(0, 0, 180 * Time.deltaTime / OreFlightDuration);
}, 1f, OreFlightDuration)
.SetEase(Ease.Linear)
.OnComplete(() => { ore.transform.position = endPos; });
// 如果要旋转,可以单独加一个旋转 tween
// ore.transform.DORotate(
// new Vector3(0, 0, 180), // 会被解释为目标角度(非增量)
// oreFlightDuration,
// RotateMode.FastBeyond360 // 表示可以旋转超过360度
// );
await tween.AsyncWaitForCompletion();
}
private static Vector3 CalculateBezierPoint(float t, Vector3 p0, Vector3 p1, Vector3 p2)
{
var u = 1 - t;
var tt = t * t;
float uu = u * u;
return uu * p0 + 2 * u * t * p1 + tt * p2;
}
public void ResetTrain()
{
ELog($"ResetTrain count={trainCars.Length}");
foreach (var train in trainCars)
{
if (train == null) continue;
train?.ClearOres();
var carTransform = train.carTransform;
if (!train.oreContainer)
{
train.oreContainer = carTransform.Find("OreContainer");
}
if (!train.collectPoint)
{
train.collectPoint = carTransform.Find("CollectPoint");
}
train.bar = carTransform.Find("ui/bg_bar/bar").GetComponent<Image>();
train.bar.fillAmount = 0;
train.reward = carTransform.Find("ui/bg_bar/reward");
// var anim = train.oreContainer.GetComponent<Animation>();
// anim.GoToFrame(0);
var animController = train.oreContainer.GetComponent<OreIncreaseController>();
animController.Reset();
}
}
public void UpdateData(EventPartnerGatherManager manager)
{
ELog($"UpdateData-> {manager}");
_manager = manager;
var carNumber = trainCars.Length;
for (var i = 0; i < carNumber; ++i)
{
var trainCar = trainCars[i];
trainCar.maxCapacity = manager.NodeSteps[i];
trainCar.pointsRange = manager.NodeCumulativeSteps[i];
var rewardItemNew = trainCar.reward.GetComponent<RewardItemNew>();
var dropId = _manager.GetStageReward(i);
var listItems = _playerItemData.GetItemDataByDropIdLureInflation(_manager.GatherData.InflationRate, dropId);
rewardItemNew.SetData(listItems.FirstOrDefault());
}
}
//
private static void ELog(object t)
{
#if UNITY_EDITOR
Debug.Log($"<Color=cyan> EventPartnerTrainController {t} </color>");
#endif
}
}
}