소소한 나의 하루들

탑다운 2d RPG(최종) - 피드백 추가 및 개선사항 본문

개발/유니티

탑다운 2d RPG(최종) - 피드백 추가 및 개선사항

소소한 나의 하루 2024. 2. 8. 09:36

문제점 - 해결

1. 사물 오브젝트 대화창 vs NPC 대화창 UI 배치 구분

하나의 UI 오브젝트 내에서 텍스트 크기, 초상화이미지 등 UI 배치 다르게 설정하기에는 코드 입력 상 번거로울 것 같고, NPC 대화창과 사물 오브젝트 대화창 UI를 따로 두어야할 것 같다.

*그래도 하나의 UI 오브젝트에서 대화 대상마다 배치를 다르게 두는건 언제라도 해놓으면 좋겠다. (비효율적인 작업일 것 같은데 이렇게 한다고해서 프로그래밍적으로 공부는 되겠지만 나중에 활용할 일이 없을 것 같다. 같은 대상에게 다른 UI 배치로 대화를 한다고 하면 그때도 다른 UI오브젝트를 생성해서 활용하지 않을까)

확실히 수정 전(왼쪽)보다 수정 후(오른쪽) 배치가 더 눈이 편안하다. 그런데 수정하다보니 NPC 외 사물 오브젝트에 대한 대화창에서도 왼쪽에 플레이어의 초상화나 (플레이어의 독백이므로) 아니면 해당 오브젝트의 모습이 배치되는 것이 더 알차고 허전하지 않게 느껴질 것 같아서 사물이라고 해서 대화창 UI 배치를 따로 해줄필요는 없을 것 같다.

*UI 배치 바꿔주면서 초상화 애니메이션도 수정해준다.

 

추가 문제1

로직 및 에디터 참조설정도 이상없는데 AudioSource 사운드 출력이 안되는 문제

AudioSource 컴포넌트의 Pitch 옵션 값이 음수이면 소리가 정상적으로 재생되질 않는다. (거꾸로 재생된다) 따라서 별일이 없으면 1로 설정해두는 것이 좋은 것 같다.

 

추가문제2

사물 오브젝트 대화창이 화면에 뜨기 전부터 텍스트 타이핑이 시작하는 문제

: 대화창이 화면에 완전히 뜨고나서 텍스트 타이핑이 시작했으면 좋겠다.

텍스트 타이핑이 시작하기 전에 대화창 애니메이션이 전환되어야하는데, 아무리 생각해도 대화창 등장 속도가 너무 느려서 그런 것 같았다. 그래서 확인해보니, NPC 대화창은 등장 애니메이션 프레임 간격이 0.2초인 반면 사물 대화창은 등장 애니메이션 프레임 간격이 1초였다. 그래서 프레임을 0.2초로 줄여줬더니 문제가 해결되었다.

 

사물 오브젝트 텍스트 타이핑 중 skip 안되고 바로 대화창 내려가는 문제

NPC 대화에서는 skip 기능이 정상적으로 작동하는데, 사물 오브젝트 대화에서 skip 기능이 작동하지 않는 이유를 생각해봐야한다.

public void SetMsg(string msg)
{
    if (isAnime)
    {
        Debug.Log("Hello");
        CancelInvoke();
        msgText.text = targetMsg;
        EffectEnd();
    }


Debug.Log()해보니 isAnime가 true일때(타이핑 중일때) 다시한번 SetMsg 호출이 들어오면 전체 메세지를 표시하고 타이핑을 중단하는 if문으로 진입을 안하고 있었다. isAnime 출력도 해보니 Jump키 입력 시 바로 대사가 내려가는것이 문제였다. 대사가 내려가지않도록 타이핑 중 Jump키를 눌렀을 때 isHide 애니메이션을 작동시키면 안된다.

//GameManager
public void Action(GameObject scanObj)
{
    //Get Current Object
    scanObject = scanObj;
    ObjectData objData = scanObject.GetComponent<ObjectData>();
    Talk(objData.id, objData.isNPC);

    //Visible Talk for Action
    if (objData.isNPC)
    {
        Debug.Log(isAction);
        talkSpace_Npc.SetBool("IsShow", isAction);
    }
    else
    {
        Debug.Log(isAction);
        talkSpace_Object.SetBool("IsShow", isAction);
    }
        
}

void Talk(int id, bool isNPC)
{

    int questTalkIndex = 0;
    string talkData = "";

    //Set Talk Data
    //Skip
    if (talk_NPC.isAnime)
    {
        talk_NPC.SetMsg("");
        return;
    }

    else if (talk_Object.isAnime)
    {
        talk_Object.SetMsg("");
        return;
    }
    ...
}

//TypeEffect
public void SetMsg(string msg)
{
    if (isAnime)
    {
        CancelInvoke();
        msgText.text = targetMsg;
        EffectEnd();
    }

    else
    {
        targetMsg = msg;
        EffectStart();
    }
}

플레이어가 Jump키를 누르면 GameManager의 Action() 함수가 호출되는데, NPC 대화에서 isAnime가 true일때(=텍스트 타이핑이 진행중일때) 다시한번 호출됐다면 SetMsg()로 빈 값("")을 넘겨줘서 Invoke()는 종료함으로서 텍스트 타이핑은 중단하고 '입력중이던' 완전한 대사가 대화창에 출력되도록 하고, (커서 애니메이션이 실행되도록하는) EffectEnd()함수가 호출되고 대기한다. [다음 입력이 들어오기 전까지]

→다음 입력이 들어오면 null값이 들어와서 isAction이 false가 되면서 TalkHide 애니메이션이 실행되어 IsShow채팅창이 내려간다.

수정하다보니, 초상화가 대화창에 나오는 UI 배치로만 진행해도 괜찮겠다 싶었지만 이것도 분명 나중에 활용할 때가 있지 않을까 하는 생각도 있고 도움이 될 것 같아서 상황마다 다른 대화창 UI를 활성화시키는 것도 구현해보았다.

 

2. 퀘스트 오브젝트를 퀘스트 진입 후 언제든 획득할 수 있고, 바로 퀘스트 해결가능하도록 하기

우선 퀘스트 오브젝트(동전)과 플레이어가 물리충돌을 하면 안되니까 Is Trigger 체크하고 캐릭터가 먹었을 때 사라지도록 하기

Collider2D 컴포넌트를 가진 플레이어 오브젝트와 Is Trigger 체크된 Collider2D 컴포넌트를 가진 퀘스트 아이템은 Tag를 통해 충돌여부를 판단해야할 것 같다.

그리고 기존 퀘스트 진행순서에 따라 퀘스트아이템이 활성화  → 얻는 단계에서 벗어나, 퀘스트 진행 중 (퀘스트 아이템이 활성화된 이후) 퀘스트 순서와 맞지 않게 퀘스트 아이템을 얻게된다면 퀘스트를 바로 클리어할 수 있게 그 중단 퀘스트 순리를 건너뛰도록 해야한다.

우선 필요한건 플레이어가 어떤 퀘스트 아이템을 얼마나 얻었는지에 대한 정보를 담는 변수이다. 어떤 퀘스트 아이템을 얻었는지는 id를 이용해서 판단하는게 좋을수도 있을 것 같고, QuestData처럼 획득가능한 퀘스트아이템 정보 스크립트를 따로 두는 것도 좋겠다.

 

//PlayerAction
public void OnTriggerEnter2D(Collider2D collision)
{
    if (collision.gameObject.tag == "Quest Item")
    {
        questManager.questActionIndex = 3;
        questManager.questObject[0].SetActive(false);
        //Debug.Log(questManager.questObject[0].activeSelf);
    }
        
}

위 QuestItemData 정보는 인벤토리 구현이나 퀘스트 정보창에서 활용해야할 것 같다.

아무튼 퀘스트 아이템인 Coin의 Tag를 Quest Item으로 설정하고, PlayerAction에서 OnTriggerEnter2D에서 플레이어와 충돌 시, questManager의 questActionIndex를 3으로 배치시키고 바로 NPC에게 대화를 걸면 퀘스트 클리어가 가능하도록 했다.

 

3. 진행중(단계표시) / 진행완료 + 퀘스트아이템 수집정보 퀘스트 정보창 추가 ★★★(보류)

*생각보다 고려해야할 부분도 많고 복잡하기도 해서 나중에 따로 다뤄봐야겠다. (그리고 이렇게 오브젝트를 배치해놓고, 비활성화->활성화 시키는 동작 외에 토글 형식으로 스크립트에서 UI를 추가해볼 수는 없는지도 궁금하고 또 리스트를 만들어놓고 외부 퀘스트 데이터를 추가하는 식으로 구현도 가능하다고 하니 알아볼 생각이다.)

 

우선 퀘스트 정보창을 위한 UI부터 구성해야겠다.

 

여기서 각 퀘스트 버튼은 퀘스트 제목 텍스트만 나타나도록 투명도를 0으로 설정해놓고, 마우스를 올려놨을 때 투명도 150정도, 클릭했을 때 투명도 200 정도로 조금 더 강하게 설정해야한다.

이런 식으로 UI를 배치해놓고 옆에 퀘스트 정보를 띄울 수 있는 보조 UI 창에도 관련 정보를 입력한다.

그리고 모두 비활성화해놨다가 특정 퀘스트에 진입하면 진행중인 퀘스트 / 완료한 퀘스트로 퀘스트 이름이 Button UI의 텍스트로 전달되고, 버튼을 클릭하면 해당 퀘스트의 정보가 보조 창에 나타나도록하면 구현할 수 있을 것이다.

(구현하는 방법은 알았으니 더 효율적인 방법으로 퀘스트 창을 구현하는 방법은 따로 공부해보도록 하겠다)

 

우선 더 효율적으로 퀘스트 창 시스템을 구현하려면,

각 Image 영역(퀘스트 리스트 및 퀘스트 정보란)에는 주어진 영역을 초과하면 스크롤을 내리고 올릴 수 있게 스크롤도 구현해서 능동적으로 더 넓은 공간을 제공한다. 그리고 모든 퀘스트 정보가 새로고침해서 나타날 수 있도록 한다.

 

4. 서브메뉴 열었을 때 캐릭터 애니메이션&움직임 정지(조작x)

서브메뉴를 열면 캐릭터는 움직이면 안되고, 상하좌우 조작에 의한 애니메이션도 활성화되면 안된다. 대화창 조작도 멈춰야한다.

Time.timescale = 0; //시간 정지
Time.timescale = 1; //시간 재생

그리고 서브메뉴가 활성화되었을 때 상태를 나타내는 플래그 변수를 만들어서, 플레이어 조작이 서브메뉴가 활성화되지 않았을 때만 가능하도록 한다.

//GameManager
private void Update()
{
    //Sub Menu
    if (Input.GetButtonDown("Cancel"))
    {
        SubMenuActive(); 
    }   
}

public void SubMenuActive()
{
    if (menuSet.activeSelf) //서브메뉴 닫기
    {
        Time.timeScale = 1;
        menuSet.SetActive(false);
        isSubMenu = false;
    }
    else //서브메뉴 열기
    {
        menuSet.SetActive(true);
        Time.timeScale = 0;
        isSubMenu = true;
    }
}

//PlayerAction
void Update()
{
    if (!gameManager.isSubMenu)
    {
        /* 이동 관련 모든 로직 */
    }
}

 

추가문제

서브 메뉴에서 esc키를 입력 시 다시 시간이 재생되지만, 버튼을 클릭해서 서브메뉴가 닫힐 때에도 시간을 재생시켜줘야한다. (시간 재생을 위해 TimeScale을 1로 설정도 해주고, isSubMenu를 false 값으로 바꿔준다)

//GameManager
public void GameSave()
{
    PlayerPrefs.SetFloat("PlayerX", player.transform.position.x);
    PlayerPrefs.SetFloat("PlayerY", player.transform.position.y);
    PlayerPrefs.SetInt("QuestId", questManager.questId);
    PlayerPrefs.SetInt("QuestActionIndex", questManager.questActionIndex);
    PlayerPrefs.Save();

    GameContinue();
    menuSet.SetActive(false);
    //player.x, player.y
    //Quest Id
    //Qurest Action Index
}

...

public void GameContinue()
{
    Time.timeScale = 1;
    isSubMenu = false;
}

GameLoad()에는 해줄 필요 없다.  (처음 로딩되는 함수이므로 서브메뉴가 활성화되어 시간정지된 상태가 아니다)

 

5. 대화창에 NPC 이름 띄우기 +색상으로 구분해보기

이것도 ObjectData에 id, IsNPC와 name까지 추가해서 대화 상호작용이 가능한 Object 레이어의 오브젝트에 이름을 모두 입력해주고, NPC라면 이름이 나타나도록 해준다.

그리고 이름을 표시하는 TextMeshPro - Text UI에 특정 NPC일때 특정 색상과 id에 해당하는 이름을 부여해준다.

 

※주의해야할 부분

//PlayerAction
rayHit = Physics2D.Raycast(rigid.position, dirVec, 0.7f, LayerMask.GetMask("Object"));
scanObject = rayHit.collider.gameObject;

//GameManager
public void Action(GameObject scanObj)
{
    //Get Current Object
    scanObject = scanObj;
    ObjectData objData = scanObject.GetComponent<ObjectData>();
    Talk(objData.id, objData.isNPC, objData.name);
    ...
}

void Talk(int id, bool isNPC, string name)
{

    int questTalkIndex = 0;
    string talkData = "";

    ...

    //Continue Talk
    if (isNPC)
    {
        talk_NPC.SetMsg(talkData.Split(':')[0]);

        //Continue Talk
if (isNPC)
{
    talk_NPC.SetMsg(talkData.Split(':')[0]);

    //Show Name
    if (id == 1000)
    {
        npcName.text = "<color=#47C83E>" + name + "</color>";
        //npcName.text = name;
        //npcName.color = new Color(1, 1, 0.7f, 1);
        //npcName.color = new Color32(255, 1, 128, 255);
    }
    else if (id == 2000)
        npcName.text = "<color=#A566FF>" + name + "</color>";
        ...
}

여러 오브젝트들에게 할당해주는 ObjectData 스크립트의 id같은 값들은 다른 스크립트에서 값을 가져와 활용하려고 할 때 ObjectData라고 선언해서 컴포넌트를 가져오거나, 혹은 에디터에서 드래그하여 적용시키기에도 어렵다.(여러 오브젝트가 ObjectData에서 서로 다른 id 값들을 갖고 있으니까) 따라서 GameObject로 스캔할 수 있는 여러 오브젝트 범주를 설정하고, rayHit으로 빔에 맞는 오브젝트들을 GameObject로 갖고와서 여기에 있는 ObjectData를 꺼내오면 된다.

 

텍스트 색상 변경하기

1. string 변수에 "<color=#색상코드>" + "텍스트" + "</color>" string값 대입하기 (혹은 에디터의 text란에 직접 입력)

string 변수명 = "<color=#A566FF>" + "텍스트" + "</color>";

2. Color 속성에 new Color(Red, Green, Blue, Alpha) 함수 사용하기

text.color = new Color(1, 0.5f, 1, 1); //0에서 1 사이의 float 값을 사용

3. Color 속성에 new Color32(Red, Green, Blue, Alpha) 함수 사용하기

text.color = new Color32(255, 1, 255, 255); //0에서 255까지의 byte반환(=8bits) 값을 사용

 

6. 재시작 버튼 + 기능 (재시작 버튼 누르면, 위치&퀘스트 초기화)

재시작 버튼을 누르면 플레이어의 위치가 처음 스폰된 좌표가 되어야하고, 퀘스트 단계는 초기화되어야 한다.

만약 퀘스트창을 설정했다면 퀘스트 진행 단계는 초기화되어야할 것이다.

여기서 선택할 수 있는 수단은 (1) 플레이어가 저장했던 레포지토리의 데이터를 처음 상태로 덮어씌우거나 (2) 삭제하는 방법이 있을 것 같은데 삭제하면 어떻게 될지 모르겠지만 결국 덮어씌우는게 맞는 판단인 것 같다.

결국 플레이어가 처음 스폰된 좌표를 기억해야한다.

재시작 기능은 서브메뉴에 추가하도록 한다.

우선 서브메뉴에 처음부터 시작하도록 하는 버튼을 추가한다.

사용자 데이터를 처음시점(위치/퀘스트 진행도)으로 되돌리면서 덮어씌우고, 다시 시작하는 것으로 생각해보면 '저장하기' 의 GameSave()와 GameLoad()의 기능을 한번에 갖고있으면서 한번에 save와 load를 해주는게 좋을 것 같다.

*'처음부터' 다시 새로 시작하는 기능은 유저 데이터를 갈아엎는 치명적일 수 있는 기능이기 때문에, 좋은 UX 디자인을 위해서 별도의 UI 표시(대비 색상or강조)가 들어가거나, 다시한번 확인하는 창이 떠주면 좋을 것 같다.

//GameManager
public void GameReset()
{
    PlayerPrefs.SetFloat("PlayerX", -10.0f);
    PlayerPrefs.SetFloat("PlayerY", 0.0f);
    PlayerPrefs.SetInt("QuestId", 10);
    PlayerPrefs.SetInt("QuestActionIndex", 0);
    PlayerPrefs.Save();

    //if (!PlayerPrefs.HasKey("PlayerX"))
        //return;

    float x = PlayerPrefs.GetFloat("PlayerX");
    float y = PlayerPrefs.GetFloat("PlayerY");
    int questId = PlayerPrefs.GetInt("QuestId");
    int questActionIndex = PlayerPrefs.GetInt("QuestActionIndex");


    player.transform.position = new Vector3(x, y, 0);
    questManager.questId = questId;
    questManager.questActionIndex = questActionIndex;
    questManager.ControlObject();

    GameContinue();
    menuSet.SetActive(false);
}

public void GameContinue()
{
    Time.timeScale = 1;
    isSubMenu = false;
}

 

추가문제

'처음부터' 버튼 클릭 시 서브메뉴의 퀘스트 정보란의 내용이 처음 퀘스트 정보가 아니라, 초기화하기 전 퀘스트 제목이 뜨는 문제

//QuestManager
public string CheckQuest()
{
    //Quest Name
    return questList[questId].questName;
}

//GameManager
public class GameManager : MonoBehaviour
{
    public TalkManager talkManager;
    public QuestManager questManager;
    public Animator talkSpace_Npc;
    public Animator talkSpace_Object;
    public Animator portraitAnime;
    public Sprite prePortrait;
    public TypeEffect talk_NPC;
    public TypeEffect talk_Object;
    public Image portraitImg;
    public GameObject scanObject;
    public GameObject menuSet;
    public GameObject player;
    public TextMeshProUGUI questText;
    public TextMeshProUGUI npcName;
    public bool isSubMenu;
    public bool isAction;
    public int talkIndex;

    private void Awake()
    {
        talkSpace_Npc.SetBool("IsShow", false);
        talkSpace_Object.SetBool("IsShow", false);
    }

    private void Start()
    {
        GameLoad();
        questText.text = questManager.CheckQuest();
    }

코드를 살펴보면, 맨 처음 GameManager의 Start()에서 QuestManager의 CheckQuest() 함수를 호출하는데, CheckQuest()는 현재 questList[questid]의 퀘스트 이름을 반환한다. '처음부터' 버튼을 클릭하면 QuestId는 10으로 QuestActionIndex는 0으로 (처음 퀘스트 지점)으로 값을 바꿔주는데 왜 문제가 발생하는걸까.

 

로직 실행단계를 생각해보면

1) 퀘스트가 questId가 20인 "루도의 동전 찾아주기" 단계에서 questText.text는 "루도의 동전 찾아주기" 텍스트 값을 갖고있음.
2) '저장하기' 버튼 클릭
: 플레이어의 현재 (x, y) 좌표 저장되고 현재 퀘스트 ([questId][questActionIndex]) 단계 저장된다.
※이때 여전히  questText.text는 "루도의 동전 찾아주기"라는 텍스트 값을 갖고 있는 상황

('종료'하지 않았으므로 계속 최신화되고 있는 key가 삭제되지 않았다.)

*유저가 플레이하고 있을 때 최신의 데이터를 갖고있는 key는 자동으로 최신화되고 있다. 게임을 종료하게되면 별도로 PlayerPrefs.Setxxx() 해주지 않은 key들은 삭제된다. [PlayerPrefs는 게임이 종료되고 다시 시작할 때에만 유지되는 저장소 개념]

3) '처음부터' 버튼 클릭

: 플레이어가 종료를 하지 않았으므로 이전의 데이터를 갖고있는 key가 사라지지 않았다. 따라서 questText.text는 새로 플레이어가 대화를 시도해서 questId 값이 CheckQuest()에 들어오기 전까지는 '처음부터' 버튼 클릭 직전의 퀘스트 제목인 "루도의 동전 찾아주기" 텍스트 값을 갖고 표시하게될 것이다.

 

이제 원인을 알았다.

해결책은 단순하다. 유저가 게임을 종료하지 않고 게임 플레이 중에 '처음부터' 버튼을 클릭했을 때 이전의 퀘스트 제목이 UI에 표시되지 않도록 questText.text에 공백값을 넣거나, 처음단계의 퀘스트 제목을 표시하도록 한다.

//QuestData
using System.Collections;
using System.Collections.Generic;

public class QuestData
{
    public string questName;
    public int[] npcId;

    public QuestData(string name, int[] npc)
    {
        questName = name;
        npcId = npc;
    }
}

//QuestManager
public class QuestManager : MonoBehaviour
{
    public int questId;
    public int questActionIndex;
    public GameObject[] questObject;
    public QuestItemData questData;
    public PlayerAction player;

    public Dictionary<int, QuestData> questList; 

    void Awake()
    {
        questList = new Dictionary<int, QuestData>();
        questData = GetComponent<QuestItemData>();

        GenerateData();
    }
    
    void GenerateData()
	{
    	questList.Add(10, new QuestData("마을 사람들과 대화하기", new int[] { 1000, 2000 }));
    	questList.Add(20, new QuestData("루도의 동전 찾아주기", new int[] { 1000, 2000, 5000, 2000}));
   		questList.Add(30, new QuestData("퀘스트를 클리어하였습니다.", new int[] { 0 }));
	}
    ...
    public string CheckQuest()
	{
   		//Quest Name
    	return questList[questId].questName;
	}

//GameManager
public void GameReset()
{
    PlayerPrefs.SetFloat("PlayerX", -10.0f);
    PlayerPrefs.SetFloat("PlayerY", 0.0f);
    PlayerPrefs.SetInt("QuestId", 10);
    PlayerPrefs.SetInt("QuestActionIndex", 0);
    PlayerPrefs.Save();

    //if (!PlayerPrefs.HasKey("PlayerX"))
        //return;

    float x = PlayerPrefs.GetFloat("PlayerX");
    float y = PlayerPrefs.GetFloat("PlayerY");
    int questId = PlayerPrefs.GetInt("QuestId");
    int questActionIndex = PlayerPrefs.GetInt("QuestActionIndex");


    player.transform.position = new Vector3(x, y, 0);
    questManager.questId = questId;
    questManager.questActionIndex = questActionIndex;
    questManager.ControlObject();
    questText.text = questManager.questList[questId].questName;

    GameContinue();
    menuSet.SetActive(false);
}

왼쪽 상태에서 '저장하기' 버튼을 클릭하고, 다시 '처음부터' 버튼을 클릭하면 오른쪽 초기 상태 및 퀘스트 정보로 정보가 초기화된다. 


https://github.com/pix-lin/Unity_TopDown-2d-RPG.git

 

GitHub - pix-lin/Unity_TopDown-2d-RPG

Contribute to pix-lin/Unity_TopDown-2d-RPG development by creating an account on GitHub.

github.com

퀘스트 구현 단계는 아직 많이 미숙하지만 기본적인 원리, 로직의 흐름에 대해서는 많이 파악할 수 있었던 것 같다. 강의없이 혼자서 퀘스트 시스템을 구현하라고 하면 아직도 많이 애를 먹을 것 같은데 모르면 검색해가면서 찾아가면서 프로그래밍해나가는 것은 현업에서 활약하고 있는 전문적인 프로그래머들도 마찬가지일테니 이 부분에 대해서는 걱정하지말고 더 시스템 흐름이나 원리, 로직에 친숙해지려고 노력해야겠다.

 

유니티로 개발 공부를 하다보니, 퀘스트 창, 인벤토리, 상점 및 거래/경매장, 확률가챠 등 구현해보고 싶은 시스템들이 많이 생겨서 하나씩 메모해나가고 있다. 골드메탈님의 강의 진도도 생각보다 많이 느려서 할일이 앞으로 정말 많은데 차분하게 우선순위를 정해서 하나씩 꼼꼼히 해나가야겠다. 미련하게 이론 다 끝내고 시작하려는 짓은 정말 지양하자. 나에게 주어진 시간은 많이 없으니 자신감과 확신을 갖고 효율적으로 프로젝트를 해나가는 것이 무엇보다 중요하다.

(해당 프로젝트는 유튜브 골드메탈님의 유니티 기초강좌를 따라 진행하고 있습니다.)

출처: https://youtube.com/playlist?list=PLO-mt5Iu5TeYI4dbYwWP8JqZMC9iuUIW2&feature=shared

 

📚 유니티 기초 강좌

유니티 게임 개발을 배우고 싶은 분들을 위한 기초 강좌

www.youtube.com

Comments