본문 바로가기

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

[크래프톤 정글 게임랩] 로그라이트 배 작살 게임으로 변신!

 프로젝트명: Kraken Hunter

장르: 탑다운 슈팅 로그라이크
개발 기간: 3일
플랫폼: PC
개발 인원: 4명
역할: 기획 / 프로그래밍
기술 스택: Unity (C#)


프로젝트 개요

입학시험 과제였던 “Cannon for Ship”을 베이스로 발전시킨 로그라이크 슈팅 게임.
배의 이동 조작감을 개선하고, 대포 대신 작살(Harpoon) 을 사용하여
보다 정교하고 리스크 있는 전투를 구현했다.

180초 버티기 → 보스 사냥 구조로 확장,
보스 몬스터 “Kraken”을 처치하거나, 실패 시 재화를 이용해
무기를 강화하며 재도전할 수 있는 순환형 플레이 루프를 구축했다.


 핵심 시스템

 조작

  • WASD: 이동
  • 마우스 좌클릭: 작살 발사
  • 게임오버 시: 상점으로 이동

 전투 구조

  • 작살 발사 → 충돌 → 회수
    • 투사체는 클릭 지점까지 도달하거나 장애물/적과 충돌 시 회수 시작
    • 투척 거리와 회수 시간은 비례, 업그레이드를 통해 단축 가능
    • 발사 가능한 작살 개수 제한 존재

 미끼 시스템

  • 보스 출현 전 체력 개념으로 사용
  • 플레이어는 일정 시간 동안 미끼(고래 사체)를 지켜야 함
  • 미끼가 모두 파괴되면 보스 등장 실패 및 게임 오버

 적 및 보스

  • 상어: 플레이어를 추적, 처치 시 재화(Shark Fin) 제공
  • 크라켄: 일정 시간 후 등장, 촉수로 근접 공격
    • 미끼가 존재하지 않으면 등장하지 않음

환경 요소

  • 섬: 무작위로 생성되어 투사체 경로를 방해
  • 토네이도: 일정 시간 동안 플레이어를 중심으로 회전시켜 이동 제한

 상점 및 강화 시스템

  • 재화(Shark Fin) 를 사용해 작살 시스템을 업그레이드
  • 상점 UI를 작살로 직접 공격하여 구매하는 독특한 상호작용 방식
  • 업그레이드 항목:
    • 발사 가능한 작살 개수 증가
    • 작살 회수 시간 단축

 개발 포인트

  • 단 3일 만에 완성된 Cannon for Ship의 진화판
  • 간단한 생존형 구조를 보스 레이드 + 성장형 루프로 발전시킴
  • 무작위 환경과 업그레이드 요소를 통해 로그라이크 구조 완성
  • 작살 투사체 회수, 미끼 보호, 상점 인터랙션 등 다층적 시스템 설계

 성과 및 평가

  • 짧은 개발 기간 내 완성도 높은 구조와 아트 콘셉트로 팀 내 호평
  • “1일 입학 과제를 3일 팀 프로젝트로 확장시킨 사례” 로 높은 평가
  • 캐주얼 슈팅에서 로그라이크 구조로 발전시키며 기획력과 시스템 설계 역량 입증

 느낀 점

“Kraken Hunter”는 내가 처음으로
기존 게임을 발전시켜 새로운 구조를 설계한 프로젝트였다.

짧은 기간 동안 기획–구현–테스트 전 과정을 반복하면서,
‘기존 시스템을 어떻게 더 깊게 만들 수 있을까’ 를 고민하게 되었다.
특히 팀원들과 역할을 나누어 완성도 있는 결과물을 낸 경험은
협업의 즐거움과 설계의 중요성을 동시에 느끼게 한 프로젝트였다.

 

게임오버 시 상점으로 이동
WASD(이동), 마우스 좌클릭(작살 발사)

 

최종 보스(크라켄)

public class PlayerMove : MonoBehaviour
{
    public float maxSpeed = 5f; // 최대 속도
    public float acceleration = 3f; // 가속도
    public float turnSpeed = 100f; // 회전 속도 (높을수록 빠르게 회전)

    private Rigidbody2D rb;
    private float moveInput;
    private float turnInput;
    private float deceleration = 20f; // 감속도

    void Start()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    void Update()
    {
        // 입력 받기
        moveInput = Input.GetAxis("Vertical"); // W(1) / S(-1)
        turnInput = Input.GetAxis("Horizontal"); // A(-1) / D(1)
    }

    void FixedUpdate()
    {
        // 현재 속도 가져오기
        float currentSpeed = Vector2.Dot(rb.linearVelocity, transform.up);

        // 가속 및 감속 처리
        if (moveInput != 0)
        {
            float targetSpeed = moveInput * maxSpeed;
            float speedDiff = targetSpeed - currentSpeed;
            float accelerationRate = (moveInput > 0) ? acceleration : deceleration;
            float movementForce = speedDiff * accelerationRate;
            rb.AddForce(transform.up * movementForce, ForceMode2D.Force);

            if (Mathf.Abs(currentSpeed) < maxSpeed || Mathf.Sign(targetSpeed) != Mathf.Sign(currentSpeed))
            {
                rb.AddForce(transform.up * movementForce, ForceMode2D.Force);
            }
        }
        else
        {
            rb.angularVelocity = 0;
            // 감속 시 관성을 줄이기 위한 감속 처리
            rb.linearVelocity = Vector2.Lerp(rb.linearVelocity, Vector2.zero, deceleration * Time.fixedDeltaTime);
            print(deceleration);
        }

        // 속도 제한 적용
        if (rb.linearVelocity.magnitude > maxSpeed)
        {
            rb.linearVelocity = rb.linearVelocity.normalized * maxSpeed;
        }

        //// 회전 처리
        //float turnAmount = turnInput * turnSpeed * Time.fixedDeltaTime;
        //rb.AddTorque(-turnAmount, ForceMode2D.Force);

        // 회전 처리
        float turnAmount = turnInput * turnSpeed * Time.deltaTime; // 속도에 따라 회전량 조절
        transform.Rotate(Vector3.forward, -turnAmount); // Z축 기준으로 회전
    }
}

 

공격 로직:

작살 쏘는 스크립트

public class PlayerAttack : MonoBehaviour
{
    public GameObject bullet;
    public CameraController cameraController;
    public int attackCount;
    private int maxAttackCount = 1;

    void Start() 
    {
        cameraController = Camera.main.GetComponent<CameraController>();

        maxAttackCount += StateManager.Instance.SpearCount;
        attackCount = maxAttackCount;
    }
    void Update()
    {

        // 마우스 클릭 시 이동 시작
        if (Input.GetMouseButtonDown(0) && attackCount > 0)
        {
            AttackCountDown();
            GameObject bulletObj = Instantiate(bullet, transform.position,Quaternion.Euler(transform.position - Camera.main.ScreenToWorldPoint(Input.mousePosition)));
            bulletObj.GetComponent<Spear>().targetPosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);

            if(cameraController != null)
                StartCoroutine(cameraController.ShakeCamera());
        }
    }

    public void AttackCountUp()
    {
        attackCount += 1;
    }

    public void AttackCountDown()
    {
        if (attackCount != 0)
        {
            attackCount -= 1;
        }
    }
}

 

작살 스크립트

public class Spear : MonoBehaviour
{
    public float speed = 6f; // 작살 발사 속도
    public float returnSpeed = 6f; // 작살이 돌아올 때 속도
    public Vector3 targetPosition; // 목표 위치 (클릭한 위치)
    public Vector3 startPosition; // 발사 시작 위치
    public GameObject tail;    // 작살과 플레이어를 연결하는 줄(LineRenderer)
    public float acceleration = 2f; // 가속도 (사용되지 않음)

    private float currentSpeed = 0f; // 현재 속도 (가속도용)
    private Vector3 originalScale;   // 원래 스케일 값
    private bool isMoving = false;   // 목표로 향하는 중인지 여부
    private bool isReturn = false;   // 되돌아오는 중인지 여부
    private float reloadingTime;
    private Bbb10311031_PlayerAttack _playerAttack;

    GameObject enemy;
    GameObject playerObj;

    Transform[] points = new Transform[2];

    private void Awake()
    {
        playerObj = GameObject.FindGameObjectWithTag("Player");
        _playerAttack = playerObj.GetComponent<Bbb10311031_PlayerAttack>();
    }

    void Start()
    {
        startPosition = transform.position;
        targetPosition = new Vector3(targetPosition.x, targetPosition.y, 0f); // 3D 좌표를 2D로 변환
        returnSpeed = returnSpeed * StateManager.Instance.ReloadingTime(); // 재장전 속도 보정
        SetTail();

        isMoving = true; // 발사 시작
    }

    void SetTail()
    {
        points[0] = playerObj.transform;
        points[1] = gameObject.transform;
        tail.GetComponent<LineController>().SetUpLine(points);
    }

    void Update()
    {
        if (isMoving)
        {
            transform.up = (targetPosition - startPosition).normalized; // 작살 진행 방향 조정

            // 목표 지점까지 이동
            transform.position = Vector3.Lerp(transform.position, targetPosition, speed * Time.deltaTime);

            // 목표 지점에 도달 시 처리
            if (Vector3.Distance(transform.position, targetPosition) <= 0.1f)
            {
                if (enemy != null)
                {
                    StateManager.Instance.CoinPlus();
                    //SoundManager.instance.PlaySFX("Clash"); // 충돌 사운드
                }
                else
                {
                    //SoundManager.instance.PlaySFX("SmallCanon"); // 빗나감 사운드
                }
                isReturn = true; // 돌아오는 상태로 전환
                isMoving = false;
                GetComponent<CapsuleCollider2D>().enabled = false;
            }
        }
        else if (isReturn && playerObj != null)
        {
            // 되돌아올 때 방향 보정
            transform.up = Vector3.Lerp(transform.up, (targetPosition - playerObj.transform.position).normalized, Time.deltaTime);
            transform.position = Vector3.MoveTowards(transform.position, playerObj.transform.position, returnSpeed * Time.deltaTime);

            SpearRotation();

            // 플레이어에게 도착 시
            if (Vector3.Distance(transform.position, playerObj.transform.position) < 0.1f)
            {
                _playerAttack.AttackCountUp();
                Destroy(gameObject);
            }
        }
    }

    void OnTriggerEnter2D(Collider2D other)
    {
        if (other.CompareTag("Enemy") && isMoving) // 적과 충돌 시
        {
            StateManager.Instance.CoinPlus();
            Destroy(other.gameObject);
            ReturnStart();

            // (임시 처리) 특정 적과 충돌 시 상점 이동
            if (other.GetComponent<KrakenMove>() != null)
                GameManager.Instance.GoShopScene();
        }
        if (other.CompareTag("Obstacle") && isMoving) // 장애물 충돌 시
        {
            ReturnStart();
        }
        if (other.CompareTag("Boss") && isMoving) // 보스와 충돌 시
        {
            GameManager.Instance.DamagedBossHP(2); // 보스 체력 감소
            ReturnStart();
        }
    }

    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.CompareTag("Enemy") && isMoving) // 적과 충돌 시
        {
            Destroy(collision.gameObject);
            ReturnStart();
        }
        if (collision.gameObject.CompareTag("Obstacle") && isMoving) // 장애물 충돌 시
        {
            ReturnStart();
        }
    }
    
    void SpearRotation()
    {
        Vector2 direction = transform.position - playerObj.transform.position;
        float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
        transform.rotation = Quaternion.AngleAxis(angle - 90, Vector3.forward);
    }

    // 돌아오는 상태로 전환 (충돌 시에도 호출)
    void ReturnStart()
    {
        isMoving = false;
        isReturn = true;
        GetComponent<CapsuleCollider2D>().enabled = false;
    }
}

 

작살 공격 시 화면 흔들림 구현

public class CameraController : MonoBehaviour
{
    public float shakeDuration = 0.3f; // 흔들리는 지속 시간
    public float shakeMagnitude = 0.2f; // 흔들림 강도

    Vector3 originalPosition;
    [field: SerializeField] public Vector2 ScreenArea { get; private set; } // 화면 크기
    void Start()
    {
        originalPosition = transform.position;

        // 카메라의 화면 경계를 월드 좌표로 변환
        ScreenArea = Camera.main.ScreenToWorldPoint(new Vector3(Screen.width, Screen.height, transform.position.z));
    }

    // 카메라 화면 흔들기
    public IEnumerator ShakeCamera()
    {
        float elapsedTime = 0f;

        while (elapsedTime < shakeDuration)
        {
            float offsetX = Random.Range(-1f, 1f) * shakeMagnitude;
            float offsetY = Random.Range(-1f, 1f) * shakeMagnitude;

            Camera.main.transform.position = originalPosition + new Vector3(offsetX, offsetY, 0);

            elapsedTime += Time.deltaTime;
            yield return null;
        }

        Camera.main.transform.position = originalPosition; // 원래 위치로 복귀
    }
}