Created
February 14, 2026 04:44
-
-
Save seobyeongky/a132ee00ad3307d8f2359a56ee12066e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.Collections.Generic; | |
| using Data; | |
| using MovementEffects; | |
| using Sirenix.OdinInspector; | |
| using UnityEngine; | |
| [CreateAssetMenu(fileName = "FootWalk", menuName = "FootAnim/FootWalk")] | |
| public class FootGaitAnimation : FootAnimation | |
| { | |
| [Header("Gait Settings")] | |
| public FootGaitConfig walk = new(); | |
| public FootGaitConfig run = new(); | |
| public FootGaitConfig sprint = new(); | |
| #if UNITY_EDITOR | |
| [Button] | |
| public void OpenTable() | |
| { | |
| var opts = new TableEditorShowOption(); | |
| opts.AddScriptableObject(this, "walk"); | |
| opts.AddScriptableObject(this, "run"); | |
| opts.AddScriptableObject(this, "sprint"); | |
| TableEditorUtils.Show(opts); | |
| } | |
| #endif | |
| [Header("Gait Blending")] | |
| [Range(0f, 1f)] | |
| public float gaitBlend01; | |
| FootGaitConfig cfgcfg; | |
| public void SetGaitBlend(float blend01) => gaitBlend01 = Mathf.Clamp01(blend01); | |
| public float EvaluateGaitBlendT(float? overwrite = null) => (overwrite ?? gaitBlend01); | |
| public FootGaitConfig GetBlendedConfig(float? overwrite = null) | |
| { | |
| cfgcfg ??= new FootGaitConfig(); | |
| var t = EvaluateGaitBlendT(overwrite); | |
| if (t == 0) | |
| return walk; | |
| else if (t < 0.5f) | |
| cfgcfg.Interpolate(walk, run, t * 2f); | |
| else if (t == 0.5f) | |
| return run; | |
| else if (t < 1f) | |
| cfgcfg.Interpolate(run, sprint, (t - 0.5f) * 2f); | |
| else | |
| return sprint; | |
| return cfgcfg; | |
| } | |
| public override PlayContextBase CreateContext() => new GaitContext(this); | |
| struct FootVisualTracking | |
| { | |
| public float trackStartTime; | |
| public float progressAtStart; | |
| public void Check(float pr) | |
| { | |
| trackStartTime = GodOfTime.time; | |
| progressAtStart = pr; | |
| } | |
| public float GetProgress(float duration) | |
| { | |
| return progressAtStart + (GodOfTime.time - trackStartTime) / duration; | |
| } | |
| } | |
| public class GaitContext : PlayContextBase | |
| { | |
| /// <summary> | |
| /// 0~1값 | |
| /// </summary> | |
| public float gaitProgress; | |
| FootGaitAnimation Outer => (FootGaitAnimation)base.outer; | |
| private FootGaitConfig _gaitCfg = null!; | |
| private CoroutineHandle _walkRoutine; | |
| private CoroutineHandle coStand; | |
| private CoroutineHandle coSwing; | |
| private CoroutineHandle co3; | |
| CoroutineHandle coBodyMove; | |
| CoroutineHandle coSetVisual; | |
| CoroutineHandle coDbg; | |
| private int _currentFootIndex = 0; // 0: Left, 1: Right | |
| FootVisualTracking movingFootVisualTracking; | |
| FootVisualTracking standingFootVisualTracking; | |
| float bodyMoveRatio = 0f; | |
| public GaitContext(FootGaitAnimation outer) : base(outer) { } | |
| protected override void OnStart(Vector2 movingDir) | |
| { | |
| _gaitCfg = Outer.GetBlendedConfig(Animator.gaitBlendOverwrite); | |
| var customLegInfo = _gaitCfg.customLegInfo; | |
| if (Animator.DirSign < 0) | |
| { | |
| customLegInfo.leftOriginOffsetXy.x += 0.02f; | |
| customLegInfo.rightOriginOffsetXy.x += 0.02f; | |
| } | |
| Animator.targetLegInfo = customLegInfo; | |
| foreach (var foot in foots) | |
| { | |
| foot.MoveDelta(Vector3.zero, 0.05f, FootPosLayer.DashLift); | |
| foot.MoveDelta(Vector3.zero, 0.1f, FootPosLayer.LadderOuterOffset); | |
| foot.MoveDelta(Vector3.zero, 0.1f, FootPosLayer.RopeClimbOffset, EaseSetting.EaseOutQuad); | |
| foot.groundAbsWeight = _gaitCfg.groundAbsWeight; | |
| foot.SetRotationDelta(0f, 0.05f, FootRotLayer.Base);//대쉬로부터 초기화 | |
| foot.SetVisual(FootVisualType.Step, 0f); | |
| } | |
| _walkRoutine = Timing.RunCoroutine(WalkRoutine(), Animator.FootAnimCoroutineTag); | |
| bodyMoveRatio = 0; | |
| Animator.SetFootSortingMode(FootSortingMode.FrontFootFront); | |
| } | |
| protected override void OnEnd() | |
| { | |
| Animator.ResetFootSortingMode(); | |
| if (_walkRoutine.IsInitialized) | |
| Timing.KillCoroutinesSafe(ref _walkRoutine); | |
| foreach (var foot in foots) | |
| foot.SetDebugColor(Color.white); | |
| if (coStand.IsInitialized) | |
| Timing.KillCoroutinesSafe(ref coStand); | |
| if (coSwing.IsInitialized) | |
| Timing.KillCoroutinesSafe(ref coSwing); | |
| if (co3.IsInitialized) | |
| Timing.KillCoroutinesSafe(ref co3); | |
| if (coBodyMove.IsInitialized) | |
| Timing.KillCoroutinesSafe(ref coBodyMove); | |
| if (coSetVisual.IsInitialized) | |
| Timing.KillCoroutinesSafe(ref coSetVisual); | |
| if (coDbg.IsInitialized) | |
| Timing.KillCoroutinesSafe(ref coDbg); | |
| Animator.Owner.shape.frontSortingGroupPos.def.UnsetWithDamp(DefinitePositionKey.WalkAnimation, 0.1f); | |
| } | |
| protected override void OnUpdate(float dt, Vector2 movingDir, float advanceScale, Vector3 displ) | |
| { | |
| _gaitCfg = Outer.GetBlendedConfig(Animator.gaitBlendOverwrite); | |
| if (Global.dbgOverlay.IsSubscribed(DebugOverlayChannel.Gait)) | |
| Global.dbgOverlay.Log($"gaitBlend:{Outer.EvaluateGaitBlendT(Animator.gaitBlendOverwrite)}"); | |
| } | |
| IEnumerator<float> WalkRoutine() | |
| { | |
| bool isFirstStep = true; | |
| while (true) | |
| { | |
| _gaitCfg = Outer.GetBlendedConfig(Animator.gaitBlendOverwrite); | |
| foreach (var foot in foots) | |
| foot.groundAbsWeight = _gaitCfg.groundAbsWeight; | |
| if (isFirstStep) | |
| { | |
| _currentFootIndex = SelectMovingFoot(); | |
| isFirstStep = false; | |
| } | |
| // 현재 발, 반대 발 | |
| var movingFoot = foots[_currentFootIndex]; | |
| var standingFoot = foots[1 - _currentFootIndex]; | |
| // D.Log($"move {_currentFootIndex}, stand {1 - _currentFootIndex}"); | |
| coStand = Timing.RunCoroutine(RoutineStandingFoot(standingFoot, _gaitCfg), Animator.FootAnimCoroutineTag); | |
| coSwing = Timing.RunCoroutine(RoutineSwingFoot(movingFoot, _gaitCfg), Animator.FootAnimCoroutineTag); | |
| coBodyMove = Timing.RunCoroutine(RoutineBodyMove(_gaitCfg), Animator.FootAnimCoroutineTag); | |
| coSetVisual = Timing.RunCoroutine(RoutineSetVisual(movingFoot, standingFoot, _gaitCfg), Animator.FootAnimCoroutineTag); | |
| // coDbg = Timing.RunCoroutine(RoutineDebug(_gaitCfg), Animator.FootAnimCoroutineTag); | |
| yield return Timing.WaitUntilDone(coSwing); | |
| while (standingFoot.landPose) | |
| yield return Timing.WaitForOneFrame; // 아직 발에 땅이 떼지기 전에는 | |
| if (coStand.IsInitialized) | |
| Timing.KillCoroutinesSafe(ref coStand); | |
| // 발 교체 | |
| _currentFootIndex = 1 - _currentFootIndex; | |
| } | |
| } | |
| IEnumerator<float> RoutineSwingFoot(FootBone foot, FootGaitConfig cfg) | |
| { | |
| float dirSign = Animator.DirSign; | |
| movingFootVisualTracking.Check(0); | |
| if (foot.dbgCycle) | |
| D.Log($"[{foot.Index},{Time.frameCount}] swing start"); | |
| //초기화 (와리가리할 때 이전 모션 남아있음) | |
| foot.MoveDelta(Vector3.zero, 0.1f, FootPosLayer.PushLift); | |
| foot.MoveDelta(Vector3.zero, 0.1f, FootPosLayer.PushBack); | |
| var reachForward = foot.MoveForward(cfg.reachForward * dirSign, cfg.interval * cfg.swingReachRatio, FootPosLayer.Base, cfg.reachForwardEase); | |
| co3 = RunDelayScaled(cfg.reachRotateDelay, () => | |
| { | |
| foot.SetRotationDelta(cfg.reachRotate, FootRotLayer.Base, isPlaceholder:true); | |
| }); | |
| while (!reachForward.IsDone) | |
| { | |
| float t = reachForward.Progress01; | |
| float lift = cfg.reachLiftCurve.Evaluate(t) * cfg.reachLiftHeight; | |
| foot.SetLayerOffset(FootPosLayer.SwingLift, new Vector3(0, 0, -lift)); | |
| yield return Timing.WaitForOneFrame; | |
| } | |
| // 2. 발 내리기 (Land) -> 지형 높이 고려 | |
| if (foot.dbgCycle) | |
| D.Log($"[{foot.Index},{Time.frameCount}] swing land"); | |
| FootGroundInfo ground = foot.GetGroundInfo(); | |
| var landMotionTime = cfg.interval * (1 - cfg.swingReachRatio); | |
| var landMotion = foot.MoveForward(cfg.landForward * dirSign, landMotionTime, FootPosLayer.Base, cfg.landForwardEase, true); | |
| foot.MoveDelta(Vector3.zero, landMotionTime, FootPosLayer.SwingLift, cfg.landLiftSettleEase); | |
| foot.SetRotationDelta(0f, landMotionTime,FootRotLayer.Base, cfg.landRotateSettleEase, isPlaceholder:true); | |
| while (!landMotion.IsDone) | |
| { | |
| yield return Timing.WaitForOneFrame; | |
| } | |
| coSwing = default; | |
| } | |
| IEnumerator<float> RoutineStandingFoot(FootBone foot, FootGaitConfig cfg) | |
| { | |
| float dirSign = Animator.DirSign; | |
| standingFootVisualTracking.Check(0); | |
| if (foot.dbgCycle) | |
| D.Log($"[{foot.Index},{Time.frameCount}] standing start"); | |
| float visElapsed = 0f; | |
| if (!foot.WillLandPoseOrLandPosed) | |
| { | |
| var m = foot.MoveForward(0, 0.03f, FootPosLayer.Base, toLand:true); | |
| yield return WaitUntil(() => m.IsDone); | |
| } | |
| var moveBackSpeed = (cfg.stanceHoldBackDistance / (cfg.interval * cfg.stanceHoldRatio)); | |
| bool shouldPush = false; | |
| float holdTime = cfg.stanceHoldRatio * cfg.interval; | |
| if (holdTime > 0f) | |
| { | |
| float elapsed = 0f; | |
| while (elapsed < holdTime) | |
| { | |
| elapsed += ScaledDt; | |
| visElapsed += ScaledDt; | |
| shouldPush = ShouldPush(); | |
| if (shouldPush) | |
| break; | |
| var backDir = -new Vector2(foot.FlipDirX, 0) * Animator.DirSign; | |
| foot.MoveLayerOffset(FootPosLayer.Base, backDir.xy0() * moveBackSpeed * Timing.DeltaTime); | |
| yield return Timing.WaitForOneFrame; | |
| } | |
| } | |
| visElapsed = holdTime; //중간 탈출 처리 | |
| standingFootVisualTracking.Check(visElapsed / cfg.interval); | |
| // foot.SetVisual(FootVisualType.Step, visElapsed / cfg.interval); | |
| while (NotOkToPush()) | |
| { | |
| var backDir = -new Vector2(foot.FlipDirX, 0) * Animator.DirSign; | |
| foot.MoveLayerOffset(FootPosLayer.Base, backDir.xy0() * moveBackSpeed * Timing.DeltaTime); | |
| foot.SetVisual(FootVisualType.Step, visElapsed / cfg.interval); | |
| visElapsed += ScaledDt; | |
| // D.Log("Not ok to swing yet"); | |
| yield return Timing.WaitForOneFrame; | |
| } | |
| bool NotOkToPush() // 시간이 다했음에도 발을 떄지 말아야하는 경우를 위한 체크 (아직 발이 충분히 뒤로 가지 않은 경우임) | |
| { | |
| foot.GetLegCalcInfo(out _, out _, out float angle0); | |
| return angle0 * Mathf.Rad2Deg * dirSign > -cfg.pushNotOkAngle; | |
| } | |
| bool ShouldPush()//시간다하기 전에 뛰어야하는 케이스 (각도 어색 문제 방지용) | |
| { | |
| foot.GetLegCalcInfo(out _, out _, out float angle0); | |
| return angle0 * Mathf.Rad2Deg * dirSign < -cfg.pushShouldAngle; | |
| } | |
| if (foot.dbgCycle) | |
| D.Log($"[{foot.Index},{Time.frameCount}] standing kick"); | |
| foot.CancelMove(FootPosLayer.Base); // isGrounded를 세팅해버리기 때문에 멈추기 | |
| foot.SetRotationDelta(cfg.pushRotate, FootRotLayer.Base, isPlaceholder:true); | |
| foot.UnsetLandPose(); | |
| foot.MoveDelta(new Vector3(0, 0, -cfg.pushLiftHeight), cfg.pushLiftTime, FootPosLayer.PushLift, cfg.pushEase); // 발 약간 올리기 | |
| foot.MoveForward(-cfg.pushBackDistance * dirSign, cfg.pushBackTime, FootPosLayer.PushBack, cfg.pushEase); // 발 뒤로 밀기 | |
| yield return WaitScaledSeconds(cfg.pushSettleDelay); | |
| foot.MoveDelta(Vector3.zero, cfg.pushLiftSettleTime, FootPosLayer.PushLift); // 발 내리기 | |
| foot.MoveForward(0f, cfg.pushBackSettleTime, FootPosLayer.PushBack); // 발 멈추기 | |
| foot.SetRotationDelta(0f, cfg.pushRotateSettleTime, FootRotLayer.Base, isPlaceholder:true); | |
| foot.MoveForward(cfg.pushBackSettleForward * dirSign, cfg.pushBackSettleTime, FootPosLayer.Base);//약간앞전진 | |
| coStand = default; | |
| } | |
| IEnumerator<float> RoutineBodyMove(FootGaitConfig cfg) | |
| { | |
| float elapsed = 0f; | |
| var def = Animator.Owner.shape.frontSortingGroupPos.def; | |
| while (elapsed < cfg.interval) | |
| { | |
| if (bodyMoveRatio < 1) | |
| bodyMoveRatio += ScaledDt / cfg.bodyMoveRatioRecoverTime; | |
| float t = elapsed / cfg.interval - cfg.bodyMoveStartOffsetRatio; | |
| if (t < 0) | |
| t += 1; | |
| float moveAmount = cfg.bodyMoveCurve.Evaluate(t) * cfg.bodyMoveScale * bodyMoveRatio; | |
| def.Set(DefinitePositionKey.WalkAnimation, new Vector3(0, moveAmount, 0)); | |
| elapsed += ScaledDt; | |
| gaitProgress = elapsed / cfg.interval; | |
| yield return Timing.WaitForOneFrame; | |
| } | |
| coBodyMove = default; | |
| } | |
| int SelectMovingFoot() | |
| { | |
| Span<float> footForwardAngles = stackalloc float[2]; | |
| for (int i = 0; i < 2; i++) | |
| { | |
| foots[i].GetLegCalcInfo(out Vector3 legOrigin, out float tension, out float legAngle); | |
| footForwardAngles[i] = legAngle; | |
| } | |
| int ret = footForwardAngles[0] < footForwardAngles[1] ? 0 : 1; | |
| if (Animator.DirSign < 0) | |
| ret = 1 - ret; | |
| return ret; | |
| } | |
| IEnumerator<float> RoutineSetVisual(FootBone movingFoot, FootBone standingFoot, FootGaitConfig cfg) | |
| { | |
| float ela = 0f; | |
| while (true) | |
| { | |
| ela += ScaledDt; | |
| if (ela >= cfg.interval) | |
| break; | |
| float movingPr = movingFootVisualTracking.GetProgress(cfg.interval); | |
| float standingPr = standingFootVisualTracking.GetProgress(cfg.interval); | |
| movingFoot.SetVisual(FootVisualType.GaitSwing, Mathf.Clamp01(movingPr)); | |
| standingFoot.SetVisual(FootVisualType.Step, Mathf.Clamp01(standingPr)); | |
| yield return Timing.WaitForOneFrame; | |
| } | |
| coSetVisual = default; | |
| } | |
| IEnumerator<float> RoutineDebug(FootGaitConfig cfg) | |
| { | |
| float ela = 0f; | |
| while (true) | |
| { | |
| ela += ScaledDt; | |
| Global.dbgOverlay.Log($"t : {ela / cfg.interval}"); | |
| if (ela >= cfg.interval) | |
| break; | |
| yield return Timing.WaitForOneFrame; | |
| } | |
| coDbg = default; | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using Sirenix.OdinInspector; | |
| using UnityEngine; | |
| using UnityEngine.Serialization; | |
| [Serializable, Frostory.AutoLerp.AutoLerp] | |
| public partial class FootGaitConfig | |
| { | |
| [Header("General Settings")] | |
| public float interval; | |
| public float groundAbsWeight = 0.7f; | |
| public CustomLegInfo customLegInfo; | |
| [Header("Swing Reach")] | |
| public float swingReachRatio = 0.85f; | |
| public float reachForward = 0.2f; | |
| public EaseSetting reachForwardEase; | |
| public float reachLiftHeight; | |
| public AnimationCurve reachLiftCurve = new AnimationCurve(new Keyframe(0, 0), new Keyframe(0.5f, 1), new Keyframe(1, 0)); | |
| public float reachRotateDelay = 0.05f; | |
| public FootRotateMotion reachRotate; | |
| [Header("Swing Land")] | |
| public float landForward = 0.16f; | |
| public EaseSetting landForwardEase; | |
| public EaseSetting landLiftSettleEase; | |
| public EaseSetting landRotateSettleEase; | |
| [Header("Stance Hold")] | |
| public float stanceHoldRatio = 0.5f; | |
| public float stanceHoldBackDistance = 0; // 기본적으로 groundAbsWeight에 의해 밀리지만, 추가적으로 밀고싶을 때 | |
| [Header("Stance Push")] | |
| public EaseSetting pushEase; | |
| public float pushLiftHeight = 0.05f; // +값(적용은 -Z) | |
| public float pushLiftTime = 0.1f; | |
| public float pushLiftSettleTime = 0.03f; | |
| public float pushBackTime = 0.03f; | |
| public float pushBackDistance = 0.05f; // +값(적용은 -forward) | |
| public float pushSettleDelay = 0.2f; | |
| public float pushBackSettleTime = 0.1f; | |
| public float pushBackSettleForward = -0.1f; | |
| public FootRotateMotion pushRotate; | |
| public float pushRotateSettleTime = 0.1f; | |
| [Header("Body Move")] | |
| public float bodyMoveRatioRecoverTime = 0.1f; | |
| public float bodyMoveStartOffsetRatio = -0.3f; // 음수로 offset세팅하기 | |
| public AnimationCurve bodyMoveCurve = new AnimationCurve(); | |
| public float bodyMoveScale = 0.05f; | |
| [FormerlySerializedAs("pushOkAngle")] [Header("ETC")] | |
| public float pushNotOkAngle = 20f; // 아직 변위가 다하지 못했을 때 기다려주는 최소값 | |
| public float pushShouldAngle = 45f; // 변위가 다했을 때 발을 때는 각도 | |
| [Button] | |
| public void ScaleTimes(float factor) | |
| { | |
| interval *= factor; | |
| reachRotateDelay *= factor; | |
| reachRotate.time *= factor; | |
| pushLiftTime *= factor; | |
| pushLiftSettleTime *= factor; | |
| pushBackTime *= factor; | |
| pushSettleDelay *= factor; | |
| pushBackSettleTime *= factor; | |
| pushRotate.time *= factor; | |
| pushRotateSettleTime *= factor; | |
| bodyMoveRatioRecoverTime *= factor; | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment