2026-05-22 보스스킬 : 지진
This commit is contained in:
BIN
Assets/01_Scenes/BossScene.unity
LFS
BIN
Assets/01_Scenes/BossScene.unity
LFS
Binary file not shown.
43
Assets/02_Scripts/Enemy/Skills/EarthquakeSkill.cs
Normal file
43
Assets/02_Scripts/Enemy/Skills/EarthquakeSkill.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System.Threading;
|
||||
using UnityEngine;
|
||||
|
||||
public class EarthquakeSkill : BossSkill
|
||||
{
|
||||
[Header("EarthquakeSkill")]
|
||||
[SerializeField] private float _windup = 0.5f; // 애니 시작 ~ 피해 판정 ON
|
||||
[SerializeField] private float _activeDuration = 1.5f; // 피해 판정 유지 시간
|
||||
[SerializeField] private float _recovery = 0.6f; // 판정 OFF 후 후딜
|
||||
[SerializeField] private int _damage = 15;
|
||||
[SerializeField] private float _liftForce = 0.5f; // 지면 피격 시 띄우는 세기
|
||||
|
||||
protected override async Awaitable RunSkill(CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 선딜
|
||||
await Awaitable.WaitForSecondsAsync(_windup, token);
|
||||
|
||||
Earthquake();
|
||||
|
||||
// 후딜
|
||||
await Awaitable.WaitForSecondsAsync(_recovery, token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void Earthquake()
|
||||
{
|
||||
PlayerController player = GameManager.Instance.LocalPlayer;
|
||||
if (player == null) return;
|
||||
|
||||
// 플레이어가 지면에 닿아 있을 때만 발동. 공중이면 아무 효과 없음.
|
||||
if (!player.IsGrounded) return;
|
||||
|
||||
// 지면 접촉 중: 살짝 띄우면서 데미지.
|
||||
player.Launch(new Vector2(0f, _liftForce));
|
||||
player.TakeDamage(_damage);
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/Enemy/Skills/EarthquakeSkill.cs.meta
Normal file
2
Assets/02_Scripts/Enemy/Skills/EarthquakeSkill.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d27a723f9206afc49a060c2875ccf3dc
|
||||
@@ -19,14 +19,10 @@ public class HazardSkill : BossSkill
|
||||
[SerializeField] private GameObject _hazardHitboxOrigin;
|
||||
[SerializeField] private int _hitBoxDamage = 15;
|
||||
|
||||
private SkillSupport support;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (_animator == null)
|
||||
_animator = GetComponentInChildren<Animator>();
|
||||
|
||||
support = Object.FindFirstObjectByType<SkillSupport>();
|
||||
}
|
||||
|
||||
protected override async Awaitable RunSkill(CancellationToken token)
|
||||
@@ -64,12 +60,7 @@ protected override async Awaitable RunSkill(CancellationToken token)
|
||||
|
||||
private void PredictHazards(int dirChoice)
|
||||
{
|
||||
if(support == null)
|
||||
{
|
||||
Debug.Log("Null이니");
|
||||
}
|
||||
|
||||
SkillPredict[] predicts = dirChoice switch { 0 => support.DownHazardPredicts, 1=> support.RightHazardPredicts, _=> null};
|
||||
SkillPredict[] predicts = dirChoice switch { 0 => GameManager.Instance.SkillSupporter.DownHazardPredicts, 1=> GameManager.Instance.SkillSupporter.RightHazardPredicts, _=> null};
|
||||
|
||||
for (int i = 0; i < predicts.Length; i++)
|
||||
{
|
||||
@@ -80,7 +71,7 @@ private void PredictHazards(int dirChoice)
|
||||
|
||||
private void FireHazards(int dirChoice)
|
||||
{
|
||||
Transform[] hazardPoss = dirChoice switch { 0 => support.DownHazardFirePos, 1=> support.RightHazardFirePos, _=> null};
|
||||
Transform[] hazardPoss = dirChoice switch { 0 => GameManager.Instance.SkillSupporter.DownHazardFirePos, 1=> GameManager.Instance.SkillSupporter.RightHazardFirePos, _=> null};
|
||||
Vector2 attackDir = dirChoice switch {0 => Vector2.down,1=>Vector2.right,_=>Vector2.left};
|
||||
|
||||
if (hazardPoss == null) return;
|
||||
|
||||
@@ -8,7 +8,12 @@ public class SkillSupport : MonoBehaviour
|
||||
[HideInInspector] public SkillPredict[] DownHazardPredicts;
|
||||
[HideInInspector] public SkillPredict[] RightHazardPredicts;
|
||||
|
||||
void Start()
|
||||
private void Awake()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
GameObject[] shineDownStartPoss = GameObject.FindGameObjectsWithTag("ShineDownStartPos");
|
||||
GameObject[] shineRightStartPoss = GameObject.FindGameObjectsWithTag("ShineRightStartPos");
|
||||
|
||||
29
Assets/02_Scripts/Managers/GameManager.cs
Normal file
29
Assets/02_Scripts/Managers/GameManager.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class GameManager : MonoBehaviour,ISceneInitializable
|
||||
{
|
||||
public static GameManager Instance;
|
||||
|
||||
public SkillSupport SkillSupporter {get; private set;}
|
||||
public PlayerController LocalPlayer {get; private set;}
|
||||
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this; //만들어진 자신을 인스턴스로 설정
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject); //이미 인스턴스가 있으면 자신을 파괴
|
||||
}
|
||||
}
|
||||
|
||||
public void OnSceneLoaded()
|
||||
{
|
||||
Debug.Log("aaaa");
|
||||
SkillSupporter = Object.FindFirstObjectByType<SkillSupport>();
|
||||
LocalPlayer = Object.FindFirstObjectByType<PlayerController>();
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/Managers/GameManager.cs.meta
Normal file
2
Assets/02_Scripts/Managers/GameManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5b2c0a8cf5c55bc4da93c75f61309de6
|
||||
@@ -31,19 +31,18 @@ private void Update()
|
||||
//씬이 로드되었을때 호출
|
||||
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
||||
{
|
||||
if(scene.name == "GameScene")
|
||||
{
|
||||
MonoBehaviour[] allObjs = UnityEngine.Object.FindObjectsByType<MonoBehaviour>(FindObjectsSortMode.None);
|
||||
|
||||
foreach (var obj in allObjs)
|
||||
MonoBehaviour[] allObjs = UnityEngine.Object.FindObjectsByType<MonoBehaviour>(FindObjectsSortMode.None);
|
||||
|
||||
foreach (var obj in allObjs)
|
||||
{
|
||||
if (obj is ISceneInitializable initializable)
|
||||
{
|
||||
if (obj is ISceneInitializable initializable)
|
||||
{
|
||||
//씬에서 ISceneInitializable 인터페이스를 가진 오브젝트의 초기화 로직을 실행
|
||||
initializable.OnSceneLoaded();
|
||||
}
|
||||
//씬에서 ISceneInitializable 인터페이스를 가진 오브젝트의 초기화 로직을 실행
|
||||
initializable.OnSceneLoaded();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void SetSceneLoadingProgressValue(float value)
|
||||
|
||||
@@ -27,6 +27,13 @@ public class PlayerController : MonoBehaviour,IDamageable
|
||||
private string _activeBaseState; // 현재 재생 중인 locomotion State (중복 Play 방지용)
|
||||
private bool _isInActionAnimation; // 액션 애니메이션 재생 중인지 (locomotion 잠시 양보)
|
||||
|
||||
// 피격
|
||||
[Header("Hit Animation")]
|
||||
[SerializeField] private string _knockbackAnimationState = "Knockback";
|
||||
[SerializeField] private float _knockbackDuration = 1f; // 넉백 경직 시간 (Knockback 클립 길이에 맞춰 설정)
|
||||
private float _hitstunTimer; // 경직 남은 최소 시간
|
||||
private bool _isStunned; // 경직 상태 (공중이면 착지까지 연장)
|
||||
|
||||
// ─── 점프 단계별 애니메이션 ───────────────────────────────────────────
|
||||
// 점프를 4단계(Rise/Mid/Fall/Land)로 분리해서 vy에 따라 자동 전환.
|
||||
[Header("Jump Animation")]
|
||||
@@ -253,19 +260,21 @@ private void FixedUpdate()
|
||||
TickComboWindow(); // 콤보 윈도우 카운트다운
|
||||
|
||||
// 입력 잠금이 없고, (액션 중이 아니거나 액션이 이동을 허용할 때) 좌우 입력으로 velocity 갱신.
|
||||
if (!IsMovementLocked() && (!IsActionActive() || _actionAllowsMovement))
|
||||
// 경직 중에는 이동 입력을 무시한다 (Launch로 받은 속도 + 중력만 작용).
|
||||
if (!IsStunned() && !IsMovementLocked() && (!IsActionActive() || _actionAllowsMovement))
|
||||
{
|
||||
// 백페달이면 느린 속도. 액션 중에도 동일하게 적용 (이동사격 = Down 변형 + CanMoveDuringAction).
|
||||
float moveSpeed = IsBackpedaling() ? _backpedalSpeed : _moveSpeed;
|
||||
_rb.linearVelocity = new Vector2(_moveInputX * moveSpeed, _rb.linearVelocity.y);
|
||||
}
|
||||
|
||||
if (!IsFacingLocked() && (!IsActionActive() || _actionAllowsTurn))
|
||||
if (!IsStunned() && !IsFacingLocked() && (!IsActionActive() || _actionAllowsTurn))
|
||||
UpdateFacingFromMoveInput();
|
||||
|
||||
//중력적용
|
||||
ApplyGravity();
|
||||
ResolveKinematicCollisions();
|
||||
TickHitstun(); // 피격 경직 타이머 카운트다운
|
||||
|
||||
UpdateLocomotionAnimation();
|
||||
|
||||
@@ -307,7 +316,7 @@ private void OnMoveInput(Vector2 value)
|
||||
{
|
||||
_moveInputX = value.x == 0f ? 0f : Mathf.Sign(value.x);
|
||||
_moveInputY = value.y == 0f ? 0f : Mathf.Sign(value.y);
|
||||
if (_facingLockTimer <= 0f && (!IsActionActive() || _actionAllowsTurn))
|
||||
if (!IsStunned() && _facingLockTimer <= 0f && (!IsActionActive() || _actionAllowsTurn))
|
||||
UpdateFacingFromMoveInput();
|
||||
}
|
||||
|
||||
@@ -332,6 +341,8 @@ private bool IsBackpedaling()
|
||||
// 점프 우선순위: 지상 점프 > 공중(2단) 점프
|
||||
private void OnJumpInput()
|
||||
{
|
||||
if (IsStunned()) return; // 경직 중 점프 불가
|
||||
|
||||
if (_isGrounded)
|
||||
{
|
||||
PerformJump();
|
||||
@@ -374,6 +385,8 @@ private void OnGrabSmashInput()
|
||||
// 3) 그 외에는 즉시 ExecuteComboInput으로 발화
|
||||
private void HandleComboInput(ComboInputType input)
|
||||
{
|
||||
if (IsStunned()) return; // 경직 중 공격 불가
|
||||
|
||||
// 콤보 연계(현재 노드의 트랜지션) 가능 여부 — 연계는 애니메이션 중에도 즉시 캔슬-체인.
|
||||
bool canChain = CanTransitionFromCurrentNode(input);
|
||||
|
||||
@@ -509,6 +522,7 @@ private bool IsWeaponRequirementMet(ActionData data)
|
||||
|
||||
private void ExecuteMotionNode(ComboNode root)
|
||||
{
|
||||
if (IsStunned()) return; // 경직 중 대시/구르기 불가
|
||||
if (root == null || root.Action == null) return;
|
||||
if (IsMotionOnCooldown(root.Action)) return;
|
||||
|
||||
@@ -652,6 +666,7 @@ private async Awaitable MotionRoutine(ActionData data, CancellationToken token)
|
||||
|
||||
private async void PerformGroundPound()
|
||||
{
|
||||
if (IsStunned()) return; // 경직 중 그라운드 파운드 불가
|
||||
if (_groundPoundData == null) return;
|
||||
|
||||
CancelAttack();
|
||||
@@ -1531,7 +1546,9 @@ private bool TryGetGroundHit(float checkDistance, out RaycastHit2D closestHit)
|
||||
float inset = Mathf.Min(Mathf.Max(_groundCheckRayInset, 0f), bounds.extents.x);
|
||||
float leftX = bounds.min.x + inset;
|
||||
float rightX = bounds.max.x - inset;
|
||||
float originY = bounds.min.y;
|
||||
float originLift = bounds.size.y + Mathf.Max(_collisionSkinWidth, 0f) + 0.01f;
|
||||
float originY = bounds.min.y + originLift;
|
||||
float rayDistance = checkDistance + originLift;
|
||||
float closestDistance = float.PositiveInfinity;
|
||||
bool hasHit = false;
|
||||
|
||||
@@ -1541,7 +1558,7 @@ private bool TryGetGroundHit(float checkDistance, out RaycastHit2D closestHit)
|
||||
Vector2 origin = new Vector2(Mathf.Lerp(leftX, rightX, t), originY);
|
||||
|
||||
_castResults.Clear();
|
||||
int hitCount = Physics2D.Raycast(origin, Vector2.down, filter, _castResults, checkDistance);
|
||||
int hitCount = Physics2D.Raycast(origin, Vector2.down, filter, _castResults, rayDistance);
|
||||
for (int j = 0; j < hitCount; j++)
|
||||
{
|
||||
RaycastHit2D hit = _castResults[j];
|
||||
@@ -1695,6 +1712,64 @@ private void DrawTimelineBar(ActionData data, float elapsed)
|
||||
GUI.color = Color.white;
|
||||
}
|
||||
|
||||
// 외부(보스 스킬 등)에서 플레이어의 지면 접촉 여부를 조회.
|
||||
public bool IsGrounded => _isGrounded;
|
||||
|
||||
// 외부에서 플레이어를 강제로 띄우거나 밀어낼 때 사용 (지진 스킬 등).
|
||||
// 넉백 = 지정 속도로 띄우고 넉백 모션으로 경직시킨다. 키네마틱 RB라 속도 직접 설정.
|
||||
public void Launch(Vector2 velocity)
|
||||
{
|
||||
if (_rb == null) return;
|
||||
_rb.linearVelocity = velocity;
|
||||
EnterHitstun(_knockbackAnimationState, _knockbackDuration);
|
||||
}
|
||||
|
||||
// 피격 경직 진행 여부. 경직 중에는 이동/방향전환/점프/공격/대시 입력을 모두 잠근다.
|
||||
private bool IsStunned() => _isStunned;
|
||||
|
||||
// 피격 경직(범용). 지정 모션을 재생하고 duration초 동안 입력을 잠근다. 재호출 시 갱신(refresh).
|
||||
// 진행 중이던 플레이어 액션은 취소한다 (피격이 우선).
|
||||
// duration은 호출자가 모션 클립 길이에 맞춰 넘긴다 (넉백은 _knockbackDuration).
|
||||
// 일반 경직 / 넉백 등 종류별로 animationState만 바꿔 호출하면 된다.
|
||||
public void EnterHitstun(string animationState, float duration)
|
||||
{
|
||||
_isStunned = true;
|
||||
_hitstunTimer = duration; // 경직 시간 (재호출 시 갱신/refresh)
|
||||
|
||||
CancelAttack(); // 진행 중이던 공격/모션을 끊는다 (피격이 우선)
|
||||
CancelMotion();
|
||||
|
||||
// 경직 모션은 액션 애니메이션 취급 — _isInActionAnimation으로 locomotion이 못 덮게 막는다.
|
||||
if (_anim != null && !string.IsNullOrEmpty(animationState))
|
||||
{
|
||||
_isInActionAnimation = true;
|
||||
_activeBaseState = null;
|
||||
_anim.speed = 1f;
|
||||
_anim.Play(animationState);
|
||||
_anim.Update(0f);
|
||||
}
|
||||
}
|
||||
|
||||
// 경직 타이머 카운트다운. 클립을 다 재생했고(타이머 종료) + 착지했을 때만 경직을 해제한다.
|
||||
// 넉백으로 공중에 떠 있는 동안엔 경직/모션을 유지 → 점프 모션으로 안 끊긴다.
|
||||
private void TickHitstun()
|
||||
{
|
||||
if (!_isStunned) return;
|
||||
|
||||
if (_hitstunTimer > 0f)
|
||||
_hitstunTimer -= Time.fixedDeltaTime;
|
||||
|
||||
Debug.Log($"_hitstunTimer : {_hitstunTimer}, _isGrounded : {_isGrounded} ");
|
||||
|
||||
if (_hitstunTimer <= 0f && _isGrounded && !IsActionActive())
|
||||
{
|
||||
_isStunned = false;
|
||||
_isInActionAnimation = false;
|
||||
_activeBaseState = null;
|
||||
_hitstunTimer = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
public void TakeDamage(int amount, Vector2 hitVelocity = default, string hitReactionAnimationState = null, Vector2? hitTargetPosition = null, bool correctHitTargetY = false, int hitPositionSolidMask = 0, float hitPositionCorrectionDuration = 0, float hitStunDuration = -1f)
|
||||
{
|
||||
if (_health == null || _health.IsDead) return;
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,5 +1,5 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5ca885ea8137440448a536a8e408352c
|
||||
guid: e8e33019b52d4774fb6be20b28f1c2eb
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
BIN
Assets/08_Objects/BossSkills/EarthquakeSkill/EarthquakeSkill.prefab
LFS
Normal file
BIN
Assets/08_Objects/BossSkills/EarthquakeSkill/EarthquakeSkill.prefab
LFS
Normal file
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b6e71f45f6a6eb047a72d095e2960e73
|
||||
DefaultImporter:
|
||||
guid: 8206c9420439a754e9e9088c718c0902
|
||||
PrefabImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
BIN
Assets/_Recovery/0 (1).unity
LFS
BIN
Assets/_Recovery/0 (1).unity
LFS
Binary file not shown.
@@ -1,7 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 509cab3a1e54dbc4c880461706c573ee
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Assets/_Recovery/0.unity
LFS
BIN
Assets/_Recovery/0.unity
LFS
Binary file not shown.
Reference in New Issue
Block a user