LastSanctuary

이준영·2025년 9월 5일

주제 : 2D 액션 플랫포머 / 메트로배니아 / 소울라이크 게임
개발 기간 : 2025.06.21. ~ 2025.08.14. (약 8주)
개발 인원 : 기획자(2명), Unity(3명)
사용 기술 : FSM(Interface), ScriptableObject, Delegate
Github : https://github.com/leejy1685/LastSanctuary_Code
SA : LastSanctuary Draw.io

LastSanctuary

주제 선택 사유

  • 최종 프로젝트는 시간이 넉넉한 만큼 도전적인 프로젝트를 하고 싶었음.
  • 그러던 중 2D 액션 플랫포머 장르를 기획한 기획자에게 제안이 와서 프로젝트를 진행.
  • 참고 게임 : Blasphemous, Hollow Knight

기술 스택

FSM(Interface), ScriptableObject, Delegate

기술적 의사결정

FSM(Interface)

Player 구조도
  • 사용 이유 : 객체 지향 프로그래밍의 원칙을 잘 따르고, 유연하고 확장 가능한 시스템을 구축할 수 있기 때문
  1. 느슨한 결합

    • 인터페이스는 특정 클래스가 아닌 '기능'에 초점
    • 코드 수정 시 다른 부분에 미치는 영향을 최소화
  2. 확장성 및 재사용성

    • 새로운 상태를 추가하거나 기존 상태를 수정하기가 매우 용이
    • 다른 캐릭터나 심지어 적 캐릭터의 상태 관리에도 동일한 인터페이스를 재사용할 수 있게 하여 코드의 재사용성이 높음.
  3. 단일 책임 원칙

    • 각 상태 클래스는 '해당 상태에서의 로직'이라는 단일 책임
    • 코드의 가독성을 높이고 유지보수를 용이
  4. 구현 예시

  5. 트러블 슈팅

ScriptableObject

구조도
  • 사용 이유 : 게임의 데이터와 로직을 분리하여 효율적인 개발 환경을 구축하기 위함
  1. 데이터와 로직의 분리

    • MonoBehaviour 컴포넌트는 데이터(변수)와 로직(메서드)이 함께 존재
    • ScriptableObject는 데이터만 저장하는 컨테이너 역할
  2. 메모리 효율성

    • ScriptableObject는 프로젝트 내에 한 번만 로드되고, 모든 게임 오브젝트가 그 데이터를 참조해서 사용
    • ScriptableObject를 사용하면 모든 몬스터 A가 동일한 ScriptableObject를 참조하므로 메모리 사용량이 획기적으로 줄어듭니다.
  3. 재사용성 및 유연성

    • 하나의 ScriptableObject 에셋을 여러 곳에서 재사용
  4. 쉬운 유지보수와 워크플로우 개선

    • 에디터 내에서 에셋 파일로 존재하기 때문에, 직관적으로 데이터를 생성하고 관리
    • 개발 시간을 단축하고, 팀 협업을 용이
  5. 예시 코드
    - 코드 전체

    PlayerSO

Delegate

  • 사용 이유 : 객체 간의 결합도를 낮추고 유연성과 확장성을 확보하기 위함
  1. 느슨한 결합

    • 이벤트를 발생시키는 객체와 이벤트에 반응하는 객체는 서로 독립적
  2. 확장성 및 재사용성

    • 새로운 기능을 추가하거나 기존 기능을 변경할 때 매우 유연하게 대처
    • 개발 속도를 높이고 재사용성을 극대화
  3. 단일 책임 원칙

    • 델리게이트를 사용하면 각 클래스의 역할이 명확
    • 역할 분리는 코드를 더 간결하고 이해하기 쉽게 만들어 유지보수성을 크게 향상
  4. 구현 예시

  5. 트러블 슈팅

배운 점

Interface방식 FSM

코드 리펙토링 : 플레이어 공격 개선

  1. 개선 이유
    • 어차피 공격의 방식은 Weapon에 대미지 할당 방식인데, 자식 스크립트에서 전부 할당중.
    • 점프 어택은 콤보 어택1을 활용 중이나 인풋 방식 때문에 따로 구현되어 있음.
    • 콤보 어택은 이어지는 공격을 계속해서 다음 정보를 가져오는 방식임.
    • 기대 효과 : 코드 압축 및 가독성 증가
  2. 개선 방안
    • 점프 어택, 대쉬 어택 같은 후속타가 없는 것은 PlayerAttackState 스크립트 하나로 AttackInfo를 할당해서 변수 이름만 다르게 하여 구성, 후속타가 있는 ComboAttackState는 리스트로 구현하여 연결.
  3. 예시 코드
이전 코드
  • 공격마다 스크립트가 구현되어 있는 모습
개선된 코드
  • 연속 공격 처럼 특수한 경우를 제외하고 PlayerAttackState 하나로 구현 가능하게 변경. => 확장성 증가

트러블 슈팅 : 자연스러운 몬스터 패턴 구현

  1. 개선 이유

    • 몬스터가 공격 애니메이션 중에 움직여서 부자연스러움.
    • 공격 범위와 공격 시작 범위에 대한 인식 문제.
  2. 개선 방안

    • 몬스터의 애니메이션이 종료 후 모든 행동을 시작하는 방식으로 코드 변경.
    • 공격 사정거리와 추적 사정거리로 분류하여 코드를 작성.
  3. 예시 코드

    • 몬스터의 애니메이션이 종료 후 모든 행동을 시작하는 방식으로 변경.
    이전 코드개선된 코드

    • 공격 사정거리와 추적 사정거리로 분류
    개선된 코드

Interface방식 FSM 구조에서 코루틴 사용법

  1. 실패한 경험

    • Interface방식 FSM 구조 클래스 안에서 코루틴을 사용하고 싶음.
    • Interface인 Istate를 할당방은 BaseState에 MonoBehavior를 상속.
    • 오브젝트에 할당되지 않은 MonoBehavior 클래스에서 코루틴을 실행.
    • 오류 발생.
    실패한 경험
  2. 성공한 경험

    • 이후 프로젝트를 진행하던 중 실행 방법을 발견.
    • 인터페이스만 상속 받은 클래스에서 코루틴을 정의.
    • 코드를 실행 시, MonoBehavior를 상속 받은 클래스에 실행을 할당.
    • ex) (MonoBehavior를 상속 받고 오브젝트에 할당된 클래스).StartCoroutine(정의한 코루틴);
    성공한 경험

아이템 슬롯 UI

  1. 구현 이유

    • 하나의 UI에 좌/우클릭 기능을 나눠서 구현 필요.
    • 좌클릭 : 아이템 정보 확인
    • 우클릭 : 아이템 장착/해제
  2. 구현 방법

    • 슬롯 UI에 IPointerClickHandler를 상속.
    • 좌/우 클릭 시 작동하는 Delegate 선언.
    • 상위 UI에 기능을 정의 후 Delegate에 할당.
  3. 구현 예시 : 다양한 클릭 처리

기능 실행(SlotUI.cs)기능 할당(RelicUI.cs)
  1. 트러블 슈팅 : Delegate 사용시 유의점

    • 오류 발생 : 장비가 장착되지 않는 오류가 발생.
    • 원인 분석 : UI에 들어갈 때 마다 이벤트가 계속해서 등록됨.
    • 해결 방안 1 : UnifiedUI에서 3번 연속 등록된 콜백 함수를 들어갈 때 넣어주고 나갈 때 초기화 해주기.
    이전 코드 : 생성자에서 등록(상속 받은 상태머신 마다 실행 됨)
    개선된 코드 : Enter메서드에서 등록(상태머신에 들어갈 때 등록)

    • 해결 방안 2 : RelicUI에 들어갈 때 델리게이트에 등록하고 나갈 때 델리게이트 초기화 해주기.
    RelicUI Enter메서드에서 실행
    RelicUI Exit메서드에서 실행

MuffleSound 구현

MuffleSound 구현

  1. 구현 이유

    • 플레이어가 피격 시 사운드가 잠시 먹먹해지는 효과가 필요.
  2. 구현 방법

    • 이미 사용 중이던 Audio Mixer의 기능을 활용.
    • Snapshot에 Low Pass Filter를 적용한 사운드를 저장.
    • MuffleSound가 필요할 때 해당 Snapshot을 호출하는 방식으로 MuffleSound를 구현.
  3. 구현 예시

    SnapShot Normal 설정
    SnapShot muffle 설정
    SoundManager.cs
    • string 값으로 해당 SnapShout을 가져와서 저장.
    • 메서드를 구현하여 필요할 때 마다 호출.

구조체로 필요한 데이터 묶기(코드 리펙토링)

공격에 필요한 매개변수 구조체로 묶어보기

  1. 구현 이유

    • 공격 시 필요한 정보가 늘어날 때 마다 수정해야 할 코드가 너무 많아 필요한 데이터를 구조체로 묶기로 하였다.
    • 기대 효과 : 적은 노력으로 기능 확장 가능
  2. 구현 방법

    • 공격 시 필요한 정보를 구조체 생성(공격력, 넉백 파워, 그로기 대미지 등)
    • 공격 시 사용되는 다양한 인터페이스(대미지 전달, 넉백, 그로기 대미지 등)의 매개변수를 구조체로 수정.
  3. 구현 예시

    이전 코드개선한 코드
    • 전부 다 공격에서 사용되는 함수들이지만, 중첩되는 데이터도 있고 아닌 데이터도 있다.
    • 그러한 데이터들을 전부 WeaponInpo 구조체에 묶었다.
    WeaponInfo 구조체

partial Class 사용(코드 리펙토링)

partial class 사용해보기

  1. 구현 이유

    • PlayerCondition에 기능이 너무 많이 할당되어 있어 가독성이 나쁨.
    • 플레이어의 스탯 정보와 전투에 필요한 메서드를 나눠서 작성.
    • 기대 효과 : 가독성 증가
  2. 구현 방법

    • PlayerCondition을 partial Class로 변경.
    • 스탯 정보와 관련된 변수, 프로퍼티, 메서드는 PlayerCondition에 작성.
    • 전투 정보와 관련된 메서드는 PlayerConditionBattle에 작성.
    • Player의 오브젝트에는 PlayerCondition 컴포넌트만 넣어도 클래스 둘 다 실행.
  3. 구현 예시

    이전 코드
    • 가독성을 위하여 region으로 묶긴하였지만, 코드가 너무 긺(285줄)
    개선된 코드
    • 플레이어의 상태와 전투에 필요한 상태 메서드를 나눠서 작성하였다.

    추가 개선
    • 프로퍼티의 장점을 살려서 최소 값과 최대 값을 넘지 못하게 요소들을 설정해주었다.

Gizmo 활용한 상호작용 오브젝트 표시

Gizmo 활용한 상호작용 오브젝트 표시

  1. 구현 이유

    • MoveObject는 Lever를 작동시키면 맵에 변화가 생기는 오브젝트들
    • 작동된 이후의 오브젝트의 모습을 눈으로 확인하기 힘들어서 배치에 어려움.
    • 기대 효과 : 맵 구현 편의성 증가.
  2. 구현 방법

    • Gizmo 코드를 추가하여 작동된 모습을 시각적으로 표현.
  3. 구현 예시

    이전개선

구현 기능

  1. 플레이어
    • 대부분의 상태 구현
    • 피격 연출
    • 퍼펙트 가드 연출
  2. 몬스터
    • 몬스터의 기초 구조 구현
  3. 보스
    • 첫 번째 보스 공격 구조 구현
    • 첫 번째 보스 등장 연출 구현
    • 두 번째 보스 모든 상태 및 연출 구현
  4. UI
    • 메인 UI
    • 장비 UI
    • 스킬 UI
  5. 전투 구조 구현
    • Weapon 상속 구조로 대미지 전달
    • 추상 클래스와 인터페이스 활용한 다양한 Conditon 구조
  6. 아이템
    • 구조가 아닌 실제 데이터 구현
  7. 사운드
    • 구조가 아닌 실행 데이터 넣기

시연 영상

LastSanctuary_20250801 개발 영상

플레이 해보기

다운로드 링크

profile
게임 개발자가 되기 위해서 공부하는 중입니다.

0개의 댓글