This commit is contained in:
SoulliesOfficial
2025-12-17 04:19:38 -05:00
parent 7c1cb7e8e1
commit d15957c719
4315 changed files with 8260710 additions and 2940 deletions

View File

@@ -25,6 +25,7 @@ namespace Cielonos.MainGame.Characters
[TitleGroup("Data & Presets")]
public AttributeData attributeData;
public VFXData vfxData;
public BaseAnimationGroup baseAnimationGroup;
[TitleGroup("Submodules")]
@@ -50,8 +51,8 @@ namespace Cielonos.MainGame.Characters
protected virtual void Awake()
{
InitializeSubmodules();
InitializeSubcontrollers();
InitializeSubmodules();
}
#if UNITY_EDITOR
@@ -63,6 +64,8 @@ namespace Cielonos.MainGame.Characters
protected virtual void Start()
{
vfxData.Initialize(this);
if (fraction == Fraction.Enemy)
{
BattleManager.EnemySm.activeEnemiesList.Add(this);
@@ -175,16 +178,45 @@ namespace Cielonos.MainGame.Characters
public partial class CharacterBase
{
public virtual bool GetHit(BreakthroughType breakthroughType,
public virtual bool GetHit(BreakthroughType breakthroughType, out float recoveryTime,
DisruptionType disruptionType = DisruptionType.NormalExternal, Vector3 direction = default)
{
renderSc.GetHitBlink();
if (animationSc.SetGetHitDisruption(disruptionType, breakthroughType))
{
animationSc.PlayGetHitMediumAnimation(direction);
switch (breakthroughType)
{
case BreakthroughType.Medium:
animationSc.PlayGetHitMediumAnimation(out recoveryTime, direction);
break;
case BreakthroughType.Heavy:
animationSc.PlayGetHitHeavyAnimation(out recoveryTime,direction);
break;
case BreakthroughType.Disruption:
case BreakthroughType.Forced:
animationSc.PlayGetHitDisruptionAnimation(out recoveryTime,direction);
break;
default:
animationSc.PlayGetHitBoneShake(0.4f, direction);
recoveryTime = 0f;
break;
}
return true;
}
float intensity = breakthroughType switch
{
BreakthroughType.None => 0,
BreakthroughType.Weak => 0.2f,
BreakthroughType.Medium => 0.4f,
BreakthroughType.Heavy or BreakthroughType.Disruption or BreakthroughType.Forced => 0.8f,
_ => 0
};
recoveryTime = 0f;
animationSc.PlayGetHitBoneShake(intensity, direction);
return false;
}
}

View File

@@ -5,6 +5,7 @@ using SLSFramework.General;
using SLSUtilities.FunctionalAnimation;
using UnityEngine;
using UnityEngine.Serialization;
using Random = UnityEngine.Random;
namespace Cielonos.MainGame.Characters
{
@@ -57,6 +58,7 @@ namespace Cielonos.MainGame.Characters
protected virtual void LateUpdate()
{
fullBodyFuncAnimSm?.UpdateEvents();
BoneShakeLateUpdate();
}
}
@@ -79,6 +81,90 @@ namespace Cielonos.MainGame.Characters
}
}
public partial class AnimationSubcontrollerBase
{
private class BoneShakeState
{
public Transform bone;
public Vector3 shakeAxis; // 震动轴
// 物理变量
public float currentAngle = 0f; // 当前偏离角度 (位移 x)
public float velocity = 0f; // 当前震动速度 (速度 v)
}
[Header("Bone Shake Settings")]
// 刚度:越大越硬,回弹越快。对于重型机甲,建议 150-300轻型无人机建议 80-150。
public float stiffness = 200f;
// 阻尼:越大停得越快。建议 10-20。如果太小机器人会像果冻一样晃。
public float damping = 15f;
// 冲击力倍率:将受击力度转化为弹簧的初始速度
public float impactForceMultiplier = 500f;
public List<Transform> testShakeBones; // 用于测试的骨骼列表
private List<BoneShakeState> activeShakes = new List<BoneShakeState>();
private void BoneShakeLateUpdate()
{
float dt = Time.deltaTime;
for (int i = activeShakes.Count - 1; i >= 0; i--)
{
var state = activeShakes[i];
if (state.bone == null) {
activeShakes.RemoveAt(i);
continue;
}
// --- 核心:阻尼弹簧物理公式 (Hooke's Law + Damping) ---
// F = -k * x - d * v
// force = -stiffness * displacement - damping * velocity
float force = -stiffness * state.currentAngle - damping * state.velocity;
// a = F / m (假设质量为1简化计算)
// v += a * dt
state.velocity += force * dt;
// x += v * dt
state.currentAngle += state.velocity * dt;
// --- 应用旋转 ---
// 将计算出的角度应用到轴向上
Quaternion shakeRot = Quaternion.AngleAxis(state.currentAngle, state.shakeAxis);
state.bone.localRotation = state.bone.localRotation * shakeRot;
// --- 移除条件 ---
// 当能量非常小速度和位移都接近0时移除节省性能
if (Mathf.Abs(state.currentAngle) < 0.1f && Mathf.Abs(state.velocity) < 0.1f)
{
activeShakes.RemoveAt(i);
}
}
}
public void ApplyBoneShake(Transform hitBone, Vector3 hitDirection, float intensity)
{
var state = activeShakes.Find(x => x.bone == hitBone);
if (state == null)
{
state = new BoneShakeState();
state.bone = hitBone;
activeShakes.Add(state);
}
// 机械特质:受击瞬间不是直接设置位移,而是给予一个巨大的“初速度” (Impulse)
// 这会让骨骼瞬间弹出去,然后被弹簧拉回来,非常有力量感
state.velocity += intensity * impactForceMultiplier;
// 计算震动轴:依旧是垂直于攻击方向
Vector3 axis = Vector3.Cross(hitDirection, Vector3.up).normalized;
if (axis == Vector3.zero) axis = Vector3.right;
state.shakeAxis = axis;
}
}
public partial class AnimationSubcontrollerBase
{
public virtual void RegisterDefaultFunctions()
@@ -116,47 +202,68 @@ namespace Cielonos.MainGame.Characters
return false;
}
public virtual void PlayGetHitMediumAnimation(Vector3 direction = default)
public virtual void PlayGetHitBoneShake(float intensity, Vector3 direction = default)
{
if (direction == default)
{
direction = owner.transform.right;
}
else
{
direction = Quaternion.Euler(0, 90, 0) * direction;
}
foreach (Transform bone in testShakeBones)
{
ApplyBoneShake(bone, direction, intensity);
}
}
protected virtual void PlayGetHitAnimation(string getHitAnimPrefix, out float animDuration, Vector3 direction = default)
{
int fullBodyActionIndex = animator.GetLayerIndex("FullBodyAction");
if (animator.HasState(fullBodyActionIndex, Animator.StringToHash("GetHitMediumFront")))
string getHitFrontAnim = getHitAnimPrefix + "Front";
string getHitAnim = getHitAnimPrefix + "Front";
float normalizedTransitionDuration = 0.1f / animator.GetCurrentAnimatorStateInfo(fullBodyActionIndex).length;
animDuration = 0f;
if (animator.HasState(fullBodyActionIndex, Animator.StringToHash(getHitAnim)))
{
float normalizedTransitionDuration = 0.1f / animator.GetCurrentAnimatorStateInfo(fullBodyActionIndex).length;
if (direction == default)
{
animator.CrossFade("GetHitMediumFront", normalizedTransitionDuration, fullBodyActionIndex, 0);
return;
}
direction.y = 0;
direction = direction.normalized;
float angle = Vector3.SignedAngle(transform.forward, direction, Vector3.up);
if (angle > -45f && angle <= 45f)
string directionStr = angle switch
{
animator.CrossFade("GetHitMediumBack", normalizedTransitionDuration, fullBodyActionIndex, 0);
}
else if (angle > 45f && angle <= 135f)
> -45f and <= 45f => "Back",
> 45f and <= 135f => "Left",
> -135f and <= -45f => "Right",
_ => "Front"
};
getHitAnim = getHitAnimPrefix + directionStr;
if (direction == default || !animator.HasState(fullBodyActionIndex, Animator.StringToHash(getHitAnim)))
{
animator.CrossFade("GetHitMediumLeft", normalizedTransitionDuration, fullBodyActionIndex, 0);
}
else if (angle > -135f && angle <= -45f)
{
animator.CrossFade("GetHitMediumRight", normalizedTransitionDuration, fullBodyActionIndex, 0);
animator.CrossFade(getHitFrontAnim, normalizedTransitionDuration, fullBodyActionIndex, 0);
}
else
{
animator.CrossFade("GetHitMediumFront", normalizedTransitionDuration, fullBodyActionIndex, 0);
animator.CrossFade(getHitAnim, normalizedTransitionDuration, fullBodyActionIndex, 0);
}
animDuration = animator.GetCurrentAnimatorStateInfo(fullBodyActionIndex).length;
}
else if (animator.HasState(fullBodyActionIndex, Animator.StringToHash(getHitAnimPrefix)))
{
getHitAnim = getHitAnimPrefix;
animator.CrossFade(getHitAnim, normalizedTransitionDuration, fullBodyActionIndex, 0);
animDuration = animator.GetCurrentAnimatorStateInfo(fullBodyActionIndex).length;
}
else
{
if (animator.HasState(fullBodyActionIndex, Animator.StringToHash("GetHit")))
{
animator.CrossFade("GetHit", 0.1f, fullBodyActionIndex, 0);
animator.CrossFade("GetHit", normalizedTransitionDuration, fullBodyActionIndex, 0);
animDuration = animator.GetCurrentAnimatorStateInfo(fullBodyActionIndex).length;
}
else
{
@@ -164,5 +271,20 @@ namespace Cielonos.MainGame.Characters
}
}
}
public virtual void PlayGetHitMediumAnimation(out float animDuration, Vector3 direction = default)
{
PlayGetHitAnimation("GetHitMedium", out animDuration, direction);
}
public virtual void PlayGetHitHeavyAnimation(out float animDuration, Vector3 direction = default)
{
PlayGetHitAnimation("GetHitHeavy", out animDuration, direction);
}
public virtual void PlayGetHitDisruptionAnimation(out float animDuration, Vector3 direction = default)
{
PlayGetHitAnimation("GetHitDisruption", out animDuration, direction);
}
}
}

View File

@@ -1,13 +1,15 @@
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.Serialization;
namespace Cielonos.MainGame.Characters
{
public class BodyPartsSubcontroller : SubcontrollerBase<CharacterBase>
{
[Title("Main Parts")]
public Transform centerPoint;
[FormerlySerializedAs("centerPoint")] [Title("Main Parts")]
public Transform flexibleCenterPoint;
public Transform staticCenterPoint;
public Transform footPoint;
public Transform head;
public Transform leftHand;

View File

@@ -48,6 +48,32 @@ namespace Cielonos.MainGame.Characters
{
characterTransform.DOLookAt(target.transform.position, duration, AxisConstraint.Y);
}
public void TurnToDirection(Vector3 direction, float duration = 0f)
{
Vector3 dashRotation = Vector3.zero;
float angle = Vector3.SignedAngle(Vector3.forward, direction, Vector3.up);
if (owner is Player player)
{
dashRotation.y = player.viewSc.isLockedOn
? player.viewSc.cameraRotationSm.cinemachineEndLockYaw + angle
: player.viewSc.cameraRotationSm.cinemachineTargetYaw + angle;
}
else
{
dashRotation = new Vector3(0, angle, 0);
}
if (duration > 0)
{
characterTransform.DORotateQuaternion(Quaternion.Euler(dashRotation), duration);
}
else
{
characterTransform.rotation = Quaternion.Euler(dashRotation);
}
}
}
public partial class MovementSubcontrollerBase

View File

@@ -133,8 +133,27 @@ namespace Cielonos.MainGame.Characters
{
getHitBlinkTween?.Kill(true);
getHitBlinkTween = DOTween.Sequence();
getHitBlinkTween.OnPlay(() =>
foreach (Material mat in baseRenderMaterials)
{
Tweener rimTween = mat.DOVector(new Vector4(1, 1, 4, 1), "_RimParams", 0.5f)
.From(new Vector4(0, 1, 4, 1))
.OnPlay(() =>
{
mat.EnableKeyword("_RIM");
})
.SetEase(Ease.OutQuad)
.OnComplete(() =>
{
mat.DisableKeyword("_RIM");
});
getHitBlinkTween.Join(rimTween);
}
getHitBlinkTween.Play();
/*getHitBlinkTween.OnPlay(() =>
{
effectContainers["GetHitBlink"].SetActive(true);
});
@@ -157,7 +176,7 @@ namespace Cielonos.MainGame.Characters
getHitBlinkTween.OnComplete(() =>
{
effectContainers["GetHitBlink"].SetActive(false);
});
});*/
getHitBlinkTween.Play();
}

View File

@@ -10,8 +10,8 @@ namespace Cielonos.MainGame.Characters
public AdditionalForceSubmodule(CharacterBase character) : base(character)
{
additionalForceXZ = new LerpVector3(Vector3.zero, 5f);
additionalForceY = new LerpFloat(0f, 5f);
additionalForceXZ = new LerpVector3(Vector3.zero, 1f);
additionalForceY = new LerpFloat(0f, 1f);
}
public void AddForce(Vector3 force)

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using Cielonos.MainGame.Characters;
using Sirenix.OdinInspector;
using SLSUtilities.FunctionalAnimation;
using UnityEngine;
@@ -23,8 +24,11 @@ namespace Cielonos.MainGame
public float currentPlaySpeedMultiplier = 1f;
public float currentPlayTime => currentRuntimeFuncAnim.currentPlayTime;
[ShowInInspector]
public float currentPlayTime => currentRuntimeFuncAnim?.currentPlayTime ?? 0f;
public float currentNormalizedPlayTime => Mathf.Min(1, currentPlayTime / currentClip.length);
[ShowInInspector]
public float currentFrame => currentRuntimeFuncAnim?.currentPlayTime * currentRuntimeFuncAnim?.funcAnimData.animationClip.frameRate ?? 0f;
public float currentScaledClipLength => currentClip.length / currentPlaySpeedMultiplier;
@@ -216,7 +220,7 @@ namespace Cielonos.MainGame
return;
}
currentRuntimeFuncAnim.currentPlayTime += Time.deltaTime * currentData.animInfo.overridePlaySpeed * currentPlaySpeedMultiplier;
currentRuntimeFuncAnim.currentPlayTime += owner.owner.selfTimeSm.DeltaTime * currentData.animInfo.overridePlaySpeed * currentPlaySpeedMultiplier;
if (currentPlayTime >= currentClip.length)
{

View File

@@ -1,10 +1,13 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Cielonos.MainGame.Characters;
using UniRx;
using UnityEngine;
namespace Cielonos.MainGame.Characters
{
public class SelfTimeSubmodule : SubmoduleBase<CharacterBase>
public partial class SelfTimeSubmodule : SubmoduleBase<CharacterBase>
{
public FloatReactiveProperty timeScaleCoefficient;
public float TimeScale => timeScaleCoefficient.Value * Time.timeScale;
@@ -13,10 +16,150 @@ namespace Cielonos.MainGame.Characters
public SelfTimeSubmodule(CharacterBase entity) : base(entity)
{
timeScaleCoefficient = new FloatReactiveProperty(1);
if (entity.animationSc != null)
{
timeScaleCoefficient.Subscribe(x =>
{
entity.animationSc.fullBodyFuncAnimSm.currentPlaySpeedMultiplier = x;
});
}
if (entity.animationSc.animator != null)
{
timeScaleCoefficient.Subscribe(x => { entity.animationSc.animator.speed = x; });
timeScaleCoefficient.Subscribe(x =>
{
entity.animationSc.animator.speed = x;
});
}
}
}
public partial class SelfTimeSubmodule
{
/// <summary>
/// 添加一个基于本地时间Local DeltaTime的计时器
/// </summary>
public IDisposable AddLocalTimer(float duration, Action onComplete, Action onUpdate = null)
{
// 用于记录累积时间
float accumulatedTime = 0f;
return Observable.EveryUpdate()
.Select(_ => DeltaTime) // 1. 获取每帧的真实 DeltaTime
.TakeWhile(dt =>
{
// 2. 累加时间
accumulatedTime += dt;
// 3. 如果累积时间小于总时长,继续流;否则停止流并触发 OnCompleted
return accumulatedTime < duration;
})
.Subscribe(
_ => onUpdate?.Invoke(), // 每帧更新时执行 Action
() => onComplete?.Invoke() // 4. 流结束时TakeWhile 返回 false执行 Action
).AddTo(owner); // 5. 绑定生命周期到角色,防止内存泄漏
}
}
public partial class SelfTimeSubmodule
{
// 缓存一个默认的抛物线曲线,避免每次 null 时都 new 一个
// 形状:(0,0) -> (0.5, 1) -> (1, 0)
private static readonly AnimationCurve DefaultParabola = new AnimationCurve(
new Keyframe(0f, 0f),
new Keyframe(0.5f, 1f),
new Keyframe(1f, 0f)
);
private IDisposable hitStopDisposable;
/// <summary>
/// 应用顿帧Hit Stop
/// </summary>
/// <param name="duration">持续时间(秒,基于全局游戏时间)</param>
/// <param name="targetScale">目标缩放倍率(通常为 0 或 0.1</param>
public void ModifyTimeScale(float duration, float targetScale = 0f)
{
// 1. 如果之前有正在进行的顿帧,先取消它(防止旧的恢复逻辑覆盖新的设置)
hitStopDisposable?.Dispose();
// 2. 设置当前的缩放倍率
timeScaleCoefficient.Value = targetScale;
// 3. 开启计时器
// 注意:这里使用 Scheduler.MainThread它是基于 Time.time (全局时间) 的。
// 这意味着:
// - 它会受到 Time.timeScale (全局暂停) 的影响(符合预期,游戏暂停时顿帧也该暂停)。
// - 它 *不会* 受到 timeScaleCoefficient (我们自己改的本地时间) 的影响(关键!)。
hitStopDisposable = Observable.Timer(TimeSpan.FromSeconds(duration), Scheduler.MainThread)
.Subscribe(_ =>
{
// 计时结束,恢复为 1
timeScaleCoefficient.Value = 1f;
hitStopDisposable = null;
})
.AddTo(owner); // 安全性:如果角色在顿帧期间死亡/销毁,自动取消计时器
}
/// <summary>
/// 使用曲线动态修改本地时间流速
/// </summary>
/// <param name="duration">持续时间(秒)</param>
/// <param name="start">曲线值为0时对应的时间倍率通常是初始值</param>
/// <param name="peak">曲线值为1时对应的时间倍率通常是极值</param>
/// <param name="curve">时间变化曲线归一化X轴0~1Y轴通常0~1。如果为null则使用默认的“先升后降”抛物线。</param>
public void ModifyTimeScale(float duration, float start, float peak, AnimationCurve curve = null)
{
// 1. 清理旧的计时器
hitStopDisposable?.Dispose();
// 2. 处理默认曲线逻辑
curve ??= DefaultParabola;
// 3. 记录开始时的累计时间
float timer = 0f;
// 4. 开启每帧更新的流
hitStopDisposable = Observable.EveryUpdate()
.TakeWhile(_ => timer < duration) // 当时间超过 duration 时结束流
.Subscribe(
_ =>
{
// 累加时间 (使用 Time.deltaTime 以响应全局暂停)
timer += Time.deltaTime;
// 计算归一化进度 (0.0 ~ 1.0)
float progress = Mathf.Clamp01(timer / duration);
// 核心逻辑:
// A. 从曲线获取当前的“强度” (Y轴值)
float curveValue = curve.Evaluate(progress);
// B. 在 start 和 peak 之间根据强度进行插值
// 当 curveValue = 0 时,结果为 start
// 当 curveValue = 1 时,结果为 peak
float currentScale = Mathf.Lerp(start, peak, curveValue);
// C. 应用到响应式属性
timeScaleCoefficient.Value = currentScale;
},
() =>
{
// 5. 计时结束后的收尾工作
// 通常为了安全,结束后我们会强制恢复到 1.0 (正常速度)
// 或者你可以恢复到 start视具体需求而定
timeScaleCoefficient.Value = 1f;
hitStopDisposable = null;
}
)
.AddTo(owner); // 绑定生命周期
}
// 可选:提供一个强制恢复的方法,用于因为某些逻辑需要立刻打断顿帧时调用
public void ResetTimeScale()
{
hitStopDisposable?.Dispose();
timeScaleCoefficient.Value = 1f;
}
}
}

View File

@@ -12,6 +12,8 @@ namespace Cielonos.MainGame.Characters
Disarm = 2, //缴械,无法攻击(使用主武器)
Restraint = 3, //束缚,无法移动
Stun = 100, //眩晕
//正面状态
Invincible = 1000, //无敌
Invisible = 1001, //隐身