원래는 AI 난이도 올리도 최종 수정해서 끝내려고 했는데, 제약 시스템이라는 아이디어가 너무 좋은거 같아서 이 기능까지 넣으려고한다.
플레이어가 다음 스테이지로 향했을 때, 랜덤으로 나오는 선택지인 제약을 선택해야 한다.
각 제약 선택지마다 버프와 너프가 동시에 존재한다.

제약 조건들
기초 자원량 -
자원 생산량 -
대포 공격력 -
대포 공격 쿨타임
큐브 체력 -
자원 생산 스피드
ConstraintManager
가장 중요한 제약을 관리하는 스크립트를 만들어 준다.
이 곳에서 버프와 너프를 나눠 두가지 선택지에 넣어주는 기능을 한다.
using NUnit.Framework;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
public class ConstraintManager : MonoBehaviour
{
[SerializeField] CanvasGroup constraintPanel; // 전체 패널 캔바스
[SerializeField] ConstraintButton[] buttons = new ConstraintButton[2]; // 사용할 버튼 두개
[SerializeField] List<ConstraintData> buffDatas = new List<ConstraintData>();
[SerializeField] List<ConstraintData> nuffDatas = new List<ConstraintData>();
[SerializeField] List<ConstraintData> buffTemp = new List<ConstraintData>();
[SerializeField] List<ConstraintData> nuffTemp = new List<ConstraintData>();
private static ConstraintManager instance;
public static ConstraintManager Instance
{
get { return instance; }
}
private void Awake()
{
if(instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
// Start is called once before the first execution of Update after the MonoBehaviour is created
void Start()
{
GameManager.Instance.gameFinish += Constraint;
// GameManager.Instance.gameStart += Constraint;
}
// Update is called once per frame
void Update()
{
}
private void OnDisable()
{
// GameManager.Instance.gameStart -= Constraint;
GameManager.Instance.gameFinish -= Constraint;
}
public void Constraint()
{
constraintPanel.gameObject.SetActive(true);
int index = 0;
for (int i = 0; i < buttons.Length; i++) // 각 버튼들
{
// 버려진 버프 데이터 채우기
buffDatas.AddRange(buffTemp);
buffTemp.Clear();
// 버려진 너프들 다시 채우기
nuffDatas.AddRange(nuffTemp);
nuffTemp.Clear();
for (int j = 0; j < 2; j++) // 버튼의 문장
{
bool isTemp = false;
if (j <= 0) // 버프 문장 선택
{
index = Random.Range(0, buffDatas.Count);
if(i == 1) // 두번째 버튼의 버프일때
{
while(buttons[0].buffData.GetType() == buffDatas[index].GetType())
{
buffTemp.Add(buffDatas[index]);
buffDatas.Remove(buffDatas[index]);
if(buffDatas.Count != 0)
{
index = Random.Range(0, buffDatas.Count);
}
else // 더이상 다른 버프가 없을 때
{
index = Random.Range(0, buffTemp.Count); // 버려진 버프중 하나 가져오기
isTemp = true;
Debug.Log("더이상 다른 버프가 없어요 2");
break;
}
}
}
if(!isTemp)
{
buttons[i].buffData = buffDatas[index];
}
else
{
buttons[i].buffData = buffTemp[index];
}
}
else if(j >= 1) // 너프 문장 선택
{
isTemp = false;
index = Random.Range(0, nuffDatas.Count);
if(i == 0)
{
while (buttons[i].buffData.GetType() == nuffDatas[index].GetType())
{
nuffTemp.Add(nuffDatas[index]);
nuffDatas.RemoveAt(index);
if (nuffDatas.Count != 0)
{
index = Random.Range(0, nuffDatas.Count);
}
else
{
isTemp = true;
index = Random.Range(0, nuffTemp.Count);
break;
}
}
}
else
{
while (buttons[i].buffData.GetType() == nuffDatas[index].GetType() || buttons[0].nuffData.GetType() == nuffDatas[index].GetType())
{
nuffTemp.Add(nuffDatas[index]);
nuffDatas.RemoveAt(index);
if(nuffDatas.Count != 0)
{
index = Random.Range(0, nuffDatas.Count);
}
else
{
isTemp = true;
index = Random.Range(0, nuffTemp.Count);
break;
}
}
}
if(!isTemp)
{
buttons[i].nuffData = nuffDatas[index];
}
else
{
buttons[i].nuffData = nuffTemp[index];
}
}
}
buttons[i].DataSetUp();
}
buffDatas.AddRange(buffTemp);
buffTemp.Clear();
nuffDatas.AddRange(nuffTemp);
nuffTemp.Clear();
StartCoroutine(FadeUI(constraintPanel));
}
public void ButtonClick() // 버튼을 클릭 했을 때,
{
constraintPanel.gameObject.SetActive(false);
}
IEnumerator FadeUI(CanvasGroup canvas)
{
canvas.alpha = 0;
float t = 0;
float duration = 1;
while(t < duration)
{
t += Time.unscaledDeltaTime;
canvas.alpha = Mathf.Lerp(0, 1, t / duration);
yield return null;
}
canvas.alpha = 1;
}
}
게임이 끝날 때만 선택이 실행되어야 하기 때문에, gameFinish에 주 함수를 구독했다.
Constraint 함수는 선택지가 생겼을 때, 각 선택지마다 버프와 너프를 하나씩 넣어주는 역할을 수행한다. 이때 중요한 점은 모든 버프와 너프의 종류가 달라야 한다는 점이다. 가장먼저 선택지 1의 버프를 채워주고, 선택지1의 너프를 채워준다. 이때 너프는 선택지1의 버프와 다른 종류여야만 선택이 된다. 그 후 선택지 2의 버프, 이때도 선택지 1의 버프의 종류와 다른 선택지여야 한다. 그리고 선택지 2의 너프를 선택하는데 이때는 선택지1의 너프와 선택지2의 버프 모두 종류가 달라야 선택이 된다.
여기서 종류가 같아 선택되지 못한 데이터는 temp 데이터로 넘어가며, 이는 다시 다른 쪽에서 버프나 너프를 가져오려할때, 고를수 있는 데이터리스트로 들어간다. 만약 고를수 있는 데이터가 없어서, 데이터리스트의 수가 0이 된다면 랜덤으로 temp의 데이터를 하나 가져온다.
ButtonClick은 선택했을 때, 창이 사라지는 기능을 하며 FadeUI는 창이 생성되었을 때, 페이드 기능을 넣는 역할을 한다.
ConstraintButton
선택지 버튼에 들어갈 스크립트로, 각각 버프, 너프 데이터, 그것의 텍스트를 담고있다.
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
public class ConstraintButton : MonoBehaviour
{
public Text buffText;
public Text nuffText;
public ConstraintData buffData;
public ConstraintData nuffData;
public void DataSetUp()
{
StartCoroutine(typeSentence(buffData.sentence, nuffData.sentence));
// myText.text = myData.sentence;
}
public void Execute() // 버튼을 눌렀을 때, 실행될 함수
{
buffData.Execute();
nuffData.Execute();
ConstraintManager.Instance.ButtonClick();
}
IEnumerator typeSentence(string sentence, string sentence2)
{
buffText.text = "";
nuffText.text = "";
foreach (char c in sentence)
{
buffText.text += c;
yield return new WaitForSecondsRealtime(0.05f);
}
foreach (char c in sentence2)
{
nuffText.text += c;
yield return new WaitForSecondsRealtime(0.05f);
}
}
}
ConstraintManager에서 모든 효과 배정이 끝나고, DataSetUp을 호출하는데 이때, 자신이 가지고 있는 버프와 너프 데이터를 typeSentence 코루틴 함수를 통해 대사출력을 시킨다.
버튼을 클릭했을 때, 버프데이터와 너프 데이터의 출력이 실행되고, 창이 사라진다.
ConstraintData
각 데이터들의 부모 클래스로, ScriptableObject로 생성한다.
using UnityEngine;
public abstract class ConstraintData : ScriptableObject
{
public abstract string sentence { get; } // 설명 문자열
public abstract void Execute();
}
사용되는 모든 변수들이 추상화로 자식 클래스에서 무조건적으로 재정의를 하게 되어있다.
사용될 문장은 프로퍼티로 선언 되어있고 선택이 되었을 때, 호출될 Execute 함수가 존재한다.
BasicResource
기초 자원량에 효과를 주는 데이터
using UnityEngine;
[CreateAssetMenu(menuName = "Scriptable/ConstraintData/BasicResource")]
public class BasicResource : ConstraintData
{
public int value; // 증가할 자원량
public override string sentence => $"Starting Resources {value}";
public override void Execute()
{
Debug.Log($"기초 자원량 {value} 증가");
StatManager.Instance.StartingResource = value;
}
}
CannonAttackSpeed
대포 공격속도에 효과를 주는 데이터
using UnityEngine;
[CreateAssetMenu(menuName = "Scriptable/ConstraintData/CannonAttackSpeed")]
public class CannonAttackSpeed : ConstraintData
{
public float attackSpeed;
public override string sentence => $"Cannon Attack speed {attackSpeed}";
public override void Execute()
{
Debug.Log($"대포 공격속도 {attackSpeed} 변화");
StatManager.Instance.CannonAttackSpeed = attackSpeed;
}
}
ResourceSpeed
자원 생산속도에 효과를 주는 데이터
using UnityEngine;
[CreateAssetMenu(menuName = "Scriptable/ConstraintData/ResourceSpeed")]
public class ResourceSpeed : ConstraintData
{
public float speed;
public override string sentence => $"Resource Production Speed {speed * 10}";
public override void Execute()
{
Debug.Log($"생산 속도 {speed} 증가");
StatManager.Instance.ResourceSpeed = speed;
}
}
CubeHealth
큐브 체력에 효과를 주는 데이터
using UnityEngine;
[CreateAssetMenu(menuName = "Scriptable/ConstraintData/CubeHealth")]
public class CubeHealth : ConstraintData
{
public int addHealth;
public override string sentence => $"Cube Health {addHealth}";
public override void Execute()
{
Debug.Log($"큐브 체력 {addHealth} 업");
StatManager.Instance.CubeHealth = addHealth;
}
}
CannonDamage
대포 공격력에 효과를 주는 데이터
using UnityEngine;
[CreateAssetMenu(menuName = "Scriptable/ConstraintData/CannonDamage")]
public class CannonDamage : ConstraintData
{
public int addDamage; // 추가되는 데미지
public override string sentence => $"Cannon {addDamage} Damage ";
public override void Execute()
{
Debug.Log($"대포 데미지 {addDamage} 증가");
StatManager.Instance.CannonDamage = addDamage;
}
}
ResourceYield
자원생산량에 효과를 주는 데이터
using UnityEngine;
[CreateAssetMenu(menuName = "Scriptable/ConstraintData/ResourceYield")]
public class ResourceYield : ConstraintData
{
public int value;
public override string sentence => $"Resource Value {value}";
public override void Execute()
{
Debug.Log($"생산자의 생산력이 {value} 증가했습니다");
StatManager.Instance.ResourceYield = value;
}
}
효과 데이터 넣어주기
이제 효과를 선택했을 때, 발동시키기 위한 작업을 해야한다.
StatManager
바뀌는 모든 스탯을 관리하는 매니저를 따로 만들어서 모든 값들을 이곳에 참조하여 사용하게끔 한다.
여러 프로퍼티와 싱글톤으로만 구성되어있다.
using UnityEngine;
public class StatManager : MonoBehaviour
{
// 초기 자원량
[SerializeField] int startResource;
public int StartingResource
{
get { return startResource; }
set
{
int result = startResource + value;
if (result < 20)
{
startResource = 20;
}
else
{
startResource = result;
}
}
}
[SerializeField] int resourceYield; // 자원 생산량
public int ResourceYield
{
get { return resourceYield; }
set
{
int result = resourceYield + value;
if(result < 5)
{
resourceYield = 5;
}
else
{
resourceYield = result;
}
}
}
[SerializeField] int cannonDamage; // 대포 공격력
public int CannonDamage
{
get { return cannonDamage; }
set
{
int result = cannonDamage + value;
if(result < 5)
{
cannonDamage = 5;
}
else
{
cannonDamage = result;
}
}
}
[SerializeField] float cannonAttackSpeed; // 대포 공격속도
public float CannonAttackSpeed
{
get { return cannonAttackSpeed; }
set
{
float result = cannonAttackSpeed + value;
if(result < 0.5f)
{
cannonAttackSpeed = 0.5f;
}
else
{
cannonAttackSpeed = result;
}
}
}
[SerializeField] int cubeHealth; // 큐브 체력
public int CubeHealth
{
get { return cubeHealth; }
set
{
int result = cubeHealth + value;
if (result < 10)
{
cubeHealth = 10;
}
else
{
cubeHealth = result;
}
}
}
[SerializeField] float resourceSpeed; // 자원 생산속도
public float ResourceSpeed
{
get { return resourceSpeed; }
set
{
float result = resourceSpeed + value;
if(result < 0.1f)
{
resourceSpeed = 0.1f;
}
else
{
resourceSpeed = result;
}
}
}
private static StatManager instance;
public static StatManager Instance { get { return instance; } }
private void Awake()
{
if(instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
}
바뀌는 모든 데이터(스탯)들을 가져올때, 프로퍼티로 가져올수 있게 만들어준다. 이때, 프로퍼티는 값에 비정상적인 값이 들어가지 않도록 막는다.
Cannon
void Start()
{
if(myCaster is InputController)
{
attackDelay = StatManager.Instance.CannonAttackSpeed;
}
~~~~~~~~~~중략~~~~~~~~~
}
IEnumerator FireBall()
{
while (true)
{
yield return new WaitForSeconds(attackDelay);
~~~~~~~~~중략~~~~~~~~~
if (target != null)
{
~~~~~~~~~~~~~중략~~~~~~~~~~
GameObject prefab = Instantiate(cannonBall, cannonBallPos.position, cannonRotation.rotation);
if(myCaster is InputController)
{
prefab.GetComponent<CannonBall>().damage = StatManager.Instance.CannonDamage;
}
}
}
}
IEnumerator TargetLine(Vector3 start, Vector3 target) // 타겟까지의 라인을 그리는 함수
{
Debug.Log("라인 생성");
lineRenderer.SetPosition(0, start);
lineRenderer.SetPosition(1, target);
lineRenderer.enabled = true;
if(attackDelay < 1)
{
yield return new WaitForSeconds(attackDelay);
}
else
{
yield return new WaitForSeconds(1f);
}
lineRenderer.enabled = false;
}
대포의 Start에서 만약 플레이어의 대포라면 StatManager의 대포 공격속도 값을 가져온다.
대포알을 만들때, 대포알의 damage에 접근하여 값을 현재 스탯 값으로 바꿔준다.
TargetLine에서 라인렌더러 생성시간이 너무 짧아지면 현재 공격속도에 맞게끔 코드를 수정한다.
CannonBall
using UnityEngine;
public class CannonBall : MonoBehaviour
{
public float damage;
~~~~~~~중략~~~~~~~~~
}
damage의 접근제한자를 public으로 바꿔준다.
Cube
void Start()
{
if(myCaster is InputController)
{
hp = StatManager.Instance.CubeHealth;
}
}
Start에서 플레이어 큐브라면 현재 스탯에 있는 큐브 체력을 가져와 사용한다.
InputController
protected override void Start()
{
base.Start();
totalResource = StatManager.Instance.StartingResource;
}
protected override void Init()
{
totalResource = StatManager.Instance.StartingResource;
}
게임이 시작되었을 때, 자원 초기값으로 스탯값을 넣어준다.
Resource Producer
void Start()
{
if(myCaster is InputController)
{
delayTime = StatManager.Instance.ResourceSpeed;
resourceValue = StatManager.Instance.ResourceYield;
}
initialPos = transform.position;
StartCoroutine(Produce());
}
IEnumerator Produce() // 자원을 생산한다.
{
for(int i = 0; i < repeatNum; i++)
{
yield return new WaitForSeconds(delayTime);
float upYPos = 0.75f / repeatNum;
Vector3 currentPos = transform.position + transform.up * upYPos;
transform.position = currentPos;
}
GameObject popUpText = Instantiate(resourcePopUp_T, transform.position,Quaternion.identity);
popUpText.GetComponent<PopUpText>().TextSetUp(resourceValue);
EffectManager.Instance.PlayParticle(resourcePati, transform.position); // 생산 파티클 생성
AudioManager.Instance.EffectPlay(resourceClip, transform.position);
myCaster.totalResource += resourceValue;
// GameManager.Instance.totalResource += resourceValue;
transform.position = initialPos;
StartCoroutine(Produce());
}
자원생산자에서 Start 함수에 시전자에 따라 스탯을 반영한다.
해당 사진처럼 각 효과마다 6개씩(버프3개, 너프3개) 데이터를 만들고, 리스트에 넣어준다.

완성본

'GameDev > Cubidom' 카테고리의 다른 글
| Cubidom - 최종 수정 및 해상도 설정 (0) | 2026.05.13 |
|---|---|
| Cubidom - AI의 제약 시스템 (0) | 2026.05.09 |
| Cubidom - AI 시스템 보완 (Utility AI) (0) | 2026.05.03 |
| Cubidom - 디테일 추가 및 버그 수정 2 (0) | 2026.04.30 |
| Cubidom - 디테일 추가 및 버그 수정 1 (0) | 2026.04.29 |