본문 바로가기

프로젝트/크래프톤 정글 게임랩 3기

[크래프톤 정글 게임랩] 카타나 제로 + 크라켄

 프로젝트명: Kraken Hunter (Rework)

장르: 로그라이크, 핵 앤 슬래시, 탑뷰 액션
개발 기간: 7일
플랫폼: PC
개발 인원: 개인 프로젝트
역할: 기획 / 프로그래밍 / 아트
기술 스택: Unity (C#), Particle System, Trail Renderer, Cinemachine, FSM


 프로젝트 개요

이전 주의 Kraken Hunter를 기반으로,
“카타나 제로”의 핵심 재미요소(속도감·타격감·도전성) 를 Unity에서 재현한 개인 프로젝트.

불렛 타임(Bullet Time)패링(Parry), 히트 스탑(Hit Stop)
액션 게임의 정수를 직접 구현하여
속도감과 긴장감이 공존하는 핵 앤 슬래시 스타일의 로그라이크 게임으로 발전시켰다.


기획 의도 및 핵심 목표

“단순히 공격하는 게임이 아니라, 리듬과 감각으로 몰입시키는 전투를 만들자.”

  • 기존 Kraken Hunter의 슈팅 시스템을 근접 작살 전투 시스템으로 개편
  • 빠른 템포 속에서도 컨트롤의 여유를 주는 불렛 타임 시스템 구현
  • 보스 패턴을 다단계 FSM 구조(1페이즈 → 2페이즈) 로 발전시켜
    단조로운 반복 전투에서 벗어난 “패턴 읽기형 전투” 실현

 주요 시스템

 플레이어 조작

  • W, A, S, D: 이동
  • 마우스 좌클릭: 공격 (작살 휘두르기)
  • 대시 중 불렛타임 발동

 핵심 피처

기능설명
불렛 타임 (Bullet Time) 일정 시간 동안 게임 속도를 느리게 하여, 빠른 패턴을 인지하고 반응할 기회를 제공. 불릿타임 종료 후의 속도 대비로 속도감 극대화.
근접전 (Melee Combat) 원거리 작살 대신 근거리 휘두르기 방식으로 변경. 제한된 사거리 내에서 공격적 플레이를 유도.
원샷 원킬 (One Shot, One Kill) 보스를 제외한 모든 적은 한 방에 사망. 플레이어 역시 한 방에 죽음 → 리스크·보상 구조 강화.
히트 스탑 (Hit Stop) 공격 적중 시 짧은 시간 정지 + 카메라 셰이크 적용 → 타격감 상승.
패링 (Parry) 타이밍에 맞게 공격을 튕겨내며 역공 가능. 수동적 회피 대신 적극적 대응 유도.

 적 및 보스 설계

  • 일반 적: 단발성 공격 / 근접 접근 유도
  • 보스:
    • Phase 1: 기본 공격 패턴, 시선 유도 공격
    • Phase 2: 이동형 패턴 + 연속 공격 + 타이밍 패링 유도
  • FSM 기반으로 상태 전이(Idle → Attack → Pattern → Stun → Phase 2) 설계

 비주얼 연출

  • Trail Renderer: 작살 휘두름 궤적 연출로 액션 감각 강화
  • Particle System: 피격, 폭발, 피 튀김 등 타격 효과 표현
  • Cinemachine 카메라 셰이킹: 공격 타이밍에 맞춰 강한 반동 효과 제공
  • 히트 스탑과 함께 “멈춤 후 폭발”의 타격감을 구현

 핵심 재미 요소

  • 속도감 → 불렛 타임 전후 대비로 체감 강화
  • 타격감 → 히트 스탑 + 카메라 셰이크 + 트레일
  • 도전성 → 원샷원킬 구조 + 난이도 상승형 스테이지
  • 패턴 공략 → 보스의 다단계 FSM 구조를 읽어 대응

 느낀 점

“Kraken Hunter (Rework)”는
내가 ‘게임의 핵심 재미’를 분석하고 직접 재현해본 첫 실험 프로젝트였다.

  • 불렛타임·패링·히트스탑 같은 액션 핵심 피처를 실제로 구현하며
    게임의 감각적 완성도를 설계하는 법을 배웠고,
  • 코드로 감정을 전달하는 “리듬 있는 플레이”의 중요성을 깨달았다.

짧은 7일의 작업이었지만,
기획·아트·프로그래밍이 한 사람의 시점에서 완전히 통합된 액션 게임이었다.

 

 

=

 

W,A,S,D : 이동 좌클릭: 공격
대쉬 및 불렛타임

 

불렛타임
원샷원킬
근접전
패링

 

2

 

 

보스패턴1,2,3,4

 

 

 

---------------------------------------------

불렛타임 및 히트스탑

public class TimeManager : MonoBehaviour
{
    static TimeManager _instance;
    public static TimeManager Instance { get { return _instance; } private set { } }

    public float playTime = 0;
    public float bulletTimeGauge = 100;
    private bool isbulletTime;
    private bool waiting;

    private int hitNum = 0;
    private bool isClear;

    public Image bulletBackground;

    void Awake()
    {
        if (_instance == null)
        {
            _instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
            Destroy(gameObject);
    }

    private void Update()
    {
        if (!isClear)
        {
            playTime += Time.unscaledDeltaTime;
        } 
        BulletTime();
        if (hitNum > 0 && !waiting) WaitingHitStop(0.2f);
        
        if (bulletBackground != null)
        {
            if (isbulletTime)
            {
                bulletBackground.color = Color.Lerp(bulletBackground.color, new Color32(255, 255, 255, 10), Time.unscaledDeltaTime * 10);
            }
            else
            {
                bulletBackground.color = Color.Lerp(bulletBackground.color, new Color32(255, 255, 255, 0), Time.unscaledDeltaTime * 50);
            }
        }
        else
        {
            bulletBackground = GameObject.FindGameObjectWithTag("BulletTime").GetComponent<Image>();
        }
    }

    public void Dead()
    {
        bulletTimeGauge = 100;
        isbulletTime = false;
        Time.timeScale = 1f;
        Time.fixedDeltaTime = 0.02f; // 기본값 복구
    }

    public void BulletTime()
    {
        if (Input.GetKeyDown(KeyCode.Mouse1))
        {
            isbulletTime = true;
            Time.timeScale = 0.1f;
            Time.fixedDeltaTime = 0.02f * Time.timeScale; // 물리 연산을 부드럽게
        }
        if (Input.GetKeyUp(KeyCode.Mouse1))
        {
            isbulletTime = false;
            Time.timeScale = 1f;
            Time.fixedDeltaTime = 0.02f; // 기본값 복구
        }

        if (isbulletTime && bulletTimeGauge >= 0)
        {
            bulletTimeGauge -= Time.unscaledDeltaTime * 20;
        }
        else
        {
            if (Time.timeScale != 1f && isbulletTime)
            {
                isbulletTime = false;
                Time.timeScale = 1f;
                Time.fixedDeltaTime = 0.02f; // 기본값 복구
            }
            if (bulletTimeGauge <= 100)
            {
                bulletTimeGauge += Time.unscaledDeltaTime * 10;
            }
            else
            {
                bulletTimeGauge = 100;
            }
        }
    }

    public void HitStop(float duration)
    {
        hitNum += 1;
        //if (waiting)
        //    return;
        //waiting = true;
        //Time.timeScale = 0.0f;
        //StartCoroutine(Wait(duration));
    }

    void WaitingHitStop(float duration)
    {
        if (waiting)
            return;
        waiting = true;
        Time.timeScale = 0.0f;
        StartCoroutine(Wait(duration));
    }

    IEnumerator Wait(float duration)
    {
        print("1");
        Camera.main.GetComponent<CameraController>().StartShake(0.4f, 0.2f);
        yield return new WaitForSecondsRealtime(duration);
        Time.timeScale = isbulletTime ? 0.1f : 1.0f;
        hitNum -= 1;
        waiting = false;
    }

    public void Clear()
    {
        isClear = true;
    }
}

 

 

불렛패링

public class PirateBullet : MonoBehaviour
{
    public float speed;
    private Vector3 dir;
 
    public void SetDirection(Vector3 target)
    {
        dir = target.normalized;
    }
    // Update is called once per frame
    void Update()
    {
        transform.up = dir.normalized;
        transform.position += dir * speed * Time.deltaTime;
    }

    protected virtual void OnTriggerEnter2D(Collider2D other)
    {
        if (other.tag == "Spear")
        {
            Camera.main.GetComponent<CameraController>().StartShake(0.3f, 0.3f);
            dir *= -1;
            gameObject.tag = "PlayerBullet";
            speed = 50;

        }
        if (other.tag == "Player")
        {
            print("Player die");
            GameManager.Instance.GameOver();
            Destroy(gameObject);
        }
        if (other.tag == "Obstacle")
        {
            Destroy(gameObject);
        }
    }
}