Mini Project -1

규수·2026년 4월 13일

개발 공부 일지

목록 보기
25/44

오늘부터 15일간 2D 기반의 게임 개발을 주제로 개인 프로젝트를 진행합니다.
제가 기획하는 프로젝트는 로그라이크 덱빌딩 장르인 Balatro를 모작하는 것을 주제로 선정하였습니다.

* 왜 Balatro?

Balatro(발라트로) 모작을 하게된 이유로 부트캠프에 참여하면서 들었던 얘기중 카드 게임이 기본기를 다지는데 좋은 소재라는 얘기를 들었습니다.

지금의 시점에서는 기획을 통한 게임 개발을 진행하는 것보다 배운 내용들을 복습 및 응용해보는 게임개발이 필요하다고 생각하였습니다.
따라서 발라트로의 핵심 동작 흐름을 파악하고 이를 직접 구현해보는 것을 목표로 설정하였습니다.

오늘은 발라트로 모작 개발에 있어 필요한 게임 이해하기 및 개발에 필요한 설계, 기획에 대한 내용을 정리해보겠습니다.

* 게임의 핵심 흐름

발라트로의 핵심 흐름은 다음과 같습니다.

1. 플레이어가 게임을 시작하면 전체 카드팩에서 8장의 카드가 손패로 드로우된다.
2. 사용자가 손패에서 최대 5장까지 점수계산을 위해 카드를 고른다.
3. 고른 카드를 기반으로 실시간으로 카드 족보가 계산된다.
4. 플레이어가 카드 계산하기를 누르면 해당 족보에 기여된 카드의 숫자에 따른 점수가 계산된다.
5. 카드별 점수가 계산되면 조커카드가 하나씩 발동하면서 보너스 점수가 계산된다.
6. 마지막으로 선택되지 않은(핸드에 있는)카드중 점수 계산에 영향을 주는 카드를 판별한다.
8. 판별된 카드의 추가 점수 계산을 통해 최종 점수계산이 이루어진다.
9. 최종 점수 계산이 누적되며, 총점수가 해당 라운드 목표점수를 넘어서면 게임에 승리한다.
10. 만약 카드 점수 계산 가능 횟수를 모두 소진하였음에도 넘지 못한다면 GameOver된다.

* Class 설계

  • 카드 Class

    • BaseCard
      • 부모 클래스로 지정.

      • 카드 이름, 카드 설명, 카드 이미지를 공통으로 갖도록 선언.

      • 카드 삭제(판매 or 사용), 카드 추가 등을 virtual을 이용한 메소드로 생성 후, override를 통해 자식클래스에서 개별 로직을 구현하여 상황에 맞게 사용하도록 구성

        string cardId;
        string cardName;
        string description;
        
        virtual void Add()
        virtual void Remove()
    • Card 자식 Class
      1. PlayCard(손패 / 문양, 숫자, 계산할 Mult, Chips 필요)

        string suit;
        int rank;
        int baseChip; // 카드별 고유 Chips
      2. JokerCard(조커 카드 / 희귀도, 효과 , 계산할 Mult, Chips 필요)

        string rarity;
        float addmult; // +, x Mult Float - 일부 배수가 1.2, 1.3이렇게 나옴 
        int addChip;
        int sellPrice;
      3. TaroCard(소모성 - 카드 변경(변경, 강화, 제거…))

        int sellPrice;
        Type effectType; // 강화, 변경, 파
      4. PlanetCard(소모성 - 포커 족보별 효과(Chips, Mult) 변경)

        ```jsx
        int sellPrice;
        float addMult; // 사용시 올라갈 Mult수치
        int addChip; // 사용시 올라갈 Chip수치
        CardType targetHand; // 강화할 족보 대상
        ```

        ⇒ 3,4 카드의 경우 사용시 사라지는데 중간 부모 클래스로 한번 더 묶어서 관리?

      5. 바우처카드(소모성, 플레이동안의 상태강화)

  • 계산 로직 Class

    • HandEvaluator
      1. 선택한 카드 (최대 5장)을 기반으로 족보 계산

      2. 특수 효과로 일부 족보가 완화된 경우 고려 필요(ex. 스트레이트, 플러쉬, 스티플의 조건이 5장 → 4장이 되었을 경우 족보 계산시 조건 판별 후 따로 처리)

        bool hasFourJoker = false; // 스트레이트, 플러쉬, 스티플 판정완화 조커 보유 여부
        
        struct HandResult{
        		CardType handType; // 족보
        		List<PlayingCard> scroeCards; // 족보판정에 사용된 카드 리스트 (추후 점수 판정)
        }
    • CardCalculator
      1. 족보 확인 후 각 카드의 숫자 점수 계산

      2. 카드별 강화효과(+ Chips, *Mult) 확인 후 추가

      3. List or Queue로 저장해둔 Joker효과에 따른 강화효과 확인 후 추가

      4. 최종 계산된 정보를 currentScore에 저장 및 targetScore와 비교

        int chip;
        float munt;
        int targetScore;
  • 상태 변경(StateMachine) Class

    • GameState
      1. State : Title / Playing / Shop / SelectCard / Result / GameOver

      2. Command:
        메인 화면 → 플레이 : Play, Draw, SubmitCard, Reward, GameOver
        플레이 → Reward : 게임 결과에 따른 보상 coin 획득
        플레이 → GameOver: 진행하면서 기록한 게임 결과를 출력
        GameOver → 메인 화면 : 메인화면으로 돌아가기를 통한 Title 복귀
        Reward → 상점 : ReRoll, Buy Card / Back(상점 → 플레이화면)
        상점 → 카드효과 선택 : SelectCardEffect, Back(선택 넘기기)

        GameState currentState; // 현재 게임 상태 (State)
        GameCommand currentCommand; // 현재 게임 명령 (Command)
        int earnedCoinInRound // Playing -> Reward 로 넘길 코인
        List<string> roundHistory; // Gameover시 출력할 이번 판 기록
        int enti; // 현재 단계
  • 게임 화면 Class

    • Deck & Hand

      • Deck

        1. 최초 셔플(Fisher & Swap)

        2. 전체 덱 리스트 관리 (전체덱 → 셔플 → 손패로 전달)

      • Hand
        1. 셔플된 카드 획득
        2. 카드 배치 설정( Rank or Suit에 따른 정렬)
        3. 카드 선택에 따른 처리 (버리기 or 점수계산 처리. 버리기시 Deck에서 선택한 수만큼 가져오고 버린카드에 저장)

        List<PlayingCard> fullDeck;
        List<int> discardDeck; // 버린패 (UI상에서 전체패중 소모된 패를 확인하는 용도)
        List<int> hands; // 현재 손패 - List로 한 이유는 손패가 고정되지않기 때문
        int[] selectHands; // Play하는 카드 (5로 고정하되 5미만의 숫자로 플레이할 경우 나머지 패를 -1로 고정)
        Queue<int> selectIndex; // hands에서 선택한 카드 index.
        
        Order sortCards; // Rank or Suit 정렬
    • JokerSlot

      • 조커 배치(구매 or 카드효과를 통해 생성된 순서대로 배열)

      • 판매

        JokerCard[] currentJokers;
        int maxJokerSlot = 5;
    • Inventory

      • 소지중인 타로/행성 카드, 선택 및 사용

      • 타로의 경우 특정 조건(카드 선택 시) 충족에 따라 카드 사용 가능

      • 판매

        List<TaroCard> currentTaroCard;
        List<PlanetCard> currtnePlanetCard;
        
        // Taro, Planet 사용 기록 저장 - Taro카드중 마지막 사용한 카드 한번 더 사용 카드를 위함
        Stack<TaroCard> usedTaroHistory;
        Stack<PlanetCard> usedPlanetHistory;
  • 상점 화면 Class

    • Shop

      1. 3가지 영역 분리 ( 바우처, 소모품 카드, 종합)

      2. 구매, 리롤, 상점 이용 종료 기능 구현

      3. 특정 소모품 구매시 화면 전환

        int rerollCost;
        
        // Taro, Planet를 합친 하나의 리스트를 만들어서 표시할 예정
        List<JokerCard> jokerList; 
        List<TaroCard> taroList;
        List<PlanetCard> planetList;

주요 Scene

  • Scene 분할

    • Title Scene

      • 메인 화면으로서 게임 시작 버튼이 있는 곳

      • GameOver시 돌아오는 화면

      • Game Start시 Playing Scene으로 이동한다

    • Playing Scene

      • In-Game 화면. 실제 플레이가 이루어지는 곳

      • 게임의 결과에 따른 Result Scene의 Win or Lose로 연결된다.

    • Result Scene

      • Win

        • Playing Scene에서 목표 점수를 도달하였을 때 이동되는 화면

        • 게임의 결과와 함께 클리어 보상을 안내한다.

        • 안내가 완료되면 Shop Scene으로 이동한다

      • Lose

        • Playing Scene에서 목표 점수를 도달하지 못하였을 때 혹은 주어진 라운드를 모두 클리어하였을 때 이동되는 화면

        • 최종 진행되었던 결과(도달했던 점수, Game Over의 경우 - 몇라운드까지 갔는지 )를 보여준다.

    • Shop Scene

      • Result Scene - Win에서 이동되는 화면
      • 조커, 행성, 타로, 바우처 카드를 구매할 수 있다.
      • 리롤을 통해 구매 리스트를 재구성할 수 있다.
      • 게임으로 돌아가기를 통해 Playing Scene으로 이동한다.

* 핵심 자료구조

  • 핵심 시스템에 대한 자료구조 선정
    • 카드 배치, 덱 드로우 시스템(인플레이)
      • Array
        • 조커배치 조커 배치의 경우 원작 게임과는 다르게 최대 배치수를 조절 가능하도록 하는 시스템은 구현하지 않을 예정이므로 최대 5장의 카드를 배치할 수 있도록 처리 할 예정입니다. 따라서 Array로 처리하는것이 적합하다고 생각합니다.
          이때, for문을 통해 index를 조회하여 존재하지 않는(null or -1. 아마 Object타입으로 들어가야하니 null이 적합할 것 같습니다.) index에 조커카드를 배치하고 모든 조커카드가 배치되었다면 return;처리하도록 설계할 계획입니다.
        • 소모품 카드 배치(타로카드, 행성카드) 소모품 카드 배치도 마찬가지로 고정된 크기를 가질 예정이기에 Array로 처리할 예정입니다. 다만 이때는 배열내에 들어온 타입이 무엇인지도 판별가능하도록 데이터를 추가로 처리할 예정입니다.(조커의 경우 조커카드만 들어오도록 처리할거니 굳이? 싶습니다. 아니면 방어코드로 카드의 타입을 확인하는식으로 구현이 이루어질 것 같습니다.)
      • List
        • Full Deck 손패로 드로우되기전 최초 상태인 Deck의 경우는 List로 설계할 계획입니다.
          게임의 특성상 특정 카드를 파괴하거나 강화하거나 새롭게 추가할 수 있습니다.
          그렇다보니 최초 소지갯수인 52개를 기준으로 증감이벤트 발생이 비교적 잦은 상황입니다.
          따라서 Array보다는 List를 통해 관리하는것이 구현의 편의성이나 List의 장점에 부합된다고 생각합니다.
      • Queue
        • 조커 점수 계산 Balatro의 점수 계산 순서는 카드 선택 → 족보 판정 → 족보에 해당하는 카드의 카드별 판정 → 배치된 조커 카드 효과 발동 → 핸드내 배치된 추가 효과 발동 순서입니다.
          이때 배치된 조커 카드 효과 발동은 조커 카드 배열의 index순서대로 진행(FIFO)됩니다.
          따라서 현재의 구현 계획은 조커 점수 카드 계산 시 Queue 초기화 → 배열을 순회하여 Enqueue → Dequeue를통해 점수 효과 계산 진행 식으로 구현할 예정입니다.
      • Stack
        • 소모카드 기록 저장 Balatro의 타로 카드의 효과 중에는 가장 최근에 사용한 타로 혹은 행성 카드를 소모품에 다시 배치하는 효과가 있습니다.
          따라서 행성 카드나 타로 카드를 사용할 때마다 Stack에 저장한 다음 가장 마지막에 사용했던 카드를 Peek로 다시 가져오는 방식으로 설계할 계획입니다.
          (Peek를 사용하는 이유는 Pop으로 꺼내올 경우, 만약 2번 연속 마지막 카드를 뽑아보는 타로카드를 사용했을 경우 꺼낸 카드 다음의 Stack에 위치한 카드를 가져와야 하는데 가장 마지막에 사용한이 조건이며 사용을 하지는 않았기 때문에 Peek()로 가져오는 것이 옳다고 생각하였습니다.)
          또한, 이때 마지막 카드를 발동시키는 카드는 따로 Stack에 포함되지않도록 조건처리할 예정입니다.
      • Dictionary
        • 핸드에 배치된 특수 카드 확인 일부 카드는 핸드에 존재해야만이 카드 효과가 추가되는 케이스가 있습니다.
          이를 이용하여 핸드 점수 계산시 마지막 과정으로 남은 손패의 정보를 Dictionary에 Dictionary<int, bool> handsEffct와 같이 추가한 후, HasAddMultipleEffect가 true인 카드를 판별하는 식으로 사용해보는 것을 기획중에 있습니다.
          (약간 억지로 사용하는 것인지 학습적으로 충분히 다뤄봐도 좋은 상황인지 확신이 서질 않습니다.)

오늘은 현재까지 준비한 발라트로 개발에 대한 핵심 게임 기획서를 정리하는 시간을 가졌습니다.
처음으로 제대로된 게임 개발을 하는 만큼 배웠던 내용을 바로 응용해보지는 못할 것을 알기에 일일단위로 구현한 내용과 트러블슈팅에 대해서 정리하며 복습을 통해 기본기를 다져가보겠습니다

0개의 댓글