做不出来

This commit is contained in:
SoulliesOfficial
2026-06-30 01:48:58 -04:00
parent 9a9e48f8a5
commit ddd387ef35
132 changed files with 8945 additions and 2943 deletions

View File

@@ -160,26 +160,26 @@ namespace Cielonos.MainGame
#region TimeSubmodule
public T SetTimeSubmodule<T>(float lifeTime) where T : AttackAreaBase
{
timeSm = new TimeSubmodule(this, lifeTime);
float graceBefore = targetFractions.Contains(Fraction.Player) ? 0.1f : 0f;
timeSm = new TimeSubmodule(this, lifeTime, 0f, 0.04f, null, null, graceBefore);
return this as T;
}
public T SetTimeSubmodule<T>(float lifeTime, float delayTime, float enableTime = 0.04f,
Action enableAction = null, Action timeOutAction = null) where T : AttackAreaBase
{
timeSm = new TimeSubmodule(this, lifeTime, delayTime, enableTime, enableAction, timeOutAction);
return this as T;
}
/// <summary>
/// 设置带反应 grace window 的时间子模块。graceBefore/graceAfter 为 0 时行为与无 grace window 一致。
/// 设置带反应 grace window 的时间子模块。graceBefore 为 0 时行为与无 grace window 一致。
/// </summary>
public T SetTimeSubmodule<T>(float lifeTime, float delayTime, float enableTime,
Action enableAction, Action timeOutAction, float graceBefore) where T : AttackAreaBase
public T SetTimeSubmodule<T>(float lifeTime, float delayTime, float enableTime = 0.04f,
Action enableAction = null, Action timeOutAction = null, float graceBefore = -1f) where T : AttackAreaBase
{
if (graceBefore < 0f)
{
graceBefore = targetFractions.Contains(Fraction.Player) ? 0.1f : 0f;
}
timeSm = new TimeSubmodule(this, lifeTime, delayTime, enableTime, enableAction, timeOutAction, graceBefore);
return this as T;
}
#endregion
#region HitSubmodule

View File

@@ -117,7 +117,7 @@ namespace Cielonos.MainGame
{
public void Update()
{
if (!isEnabling || currentHitIndex >= originalHitCount - 1 || attackArea.timeSm.delayTime > 0)
if (!isEnabling || currentHitIndex >= originalHitCount || attackArea.timeSm.delayTime > 0)
{
return;
}
@@ -134,7 +134,7 @@ namespace Cielonos.MainGame
currentHitIndex++;
}
if (currentHitIndex >= originalHitCount - 1)
if (currentHitIndex >= originalHitCount)
{
attackArea.isEnabling = false;
}

View File

@@ -57,11 +57,11 @@ namespace Cielonos.MainGame
}
});
this.reactionGraceBefore = 0.1f;
this.reactionGraceBefore = 0f;
}
public TimeSubmodule(AttackAreaBase attackArea, float lifeTime, float delayTime, float enableTime,
Action enableAction, Action timeOutAction, float graceBefore = 0.1f) : base(attackArea)
Action enableAction, Action timeOutAction, float graceBefore = 0f) : base(attackArea)
{
this.isEnabling = true;
this.lifeTime = lifeTime;
@@ -118,6 +118,14 @@ namespace Cielonos.MainGame
return true;
}
// 过渡帧桥接delay 刚结束delayTime 被扣至 <=0但 enable 阶段尚未在下一帧
// 的 timeSm.Update() 中激活。此帧 isEnabling 为 false 且 delayTime 不再 >0
// 若不补此条件会导致 1 帧的反应窗口真空,玩家的完美闪避/格挡可能被跳过。
if (reactionGraceBefore > 0f && delayTime <= 0f && !hasInvokedEnableAction)
{
return true;
}
return false;
}
}

View File

@@ -22,7 +22,7 @@ namespace Cielonos.MainGame.Buffs.Character
private bool _markSpawned = false;
private VFXObject _markVFX;
public CowardicePenalty(float thresholdDuration = 20f, float recoveryPerSecond = 20f)
public CowardicePenalty(float thresholdDuration = 30f, float recoveryPerSecond = 20f)
{
Initialize(BuffType.Neutral, BuffDispelLevel.Undispellable);
contentSubmodule = new ContentSubmodule(this);

View File

@@ -1,3 +1,4 @@
using Cielonos.MainGame;
using Cielonos.MainGame.Characters;
using SLSUtilities.FunctionalAnimation;
using SLSUtilities.WwiseAssistance;
@@ -81,8 +82,8 @@ namespace Cielonos.MainGame.Buffs.Character
protected override void OnProgressComplete()
{
float freezeDuration = 3f * sourceCharacter.attributeSm["NegativeEffectDealtDurationMultiplier"]
* attachedCharacter.attributeSm["NegativeEffectTakenDurationMultiplier"];
float freezeDuration = 3f * sourceCharacter.attributeSm[CharacterAttribute.NegativeBuffDealtDurationMultiplier]
* attachedCharacter.attributeSm[CharacterAttribute.NegativeBuffReceivedDurationMultiplier];
new Freeze(freezeDuration).Apply(attachedCharacter, sourceCharacter);
}
}

View File

@@ -1,5 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using Cielonos.MainGame;
using Cielonos.MainGame.Characters;
using SLSUtilities.General;
using UnityEngine;
@@ -81,8 +83,8 @@ namespace Cielonos.MainGame.Buffs.Character
protected override void OnProbabilityComplete()
{
float duration = 3f * sourceCharacter.attributeSm["NegativeEffectDealtDurationMultiplier"]
* attachedCharacter.attributeSm["NegativeEffectTakenDurationMultiplier"];
float duration = 3f * sourceCharacter.attributeSm[CharacterAttribute.NegativeBuffDealtDurationMultiplier]
* attachedCharacter.attributeSm[CharacterAttribute.NegativeBuffReceivedDurationMultiplier];
new Weak(duration).Apply(attachedCharacter, sourceCharacter);
}
}

View File

@@ -65,7 +65,8 @@ namespace Cielonos.MainGame.Characters
{
selfTimeSm?.SetUp(this);
Action<AttackAreaBase, CharacterBase, Attack.Result> onAttackHit = OnAttackHitRecoverEnergy;
eventSm.onFinishAttack.Add("AttackEnergyGain", onAttackHit.ToPrioritized());
}
protected virtual void Update()
@@ -263,11 +264,11 @@ namespace Cielonos.MainGame.Characters
public virtual void RecoverEnergy(float energyAmount, bool spawnText = true)
{
if (energyAmount <= 0) return;
if (Mathf.Abs(energyAmount) < 1e-5f) return;
if (!attributeSm.Has(CharacterAttribute.Energy) || !attributeSm.Has(CharacterAttribute.MaximumEnergy)) return;
attributeSm[CharacterAttribute.Energy] += energyAmount;
attributeSm[CharacterAttribute.Energy] = Mathf.Min(attributeSm[CharacterAttribute.Energy], attributeSm[CharacterAttribute.MaximumEnergy]);
attributeSm[CharacterAttribute.Energy] = Mathf.Clamp(attributeSm[CharacterAttribute.Energy], 0f, attributeSm[CharacterAttribute.MaximumEnergy]);
if (spawnText)
{
@@ -279,6 +280,18 @@ namespace Cielonos.MainGame.Characters
}
}
private void OnAttackHitRecoverEnergy(AttackAreaBase area, CharacterBase target, Attack.Result result)
{
if (attributeSm.Has(CharacterAttribute.EnergyGainByAttack))
{
float energyGain = attributeSm[CharacterAttribute.EnergyGainByAttack];
if (Mathf.Abs(energyGain) > 1e-5f)
{
RecoverEnergy(energyGain, true);
}
}
}
/// <summary>
/// 对角色施加韧性削减。适用于常规攻击造成的削韧。
/// </summary>

View File

@@ -130,7 +130,7 @@ namespace Cielonos.MainGame.Characters
float currentMaxAngle = state.isWeak ? maxShakeAngle * weakShakeMultiplier : maxShakeAngle;
// ③ 高频正弦波,三轴不同频率 + 骨骼唯一种子
float t = Time.time * shakeFrequency;
float t = deltaTime * shakeFrequency;
float aX = currentMaxAngle * p * Mathf.Sin(t + state.seedX);
float aY = currentMaxAngle * p * Mathf.Sin(t * 1.3f + state.seedY);
float aZ = currentMaxAngle * p * Mathf.Sin(t * 0.7f + state.seedZ);

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9af2cadfa58fe0443a1272f0e8521c3d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -1,8 +1,11 @@
using System;
using DG.Tweening;
using Sirenix.OdinInspector;
using SLSUtilities.General;
using UnityEngine;
using UnityEngine.AI;
using UnityEngine.Serialization;
using Cielonos.MainGame.Effects;
namespace Cielonos.MainGame.Characters
{
@@ -48,7 +51,7 @@ namespace Cielonos.MainGame.Characters
}
}
public void SmartTurnToTarget(CharacterBase target, float maxTurnAngle = 360f)
public void SmartTurnToTarget(CharacterBase target, float maxTurnAngle = 360f, bool isInstant = false)
{
Vector3 directionToTarget = (target.CenterPoint.position - owner.CenterPoint.position).Flatten();
if (directionToTarget.sqrMagnitude < 0.001f) return;
@@ -68,7 +71,8 @@ namespace Cielonos.MainGame.Characters
else
{
float duration = Mathf.Lerp(0.1f, 0.2f, (absAngle - 45f) / 135f);
owner.transform.DORotateQuaternion(Quaternion.LookRotation(directionToTarget), duration).Play();
if(!isInstant) owner.transform.DORotateQuaternion(Quaternion.LookRotation(directionToTarget), duration).Play();
else owner.transform.rotation = Quaternion.LookRotation(directionToTarget);
}
}
@@ -184,4 +188,5 @@ namespace Cielonos.MainGame.Characters
ByMovement,
}
}
}

View File

@@ -0,0 +1,217 @@
using System;
using UnityEngine;
using UnityEngine.AI;
using Cielonos.MainGame.Effects;
using SLSUtilities.General;
namespace Cielonos.MainGame.Characters
{
public partial class MovementSubcontrollerBase
{
/// <summary>
/// 根据目标敌人的碰撞体边缘,计算不产生重叠/挤压的安全折跃落脚点。
/// </summary>
/// <param name="target">折跃目标</param>
/// <param name="stopDistance">目标表面前的停止缓冲距离(默认为 1.0f</param>
/// <returns>安全世界坐标</returns>
public Vector3 GetSafePositionNearTarget(CharacterBase target, float stopDistance = 1.0f)
{
if (target == null)
{
return owner.transform.position;
}
Vector3 selfPos = owner.transform.position;
Vector3 targetPos = target.transform.position;
// 计算平面方向向量
Vector3 dir = (targetPos - selfPos).Flatten();
if (dir.sqrMagnitude < 0.001f)
{
dir = owner.transform.forward;
}
else
{
dir.Normalize();
}
// 获取双方的碰撞半径(如果有 CharacterController
float selfRadius = 0.5f;
float targetRadius = 0.5f;
if (owner.collisionSc != null && owner.collisionSc.characterController != null)
{
selfRadius = owner.collisionSc.characterController.radius;
}
else
{
var cc = owner.GetComponent<CharacterController>();
if (cc != null) selfRadius = cc.radius;
}
if (target.collisionSc != null && target.collisionSc.characterController != null)
{
targetRadius = target.collisionSc.characterController.radius;
}
else
{
var cc = target.GetComponent<CharacterController>();
if (cc != null) targetRadius = cc.radius;
}
// 安全距离 = 双方半径之和 + 额外的缓冲停止距离
float safeDistance = selfRadius + targetRadius + stopDistance;
// 目标点位于:目标中心 沿着 玩家至目标的方向 往回拉 safeDistance 距离
Vector3 finalDest = targetPos - dir * safeDistance;
// 保持 Y 轴与目标地面或自身对齐(避免陷地或腾空)
finalDest.y = targetPos.y;
return finalDest;
}
/// <summary>
/// 瞬移/闪现至目标位置(支持高速移动过程)
/// </summary>
/// <param name="destination">目标位置</param>
/// <param name="duration">移动时间(秒)。若为 0 则为即时瞬移</param>
/// <param name="onComplete">瞬移完成后(或即时瞬移时)的回调函数,可用于生成 VFX/音效</param>
public void Teleport(Vector3 destination, float duration, Action onComplete = null)
{
// 1. 进行 NavMesh 采样验证(寻找最近的可行进点)
Vector3 validDestination = destination;
if (NavMesh.SamplePosition(destination, out NavMeshHit hit, 5.0f, NavMesh.AllAreas))
{
validDestination = hit.position;
}
// 2. 如果是即时瞬移duration <= 0
if (duration <= 0f)
{
PerformMove(validDestination);
onComplete?.Invoke();
return;
}
// 3. 带有延迟/移动过程的瞬移
// 记录起始点
Vector3 startPos = owner.transform.position;
// 赋予无敌状态(防受击)
owner.statusSm.AddStatus(StatusType.Invincible, duration);
// 锁定移动和旋转输入(使用高优先级 99 隔离普通移动控制)
canMove.Modify(false, 99);
canRotate.Modify(false, 99);
// 开启残影生成(位移开始时生成单个残影)
var afterImageGen = owner.GetComponent<AfterImageGenerator>();
if (afterImageGen != null)
{
afterImageGen.CreateAfterImageSnapshot();
}
// 获取并临时关闭物理/寻路组件,避免移动中碰撞卡顿或位置拉回
var agent = owner.GetComponent<NavMeshAgent>();
bool wasAgentActive = agent != null && agent.isActiveAndEnabled;
if (wasAgentActive)
{
agent.enabled = false;
}
var cc = owner.collisionSc != null ? owner.collisionSc.characterController : null;
bool wasCcActive = cc != null && cc.enabled;
if (wasCcActive)
{
cc.enabled = false;
}
var rb = owner.collisionSc != null ? owner.collisionSc.mainRigidbody : null;
bool wasRbKinematic = rb != null && rb.isKinematic;
if (rb != null)
{
rb.isKinematic = true;
}
// 使用本地计时器,更新过程中的插值位移
float elapsed = 0f;
owner.selfTimeSm.AddLocalTimer(duration,
onComplete: () =>
{
// 确保移动完毕并最终强制对齐
owner.transform.position = validDestination;
// 恢复组件状态
if (rb != null)
{
rb.isKinematic = wasRbKinematic;
}
if (cc != null && wasCcActive)
{
cc.enabled = true;
}
if (agent != null && wasAgentActive)
{
agent.enabled = true;
agent.Warp(validDestination);
}
// 解锁移动和旋转输入
canMove.Modify(true, 99);
canRotate.Modify(true, 99);
// 触发完成回调
onComplete?.Invoke();
},
onUpdate: () =>
{
elapsed += DeltaTime;
float t = Mathf.Clamp01(elapsed / duration);
Vector3 currentPos = Vector3.Lerp(startPos, validDestination, t);
// 平滑设置物理和变换位置
if (rb != null)
{
rb.position = currentPos;
}
owner.transform.position = currentPos;
}
);
}
/// <summary>
/// 底层物理突变移动处理
/// </summary>
private void PerformMove(Vector3 targetPos)
{
var agent = owner.GetComponent<NavMeshAgent>();
if (agent != null && agent.isActiveAndEnabled)
{
agent.Warp(targetPos);
}
else if (owner.collisionSc != null)
{
if (owner.collisionSc.useCharacterController && owner.collisionSc.characterController != null)
{
owner.collisionSc.characterController.enabled = false;
owner.transform.position = targetPos;
owner.collisionSc.characterController.enabled = true;
}
else if (owner.collisionSc.mainRigidbody != null)
{
owner.collisionSc.mainRigidbody.position = targetPos;
owner.transform.position = targetPos;
}
else
{
owner.transform.position = targetPos;
}
}
else
{
owner.transform.position = targetPos;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5d9130106184f0142a4fdf38d6a574e6

View File

@@ -1,3 +1,4 @@
using Cielonos.MainGame.Effects;
using Cielonos.MainGame.Effects.Feedback;
using SLSUtilities.General;
using Unity.Mathematics;
@@ -31,7 +32,7 @@ namespace Cielonos.MainGame.Characters
player.landMovementSc.TurnToDirection(direction, 0f);
}
}
if(player.eventSm is PlayerEventSubmodule playerEventSm)
{
foreach(var action in playerEventSm.onDodgeAttempt.Values) action.Invoke(success);

View File

@@ -40,10 +40,11 @@ namespace Cielonos.MainGame.Characters
{
backpackSm.ObtainItem<Polychrome>();
backpackSm.ObtainItem<FutureWand>();
backpackSm.ObtainItem<DualHarmony>();
backpackSm.ObtainItem<Ascension>();
backpackSm.ObtainItem<Passion>();
backpackSm.ObtainItem<ThermalDetonator>();
backpackSm.ObtainItem<SpatialWarpCaliper>();
backpackSm.ObtainItem<PhotonWarper>();
foreach (MainWeaponBase mainWeapon in backpackSm.mainWeapons)
{

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using Cielonos.MainGame;
using Cielonos.MainGame.Buffs.Character;
using Cielonos.MainGame.Effects.Feedback;
using Cielonos.MainGame.UI;
@@ -197,9 +198,34 @@ namespace Cielonos.MainGame.Characters
public override void RecoverEnergy(float energyAmount, bool spawnText = true)
{
if (energyAmount <= 0) return;
if (Mathf.Abs(energyAmount) < 1e-5f) return;
AddEnergy(energyAmount);
if (energyAmount > 0)
{
float current = attributeSm[CharacterAttribute.Energy];
float max = attributeSm[CharacterAttribute.MaximumEnergy];
float availableSpace = max - current;
if (energyAmount > availableSpace)
{
attributeSm[CharacterAttribute.Energy] = max;
float conversionRate =
attributeSm.Has(CharacterAttribute.OverloadConversionRate) ?
attributeSm[CharacterAttribute.OverloadConversionRate] : 1f;
float overflowEnergy = (energyAmount - availableSpace) * conversionRate;
DistributeOverloadEnergy(overflowEnergy);
}
else
{
attributeSm[CharacterAttribute.Energy] += energyAmount;
}
}
else
{
attributeSm[CharacterAttribute.Energy] += energyAmount;
attributeSm[CharacterAttribute.Energy] = Mathf.Max(0, attributeSm[CharacterAttribute.Energy]);
}
if (spawnText)
{

View File

@@ -24,40 +24,7 @@ namespace Cielonos.MainGame.Characters
RecoverEnergy(energyRegenRate, false);
}
}
/// <summary>
/// 增减能量值,正值为增加(溢出部分按 OverloadConversionRate 转换为过载能量),负值为消耗。
/// UI 更新和 onEnergyChanged 事件由属性值变更回调自动触发。
/// </summary>
public void AddEnergy(float amount)
{
if (amount == 0) return;
if (amount > 0)
{
float current = attributeSm[CharacterAttribute.Energy];
float max = attributeSm[CharacterAttribute.MaximumEnergy];
float availableSpace = max - current;
if (amount > availableSpace)
{
attributeSm[CharacterAttribute.Energy] = max;
float conversionRate = attributeSm.Has(CharacterAttribute.OverloadConversionRate) ? attributeSm[CharacterAttribute.OverloadConversionRate] : 1f;
float overflowEnergy = (amount - availableSpace) * conversionRate;
DistributeOverloadEnergy(overflowEnergy);
}
else
{
attributeSm[CharacterAttribute.Energy] += amount;
}
}
else
{
attributeSm[CharacterAttribute.Energy] += amount;
attributeSm[CharacterAttribute.Energy] = Mathf.Max(0, attributeSm[CharacterAttribute.Energy]);
}
}
private void DistributeOverloadEnergy(float totalOverflowAmount)
{

View File

@@ -8,6 +8,7 @@ using UnityEngine.UI;
using SLSUtilities.General;
using Unity.Cinemachine;
using Ease = DG.Tweening.Ease;
using Cielonos.MainGame.Effects.Feedback;
namespace Cielonos.MainGame.Characters
{
@@ -44,6 +45,7 @@ namespace Cielonos.MainGame.Characters
// 用于存储独立的摄像机平滑旋转状态,防止跟随 Transform 的跳变
private float currentYaw;
private float currentPitch;
private bool wasShakingLastFrame;
private Tweener iconTween;
@@ -117,9 +119,29 @@ namespace Cielonos.MainGame.Characters
// 【2】接管并强制控制 OrbitalFollow 的轨道位置
var orbitalFollow = viewSc.lockingTargetCamera.GetComponent<CinemachineOrbitalFollow>();
// 直接强制摄像机在水平和垂直轨道上移动
orbitalFollow.HorizontalAxis.Value = currentYaw;
orbitalFollow.VerticalAxis.Value = currentPitch;
float horizontalOffset = 0f;
float verticalOffset = 0f;
var orbitShaker = viewSc.lockingTargetCamera.GetComponent<CameraOrbitShaker>();
bool isOrbitActive = orbitShaker != null && orbitShaker.enabled && orbitShaker.HasActiveShakes;
if (isOrbitActive)
{
horizontalOffset = orbitShaker.CurrentHorizontalOffset;
verticalOffset = orbitShaker.CurrentVerticalOffset;
wasShakingLastFrame = true;
}
else if (wasShakingLastFrame)
{
// 刚结束大招回旋的帧:将相机的最终物理轴值写入内部状态,防止数值突变
currentYaw = orbitalFollow.HorizontalAxis.Value;
currentPitch = orbitalFollow.VerticalAxis.Value;
wasShakingLastFrame = false;
}
// 直接强制摄像机在水平和垂直轨道上移动(包含大招/反馈的 Orbit 回旋偏差量)
orbitalFollow.HorizontalAxis.Value = currentYaw + horizontalOffset;
orbitalFollow.VerticalAxis.Value = currentPitch + verticalOffset;
float OF_Fade = camLockData.orbitalFollowFadeCurve.Evaluate(Mathf.Clamp01((horizontalDistance - camLockData.orbitalFollowFadeDistanceRange.x) /
(camLockData.orbitalFollowFadeDistanceRange.y - camLockData.orbitalFollowFadeDistanceRange.x)));
@@ -226,6 +248,7 @@ namespace Cielonos.MainGame.Characters
this.isLocking = false;
this.isAutoRotate = false;
viewSc.stateDrivenCamera.GetComponent<Animator>().SetBool("isLockTarget", false);
viewSc.currentCamera = viewSc.freeLookCamera; // 核心修复:更新当前相机引用为自由相机
Transform oldTargetPoint = targetPoint;
this.lockTarget = null;

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ea89affa681d8784eb0146116baeae51
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,146 @@
using System.Collections.Generic;
using UnityEngine;
using Cielonos.MainGame.Characters;
namespace Cielonos.MainGame.Effects
{
public enum AfterImageSpawnMode
{
TimeInterval,
DistanceInterval
}
/// <summary>
/// 残影发生器,挂载在角色身上,用于在高速移动时动态生成姿态残影。
/// </summary>
public class AfterImageGenerator : MonoBehaviour
{
private CharacterBase character;
private List<SkinnedMeshRenderer> skinnedRenderers = new List<SkinnedMeshRenderer>();
private bool isSpawning = false;
private float timer;
private Vector3 lastSpawnPos;
[Header("AfterImage Settings")]
[Tooltip("残影所使用的菲涅尔发光材质")]
public Material afterImageMaterial;
[Tooltip("单个残影的持续消失时间(秒)")]
public float afterImageDuration = 0.3f;
[Tooltip("残影生成模式:时间间隔 或 距离间隔")]
public AfterImageSpawnMode spawnMode = AfterImageSpawnMode.TimeInterval;
[Tooltip("生成残影的时间间隔TimeInterval 模式)")]
public float spawnInterval = 0.03f;
[Tooltip("生成残影的距离间隔DistanceInterval 模式)")]
public float spawnDistance = 0.8f;
private void Start()
{
character = GetComponent<CharacterBase>();
GetComponentsInChildren(true, skinnedRenderers);
}
public void StartSpawning()
{
isSpawning = true;
timer = 0f; // 时间模式下启动时立即生成一个
lastSpawnPos = transform.position; // 距离模式下重置起始点
if (spawnMode == AfterImageSpawnMode.DistanceInterval)
{
CreateAfterImageSnapshot(); // 距离模式启动时也立即生成第一个
}
}
public void StopSpawning()
{
isSpawning = false;
}
#region Dynamic Configuration APIs
public void SetSpawnMode(AfterImageSpawnMode mode) => spawnMode = mode;
public void SetSpawnInterval(float interval) => spawnInterval = interval;
public void SetSpawnDistance(float distance) => spawnDistance = distance;
public void SetAfterImageDuration(float duration) => afterImageDuration = duration;
public void SetMaterial(Material mat) => afterImageMaterial = mat;
#endregion
private void Update()
{
if (!isSpawning || character == null) return;
if (spawnMode == AfterImageSpawnMode.TimeInterval)
{
timer -= character.selfTimeSm.DeltaTime;
if (timer <= 0f)
{
CreateAfterImageSnapshot();
timer = spawnInterval;
}
}
else if (spawnMode == AfterImageSpawnMode.DistanceInterval)
{
Vector3 currentPos = transform.position;
float dist = Vector3.Distance(lastSpawnPos, currentPos);
if (dist >= spawnDistance)
{
CreateAfterImageSnapshot();
lastSpawnPos = currentPos;
}
}
}
/// <summary>
/// 手动生成一个当前姿态的残影
/// </summary>
public void CreateAfterImageSnapshot()
{
if (afterImageMaterial == null) return;
// 创建残影根容器
GameObject afterImageObj = new GameObject($"{gameObject.name}_AfterImage");
List<MeshFilter> filters = new List<MeshFilter>();
List<MeshRenderer> renderers = new List<MeshRenderer>();
foreach (var skinnedRenderer in skinnedRenderers)
{
// 仅烘焙当前处于显示状态的蒙皮部件
if (skinnedRenderer == null || !skinnedRenderer.gameObject.activeInHierarchy || !skinnedRenderer.enabled)
continue;
// 创建对应蒙皮部件的静态 Mesh 副本
GameObject subObj = new GameObject(skinnedRenderer.name);
subObj.transform.SetParent(afterImageObj.transform);
subObj.transform.position = skinnedRenderer.transform.position;
subObj.transform.rotation = skinnedRenderer.transform.rotation;
subObj.transform.localScale = skinnedRenderer.transform.lossyScale;
// 烘焙蒙皮网格的当前姿态
Mesh bakedMesh = new Mesh();
skinnedRenderer.BakeMesh(bakedMesh);
MeshFilter filter = subObj.AddComponent<MeshFilter>();
filter.mesh = bakedMesh;
filters.Add(filter);
MeshRenderer renderer = subObj.AddComponent<MeshRenderer>();
renderers.Add(renderer);
}
// 如果没有成功烘焙出任何网格,直接销毁根容器
if (filters.Count == 0)
{
Destroy(afterImageObj);
return;
}
// 初始化渐隐控制
AfterImageItem afterImageItem = afterImageObj.AddComponent<AfterImageItem>();
afterImageItem.Initialize(filters.ToArray(), renderers.ToArray(), afterImageMaterial, afterImageDuration);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 757d4da67572fb847bf547e39d362e9e

View File

@@ -0,0 +1,57 @@
using UnityEngine;
namespace Cielonos.MainGame.Effects
{
/// <summary>
/// 单个残影个体生命周期与渐隐控制器。
/// </summary>
public class AfterImageItem : MonoBehaviour
{
private MeshRenderer[] renderers;
private MaterialPropertyBlock propBlock;
private float fadeSpeed;
private float currentFade = 1f;
public void Initialize(MeshFilter[] filters, MeshRenderer[] renderers, Material afterImageMat, float duration)
{
this.renderers = renderers;
this.fadeSpeed = 1f / duration;
this.propBlock = new MaterialPropertyBlock();
foreach (var renderer in renderers)
{
if (renderer != null)
{
renderer.sharedMaterial = afterImageMat;
}
}
}
private void Update()
{
currentFade -= Time.deltaTime * fadeSpeed;
if (currentFade <= 0f)
{
// 必须手动销毁运行时烘焙出的 Mesh 资源,防止显存泄漏 (GC 不会自动回收 BakeMesh 产生的网格)
foreach (var filter in GetComponentsInChildren<MeshFilter>())
{
if (filter != null && filter.sharedMesh != null)
{
Destroy(filter.sharedMesh);
}
}
Destroy(gameObject);
return;
}
propBlock.SetFloat("_Fade", currentFade);
foreach (var renderer in renderers)
{
if (renderer != null)
{
renderer.SetPropertyBlock(propBlock);
}
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e3cb1f0acf837a945ab000963b8c1541

View File

@@ -0,0 +1,115 @@
using System;
using Sirenix.OdinInspector;
using SLSUtilities.Feedback;
using UnityEngine;
namespace Cielonos.MainGame.Effects.Feedback
{
/// <summary>
/// 摄像机轨道旋转反馈动作,通过 CameraOrbitEvent 触发 CameraOrbitShaker。
/// 支持两种模式:
/// - Additive曲线值作为偏移量叠加到初始角度上
/// - TargetValue从当前值平滑运动到指定的目标角度
/// </summary>
[Serializable]
[FeedbackActionColor(0.3f, 0.7f, 0.95f)]
public class CameraOrbitAction : CinemachineActionBase
{
public override string DisplayName => "Camera Orbit";
public enum OrbitMode
{
/// <summary>
/// 曲线输出值作为角度偏移量,叠加到触发时的初始角度上。
/// </summary>
Additive,
/// <summary>
/// 类似 DoTween指定目标角度 (endValue),通过缓动曲线从当前位置平滑运动到目标。
/// </summary>
TargetValue
}
[TitleGroup("生效相机设置")]
[LabelText("在 FreeLook (自由视角) 相机下生效")]
public bool enableInFreeLook = true;
[LabelText("在 LockTarget (锁定视角) 相机下生效")]
public bool enableInLockTarget = true;
// ===== 水平公转设置 (Yaw) =====
[TitleGroup("水平公转设置 (Yaw)")]
[LabelText("启用水平公转")]
public bool enableYaw = true;
[LabelText("水平公转模式")]
[ShowIf("enableYaw")]
public OrbitMode yawMode = OrbitMode.Additive;
// Yaw Additive 参数
[LabelText("水平公转曲线 (Yaw)")]
[ShowIf("@enableYaw && yawMode == OrbitMode.Additive")]
public FloatCurveChannel horizontalCurve = FloatCurveChannel.CreateDefault(0f, 360f);
// Yaw TargetValue 参数
[LabelText("目标角度 (Yaw)")]
[Tooltip("目标水平公转角度。0° = 玩家正面180° = 玩家背面。可以超过 360°。")]
[ShowIf("@enableYaw && yawMode == OrbitMode.TargetValue")]
public float endHorizontalValue = 0f;
[LabelText("缓动曲线 (Yaw)")]
[Tooltip("水平公转缓动曲线:归一化时间 [0,1] [0,1]")]
[ShowIf("@enableYaw && yawMode == OrbitMode.TargetValue")]
[ShakeCurvePreset]
public AnimationCurve yawEaseCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
[LabelText("使用最短路径")]
[Tooltip("开启时,相机会选择最近的旋转方向移向目标角度(防止 360 度穿帮旋转);关闭时,将按照绝对数值插值。")]
[ShowIf("@enableYaw && yawMode == OrbitMode.TargetValue")]
public bool shortestPath = true;
// ===== 垂直公转设置 (Pitch) =====
[TitleGroup("垂直公转设置 (Pitch)")]
[LabelText("启用垂直公转")]
public bool enablePitch = true;
[LabelText("垂直公转模式")]
[ShowIf("enablePitch")]
public OrbitMode pitchMode = OrbitMode.Additive;
// Pitch Additive 参数
[LabelText("垂直公转曲线 (Pitch)")]
[ShowIf("@enablePitch && pitchMode == OrbitMode.Additive")]
public FloatCurveChannel verticalCurve = FloatCurveChannel.CreateDefault(0f, 0f);
// Pitch TargetValue 参数
[LabelText("目标角度 (Pitch)")]
[Tooltip("目标垂直公转角度。硬锁相机下为相对锁定仰角的偏移量;自由相机下为绝对仰角。")]
[ShowIf("@enablePitch && pitchMode == OrbitMode.TargetValue")]
public float endVerticalValue = 0f;
[LabelText("缓动曲线 (Pitch)")]
[Tooltip("垂直公转缓动曲线:归一化时间 [0,1] [0,1]")]
[ShowIf("@enablePitch && pitchMode == OrbitMode.TargetValue")]
[ShakeCurvePreset]
public AnimationCurve pitchEaseCurve = AnimationCurve.EaseInOut(0f, 0f, 1f, 1f);
protected override void TriggerEvent(FeedbackContext context)
{
CameraOrbitEvent.Trigger(context, this);
}
protected override void StopEvent(FeedbackContext context)
{
CameraOrbitEvent.Trigger(context, this, true);
}
public override bool Validate(out string error)
{
error = null;
return true;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 085667c17a7812b4ca6e0a4e53d7957d

View File

@@ -0,0 +1,326 @@
using System.Collections.Generic;
using SLSUtilities.Feedback;
using Unity.Cinemachine;
using UnityEngine;
namespace Cielonos.MainGame.Effects.Feedback
{
public class CameraOrbitShakeInstance : ShakeInstanceBase
{
// Yaw 设置
public bool enableYaw;
public CameraOrbitAction.OrbitMode yawMode;
public FloatCurveChannel horizontalCurve;
public float startHorizontalValue;
public float worldEndHorizontalValue;
public AnimationCurve yawEaseCurve;
// Pitch 设置
public bool enablePitch;
public CameraOrbitAction.OrbitMode pitchMode;
public FloatCurveChannel verticalCurve;
public float startVerticalValue;
public float worldEndVerticalValue;
public AnimationCurve pitchEaseCurve;
public CameraOrbitShakeInstance(FeedbackTimeSettings timeSettings, IFeedbackTimeProvider timeProvider,
CameraOrbitAction action, float startH, float startV, float worldEndH, float worldEndV, float duration)
: base(timeSettings, timeProvider, duration)
{
this.enableYaw = action.enableYaw;
this.yawMode = action.yawMode;
this.horizontalCurve = action.horizontalCurve;
this.startHorizontalValue = startH;
this.worldEndHorizontalValue = worldEndH;
this.yawEaseCurve = action.yawEaseCurve;
this.enablePitch = action.enablePitch;
this.pitchMode = action.pitchMode;
this.verticalCurve = action.verticalCurve;
this.startVerticalValue = startV;
this.worldEndVerticalValue = worldEndV;
this.pitchEaseCurve = action.pitchEaseCurve;
}
/// <summary>
/// 在 TargetValue 模式下,根据归一化时间计算当前水平角度。
/// </summary>
public float EvaluateHorizontal(float normalizedTime)
{
if (yawMode == CameraOrbitAction.OrbitMode.Additive)
{
return horizontalCurve.Evaluate(normalizedTime);
}
float t = yawEaseCurve != null ? yawEaseCurve.Evaluate(Mathf.Clamp01(normalizedTime)) : normalizedTime;
return Mathf.LerpUnclamped(startHorizontalValue, worldEndHorizontalValue, t);
}
/// <summary>
/// 在 TargetValue 模式下,根据归一化时间计算当前垂直角度。
/// </summary>
public float EvaluateVertical(float normalizedTime)
{
if (pitchMode == CameraOrbitAction.OrbitMode.Additive)
{
return verticalCurve.Evaluate(normalizedTime);
}
float t = pitchEaseCurve != null ? pitchEaseCurve.Evaluate(Mathf.Clamp01(normalizedTime)) : normalizedTime;
return Mathf.LerpUnclamped(startVerticalValue, worldEndVerticalValue, t);
}
}
public struct CameraOrbitEvent
{
private static event ShakeDelegate OnEvent;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void RuntimeInitialization() { OnEvent = null; }
public delegate void ShakeDelegate(
FeedbackContext feedbackContext,
CameraOrbitAction action,
bool stop
);
public static void Register(ShakeDelegate callback) { OnEvent += callback; }
public static void Unregister(ShakeDelegate callback) { OnEvent -= callback; }
public static void Trigger(
FeedbackContext feedbackContext,
CameraOrbitAction action,
bool stop = false)
{
OnEvent?.Invoke(feedbackContext, action, stop);
}
}
[AddComponentMenu("Cielonos/Feedback Shakers/Camera Orbit Shaker")]
[RequireComponent(typeof(CinemachineCamera))]
[RequireComponent(typeof(CinemachineOrbitalFollow))]
public class CameraOrbitShaker : MonoBehaviour
{
private CinemachineCamera _camera;
private CinemachineOrbitalFollow _orbitalFollow;
private CinemachineInputAxisController _inputController;
[Tooltip("目标参考的 Transform。当 CameraOrbitAction 处于 TargetValue 模式时,将以此为基准转换 endHorizontalValue 为世界空间角度。" +
"如果为空,则默认使用 MainGameManager.Player.transform若仍为空则回退到相机的 Follow 目标。")]
[SerializeField] private Transform targetReference;
private float _baseHorizontalValue;
private float _baseVerticalValue;
private bool _hasBaseValuesCaptured;
private readonly List<CameraOrbitShakeInstance> _activeShakes = new List<CameraOrbitShakeInstance>();
public float CurrentHorizontalOffset { get; private set; }
public float CurrentVerticalOffset { get; private set; }
public bool HasActiveShakes => _activeShakes.Count > 0;
private void Awake()
{
_camera = GetComponent<CinemachineCamera>();
_orbitalFollow = GetComponent<CinemachineOrbitalFollow>();
_inputController = GetComponent<CinemachineInputAxisController>();
}
private void OnEnable()
{
CameraOrbitEvent.Register(OnShakeEvent);
}
private void OnDisable()
{
CameraOrbitEvent.Unregister(OnShakeEvent);
StopAll();
}
private void Update()
{
if (_orbitalFollow == null) return;
if (_activeShakes.Count == 0)
{
CurrentHorizontalOffset = 0f;
CurrentVerticalOffset = 0f;
if (_hasBaseValuesCaptured)
{
// 所有 Shake 结束:恢复玩家输入控制器
if (_inputController != null && !_inputController.enabled)
{
_inputController.enabled = true;
}
_hasBaseValuesCaptured = false;
}
return;
}
// 首次触发时捕获当前相机角度作为基准
if (!_hasBaseValuesCaptured)
{
_baseHorizontalValue = _orbitalFollow.HorizontalAxis.Value;
_baseVerticalValue = _orbitalFollow.VerticalAxis.Value;
_hasBaseValuesCaptured = true;
}
// 区分模式与轨道进行独立计算
float finalHorizontal = _baseHorizontalValue;
float finalVertical = _baseVerticalValue;
for (int i = _activeShakes.Count - 1; i >= 0; i--)
{
CameraOrbitShakeInstance shake = _activeShakes[i];
shake.Tick();
float normalizedTime = shake.timer / shake.duration;
// 独立水平轴计算
if (shake.enableYaw)
{
if (shake.yawMode == CameraOrbitAction.OrbitMode.Additive)
{
finalHorizontal += shake.EvaluateHorizontal(normalizedTime);
}
else
{
finalHorizontal = shake.EvaluateHorizontal(normalizedTime);
}
}
// 独立垂直轴计算
if (shake.enablePitch)
{
if (shake.pitchMode == CameraOrbitAction.OrbitMode.Additive)
{
finalVertical += shake.EvaluateVertical(normalizedTime);
}
else
{
finalVertical = shake.EvaluateVertical(normalizedTime);
}
}
if (shake.IsFinished)
{
_activeShakes.RemoveAt(i);
}
}
// 管理玩家输入控制器状态(有任何活跃 Orbit Shake 时必定禁用输入)
if (_inputController != null && _inputController.enabled)
{
_inputController.enabled = false;
}
// 计算提供给锁锁相机的当前偏差量
CurrentHorizontalOffset = finalHorizontal - _baseHorizontalValue;
CurrentVerticalOffset = finalVertical - _baseVerticalValue;
// 检查当前是否在使用锁定相机的硬锁状态,若是,则由 LockTargetSubmodule 接管赋值,避免冲突
bool isLocking = MainGameManager.Player != null &&
MainGameManager.Player.viewSc.lockTargetModule != null &&
MainGameManager.Player.viewSc.lockTargetModule.isUsingLockTargetCamera &&
gameObject == MainGameManager.Player.viewSc.lockingTargetCamera.gameObject;
if (!isLocking)
{
// 非锁定状态下直接赋值
_orbitalFollow.HorizontalAxis.Value = finalHorizontal;
_orbitalFollow.VerticalAxis.Value = finalVertical;
}
}
private void OnShakeEvent(
FeedbackContext feedbackContext,
CameraOrbitAction action,
bool stop)
{
if (stop) { StopAll(); return; }
// 仅在当前组件附加的虚拟相机是当前激活相机时,才响应 Orbit 动作,防止后台相机数据被污染
bool isActiveCamera = MainGameManager.Player != null &&
MainGameManager.Player.viewSc.currentCamera != null &&
gameObject == MainGameManager.Player.viewSc.currentCamera.gameObject;
if (!isActiveCamera) return;
// 检查当前是否在使用锁定相机的硬锁状态
bool isLocking = MainGameManager.Player != null &&
MainGameManager.Player.viewSc.lockTargetModule != null &&
MainGameManager.Player.viewSc.lockTargetModule.isUsingLockTargetCamera &&
gameObject == MainGameManager.Player.viewSc.lockingTargetCamera.gameObject;
// 根据当前相机状态与配置决定是否生效
if (isLocking && !action.enableInLockTarget) return;
if (!isLocking && !action.enableInFreeLook) return;
// 获取当前轴值作为 TargetValue 模式的起始值
float currentH = _orbitalFollow != null ? _orbitalFollow.HorizontalAxis.Value : 0f;
float currentV = _orbitalFollow != null ? _orbitalFollow.VerticalAxis.Value : 0f;
// 解析水平目标值与垂直目标值
float worldEndH = currentH;
float worldEndV = action.endVerticalValue;
// 水平 (Yaw) 解析
if (action.enableYaw && action.yawMode == CameraOrbitAction.OrbitMode.TargetValue)
{
Transform reference = targetReference != null ? targetReference : (MainGameManager.Player != null ? MainGameManager.Player.transform : _camera?.Follow);
float playerYaw = reference != null ? reference.eulerAngles.y : 0f;
float targetAngle = playerYaw + action.endHorizontalValue;
if (action.shortestPath)
{
worldEndH = currentH + Mathf.DeltaAngle(currentH, targetAngle);
}
else
{
worldEndH = targetAngle;
}
}
// 垂直 (Pitch) 解析
if (action.enablePitch && action.pitchMode == CameraOrbitAction.OrbitMode.TargetValue)
{
if (isLocking)
{
// 硬锁相机下垂直仰角Pitch的 TargetValue 是相对于触发时的基准仰角的相对偏移
worldEndV = currentV + action.endVerticalValue;
}
else
{
worldEndV = action.endVerticalValue;
}
}
var instance = new CameraOrbitShakeInstance(
feedbackContext.timeSettings,
feedbackContext.player.TimeProvider,
action,
currentH,
currentV,
worldEndH,
worldEndV,
feedbackContext.duration
);
_activeShakes.Add(instance);
}
private void StopAll()
{
_activeShakes.Clear();
if (_hasBaseValuesCaptured && _orbitalFollow != null)
{
_orbitalFollow.HorizontalAxis.Value = _baseHorizontalValue;
_orbitalFollow.VerticalAxis.Value = _baseVerticalValue;
}
if (_inputController != null)
{
_inputController.enabled = true;
}
_hasBaseValuesCaptured = false;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 646548dbf6035f04a8a01e875996d327

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: be266a922a64f794d9b0b85a84e87c4c
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,17 @@
using UnityEngine;
namespace Cielonos.MainGame.Inventory.Collections
{
/// <summary>
/// 光子折跃器 / Photon Warper
/// Polychrome的扩展器若敌人在2米之外重攻击AttackRA在起手Startup结束时折跃至其身前2米。
/// </summary>
public class PhotonWarper : ExtenderBase
{
public override void OnObtained()
{
hostType = typeof(Polychrome);
base.OnObtained();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3ca70fc0fb31f3b4e82f0842a8744722

View File

@@ -1,15 +1,12 @@
using System.Collections.Generic;
using ChocDino.UIFX;
using Cielonos.MainGame.Buffs.Character;
using Cielonos.MainGame.Characters;
using Cielonos.MainGame.Effects.Feedback;
using Cielonos.MainGame.FunctionalAnimation;
using Cielonos.MainGame.UI;
using SLSUtilities.Feedback;
using SLSUtilities.General;
using SLSUtilities.FunctionalAnimation;
using SLSUtilities.WwiseAssistance;
using UnityEngine;
using UnityEngine.InputSystem;
namespace Cielonos.MainGame.Inventory.Collections
{
@@ -27,12 +24,19 @@ namespace Cielonos.MainGame.Inventory.Collections
functionSm?.Update(player.selfTimeSm.DeltaTime);
}
if (Keyboard.current.xKey.wasPressedThisFrame)
/*if (Keyboard.current.xKey.wasPressedThisFrame)
{
SetBlock();
player.reactionSc.blockSm.GetBlockSource("Polychrome_Block").PerfectBlock(null, player.CenterPosition + player.transform.forward);
RemoveBlock();
}
if (Keyboard.current.zKey.wasPressedThisFrame)
{
player.reactionSc.dodgeSm.ApplyDodge(DodgeSource.Default(player));
player.reactionSc.dodgeSm.GetCurrentDodgeSource()?.PerfectDodge(null);
player.reactionSc.dodgeSm.RemoveDodge("DefaultDodge");
}*/
}
public override void OnEquipped()
@@ -174,16 +178,6 @@ namespace Cielonos.MainGame.Inventory.Collections
}
}
private bool TryPlayParryAttack(Enemy parryTarget, List<Enemy> availableEnemies)
{
if (parryTarget != null && !availableEnemies.Contains(parryTarget))
{
float distance = Vector3.Distance(player.transform.position.Flatten(), parryTarget.transform.position.Flatten());
return PlayTargetedAnimation(distance > 2f ? "DodgeParryAttack" : "BlockParryAttack", parryTarget);
}
return PlayTargetedAnimation("BlockParryAttack");
}
public override void OnSecondaryPress()
{
if (player.statusSm.HasStatus(StatusType.Stun))
@@ -236,7 +230,7 @@ namespace Cielonos.MainGame.Inventory.Collections
return;
}
if (player.inputSc.IsHoldingSpecialA)
if (player.inputSc.IsHoldingSpecialB)
{
if (functionSm["DisruptionAttack"].IsAvailable())
{
@@ -274,11 +268,51 @@ namespace Cielonos.MainGame.Inventory.Collections
if (functionSm["HeavyAttack"].IsAvailable())
{
Enemy target = CombatManager.EnemySm.GetBestEnemy(availableEnemies);
//Enemy target = CombatManager.EnemySm.GetBestEnemy(availableEnemies);
Enemy nt = CombatManager.EnemySm.GetBestEnemy(CombatManager.EnemySm.GetEnemiesInRadius(player.transform.position, 15));
string nextNodeName = comboSm.main.GetNextNodeName("R");
bool keepAdsorption = nextNodeName is "RC";
if (PlayTargetedAnimation("Attack" + nextNodeName, target, 1f, keepAdsorption))
bool shouldWarp = false;
Vector3 targetPos = Vector3.zero;
if (nt != null && nextNodeName == "RA" && HasExtender<PhotonWarper>())
{
nextNodeName = "RB";
float distance = Vector3.Distance(player.transform.position, nt.transform.position);
if (distance > 2f)
{
shouldWarp = true;
player.movementSc.SmartTurnToTarget(nt, 360, true);
targetPos = player.movementSc.GetSafePositionNearTarget(nt, 1.0f);
}
}
if (nt != null && (shouldWarp || nextNodeName is "RC"))
{
player.movementSc.SmartTurnToTarget(nt, 360, true);
}
if (PlayTargetedAnimation("Attack" + nextNodeName, nt, 1f, keepAdsorption))
{
if (shouldWarp)
{
var interval = fullBodyFuncAnimSm.currentData.Interval(IntervalType.Startup);
float warpDuration = fullBodyFuncAnimSm.GetIntervalScaledDuration(IntervalType.Startup);
fullBodyFuncAnimSm.currentRuntimeFuncAnim.AddAnimEvent(interval.StartTime, new SetFuncAnimSpeed()
{
applyMode = SetFuncAnimSpeed.SpeedApplyMode.Override,
getFromBehaviorTree = false,
targetSpeed = 1f
});
fullBodyFuncAnimSm.currentRuntimeFuncAnim.AddAnimEvent(interval.EndTime, new SetFuncAnimSpeed()
{
applyMode = SetFuncAnimSpeed.SpeedApplyMode.Override,
getFromBehaviorTree = false,
targetSpeed = 1f
});
player.movementSc.Teleport(targetPos, warpDuration);
}
float totalTime = fullBodyFuncAnimSm.GetIntervalScaledDuration(IntervalType.Startup) - 0.2f;
CombatManager.EnemySm.activeEnemiesList.ForEach(enemy =>
{
@@ -288,8 +322,28 @@ namespace Cielonos.MainGame.Inventory.Collections
functionSm["HeavyAttack"].Execute();
}
}
return;
bool TryPlayParryAttack(Enemy parryTarget, List<Enemy> enemies)
{
if (parryTarget != null && !enemies.Contains(parryTarget))
{
float distance = Vector3.Distance(player.transform.position.Flatten(), parryTarget.transform.position.Flatten());
return PlayTargetedAnimation(distance > 2f ? "DodgeParryAttack" : "BlockParryAttack", parryTarget);
}
return PlayTargetedAnimation("BlockParryAttack");
}
}
public override void OnSpecialAPress()
{
if (functionSm["UltimateAttack"].IsAvailable())
{
PlayTargetedAnimation("UltimateAttack");
functionSm["UltimateAttack"].Execute();
}
}
public override void OnSpecialCPress()
{
comboSm.main.Reset();
@@ -314,11 +368,6 @@ namespace Cielonos.MainGame.Inventory.Collections
player.selfTimeSm.AddLocalTimer(0.04f, () => RemoveBlock());
}
public override void OnSpecialBPress()
{
PlayTargetedAnimation("UltimateAttack");
}
}
public partial class Polychrome
@@ -400,27 +449,4 @@ namespace Cielonos.MainGame.Inventory.Collections
}
}
}
// =================================================================
// 以下为已废弃的 ExtraUIContainer 相关代码 / Deprecated ExtraUIContainer Codes
// =================================================================
/*
public partial class Polychrome
{
private PolychromeExtraUIContainer ExtraUIContainer => extraUIContainer as PolychromeExtraUIContainer;
// OnEquipped:
// extraUIContainer = Instantiate(extraUIContainerPrefab, PlayerCanvas.MainWeaponUIArea.transform).GetComponent<MainWeaponExtraUIContainer>();
// extraUIContainer.mainWeapon = this;
// OnUnequipped:
// Destroy(extraUIContainer.gameObject);
// UpdateVisuals:
// if (ExtraUIContainer != null)
// {
// ExtraUIContainer.SetStars(level);
// }
}
*/
}

View File

@@ -1,13 +1,5 @@
using System;
using System.Collections.Generic;
using ChocDino.UIFX;
using Cielonos.MainGame.Buffs.Character;
using Cielonos.MainGame.Characters;
using Cielonos.MainGame.Effects.Feedback;
using Cielonos.MainGame.UI;
using SLSUtilities.Feedback;
using SLSUtilities.General;
using SLSUtilities.FunctionalAnimation;
using SLSUtilities.WwiseAssistance;
using UnityEngine;
using Random = UnityEngine.Random;
@@ -114,11 +106,9 @@ namespace Cielonos.MainGame.Inventory.Collections
slash.Initialize<NormalArea>(player, this, Fraction.Enemy)
.SetAttackSubmodule<NormalArea>(attackUnit)
.SetTimeSubmodule<NormalArea>(2f, 0f, 0.22f)
.SetTimeSubmodule<NormalArea>(2f, 0.02f, 0.24f)
.SetHitSubmodule<NormalArea>(0.07f, 3);
slash.SetImpulseSubmodule(1f).WithRepulsion(5f);
slash.hitSm.AddHitSound(AK.EVENTS.POLYCHROME_HEAVYATTACKLHIT)
.AddHitEvent((enemy, hitPosition) =>
{

View File

@@ -1,4 +1,6 @@
using System;
using Cielonos.MainGame.Characters;
using SLSUtilities.General;
using UnityEngine;
namespace Cielonos.MainGame.Inventory.Collections
@@ -8,7 +10,7 @@ namespace Cielonos.MainGame.Inventory.Collections
/// - 提供与激情系统等级相关的属性加成。
/// - 加成效果根据激情等级逐级递增,最高可达 SSS 级别。
/// 具体效果如下:
/// - C级+4 能量回复,+0 攻击获得能量0% 攻击速度0% 暴击率0% 受到的最终伤害倍率
/// - C级+3 能量回复,+0 攻击获得能量0% 攻击速度0% 暴击率0% 受到的最终伤害倍率
/// - B级+2 能量回复,+0 攻击获得能量1% 攻击速度2% 暴击率0% 受到的最终伤害倍率
/// - A级+1 能量回复,+0 攻击获得能量2% 攻击速度4% 暴击率0% 受到的最终伤害倍率
/// - S级+0 能量回复,+1 攻击获得能量3% 攻击速度6% 暴击率5% 受到的最终伤害倍率
@@ -38,8 +40,8 @@ namespace Cielonos.MainGame.Inventory.Collections
public override void OnDiscarded()
{
base.OnDiscarded();
_passionSystem.OnLevelChanged -= Refresh;
base.OnDiscarded();
}
private void Refresh(int oldLevel, int newLevel)
@@ -51,6 +53,7 @@ namespace Cielonos.MainGame.Inventory.Collections
private void UpdateAttributes()
{
passiveAttributeSm.charAttrNumericChange[CharacterAttribute.EnergyRegeneration] = GetEnergyRegen();
passiveAttributeSm.charAttrNumericChange[CharacterAttribute.EnergyGainByAttack] = GetEnergyGainByAttack();
passiveAttributeSm.chaAttrPercentageChangeOfAccumulation[CharacterAttribute.AttackSpeed] = GetAttackSpeed();
passiveAttributeSm.charAttrNumericChange[CharacterAttribute.CriticalAttackChance] = GetCriticalAttackChance();
passiveAttributeSm.chaAttrPercentageChangeOfMultiplication[CharacterAttribute.FinalDamageReceivedMultiplier] = GetFinalDamageReceivedMultiplier();
@@ -63,7 +66,7 @@ namespace Cielonos.MainGame.Inventory.Collections
{
return PassionLevel switch
{
0 => 4.0f, // C-Rank
0 => 3.0f, // C-Rank
1 => 2.0f, // B-Rank
2 => 1.0f, // A-Rank
_ => 0.0f // S, SS, SSS (Ranks 3, 4, 5)

View File

@@ -0,0 +1,42 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1bbb15cae81a83543bbc5da32d7a03b1, type: 3}
m_Name: CieIFD_Back_00
m_EditorClassIdentifier: Assembly-CSharp::Cielonos.MainGame.MusicBeatData
serializationData:
SerializedFormat: 2
SerializedBytes:
ReferencedUnityObjects: []
SerializedBytesString:
Prefab: {fileID: 0}
PrefabModificationsReferencedUnityObjects: []
PrefabModifications: []
SerializationNodes: []
musicSwitch:
idInternal: 0
valueGuidInternal:
groupIdInternal: 0
groupGuidInternal:
WwiseObjectReference: {fileID: 11400000, guid: d65f536d666aec348baa6c8fe6a0844a, type: 2}
musicEvent:
idInternal: 0
valueGuidInternal:
WwiseObjectReference: {fileID: 11400000, guid: 490938cc29815d54498a3ff46a7b7078, type: 2}
stopMusicEvent:
idInternal: 0
valueGuidInternal:
WwiseObjectReference: {fileID: 11400000, guid: 55dc807cde468a0429fb934d73c29793, type: 2}
bpm: 240
beatsPerBar: 8
audioStartOffset: 0
totalDuration: 11.346
beatMarkers: []

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d6c3f7ba9c0b0b84e8e7953111e59b0d
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,42 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1bbb15cae81a83543bbc5da32d7a03b1, type: 3}
m_Name: CieIFD_Back_01
m_EditorClassIdentifier: Assembly-CSharp::Cielonos.MainGame.MusicBeatData
serializationData:
SerializedFormat: 2
SerializedBytes:
ReferencedUnityObjects: []
SerializedBytesString:
Prefab: {fileID: 0}
PrefabModificationsReferencedUnityObjects: []
PrefabModifications: []
SerializationNodes: []
musicSwitch:
idInternal: 0
valueGuidInternal:
groupIdInternal: 0
groupGuidInternal:
WwiseObjectReference: {fileID: 11400000, guid: 53d01043d863483429cc2d9809876da4, type: 2}
musicEvent:
idInternal: 0
valueGuidInternal:
WwiseObjectReference: {fileID: 11400000, guid: 490938cc29815d54498a3ff46a7b7078, type: 2}
stopMusicEvent:
idInternal: 0
valueGuidInternal:
WwiseObjectReference: {fileID: 11400000, guid: 55dc807cde468a0429fb934d73c29793, type: 2}
bpm: 240
beatsPerBar: 8
audioStartOffset: 0
totalDuration: 11.031
beatMarkers: []

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: ed1413903caf9d84a986254048ff5f1e
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,42 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1bbb15cae81a83543bbc5da32d7a03b1, type: 3}
m_Name: CieIFD_Back_02
m_EditorClassIdentifier: Assembly-CSharp::Cielonos.MainGame.MusicBeatData
serializationData:
SerializedFormat: 2
SerializedBytes:
ReferencedUnityObjects: []
SerializedBytesString:
Prefab: {fileID: 0}
PrefabModificationsReferencedUnityObjects: []
PrefabModifications: []
SerializationNodes: []
musicSwitch:
idInternal: 0
valueGuidInternal:
groupIdInternal: 0
groupGuidInternal:
WwiseObjectReference: {fileID: 11400000, guid: 801b89ad79583d84291be4be2c5e9949, type: 2}
musicEvent:
idInternal: 0
valueGuidInternal:
WwiseObjectReference: {fileID: 11400000, guid: 490938cc29815d54498a3ff46a7b7078, type: 2}
stopMusicEvent:
idInternal: 0
valueGuidInternal:
WwiseObjectReference: {fileID: 11400000, guid: 55dc807cde468a0429fb934d73c29793, type: 2}
bpm: 240
beatsPerBar: 8
audioStartOffset: 0
totalDuration: 16.052
beatMarkers: []

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6b565265535aa2943981ecc325e52b51
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,52 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1bbb15cae81a83543bbc5da32d7a03b1, type: 3}
m_Name: CieIFD_Func_00
m_EditorClassIdentifier: Assembly-CSharp::Cielonos.MainGame.MusicBeatData
serializationData:
SerializedFormat: 2
SerializedBytes:
ReferencedUnityObjects: []
SerializedBytesString:
Prefab: {fileID: 0}
PrefabModificationsReferencedUnityObjects: []
PrefabModifications: []
SerializationNodes: []
musicSwitch:
idInternal: 0
valueGuidInternal:
groupIdInternal: 0
groupGuidInternal:
WwiseObjectReference: {fileID: 11400000, guid: a585ced711c602c4297ec117c32e24b9, type: 2}
musicEvent:
idInternal: 0
valueGuidInternal:
WwiseObjectReference: {fileID: 11400000, guid: 490938cc29815d54498a3ff46a7b7078, type: 2}
stopMusicEvent:
idInternal: 0
valueGuidInternal:
WwiseObjectReference: {fileID: 11400000, guid: 55dc807cde468a0429fb934d73c29793, type: 2}
bpm: 240
beatsPerBar: 8
audioStartOffset: 0
totalDuration: 4.016
beatMarkers:
- time: 2
tags:
- EnemyAttack0
barIndex: 1
beatInBar: 0
- time: 2
tags:
- Normal
barIndex: 1
beatInBar: 3

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 119d0222713540649bc507c5436fc808
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,52 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1bbb15cae81a83543bbc5da32d7a03b1, type: 3}
m_Name: CieIFD_Func_01
m_EditorClassIdentifier: Assembly-CSharp::Cielonos.MainGame.MusicBeatData
serializationData:
SerializedFormat: 2
SerializedBytes:
ReferencedUnityObjects: []
SerializedBytesString:
Prefab: {fileID: 0}
PrefabModificationsReferencedUnityObjects: []
PrefabModifications: []
SerializationNodes: []
musicSwitch:
idInternal: 0
valueGuidInternal:
groupIdInternal: 0
groupGuidInternal:
WwiseObjectReference: {fileID: 11400000, guid: a7ff460b65cb2334ab875d06bcc2373d, type: 2}
musicEvent:
idInternal: 0
valueGuidInternal:
WwiseObjectReference: {fileID: 11400000, guid: 490938cc29815d54498a3ff46a7b7078, type: 2}
stopMusicEvent:
idInternal: 0
valueGuidInternal:
WwiseObjectReference: {fileID: 11400000, guid: 55dc807cde468a0429fb934d73c29793, type: 2}
bpm: 240
beatsPerBar: 8
audioStartOffset: 0
totalDuration: 4.013
beatMarkers:
- time: 2
tags:
- EnemyAttack0
barIndex: 1
beatInBar: 0
- time: 2
tags:
- Normal
barIndex: 1
beatInBar: 3

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 73c0bfd6deca2494d871db60f90c7924
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,52 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1bbb15cae81a83543bbc5da32d7a03b1, type: 3}
m_Name: CieIFD_Func_02
m_EditorClassIdentifier: Assembly-CSharp::Cielonos.MainGame.MusicBeatData
serializationData:
SerializedFormat: 2
SerializedBytes:
ReferencedUnityObjects: []
SerializedBytesString:
Prefab: {fileID: 0}
PrefabModificationsReferencedUnityObjects: []
PrefabModifications: []
SerializationNodes: []
musicSwitch:
idInternal: 0
valueGuidInternal:
groupIdInternal: 0
groupGuidInternal:
WwiseObjectReference: {fileID: 11400000, guid: e75aa8581e5c3814ca3874c1ac80d6d1, type: 2}
musicEvent:
idInternal: 0
valueGuidInternal:
WwiseObjectReference: {fileID: 11400000, guid: 490938cc29815d54498a3ff46a7b7078, type: 2}
stopMusicEvent:
idInternal: 0
valueGuidInternal:
WwiseObjectReference: {fileID: 11400000, guid: 55dc807cde468a0429fb934d73c29793, type: 2}
bpm: 240
beatsPerBar: 8
audioStartOffset: 0
totalDuration: 4.013
beatMarkers:
- time: 2
tags:
- EnemyAttack0
barIndex: 1
beatInBar: 0
- time: 2
tags:
- Normal
barIndex: 1
beatInBar: 3

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 23d5a38a23454ab47afb0c9dcd8089f7
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,52 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 1bbb15cae81a83543bbc5da32d7a03b1, type: 3}
m_Name: CieIFD_Func_03
m_EditorClassIdentifier: Assembly-CSharp::Cielonos.MainGame.MusicBeatData
serializationData:
SerializedFormat: 2
SerializedBytes:
ReferencedUnityObjects: []
SerializedBytesString:
Prefab: {fileID: 0}
PrefabModificationsReferencedUnityObjects: []
PrefabModifications: []
SerializationNodes: []
musicSwitch:
idInternal: 0
valueGuidInternal:
groupIdInternal: 0
groupGuidInternal:
WwiseObjectReference: {fileID: 11400000, guid: ef14fc900d426df4699b590122abc7bf, type: 2}
musicEvent:
idInternal: 0
valueGuidInternal:
WwiseObjectReference: {fileID: 11400000, guid: 490938cc29815d54498a3ff46a7b7078, type: 2}
stopMusicEvent:
idInternal: 0
valueGuidInternal:
WwiseObjectReference: {fileID: 11400000, guid: 55dc807cde468a0429fb934d73c29793, type: 2}
bpm: 240
beatsPerBar: 8
audioStartOffset: 0
totalDuration: 4.349
beatMarkers:
- time: 2
tags:
- EnemyAttack0
barIndex: 1
beatInBar: 0
- time: 2
tags:
- Normal
barIndex: 1
beatInBar: 3

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f9d2c5b5062107a4cabd67723cdc58eb
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,421 @@
using System.Collections.Generic;
using UnityEngine;
using Sirenix.OdinInspector;
namespace Cielonos.MainGame
{
/// <summary>
/// 战斗音乐控制器,负责处理功能音乐的无缝随机切换,以及并行的 BGM 音轨按小节边界起播/停止。
/// </summary>
[AddComponentMenu("Cielonos/Rhythm/CombatMusicController")]
public class CombatMusicController : MonoBehaviour
{
[Header("System References")]
[Tooltip("全局节拍战斗系统的引用")]
public MusicBeatSystem beatSystem;
[Header("BGM Settings")]
[Tooltip("播放背景音乐 Switch 容器的 Wwise Event")]
public AK.Wwise.Event bgmMusicEvent;
[Tooltip("所有可用的背景音乐 Wwise Segment 名称 (对应 Wwise 中的 Switch 值)")]
public List<string> bgmSegments = new List<string>
{
"Back_00",
"Back_01",
"Back_02"
};
[Header("Functional Music Settings")]
[Tooltip("播放功能音乐 Switch 容器的 Wwise Event")]
public AK.Wwise.Event functionalMusicEvent;
[Tooltip("初始播放的音乐片段 Switch 名称")]
public string initialSegment = "Func_00";
[Tooltip("所有可用的功能音乐 Wwise Segment 名称 (对应 Wwise 中的 Switch 值)")]
public List<string> functionalSegments = new List<string>
{
"Func_00",
"Func_01",
"Func_02",
"Func_03"
};
// 运行时 BGM 状态跟踪字典
private readonly Dictionary<string, bool> bgmTargetStates = new Dictionary<string, bool>();
private readonly Dictionary<string, bool> bgmIsPlaying = new Dictionary<string, bool>();
private readonly Dictionary<string, uint> bgmPlayingIDs = new Dictionary<string, uint>();
private readonly Dictionary<string, GameObject> bgmGameObjects = new Dictionary<string, GameObject>();
// 等待下一个全局 PrepareNext 边界才起播的 BGM 片段队列
private readonly HashSet<string> pendingBgmStarts = new HashSet<string>();
private void Awake()
{
// 初始化每个 BGM 段落的运行状态与子 GameObject
foreach (var seg in bgmSegments)
{
bgmTargetStates[seg] = false;
bgmIsPlaying[seg] = false;
bgmPlayingIDs[seg] = 0;
// 为每个 BGM 片段创建独立的子 GameObject以便并行播放和接收独立的节拍回调
GameObject go = new GameObject($"BGM_Layer_{seg}");
go.transform.SetParent(transform);
bgmGameObjects[seg] = go;
// 在节拍注册表中注册此子物体
MusicBeatSystem.RegisterRhythmGameObject(go);
}
}
private void OnEnable()
{
if (beatSystem == null)
{
beatSystem = GetComponent<MusicBeatSystem>();
}
if (beatSystem != null)
{
beatSystem.OnPrepareNextSegment += DecideNextSegment;
beatSystem.OnUserCueReceived += HandleTrackUserCue;
beatSystem.OnGlobalPrepareNext += OnGlobalPrepareNextFired;
}
}
private void OnDisable()
{
if (beatSystem != null)
{
beatSystem.OnPrepareNextSegment -= DecideNextSegment;
beatSystem.OnUserCueReceived -= HandleTrackUserCue;
beatSystem.OnGlobalPrepareNext -= OnGlobalPrepareNextFired;
}
}
private void OnDestroy()
{
// 释放所有动态创建的子 GameObject
foreach (var pair in bgmGameObjects)
{
if (pair.Value != null)
{
MusicBeatSystem.UnregisterRhythmGameObject(pair.Value);
Destroy(pair.Value);
}
}
}
#region BGM Inspector Controls
[Button("Play/Stop Back_00 (8 Bars)")]
public void ToggleBack_00() => ToggleBgmTrack("Back_00");
[Button("Play/Stop Back_01 (8 Bars)")]
public void ToggleBack_01() => ToggleBgmTrack("Back_01");
[Button("Play/Stop Back_02 (16 Bars)")]
public void ToggleBack_02() => ToggleBgmTrack("Back_02");
#endregion
#region BGM Implementation
private void ToggleBgmTrack(string segmentName)
{
if (!bgmTargetStates.ContainsKey(segmentName)) return;
bgmTargetStates[segmentName] = !bgmTargetStates[segmentName];
Debug.Log($"[CombatMusicController] Toggle BGM Segment '{segmentName}': Target state is now {bgmTargetStates[segmentName]}");
if (bgmTargetStates[segmentName])
{
// 确保 beatSystem 已激活(用于接收 PrepareNext 通知)
if (beatSystem != null && !beatSystem.IsActive)
{
beatSystem.Activate(null);
}
if (!bgmIsPlaying[segmentName] && !pendingBgmStarts.Contains(segmentName))
{
// 如果没有任何音乐在播放或排队,立即起播(第一次)
// 否则,排入队列等待下一个全局 PrepareNext 边界对齐
if (!AnyBgmPlaying() && !AnyBgmPending())
{
Debug.Log($"[CombatMusicController] No music playing, starting BGM '{segmentName}' immediately.");
StartBgmImmediately(segmentName);
}
else
{
pendingBgmStarts.Add(segmentName);
Debug.Log($"[CombatMusicController] BGM '{segmentName}' queued, will start at next PrepareNext boundary.");
}
}
}
else
{
// 取消播放:从待播队列中移除(如果还没起播)
pendingBgmStarts.Remove(segmentName);
}
}
private bool AnyBgmPlaying()
{
foreach (var pair in bgmIsPlaying)
{
if (pair.Value) return true;
}
return false;
}
private bool AnyBgmPending()
{
return pendingBgmStarts.Count > 0;
}
/// <summary>
/// 当全局主音乐Func 轨)的 PrepareNext 到达时,将所有等待中的 BGM 片段一起起播。
/// Wwise 引擎会将它们精确对齐到 Exit Cue 边界!
/// </summary>
private void OnGlobalPrepareNextFired()
{
if (pendingBgmStarts.Count == 0) return;
var toStart = new List<string>(pendingBgmStarts);
pendingBgmStarts.Clear();
uint callbackFlags = (uint)(
AkCallbackType.AK_MusicSyncBeat |
AkCallbackType.AK_MusicSyncEntry |
AkCallbackType.AK_MusicSyncUserCue |
AkCallbackType.AK_EndOfEvent
);
foreach (var seg in toStart)
{
if (!bgmTargetStates.ContainsKey(seg) || !bgmTargetStates[seg]) continue;
if (bgmIsPlaying[seg]) continue;
bgmIsPlaying[seg] = true;
AkUnitySoundEngine.SetSwitch(beatSystem.musicSegmentSwitchGroup, seg, bgmGameObjects[seg]);
uint newID = bgmMusicEvent.Post(
bgmGameObjects[seg],
callbackFlags,
beatSystem.OnWwiseMusicCallback,
null
);
if (newID != 0)
{
bgmPlayingIDs[seg] = newID;
Debug.Log($"[CombatMusicController] BGM '{seg}' started at PrepareNext boundary. ID={newID}");
}
else
{
bgmIsPlaying[seg] = false;
Debug.LogError($"[CombatMusicController] Failed to post BGM event for '{seg}'");
}
}
}
private void StartBgmImmediately(string segmentName)
{
if (bgmMusicEvent == null || !bgmMusicEvent.IsValid())
{
Debug.LogError($"[CombatMusicController] Cannot play BGM: Event is invalid.");
return;
}
GameObject go = bgmGameObjects[segmentName];
uint callbackFlags = (uint)(
AkCallbackType.AK_MusicSyncBeat |
AkCallbackType.AK_MusicSyncEntry |
AkCallbackType.AK_MusicSyncUserCue |
AkCallbackType.AK_EndOfEvent
);
// 设置 Wwise Switch 以让此子物体播放当前 segment 音频
AkUnitySoundEngine.SetSwitch(beatSystem.musicSegmentSwitchGroup, segmentName, go);
uint playingID = bgmMusicEvent.Post(
go,
callbackFlags,
beatSystem.OnWwiseMusicCallback,
null
);
if (playingID != 0)
{
bgmIsPlaying[segmentName] = true;
bgmPlayingIDs[segmentName] = playingID;
Debug.Log($"[CombatMusicController] Posted BGM Event for '{segmentName}' immediately on '{go.name}', playingID={playingID}");
}
}
private void HandleTrackUserCue(uint playingID, string cueName)
{
// 查找是哪个 BGM 片段触发的回调
string triggeringSegment = null;
foreach (var pair in bgmPlayingIDs)
{
if (pair.Value == playingID)
{
triggeringSegment = pair.Key;
break;
}
}
if (triggeringSegment == null) return;
if (cueName == "PrepareNext")
{
uint callbackFlags = (uint)(
AkCallbackType.AK_MusicSyncBeat |
AkCallbackType.AK_MusicSyncEntry |
AkCallbackType.AK_MusicSyncUserCue |
AkCallbackType.AK_EndOfEvent
);
// 1. 如果该 BGM 被要求继续播放,由于 Wwise 可能没有配置原生循环,
// 我们在 PrepareNext 时刻(提前)再次 Post Event让 Wwise 引擎将其调度到下一个 Exit 点无缝衔接起播!
if (bgmTargetStates[triggeringSegment])
{
uint newID = bgmMusicEvent.Post(
bgmGameObjects[triggeringSegment],
callbackFlags,
beatSystem.OnWwiseMusicCallback,
null
);
if (newID != 0)
{
bgmPlayingIDs[triggeringSegment] = newID;
Debug.Log($"[CombatMusicController] Loop transition: Re-posted Event for BGM '{triggeringSegment}'. New ID: {newID}");
}
}
// 如果被要求停止,我们什么都不做,让当前片段自然播放完毕并触发 EndOfEvent 即可。
// 2. 检查是否有排队等待播放的其他 BGM 片段。
// 同样在此时刻 Post 它们Wwise 引擎会自动将它们与当前音频的小节边界完美对齐!
foreach (var seg in bgmSegments)
{
if (seg != triggeringSegment && bgmTargetStates[seg] && !bgmIsPlaying[seg])
{
bgmIsPlaying[seg] = true;
// 确保对应的 GameObject Switch 正确
AkUnitySoundEngine.SetSwitch(beatSystem.musicSegmentSwitchGroup, seg, bgmGameObjects[seg]);
uint newSegID = bgmMusicEvent.Post(
bgmGameObjects[seg],
callbackFlags,
beatSystem.OnWwiseMusicCallback,
null
);
if (newSegID != 0)
{
bgmPlayingIDs[seg] = newSegID;
Debug.Log($"[CombatMusicController] Queued start: Posted Event for BGM '{seg}' during PrepareNext. ID: {newSegID}");
}
}
}
}
else if (cueName == "EndOfEvent")
{
// 注意:由于我们在 PrepareNext 处更新了 bgmPlayingIDs
// 此时旧的 playingID 已经与字典中的不匹配了,所以旧事件的 EndOfEvent 会在上面的循环中返回 null 并忽略!
// 只有真正停止播放(未更新 ID的 EndOfEvent 才会走到这里,实现完美的状态清理!
bgmIsPlaying[triggeringSegment] = false;
bgmPlayingIDs[triggeringSegment] = 0;
Debug.Log($"[CombatMusicController] BGM Segment '{triggeringSegment}' terminated naturally in Wwise (EndOfEvent).");
if (!AnyBgmPlaying())
{
bool queuedBgmStarted = false;
foreach (var seg in bgmSegments)
{
if (bgmTargetStates[seg] && !bgmIsPlaying[seg])
{
Debug.Log($"[CombatMusicController] Starting queued BGM '{seg}' since all others stopped.");
if (beatSystem != null && !beatSystem.IsActive)
{
beatSystem.Activate(null);
}
StartBgmImmediately(seg);
queuedBgmStarted = true;
}
}
if (!queuedBgmStarted && beatSystem != null && beatSystem.IsActive)
{
beatSystem.Deactivate();
}
}
}
else if (cueName.StartsWith("Entry_"))
{
bgmIsPlaying[triggeringSegment] = true;
Debug.Log($"[CombatMusicController] BGM Segment '{triggeringSegment}' entered and is now running.");
}
}
#endregion
#region Functional Music Implementation
[Button("Play Functional Music")]
public void PlayFunctionalMusic()
{
if (beatSystem == null)
{
beatSystem = GetComponent<MusicBeatSystem>();
}
if (beatSystem != null)
{
beatSystem.Activate(functionalMusicEvent, initialSegment);
}
else
{
Debug.LogError("[CombatMusicController] Cannot play: MusicBeatSystem reference is missing.");
}
}
private string DecideNextSegment(GameObject targetGO, string currentSegmentName)
{
if (beatSystem == null || targetGO != beatSystem.gameObject)
{
return "";
}
if (functionalSegments == null || functionalSegments.Count == 0)
{
return "";
}
List<string> candidates = new List<string>(functionalSegments);
if (!string.IsNullOrEmpty(currentSegmentName))
{
candidates.Remove(currentSegmentName);
}
if (candidates.Count > 0)
{
int randomIndex = UnityEngine.Random.Range(0, candidates.Count);
string selectedSegment = candidates[randomIndex];
Debug.Log($"[CombatMusicController] Transition Decision: GameObject '{targetGO.name}' currently playing '{currentSegmentName}', selected next: '{selectedSegment}'");
return selectedSegment;
}
return functionalSegments[0];
}
#endregion
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2d333c7739ebda943914444d847d66bc

View File

@@ -0,0 +1,60 @@
using UnityEngine;
namespace Cielonos.MainGame
{
/// <summary>
/// 挂载在敌人等 GameObject 上的局部节奏追踪组件,用来追踪该物体上独立播放的 Wwise Segment。
/// </summary>
[AddComponentMenu("Cielonos/Rhythm/LocalRhythmTracker")]
public class LocalRhythmTracker : MonoBehaviour, ILocalRhythmTracker
{
public MusicBeatData CurrentBeatData { get; private set; }
public float CurrentSongTime { get; private set; }
public bool IsPlaying { get; private set; }
private float pendingSyncTime = -1f;
private void OnEnable()
{
// 自动将自身注册到全局物体映射表中,使 Wwise 回调能识别此 GameObject
MusicBeatSystem.RegisterRhythmGameObject(gameObject);
}
private void OnDisable()
{
MusicBeatSystem.UnregisterRhythmGameObject(gameObject);
}
public void OnSegmentTransitioned(MusicBeatData localBeatData)
{
CurrentBeatData = localBeatData;
CurrentSongTime = localBeatData != null ? localBeatData.audioStartOffset : 0f;
IsPlaying = localBeatData != null;
pendingSyncTime = -1f;
Debug.Log($"[LocalRhythmTracker] Segment transitioned on '{gameObject.name}' to '{localBeatData?.name}'");
}
public void ReceiveSyncBeat(float musicPositionSec, float beatDuration)
{
float offset = CurrentBeatData != null ? CurrentBeatData.audioStartOffset : 0f;
pendingSyncTime = musicPositionSec + offset;
}
private void Update()
{
if (CurrentBeatData == null) return;
if (pendingSyncTime >= 0f)
{
CurrentSongTime = pendingSyncTime;
pendingSyncTime = -1f;
IsPlaying = true;
}
if (IsPlaying)
{
CurrentSongTime += Time.deltaTime;
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 59460bbbea550b04ba6b882bfd02692b

View File

@@ -7,6 +7,27 @@ using UnityEngine;
namespace Cielonos.MainGame
{
[System.Serializable]
public struct SegmentDataMapping
{
[Tooltip("Wwise 中 Music Segment 的名称 (例如 BGM_Intensity_01)")]
public string segmentName;
[Tooltip("对应的 Unity MusicBeatData 谱面数据")]
public MusicBeatData beatData;
}
/// <summary>
/// 局部节拍追踪器接口,用于接收物体上局部播放的音乐片段切换事件
/// </summary>
public interface ILocalRhythmTracker
{
void OnSegmentTransitioned(MusicBeatData localBeatData);
void ReceiveSyncBeat(float musicPositionSec, float beatDuration);
bool IsPlaying { get; }
float CurrentSongTime { get; }
MusicBeatData CurrentBeatData { get; }
}
/// <summary>
/// 音乐节拍战斗系统。激活时覆盖 BackgroundMusicManager 播放对应 BGM
/// 通过 Wwise AK_MusicSyncBeat 回调 + MusicBeatData 谱面进行双轨节拍追踪,
@@ -71,6 +92,19 @@ namespace Cielonos.MainGame
[ShowInInspector, ReadOnly]
public float CurrentBPM { get; private set; }
/// <summary>
/// 当前播放的 Wwise Segment 名称 (例如 Func_00)
/// </summary>
[ShowInInspector, ReadOnly]
public string CurrentWwiseSegmentName { get; private set; }
/// <summary>
/// 当前小节内的拍号1-based。由 Wwise MusicSyncBeat 回调每拍递增,在 Entry 时归 1。
/// 例如 4/4 拍时值为 1~4 循环。
/// </summary>
[ShowInInspector, ReadOnly]
public int CurrentBarBeat { get; private set; } = 1;
#endregion
#region Events
@@ -95,6 +129,18 @@ namespace Cielonos.MainGame
/// </summary>
public event Action OnDeactivated;
/// <summary>
/// 当收到任何 Wwise User Cue 时触发。
/// 参数1. playingID, 2. cueName。
/// </summary>
public event Action<uint, string> OnUserCueReceived;
/// <summary>
/// 当全局主音乐MusicBeatSystem 自身 GameObject收到 PrepareNext Cue 时触发。
/// BGM 系统可以监听此事件,在音乐边界精确地起播 Back 音轨。
/// </summary>
public event Action OnGlobalPrepareNext;
#endregion
#region Private Fields
@@ -104,6 +150,11 @@ namespace Cielonos.MainGame
/// </summary>
private int nextBeatIndex;
/// <summary>
/// 由 MusicSyncBeat 回调累计的原始拍号计数(每个 Entry 时清零),用于推算 CurrentBarBeat
/// </summary>
private int rawBeatCount;
/// <summary>
/// Wwise 播放实例 ID
/// </summary>
@@ -120,21 +171,47 @@ namespace Cielonos.MainGame
[ShowInInspector]
private volatile float pendingSyncTime = -1f;
/// <summary>
/// Wwise 回调报告的 beatDuration用于反推实际 BPM
/// </summary>
[ShowInInspector]
private volatile float pendingBeatDuration = -1f;
[Header("Dynamic Music Configurations")]
[Tooltip("Wwise 中控制音乐片段切换的 Switch Group 名称")]
public string musicSegmentSwitchGroup = "Music_Segment";
[Tooltip("Wwise 音乐片段与 Unity 谱面资产的映射表")]
public List<SegmentDataMapping> segmentMappings = new List<SegmentDataMapping>();
/// <summary>
/// 当音乐运行到 PrepareNext 标记时触发。
/// 参数1. 触发该事件的 GameObject2. 当前播放的片段名称。
/// 返回值下一个要播放的片段名Switch 值)。
/// </summary>
public event Func<GameObject, string, string> OnPrepareNextSegment;
private struct PendingCallbackData
{
public enum Type { Entry, Prepare, BeatSync }
public Type callbackType;
public GameObject targetGO;
public string segmentName;
public float musicPositionSec;
public float beatDuration;
public uint playingID;
}
private readonly List<PendingCallbackData> pendingCallbacks = new List<PendingCallbackData>();
private readonly object callbackLock = new object();
private static readonly List<GameObject> activeRhythmGameObjects = new List<GameObject>();
#endregion
#region Lifecycle
public MusicBeatData testData;
private void Awake()
{
bgmManager = AudioManager.Instance.backgroundMusicManager;
RegisterRhythmGameObject(gameObject);
}
private void Update()
@@ -144,6 +221,9 @@ namespace Cielonos.MainGame
// 处理 Wwise 回调带来的时间校准
ProcessPendingSync();
// 处理 Entry/Prepare 等回调队列事件
ProcessPendingCallbacks();
if (!IsPlaying) return;
// 推进音乐时间
@@ -155,6 +235,7 @@ namespace Cielonos.MainGame
private void OnDestroy()
{
UnregisterRhythmGameObject(gameObject);
if (IsActive)
{
Deactivate();
@@ -165,61 +246,60 @@ namespace Cielonos.MainGame
#region Activate / Deactivate
[Button("Activate Test Data")]
[Button("Play Music")]
public void Activate()
{
if (testData != null)
var controller = GetComponent<CombatMusicController>();
if (controller != null)
{
Activate(testData);
controller.PlayFunctionalMusic();
}
else
{
Debug.LogWarning("[MusicBeatSystem] No test data assigned for activation");
Debug.LogWarning("[MusicBeatSystem] No CombatMusicController or testData found to play music.");
}
}
/// <summary>
/// 激活节拍系统:加载谱面、覆盖 BGM、注册 Wwise 回调
/// </summary>
/// <param name="beatData">要加载的谱面数据</param>
public void Activate(MusicBeatData beatData)
{
if (beatData == null)
{
Debug.LogError("[MusicBeatSystem] Activate failed: beatData is null");
return;
}
/// <summary>
/// 激活节拍系统:使用 Wwise 事件和初始 Switch 直接启动动态音乐流程
/// </summary>
public void Activate(AK.Wwise.Event wwiseEvent, string initialSwitch = null)
{
if (IsActive)
{
Deactivate();
}
// Ensure beat markers are sorted before starting tracking
beatData.SortBeats();
CurrentBeatData = beatData;
CurrentBPM = beatData.bpm;
CurrentBeatData = null;
CurrentBPM = 240f; // 默认临时 BPM直到 Entry Cue 触发表格映射替换
CurrentSongTime = 0f;
nextBeatIndex = 0;
IsPlaying = false;
IsActive = true;
// 覆盖 BackgroundMusicManager:先停止当前 BGM再标记覆盖
// 覆盖 BackgroundMusicManager
if (bgmManager != null)
{
bgmManager.StopMusic();
bgmManager.SetOverride(true);
}
// 在 MusicBeatSystem 自身的 gameObject 上播放节拍音乐
// 与 BackgroundMusicManager 的 gameObject 隔离,避免 Stop Event 作用域冲突
if (beatData.musicEvent != null && beatData.musicEvent.IsValid())
if (wwiseEvent != null && wwiseEvent.IsValid())
{
uint callbackFlags = (uint)(AkCallbackType.AK_MusicSyncBeat | AkCallbackType.AK_EndOfEvent);
beatData.musicSwitch.SetValue(gameObject); // 设置 Switch 以选择正确的音乐变体
uint callbackFlags = (uint)(
AkCallbackType.AK_MusicSyncBeat |
AkCallbackType.AK_MusicSyncEntry |
AkCallbackType.AK_MusicSyncUserCue |
AkCallbackType.AK_EndOfEvent
);
if (!string.IsNullOrEmpty(initialSwitch))
{
AkUnitySoundEngine.SetSwitch(musicSegmentSwitchGroup, initialSwitch, gameObject);
}
PlayerCanvas.CombatSystemsUIArea.beatTimelineUI.Initialize(this);
wwisePlayingID = beatData.musicEvent.Post(
wwisePlayingID = wwiseEvent.Post(
gameObject,
callbackFlags,
OnWwiseMusicCallback,
@@ -228,24 +308,19 @@ namespace Cielonos.MainGame
if (wwisePlayingID == 0)
{
Debug.LogWarning("[MusicBeatSystem] Wwise Post returned playingID 0, music may not play. " +
"Check: 1) musicEvent references a Music type (not Sound SFX) " +
"2) SoundBank is loaded 3) gameObject is active");
Debug.LogWarning("[MusicBeatSystem] Wwise Post returned playingID 0 for dynamic music event");
}
else
{
Debug.Log($"[MusicBeatSystem] Activated with '{beatData.name}', playingID={wwisePlayingID}, " +
$"posting on GameObject '{gameObject.name}'");
Debug.Log($"[MusicBeatSystem] Activated dynamic music flow with Event: '{wwiseEvent.Name}', playingID={wwisePlayingID}");
}
}
else
{
// 无 Wwise Event 时,使用纯谱面模式(仅基于 deltaTime 和谱面数据)
Debug.LogWarning("[MusicBeatSystem] No valid Wwise Event on beatData, running in offline mode");
IsPlaying = true;
Debug.LogWarning("[MusicBeatSystem] No valid Wwise Event provided for dynamic music activation");
}
OnActivated?.Invoke(beatData);
OnActivated?.Invoke(null);
}
/// <summary>
@@ -261,7 +336,8 @@ namespace Cielonos.MainGame
nextBeatIndex = 0;
pendingSyncTime = -1f;
pendingBeatDuration = -1f;
CurrentWwiseSegmentName = null;
// 停止 MusicBeatSystem 自己 Post 的节拍音乐
if (wwisePlayingID != 0)
{
@@ -421,8 +497,11 @@ namespace Cielonos.MainGame
/// <summary>
/// Wwise 音乐回调处理器(可能在非主线程调用)
/// </summary>
private void OnWwiseMusicCallback(object in_cookie, AkCallbackType in_type, AkCallbackInfo in_info)
internal void OnWwiseMusicCallback(object in_cookie, AkCallbackType in_type, AkCallbackInfo in_info)
{
GameObject go = GetGameObjectFromWwiseId(in_info.gameObjID);
if (go == null) go = gameObject;
if (in_type == AkCallbackType.AK_MusicSyncBeat)
{
if (in_info is AkMusicSyncCallbackInfo syncInfo)
@@ -431,19 +510,73 @@ namespace Cielonos.MainGame
// segmentInfo_iCurrentPosition 是当前 segment 内的播放位置(毫秒)
float musicPositionSec = syncInfo.segmentInfo_iCurrentPosition / 1000f;
float beatDuration = syncInfo.segmentInfo_fBeatDuration;
// 将校准数据传递到主线程处理
pendingSyncTime = musicPositionSec + CurrentBeatData.audioStartOffset;
pendingBeatDuration = beatDuration;
if (go == gameObject)
{
// 将校准数据传递到主线程处理 (防空保护)
float offset = CurrentBeatData != null ? CurrentBeatData.audioStartOffset : 0f;
pendingSyncTime = musicPositionSec + offset;
pendingBeatDuration = beatDuration;
}
else
{
lock (callbackLock)
{
pendingCallbacks.Add(new PendingCallbackData
{
callbackType = PendingCallbackData.Type.BeatSync,
targetGO = go,
segmentName = "",
musicPositionSec = musicPositionSec,
beatDuration = beatDuration
});
}
}
}
else
{
Debug.LogWarning($"[MusicBeatSystem] Received MusicSync callback with unexpected info type: {in_info.GetType().Name}");
}
}
else if (in_type == AkCallbackType.AK_EndOfEvent)
else if (in_type == AkCallbackType.AK_MusicSyncUserCue && in_info is AkMusicSyncCallbackInfo cueInfo)
{
// 音乐播放结束
string cueName = cueInfo.userCueName;
lock (callbackLock)
{
// 所有的 User Cue 都会触发 OnUserCueReceived
pendingCallbacks.Add(new PendingCallbackData
{
callbackType = PendingCallbackData.Type.Prepare,
targetGO = go,
segmentName = cueName,
playingID = cueInfo.playingID
});
// 如果是 Entry_ 开头,则同时触发 Entry 谱面替换逻辑
if (!string.IsNullOrEmpty(cueName) && cueName.StartsWith("Entry_"))
{
string segmentName = cueName.Substring(6);
pendingCallbacks.Add(new PendingCallbackData
{
callbackType = PendingCallbackData.Type.Entry,
targetGO = go,
segmentName = segmentName
});
}
}
}
else if (in_type == AkCallbackType.AK_EndOfEvent && in_info is AkEventCallbackInfo eventInfo)
{
lock (callbackLock)
{
pendingCallbacks.Add(new PendingCallbackData
{
callbackType = PendingCallbackData.Type.Prepare,
targetGO = go,
segmentName = "EndOfEvent",
playingID = eventInfo.playingID
});
}
Debug.Log("[MusicBeatSystem] Music playback ended");
}
}
@@ -493,6 +626,163 @@ namespace Cielonos.MainGame
}
}
private void ProcessPendingCallbacks()
{
List<PendingCallbackData> localCallbacks = null;
lock (callbackLock)
{
if (pendingCallbacks.Count > 0)
{
localCallbacks = new List<PendingCallbackData>(pendingCallbacks);
pendingCallbacks.Clear();
}
}
if (localCallbacks == null) return;
foreach (var callback in localCallbacks)
{
if (callback.callbackType == PendingCallbackData.Type.Prepare)
{
OnUserCueReceived?.Invoke(callback.playingID, callback.segmentName);
if (callback.segmentName == "PrepareNext" && callback.targetGO == gameObject)
{
string nextSegment = OnPrepareNextSegment?.Invoke(callback.targetGO, CurrentWwiseSegmentName);
if (!string.IsNullOrEmpty(nextSegment))
{
AkUnitySoundEngine.SetSwitch(musicSegmentSwitchGroup, nextSegment, callback.targetGO);
Debug.Log($"[MusicBeatSystem] PrepareNext: transition scheduled on '{callback.targetGO.name}' from '{CurrentWwiseSegmentName}' to '{nextSegment}'");
}
// 通知所有订阅者:全局主音乐到达了 PrepareNext 边界
OnGlobalPrepareNext?.Invoke();
}
}
else if (callback.callbackType == PendingCallbackData.Type.BeatSync)
{
var localTracker = callback.targetGO.GetComponent<ILocalRhythmTracker>();
if (localTracker != null)
{
localTracker.ReceiveSyncBeat(callback.musicPositionSec, callback.beatDuration);
}
}
else if (callback.callbackType == PendingCallbackData.Type.Entry)
{
// Find mapped MusicBeatData
MusicBeatData mappedData = null;
if (segmentMappings != null)
{
foreach (var mapping in segmentMappings)
{
if (mapping.segmentName == callback.segmentName)
{
mappedData = mapping.beatData;
break;
}
}
}
if (mappedData != null)
{
if (callback.targetGO == gameObject)
{
// Global Master BGM Transition
CurrentWwiseSegmentName = callback.segmentName;
CurrentBeatData = mappedData;
CurrentBPM = mappedData.bpm;
CurrentSongTime = mappedData.audioStartOffset;
nextBeatIndex = 0;
rawBeatCount = 0;
CurrentBarBeat = 1;
IsPlaying = true;
// Re-initialize UI
PlayerCanvas.CombatSystemsUIArea.beatTimelineUI.Initialize(this);
Debug.Log($"[MusicBeatSystem] Global Transitioned to Segment: '{callback.segmentName}', BPM={mappedData.bpm}");
OnActivated?.Invoke(mappedData);
}
else
{
// Local GameObject Segment Transition (e.g. Enemy)
var localTracker = callback.targetGO.GetComponent<ILocalRhythmTracker>();
if (localTracker != null)
{
localTracker.OnSegmentTransitioned(mappedData);
}
Debug.Log($"[MusicBeatSystem] Local Segment Entry: '{callback.segmentName}' on GameObject '{callback.targetGO.name}'");
}
}
else
{
Debug.LogWarning($"[MusicBeatSystem] Unmapped segment entry callback: '{callback.segmentName}' on '{callback.targetGO.name}'");
}
}
}
}
public static void RegisterRhythmGameObject(GameObject go)
{
if (go == null) return;
lock (activeRhythmGameObjects)
{
if (!activeRhythmGameObjects.Contains(go))
{
activeRhythmGameObjects.Add(go);
}
}
}
public static void UnregisterRhythmGameObject(GameObject go)
{
if (go == null) return;
lock (activeRhythmGameObjects)
{
activeRhythmGameObjects.Remove(go);
}
}
private GameObject GetGameObjectFromWwiseId(ulong gameObjID)
{
if (gameObjID == (ulong)gameObject.GetInstanceID()) return gameObject;
lock (activeRhythmGameObjects)
{
for (int i = activeRhythmGameObjects.Count - 1; i >= 0; i--)
{
var go = activeRhythmGameObjects[i];
if (go == null)
{
activeRhythmGameObjects.RemoveAt(i);
continue;
}
if ((ulong)go.GetInstanceID() == gameObjID)
{
return go;
}
}
}
// Fallback lookup
#if UNITY_6000_0_OR_NEWER
var objs = FindObjectsByType<AkGameObj>(FindObjectsSortMode.None);
#else
var objs = FindObjectsOfType<AkGameObj>();
#endif
foreach (var obj in objs)
{
if (obj != null && (ulong)obj.gameObject.GetInstanceID() == gameObjID)
{
RegisterRhythmGameObject(obj.gameObject);
return obj.gameObject;
}
}
return null;
}
/// <summary>
/// 检查是否到达下一个节拍,触发 onBeat 事件
/// </summary>
@@ -508,6 +798,11 @@ namespace Cielonos.MainGame
BeatMarker beat = markers[nextBeatIndex];
nextBeatIndex++;
// 更新 CurrentBarBeat根据 BPM 和小节拍数推算)
rawBeatCount++;
int beatsPerBar = CurrentBeatData.beatsPerBar > 0 ? CurrentBeatData.beatsPerBar : 4;
CurrentBarBeat = ((rawBeatCount - 1) % beatsPerBar) + 1;
OnBeat?.Invoke(beat);
}
}

View File

@@ -43,40 +43,40 @@ MonoBehaviour:
- time: 2.5945945
tags:
- EnemyAttack0
barIndex: 4
barIndex: 1
beatInBar: 0
- time: 2.5945945
tags:
- Normal
barIndex: 1
beatInBar: 0
- time: 5.189189
tags:
- EnemyAttack0
barIndex: 2
beatInBar: 0
- time: 5.189189
tags:
- Normal
barIndex: 2
beatInBar: 0
- time: 7.7837834
tags:
- EnemyAttack0
barIndex: 3
beatInBar: 0
- time: 7.7837834
tags:
- Normal
barIndex: 3
beatInBar: 0
- time: 10.378378
tags:
- EnemyAttack0
barIndex: 4
beatInBar: 0
- time: 5.189189
tags:
- EnemyAttack0
barIndex: 8
beatInBar: 0
- time: 5.189189
tags:
- Normal
barIndex: 8
beatInBar: 0
- time: 7.7837834
tags:
- EnemyAttack0
barIndex: 12
beatInBar: 0
- time: 7.7837834
tags:
- Normal
barIndex: 12
beatInBar: 0
- time: 10.378378
tags:
- EnemyAttack0
barIndex: 16
beatInBar: 0
- time: 10.378378
tags:
- Normal
barIndex: 16
barIndex: 4
beatInBar: 0