자원 생산 생성 튜토리얼을 만들것이다.
개발 순서는 튜토리얼 순서대로 만들었다.

TutorialManager
자원 생산 생성 튜토리얼 부터 큐브 생성 튜토리얼 시작까지 만들었다.
using NUnit.Framework;
using System.Collections;
using UnityEngine;
using UnityEngine.UI;
public class TutorialManager : MonoBehaviour
{
private static TutorialManager instance;
public static TutorialManager Instance
{
get { return instance; }
}
public bool isTutorial { get; private set; } = true; // 튜토리얼 상태인지 판단
public float totalCameraMove = 0;
public float totalCameraScroll = 0;
private float maxCameraMove = 700;
private float maxCameraScroll = 30;
[SerializeField] CanvasGroup cameraGroup; // 카메라 제어 UI
[SerializeField] Image cameraMoveBar; // 카메라 회전 게이지바
[SerializeField] Image cameraScrollBar; // 카메라 휠 게이지바
[SerializeField] CanvasGroup resourceGroup; // 자원 생산 UI
[SerializeField] CanvasGroup seletePanel; // 생성창 선택UI
private Coroutine resourceCoroutine; // 자원 생산 UI 코루틴
[SerializeField] GameObject seletePlane; // 생성 선택 부분
private GameObject currentSeletePanel; // 생성된 선택 부분
public enum TutorialState // 튜토리얼의 단계 (할수 있는 입력이 늘어난다.)
{
CAMERAMOVE = 0, // 카메라 조작 단계
RESOURCE = 1, // 생산자 생성 단계
BLOCK = 2, // 블럭 생성 단계
CANNON = 3, // 대포 생성 단계
}
public TutorialState myState;
[SerializeField] GameObject tutorialText;
void Awake()
{
if (instance == null)
{
instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
myState = TutorialState.CAMERAMOVE; // 현재 할수 있는 행동은 카메라 제어까지
}
private void Start()
{
}
private void Update()
{
if(myState == TutorialState.CAMERAMOVE)
{
cameraMoveBar.fillAmount = totalCameraMove / maxCameraMove;
cameraScrollBar.fillAmount = totalCameraScroll / maxCameraScroll;
if(totalCameraMove >= maxCameraMove && totalCameraScroll >= maxCameraScroll)
{
myState = TutorialState.RESOURCE;
StartCoroutine(UIFade(cameraGroup,1, false));
ResourceTutorial();
}
}
}
public void TutorialStart() // start 버튼이후 플레이 시작
{
tutorialText.SetActive(true);
GameManager.Instance.GameStart(); // 튜토리얼 시작
StartCoroutine(UIFade(cameraGroup,3,true));
}
public void ResourceTutorial() // 자원 생산자 생성 튜토리얼 시작
{
StartCoroutine(UIFade(resourceGroup,1, true));
PlaneSetting();
}
public void SeletePanel() // 자원 생산 튜토리얼 블럭 생성창
{
if(resourceCoroutine != null)
{
StopCoroutine(resourceCoroutine);
}
resourceCoroutine = StartCoroutine(UIFade(seletePanel,0.5f, true));
}
private void PlaneSetting() // 선택 면 생성 설정
{
Vector3 InsPosition = new Vector3(0, 0.5f, -0.5001f);
currentSeletePanel = Instantiate(seletePlane, InsPosition, Quaternion.identity);
}
public void BlockTutorial() // 자원 생산이후 블럭 튜토리얼 시작
{
if(resourceCoroutine != null)
{
StopCoroutine(resourceCoroutine);
}
resourceCoroutine = StartCoroutine(UIFade(seletePanel, 0.1f, false));
myState = TutorialState.BLOCK;
currentSeletePanel.SetActive(false);
StartCoroutine(UIFade(resourceGroup, 1.5f, false));
}
IEnumerator UIFade(CanvasGroup canvas,float duration, bool state)
{
canvas.gameObject.SetActive(true);
float start = canvas.alpha;
float end;
if(state) // FadeIn
{
end = 1;
}
else // FadeOut
{
end = 0;
}
float t = 0;
while(t < duration)
{
t += Time.deltaTime;
canvas.alpha = Mathf.Lerp(start, end, t / duration);
yield return null;
}
canvas.alpha = end;
}
}
resourceGroup은 자원 생산자 생성 단계에서 맵에 표시될 텍스트를 지니며, seletePanel은 블럭 생성창에 자원생산자를 클릭하라는 이미지 및 텍스트를 지닌다.
resourceCoroutine은 seletePanel의 UIFade 코루틴 함수를 관리하며 seletePlane은 자원 생산자를 생성할수 있는 큐브의 위치를 표시하는 오브젝트이고, currentSeletePanel은 생성한 seletePlane을 관리하기 위한 변수이다.
처음 시작은 ResourceTutorial함수에서 시작된다. resourceGroup이라는 텍스트가 나오게하며, PlaneSetting이라는 함수를 호출시켜서 선택할수 있는 면에 강조면을 생성시킨다.
SeletePanel은 선택 UI창이 생성되었을 때, 자원 생산자를 클릭하라는 추가 표시 이미지 및 텍스트를 생성시킨다.
BlockTutorial의 경우 자원 생산자를 생성하고 나서 다음 튜토리얼의 시작 함수이다. 전의 튜토리얼 흔적을 지운다.
UIFade 코루틴함수의 변경점은 처음 canvas의 알파값을 state를 통해 0 또는 1이 아니라,현재 알파값을 가진다. 파라미터로 duration을 추가했다. 때문에 유니티 에디터에서 사용하는 CanvasGroup의 알파값을 0으로 초깃값을 조정해야한다.
InputManager
지정된 위치를 제외한 생성 위치를 제한하고, 자원 생산자를 제외한 생성을 제한한다.
void Update()
{
if(GameManager.Instance.myState == GameManager.GameState.START)
{
LeftClick();
OnMouseRay();
ShotKey();
}
else if(GameManager.Instance.myState == GameManager.GameState.TUTORIAL && TutorialManager.Instance.myState >= TutorialManager.TutorialState.RESOURCE) // 튜토리얼 상태일 때, 자원생산자 이상단계라면
{
LeftClick();
OnMouseRay();
ShotKey();
}
Interrupt();
}
void LeftClick() // 블럭을 좌클릭 하면 생성할 블럭의 종류 UI를 표시한다.
{
if(Input.GetMouseButtonDown(0) && !isSeleting)
{
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); // 카메라로 부터 클릭한 월드좌표까지 광선
if (Physics.Raycast(ray, out hit, Mathf.Infinity, LayerMask.GetMask("Cube"))) // 광선에 맞은 충돌체를 담는다.
{
if (TutorialManager.Instance.myState == TutorialManager.TutorialState.RESOURCE) // 자원 생산 튜토리얼 단계일 때
{
if (hit.normal != new Vector3(0, 0, -1) || hit.transform.GetComponent<Block>().myCaster != this) // 플레이어 큐브 뒤쪽을 클릭하지 않았을 때
{
return;
}
}
if (hit.transform.GetComponent<Block>().myCaster == this)
{
UIManager.Instance.ShowSeleteBlock(Input.mousePosition); // 큐브 선택창 생성
isSeleting = true;
seletePlane.InsPlane(hit);
if(TutorialManager.Instance.myState == TutorialManager.TutorialState.RESOURCE) // 자원 생산 튜토리얼 단계일 때
{
TutorialManager.Instance.SeletePanel();
}
// cubeCreator.InsCube(hit.normal, hit.transform, this); // 큐브 생성하기
}
camera_M.ChengeTarget();
Debug.Log("큐브 클릭");
cubeShader.CubeOutLine(hit.transform.gameObject);
previewManager.PreviewObject(hit);
}
}
else if(Input.GetMouseButtonDown(0)) // 선택중일 때, 다른 자신의 큐브 클릭 처리
{
if (TutorialManager.Instance.myState == TutorialManager.TutorialState.RESOURCE) // 자원 생산 튜토리얼 단계일 때
{
return;
}
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); // 카메라로 부터 클릭한 월드좌표까지 광선
if (Physics.Raycast(ray, out hit2 , Mathf.Infinity, LayerMask.GetMask("Cube"))) // 광선에 맞은 충돌체를 담는다.
{
hit = hit2;
if (hit.transform.GetComponent<Block>().myCaster == this) // 자신의 큐브를 선택했을 때 선택 면 재위치
{
seletePlane.DeletePlane();
seletePlane.InsPlane(hit);
}
else // 다른 큐브를 선택했을 때 선택못하게하기
{
CancelSelete();
UIManager.Instance.InActiveSeleteBlock();
}
camera_M.ChengeTarget(); // 타겟 변경
cubeShader.CubeOutLine(hit.transform.gameObject); // 외곽선 처리
previewManager.PreviewObject(hit); // 미리보기 처리
}
}
}
public void seleteBlock(int num)
{
RaycastHit currentHit = hit;
if(currentHit.transform == null)
{
return;
}
if(isSeleting)
{
if (TutorialManager.Instance.myState == TutorialManager.TutorialState.RESOURCE && (num == 1 || num == 3)) // 자원 생산 튜토리얼 단계일 때
{
Debug.Log("선택중 패스");
return;
}
else if (TutorialManager.Instance.myState == TutorialManager.TutorialState.RESOURCE && num == 2) // 자원 생산 튜토리얼 단계일 때
{
TutorialManager.Instance.BlockTutorial();
}
isSeleting = false;
switch (num)
{
case 1:
Debug.Log(currentHit.normal + " " + currentHit.transform);
cubeCreator.InsCube(currentHit.normal, currentHit.transform, this); // 큐브 생성하기
break;
case 2:
cubeCreator.Conversion(currentHit.normal, currentHit.transform, this); // 큐브 생성하기
break;
case 3:
cubeCreator.CreateCannon(currentHit.normal, currentHit.transform, this); // 큐브 생성하기
break;
default: break;
}
UIManager.Instance.InActiveSeleteBlock();
seletePlane.DeletePlane();
}
}
먼저 Update에서 else if를 통해 현재 튜토리얼 상태에서 자원생산자 이상일 때, 입력이 가능하게끔 했다.(튜토리얼 상태에서 카메라 제어 상태일때의 입력을 제한한다.)
LeftClick 함수에서 큐브를 클릭했을 때, 가장 먼저 튜토리얼 상태가 자원 생산자일 때를 확인하고 만약 그러하다면 클릭한 위치가, 뒤쪽 방향이 아니거나, 플레이어 큐브가 아닐 때 그 이후의 실행을 무시한다. (즉 플레이어 코어의 뒤쪽을 클릭하지 않으면, 클릭을 하지 않은 것과 동일한 상태가 된다.)
그렇게 해서 만약 플레이어 코어의 뒤쪽을 클릭했다면 이때 튜토리얼 상태가 자원 생산이라면 블럭 생성 선택UI창에 자원생산자를 강조한다. (SeletePanel)
첫번째 else if에서 자원 생산 위치를 클릭하고 다른 생성위치를 클릭했을 때, 입력받는 것을 방지하기위해 튜토리얼상태를 판단하고 아래 코드를 무시한다.
seleteBlock에서는 블럭 생성을 방지하는데, 만약 자원 생산 튜토리얼 상태에서 큐브나 대포를 생성하려고 하려고 하면 즉시 중단하고, 자원 생산자를 생성했을 때만 생성이 되며 그 이후 Block 튜토리얼로 넘어간다. 이때 Seleting을 계속 true로 유지시켜 다른 표면을 선택하지 못하게 방지한다.
또한 hit에 값이 중간에 변경되지 않도록 currentHit에 현재 클릭중인 RaycastHit를 넣어 처리한다.
UIManager
생성선택 창이 사라지는 함수를 예외처리 시켜 생성선택 창이 사라지지 않도록 한다.
public void InActiveSeleteBlock()
{
if (TutorialManager.Instance.myState == TutorialManager.TutorialState.RESOURCE) // 자원 생산 튜토리얼 단계일 때
{
return;
}
// seleteBlock.SetActive(false);
seleteBlock.Finish();
}
자원 생산자 튜토리얼 상태일 때, 생성 선택창이 사라지지 않도록 하여, 카메라 움직임에 대해 사라지는 것을 방지한다.
UI 설정
SeletePanel(변수명) UI의 설정이다. (Tutorial Click이 오브젝트 이름이다.)

SeletePanel이라는 부모 오브젝트에는 LayoutGroup이 있기 때문에, Tutorial Click에 Layout Element를 추가하고, 레이아웃 무시를 통해서 Selete Panel의 자식이지만 레이아웃 영향은 받지 않게 설계한다.
시스템 흐름

'GameDev > Cubidom' 카테고리의 다른 글
| Cubidom - 디테일 추가 및 버그 수정 1 (0) | 2026.04.29 |
|---|---|
| Cubidom - FTUE 3 (0) | 2026.04.28 |
| Cubidom - FTUE 만들기 1 (0) | 2026.04.26 |
| Cubidom - 전체적인 디자인 추가 (0) | 2026.04.25 |
| Cubidom - SFX 넣기 2 (0) | 2026.04.22 |