GameDev/Cubidom

Cubidom 스테이지 시스템

SMNNMN 2026. 3. 26. 02:40

게임이 너무 빨리 끝나는 감도 있고, 스테이지 식으로 진행되면 좋을 것 같아서 스테이지 시스템을 만들 것이다.

플레이어의 코어가 부서지게 되면, 전체 체력(도전횟수)이 차감되고, 0이 되면 리타이어 되게끔 생각중이고,

적의 코어가 부서지면, 다음 스테이지로 이동하고, 더 강화된 적과 싸우게되는 시스템이다. 만약 마지막 적의 코어까지 파괴했다면 게임이 끝이 난다.

 

 

적 스테이지별 데이터 변수 만들기

먼저 구현해야 하는 건 각 스테이지별 적의 데이터로 각 레벨당 스테이지를 5개로 구현한다고 할때, 추가로 12개의 적 데이터를 만들어야한다.

EnemyAIData

using UnityEngine;

[CreateAssetMenu(menuName = "Scriptable/EnemyAIData",fileName = "Enemy AI")]
public class EnemyAIData : ScriptableObject
{
    public int id; // 각 데이터마다의 고유 식별번호

    public float behaviourRandomness; // 행동 판단 랜덤계수
    public float positionRandomness; // 생성 위치 판단 랜덤계수
    public float thinkDelay; // 생각하는 시간
    public float totalResource; // 초기 자원량
    public float attackWeight; // 공격행동 가중치
    public float defenceWeight; // 블럭행동 가중치
    public float resourceWeight; // 생산행동 가중치
}

먼저 데이터가 많아지기 때문에 관리하기 쉽게 id라는 변수를 넣어 사용할 데이터를 찾기 수월하게 한다.

이후 추가한 변수는 다양한 전략과, 난이도를 만들기 위해 초기 자원량과 3개의 행동 가중치 데이터를 만들었다.

ID의 경우 개별적인 규칙성을 띄게 적는다. (후에 설명)

 

적 데이터 가져오기

그리고 구현해야 하는 건, GameManager에 있는 변수에 따라서(level, stage)사용할 데이터를 변경시켜주는 기능이다.

GameManager

    public int enemyLevel = 1;
    public int enemyStage = 1;

enemyStage라는 변수를 추가해서, 현재 스테이지를 확인할 수 있게한다.

 

EnemyDataBase

using NUnit.Framework;
using UnityEngine;
using System.Collections.Generic;

public class EnemyDataBase : MonoBehaviour
{
    public List<EnemyAIData> data = new List<EnemyAIData>(); // Enemy의 모든 데이터

}

적의 데이터를 담기위한 스크립트를 하나 추가해 주었다. 

이 스크립트는 적이 사용할수 있는 모든 스테이지별 데이터를 리스트로 가진다. (원래 EnemyAI에 있던 코드를 가져온 것이다.)

 

EnemyAI

EnemyAI 에서 데이터베이스에 있는 데이터를 가져오고 직접 데이터를 적용시키는 코드를 작성했다.

    private Dictionary<int , EnemyAIData> aiData = new Dictionary<int,EnemyAIData>();

    public EnemyAIData enemyData; // 적의 난이도 레벨
    
      protected override void Awake()
    {
        base.Awake();
        currentState = EnemyState.Alive;
        // haveBlock.Add(core);
        // haveCube.Add(core);
        core.myCaster = this;

        DataInit();
    }
    protected override void Start()
    {
        base.Start();
        GameManager.Instance.gameStart += LevelSetUp;
        GameManager.Instance.gameStart += StartMyCoroutine;
        // GameManager.Instance.gameInit += StartMyCoroutine;
        GameManager.Instance.reGame += StopThink;
    }
    protected override void Init()
    {
        // totalResource = 50;
        // enemyData = aiData[GameManager.Instance.enemyLevel - 1];

        DataSetUp();

        // Debug.Log(enemyData);
    }
    void LevelSetUp()
    {
        //enemyData = aiData[GameManager.Instance.enemyLevel - 1];
        DataSetUp();

        // thinkDelay = enemyData.thinkDelay;
        // behaviourRandomness = enemyData.behaviourRandomness;
        // positionRandomness = enemyData.positionRandomness;
    }
    void DataInit() // 데이터들을 처음 딕셔너리에 등록하는 함수
    {
        EnemyDataBase dataBase = GetComponent<EnemyDataBase>();
        foreach (var myData in dataBase.data)
        {
            if(aiData.ContainsKey(myData.id))
            {
                Debug.Log("중복 id 있음" + myData + " " + aiData[myData.id]);
            }
            else
            {
                aiData.Add(myData.id, myData);
                Debug.Log(myData.id);
            }
        }
    }
    void DataSetUp() // 데이터를 확인해서 적용하는 함수
    {
        int level = GameManager.Instance.enemyLevel;
        int stage = GameManager.Instance.enemyStage;
        int findId = (level * 100) + stage;
        if (aiData.ContainsKey(findId))
        {
            enemyData = aiData[findId];
            Debug.Log(enemyData + " " + findId);
        }

        totalResource = enemyData.totalResource;
        thinkDelay = enemyData.thinkDelay;
        behaviourRandomness = enemyData.behaviourRandomness;
        positionRandomness = enemyData.positionRandomness;

        EnemyAttack attack = GetComponent<EnemyAttack>();
        EnemyDefence defence = GetComponent<EnemyDefence>();
        EnemyResource resource = GetComponent<EnemyResource>();

        attack.weight = enemyData.attackWeight;
        defence.weight = enemyData.defenceWeight;
        resource.weight = enemyData.resourceWeight;
        
    }

먼저 Dictionary를 통해서 EnemyDataBase에 있는 데이터를 데이터의 id를 키값으로 저장한다.

DataInit 함수는 Awake에서 1회만 실행된다. (id 키값과 데이터를 저장하는 딕셔너리에 저장하는 함수)

DataSetUp 함수의 경우 GameManager에서 Enemylevel값과 enemyState값을 받아와서 id를 만들어 낸 이후, 해당 id에 맞는 데이터를 enemyData에 저장하고, enemyData안의 데이터들을 EnemyAI의 여러 값들에 저장시켜서 데이터를 변경해준다.

데이터들의 ID는 백의 자리수는 현재 레벨, 십의 자리수까지는 현재 스테이지를 나타내는 규칙성을 가진다.

 

 

스테이지 변경하기

스테이지 변경은 플레이어가 적 코어를 파괴했을 때, Retry버튼 대신 Next버튼이 나오고, 이 버튼을 눌렀을 때, 다음 스테이지의 데이터를 가진 Ai가 상대하게끔 한다.

GameManager

    public void NextStage()
    {
        enemyStage += 1;
        ReGame();
    }
    public void ReGame()
    {
        reGame?.Invoke();
        gameInit?.Invoke();
        gameStart?.Invoke();

        myState = GameState.START;
        winner = Winner.NONE;
        Time.timeScale = 1f;
    }

가장 먼저 작성한 코드는 GameManager에서 NextStage라는 함수를 만들어, 호출될 때 enemyStage에 1을 추가한다음, ReGame을 호출시켜, 모든 큐브의 삭제와 초기화 이후 gameStart 이벤트에서 적 레벨셋업과 게임의 재시작을 빌드한다.

 

UIManager

    [SerializeField] GameObject nextButton;
    
        void FinishUI()
    {
        finishPanel.SetActive(true);
        seleteBlock.SetActive(false);
        InputController.Instance.CancelSelete();

        if (GameManager.Instance.winner == GameManager.Winner.ENEMY)
        {
            enemyWin_t.SetActive(true);
            retryButton.SetActive(true);
        }
        else if(GameManager.Instance.winner == GameManager.Winner.PLAYER)
        {
            playerWin_t.SetActive(true);
            nextButton.SetActive(true);
        }
    }
    void ReStartUI()
    {
        finishPanel.SetActive(false);
        retryButton.SetActive(false);
        enemyWin_t.SetActive(false);
        playerWin_t.SetActive(false);
        nextButton.SetActive(false);
    }

nextButton이라는 변수를 만들어, Retry버튼과 동일하지만 이긴 시전자에 따라 생성되는 버튼이 다르게 구현한다.

(기존에 적 레벨을 고르는 단계에서 플레이 화면으로 가기위한 nextButton은 playButton으로 이름 변경해 주었다.)

 

인스펙터 창에서 Enemy Data가 변경되는 것을 볼수 있다. (임시 easy : 101, normal : 102)

 

 

레벨 및 스테이지 UI창 만들기

게임 중 현재 레벨과 스테이지를 알려주는 UI를 만들 것이다.

GameManager

    public enum EnemyLevel_E
    {
        Easy,
        Normal,
        Hard
    }
    public EnemyLevel_E enemyLevel_E
    {
        get; private set;
    }
        public void EnemyLevel(int level)
    {
        enemyLevel = level;
        changeMaterial?.Invoke();
        switch(level)
        {
            case 1: enemyLevel_E = EnemyLevel_E.Easy; break;
            case 2: enemyLevel_E = EnemyLevel_E.Normal; break;
            case 3: enemyLevel_E = EnemyLevel_E.Hard; break;
        }
    }

먼저 EnemyLevel_E처럼 열거형으로 레벨을 선언한 이유는 여기서 현재 레벨을 판단하고 UIManager에서 열거형의 값을 텍스트로 활용하기 위해서이다.

EnemyLevel에서 레벨이 변경될 때 switch문으로 현재 EnemyLevel_E값을 변경한다.

 

UIManager

    [SerializeField] Text levelStage_T;
    void playStart() // 게임시작 시 UI
    {
        enemyLevels.SetActive(false);
        playButton.SetActive(false);

        resourceValue_T.gameObject.SetActive(true);
        levelStage_T.text = "Level : " + GameManager.Instance.enemyLevel_E.ToString() + "\nStage : " + GameManager.Instance.enemyStage.ToString();
        levelStage_T.gameObject.SetActive(true);
    }    
        void MainUI()
    {
        ReStartUI();
        startButton.SetActive(true);
        resourceValue_T.gameObject.SetActive(false);
        levelStage_T.gameObject.SetActive(false);
    }

levelStage_T라는 텍스트로 레벨과 스테이지를 표시한다.

게임 시작시 levelStage_T 텍스트를 GameManager의 레벨 열거형과, 스테이지 변수값을 사용해서 적는다.

Main 화면에서는 비활성화하게 해준다.

 

 

각 스테이지별 데이터 레벨디자인

임의로 작성해본 스테이지별 특징이다.

난이도가 올라갈수록 판단속도와 초기자금이 기본적으로 높아진다.

만든 데이터들을 EnemyData Base에 넣으면 스테이지 완성이다.

 

스테이지 완료시 메인이동 / 메인 이동시 스테이지 초기화

마지막 스테이지까지 클리어시, Next버튼을 누르지 못하게 하고 Main버튼만 누를수 있게해야한다. 

UIManager

    void FinishUI()
    {
        finishPanel.SetActive(true);
        seleteBlock.SetActive(false);
        InputController.Instance.CancelSelete();

        if (GameManager.Instance.winner == GameManager.Winner.ENEMY)
        {
            enemyWin_t.SetActive(true);
            retryButton.SetActive(true);
        }
        else if(GameManager.Instance.winner == GameManager.Winner.PLAYER)
        {
            playerWin_t.SetActive(true);
            if (GameManager.Instance.enemyStage < 5)
            {
                nextButton.SetActive(true);
            }
        }
    }

게임 종료시 현재 스테이지값을 확인하고 5미만일 경우에만 nextButton을 활성화 시킨다.

 

GameManager

    public void MainScene()
    {
        reGame?.Invoke();
        gameInit?.Invoke();
        myState = GameState.STANDBY;
        winner = Winner.NONE;
        enemyStage = 1;
        mainScene?.Invoke();
        Time.timeScale = 1f;
    }

메인으로 이동할 경우 enemyState를 1로 값을 변경시켜 다시 게임시작이 처음부터 스테이지가 진행되도록 한다.

 

 

이제 해아할 부분은 대포의 공격 방향을 자동으로 만드는 것, 디자인 정도가 남아있다.