스테이지 생성방식 2

[이전 글 확인] https://cheondi.github.io/mess/2021/12/14/mess6.html

4.단어 총 갯수 확인 및 test

단어의 갯수와 test를 통해 추가적으로 단어가 이상하게 겹쳐지는 부분이 없는지 확인한 이후 문제가 생길 경우 게임을 재생성하는 방식이다.

        if(!test() || wordLists.Count() < index*2-10)
        {
            Debug.Log("retry");
            SetWordPosition();
            return;
        }
    public bool test()
    {
        for (int i = 0; i < index; i++) //array의 모든 칸을 채운 후, -1인 단어가 들어갈 수 없는 칸을 0으로 돌려놓는다.(편함)
        {
            for (int j = 0; j < index; j++)
            {
                if (array[i, j] == "-1")
                    array[i, j] = "0";
            }
        }
        for (int i = 0; i < index; i++)
        {
            for (int j = 0; j < index; j++)
            {
                //array의 모든 칸을 돌면서 확인한다.(2음절이 되버린 단어를 찾는다) 0110의 가로방향 세로방향을 모두 찾는다.
                if (i + 3 < index && array[i, j] == "0" && array[i + 1, j] == "1" && array[i + 2, j] == "1" && array[i + 3, j] == "0")
                {
                    Debug.Log("retry");
                    return false;
                }
                if (j + 3 < index && array[i, j] == "0" && array[i, j + 1] == "1" && array[i, j + 2] == "1" && array[i, j + 3] == "0")
                {
                    Debug.Log("retry");
                    return false;
                }
                if (i == 0 && array[i, j] == "1" && array[i + 1, j] == "1" && array[i + 2, j] == "0")
                {
                    Debug.Log("retry");
                    return false;
                }
                if (j == 0 && array[i, j] == "1" && array[i, j + 1] == "1" && array[i, j + 2] == "0")
                {
                    Debug.Log("retry");
                    return false;
                }
                if (i == index - 1 && array[i, j] == "1" && array[i - 1, j] == "1" && array[i - 2, j] == "0")
                {
                    Debug.Log("retry");
                    return false;
                }
                if (j == index - 1 && array[i, j] == "1" && array[i, j - 1] == "1" && array[i, j - 2] == "0")
                {
                    Debug.Log("retry");
                    return false;
                }
            }
        }
        //0110을 찾아서 나올 경우 false를 return하고 아닐 경우 true를 return한다.
        Debug.Log("Test success");
        return true;
        
    }


예외 상황을 모두 처리 하였음에도 불구하고, 1000판 중 1판 정도씩 버그가 튀어나와 test함수를 만들어 실행한다.

5.단어 리스트 생성

단어는 뒤끝차트에 csv형식 파일을 업로드하여, 게임이 시작될 때 현재 다운로드되어 있는 파일과 비교하여 최신버전이 아닐 경우 새로이 다운로드한다. 뒤끝에 관한 사항은 뒤쪽 포스트에서 작성할 예정이고, 지금 포스트에서는 json파일로 저장되어 있는 단어 파일을 list를 옮기는 과정을 하려 한다.

    public List<List<string>> word3 = new List<List<string>>();
    public List<List<string>> word4 = new List<List<string>>();
    public void SetJson()
    {
        JsonData chartJson = JsonMapper.ToObject( Backend.Chart.GetLocalChartData("word3") ); //서버에서 다운로드 받은 json파일
        //3음절 단어 파일이다.
        var rows = chartJson["rows"];
        List<string> word3_0 = new List<string>();
        List<string> word3_1 = new List<string>();
        List<string> word3_2 = new List<string>();
        List<string> word3_3 = new List<string>();
        List<string> word3_4 = new List<string>();
        //list 생성 방식은 3음절 단어 파일은 약 14000개의 단어로 이루어져 있다.
        //단어를 찾을 때 만약 한번 확인할 때 14000개의 단어 파일을 한번에 루프문을 회전한다면 시간이 오래걸려 스테이지를 생성하는데   //오래 걸리기에 단어를 5개의 list를 통해 관리하게 된다.
        for(int i =0;i<rows.Count;i++)
        {
            if(i%5==0)
                word3_0.Add(rows[i]["name"]["S"].ToString());
            else if(i%5==1)
                word3_1.Add(rows[i]["name"]["S"].ToString());
            else if(i%5==2)
                word3_2.Add(rows[i]["name"]["S"].ToString());
            else if(i%5==3)
                word3_3.Add(rows[i]["name"]["S"].ToString());
            else
                word3_4.Add(rows[i]["name"]["S"].ToString());
        }
        JsonData chartJson2 = JsonMapper.ToObject( Backend.Chart.GetLocalChartData("word4") );
        //위와 동일하다.
        var rows2 = chartJson2["rows"];
        List<string> word4_0 = new List<string>();
        List<string> word4_1 = new List<string>();
        List<string> word4_2 = new List<string>();
        List<string> word4_3 = new List<string>();
        List<string> word4_4 = new List<string>();
        for(int i =0;i<rows2.Count;i++)
        {
            if(i%5==0)
                word4_0.Add(rows2[i]["name"]["S"].ToString());
            else if(i%5==1)
                word4_1.Add(rows2[i]["name"]["S"].ToString());
            else if(i%5==2)
                word4_2.Add(rows2[i]["name"]["S"].ToString());
            else if(i%5==3)
                word4_3.Add(rows2[i]["name"]["S"].ToString());
            else
                word4_4.Add(rows2[i]["name"]["S"].ToString());
        }
        word3.Add(word3_0);
        word3.Add(word3_1);
        word3.Add(word3_2);
        word3.Add(word3_3);
        word3.Add(word3_4);
        word4.Add(word4_0);
        word4.Add(word4_1);
        word4.Add(word4_2);
        word4.Add(word4_3);
        word4.Add(word4_4);
        //List<list<string>> 형식인 list를 추가한다.
    }



6.첫 단어 생성

    public void SetFirstWord()
    {
        System.Random random =new System.Random();
        if (wordLists[0].len == 3) // 첫 단어의 길이가 3일 경우
        {
            List<string> file = new List<string>();
            file = word3[random.Next(0, 5)];
            int index11 = random.Next(0, file.Count());
            wordLists[0].word = file[index11].ToString();
        }
        else //첫 단어의 길이가 4일 경우
        {
            List<string> file = new List<string>();
            file = word4[random.Next(0, 5)];
            int index11 = random.Next(0, file.Count());
            wordLists[0].word = file[index11].ToString();
        }
        Debug.Log(wordLists[0].word);
        for (int i = 0; i < wordLists[0].len; i++)
        {
            if (wordLists[0].direction)
            {
                array[wordLists[0].x, wordLists[0].y + i] = wordLists[0].word[i].ToString(); 
                //array에 단어를 표시한다.
            }
            else
            {
                array[wordLists[0].x + i, wordLists[0].y] = wordLists[0].word[i].ToString();
            }
        }
        if (wordLists[1].len == 3) //다음 단어의 길이가 3일 경우
            WordSet(1, false, random.Next(0, 5), 1);
        else //다음 단어의 길이가 4일 경우
            WordSet(1, false, random.Next(0, 5), 1); //다음 단어 생성 함수
    }


첫 단어를 생성하는 함수는 위와 같다. 3음절인지 4음절인지를 판단하여 단어의 정보를 랜덤하게 찾아 입력한다.

7.다음 단어 생성

    public int funcCnt; //findword 함수 회전 횟수(만약 함수가 200회를 넘어가게 된다면, 처음부터 다시 시작하게 된다)
    //함수는 단어를 찾을 때까지 반복하기 때문에, 다른 단어가 파생할 수 없는 단어가 생성된 경우에 함수를 굉장히 많이 반복하게 된다.
    //이 때 스테이지를 생성하는 시간이 많으면 10초 정도 걸릴 수 있으므로, 생성하였다.(스테이지 생성은 1~3초 정도 걸림)
    public void WordSet(int index, bool flag, int filenum, int cnt)
    //index는 wordlist에서의 단어의 순번,
    //filenum은 word3 or word4의 list의 갯수이다.
    //cnt 단어의 파일을 찾을 index이다.
    {
        if(funcCnt >200)
        {
            funcCnt = 0;
            Debug.Log("break");
            foreach(var row in wordLists)
            {
                row.word="";
                row.words.Clear();
            }
            funcflag = false; //findword 함수가 200회 이상 작동하면 플래그가 올라가게 되어 스테이지를 다시 생성 시작한다.
            return ;
        }
        System.Random random =new System.Random(); 
        //초기 버전에서는 서버에서 차트를 받아오는 식이 아닌, 게임 리소스 안에 csv 파일을 저장하고 빌드를 하여 사용하였다.
        //아무리 빌드를 해도, 안드로이드에서 테스트를 할 경우에 csv 파일을 찾을 수 없다고 하여, csvReader라고 구글에 떠도는 파일을
        //사용한 것이 잘못된 것이라 판단하여, 특정 함수를 지우기 위해 UnityEngine.Random이 아닌 System.Random을 사용하게 되었다.
        //이후 원인을 찾은 후에도 따로 작동하는데 문제가 없어 수정하진 않았다.
        if (flag&&wordLists[index].words.Count() > 0)
        //wordInfo에 words라는 list가 있는데, 이것은 단어에 적합한 단어들의 목록이다.
        {
            wordLists[index].word = wordLists[index].words[random.Next(0, wordLists[index].words.Count)];
            wordLists[index].words.Remove(wordLists[index].word);
           // Debug.Log(wordLists[index].word);
            for (int i = 0; i < wordLists[index].len; i++)
            //단어를 정했을 경우 array에 입력한다.
            {
                if (wordLists[index - 1].direction)
                    array[wordLists[index].x, wordLists[index].y + i] = wordLists[index].word[i].ToString();
                else
                    array[wordLists[index].x + i, wordLists[index].y] = wordLists[index].word[i].ToString();
            }
            if (index == wordLists.Count - 1) //마지막 단어라면 메인 함수로 되돌아간다.
                return;
            else //아니라면 다음 단어를 실행한다. 
            {
                WordSet(index + 1, false, random.Next(0, 5), 1);
                return;
            }

        }
        else
        {
            for (int i = 0; i < wordLists[index].len; i++) 
            //for문을 돌면서 이전 단어와 겹치는 부분을 찾는다.
            {
                if (wordLists[index].direction && array[wordLists[index].x, wordLists[index].y + i] != "1")
                {
                    wordLists[index].words = FindWord(index, i, array[wordLists[index].x, wordLists[index].y + i], filenum, cnt);
                }
                else if (!wordLists[index].direction && array[wordLists[index].x + i, wordLists[index].y] != "1")
                {
                    wordLists[index].words = FindWord(index, i, array[wordLists[index].x + i, wordLists[index].y], filenum, cnt);
                    //FindWord는 단어의 겹치는 정보와 길이를 입력하여 들어갈 수 있는 단어의 list를 반환한다.
                    //단어 list를 index의 해당하는 words에 넣는다.
                }
            }
            if ((wordLists[index].len == 3 &&cnt==5) ||(wordLists[index].len ==4 &&cnt==5))
            //마지막까지 돌았음에도 불구하고, 단어를 찾지 못할 경우
            {
                wordLists[index - 1].excepts.Add(wordLists[index - 1].word);
                //이전 단어가 다시 나올 수도 있기에 제외 목록에 추가해 둔다.
                wordLists[index - 1].word = "";
                //이전 단어의 정보를 지운다.
                for (int i = 0; i < wordLists[index - 1].len; i++)
                {
                    if (wordLists[index - 1].direction)
                        array[wordLists[index - 1].x, wordLists[index - 1].y + i] = "1";
                    else
                        array[wordLists[index - 1].x + i, wordLists[index - 1].y] = "1";
                    //array에 정보도 지운다.
                }
                if (index == 1) 
                //만약 index가 1일 경우 처음 단어를 생성하는 함수로 이동(1일 경우 2번째 단어이다.)
                {
                    SetFirstWord();
                    return;
                }
                else
                //아닐 경우에는 이전 단어로 되돌아간다.
                {
                    WordSet(index - 1, true, random.Next(0, 5), 1);
                    return;
                }
            }
            if (wordLists[index].words.Count > 0)
            //스테이지를 만들 때, 단어의 일부분이 중복되는 경우가 많다.
            //예를 들면, "사회주의", "민주주의" 등 어근과 어근이 합쳐지는 합성어들이 주로 이런 경우가 많이 나타난다.
            //이런 경우를 방지하기 위해서 예비 단어 list가 생성되면, wordlist를 확인하여 같은 어근을 갖고 있는 단어를
            //list에서 삭제한다. 
            {
                List<string> deleteWord = new List<string>();
                foreach(var row in wordLists[index].words)
                {
                    for (int i = 0; i < wordLists[index - 1].len - 1; i++)
                    {
                        if (row.Contains(wordLists[index - 1].word.Substring(i, 2)))
                            deleteWord.Add(row);
                    }
                }
                foreach(var row in deleteWord)
                {
                    for (int i = 0; i < index; i++)
                    {
                        if (wordLists[index].words.Contains(row))
                        {
                            //Debug.Log("삭제" + row);
                            wordLists[index].words.Remove(row);
                        }
                    }
                }
                if(wordLists[index].words.Count > 0)
                //어근이 겹치는 단어를 지우고도 단어가 남아있는 경우
                {
                    wordLists[index].word = wordLists[index].words[random.Next(0, wordLists[index].words.Count)];
                    //Debug.Log(wordLists[index].word);
                    wordLists[index].words.Remove(wordLists[index].word);
                    wordLists[index].words = wordLists[index].words;
                    //단어 선정
                    for (int i = 0; i < wordLists[index].len; i++)
                    //array에 적용
                    {
                        if (wordLists[index].direction)
                            array[wordLists[index].x, wordLists[index].y + i] = wordLists[index].word[i].ToString();
                        else
                            array[wordLists[index].x + i, wordLists[index].y] = wordLists[index].word[i].ToString();
                    }
                    if (index == wordLists.Count - 1)
                    //마지막 단어일 경우 
                    {
                        Debug.Log("끝");
                        return;
                    }
                    else
                    //마지막 단어가 아닐 경우 다음 함수로 이동
                    {
                        if (wordLists[index + 1].len == 3)
                        {
                            WordSet(index + 1, false, random.Next(0, 5), 1);
                            return;
                        }
                        else
                        {
                            WordSet(index + 1, false, random.Next(0, 5), 1);
                            return;
                        }
                    }
                }
                else
                //만약 단어가 남아 있지 않을 경우 다음 단어 list를 열어 실행한다.
                {
                    WordSet(index, false, filenum + 1, cnt + 1);
                }
            }
            else
            //단어를 찾지 못할 경우 다음 파일로 이동
            {
                WordSet(index, false, filenum + 1, cnt + 1);
            }
        }
    }


제일 버그가 많이 터지는 함수이다. 조금만 변수를 잘못 건드려도, 이상한 단어가 튀어나오곤 한다.

    List<string> FindWord(int index, int num, string syllable, int filenum, int cnt)
    //원하는 단어를 찾는 함수이다. index는 찾는 단어의 index이고, num은 겹치는 단어의 index이고 syllable은 겹치는 단어의 음절
    //filenum은 찾고자 하는 list의 index, cnt는 findword 함수를 몇 번 돌았나 확인하는 변수이다.
    {
        funcCnt++;
        List<string> words = new List<string>();
        List<string> file = new List<string>();
        if (wordLists[index].len == 3)
        {
            if (cnt == 6)
            {
                words = null;
                return words;
                //만약 모든 list를 다 돌게 된다면, null을 return 한다.
            }
            file = word3[filenum % 5];
        }
        else
        {
            if (cnt == 6)
            {
                words = null;
                return words;
            }
            file = word4[filenum % 5];
        }
        for (int i = 0; i < file.Count; i++)
        {
            if (file[i].ToString()[num].ToString() == syllable && !wordLists[index].excepts.Contains(file[i].ToString()))
            //제외되는 단어에 포함되지 않고, 겹치는 부분이 같은 단어를 찾는다. 
            {
                if (wordLists.Find(x => x.word == file[i].ToString()) == null)
                //같은 단어가 아닐 경우
                {
                    words.Add(file[i].ToString());
                }
            }
        }
        return words;
        //단어 list를 return
    }



8.스테이지 저장

스테이지 저장 방식은 여러가지가 있는데, 나는 사용하기 편한 PlayerPrefs를 이용하여 string 형태로 저장하였다. 앱이 비활성화되거나, 메인화면으로 나갈 경우 게임이 저장된다.

    void OnApplicationPause(bool pauseStatus) //앱이 비활성화 될때(pauseStatus가 true가 됨)
    {
        if(pauseStatus &&done)//done은 bool 형식의 stage가 완성되면 true로 변한는 변수이다.
            Save();
    }


    //저장 : 플레이시간, 메인패널, 서브패널, emptyTiles 갯수, 단어리스트,힌트 갯수
    public void Save()
    //저장 방식은 플레이시간#메인패널#서브패널#emptytiles의 갯수#단어리스트#남은 힌트 갯수
    //형식으로 저장되어서 string의 split함수를 이용하여 나누어 정보를 load하게 된다.
    {
        if(wordLists.Count<6)
            return; //만약 스테이지에 버그가 났을 때 저장이 안된다.
        save = (((int)Time.time)-startTime).ToString()+"#";//플레이시간 저장한 후 '#' 추가
        for(int i =0;i<index*index;i++)
        //모든 메인 패널을 확인하여, 단어가 들어갈 빈 칸 타일, 완성된 단어 타일, 단어가 들어갈 수 없는 빈 칸 타일,
        //단어를 입력했지만 아직 맞추지 못한 타일로 4가지 타일을 구분하여 저장한다.
        {
            var row =mainPanel.transform.GetChild(i);
            if(row.tag=="Done")
            {
                if(row.transform.GetChild(0).GetComponent<Text>().text=="")
                {
                    save = save + "33";
                }
                else
                {
                    save = save+"2"+row.GetChild(0).GetComponent<Text>().text;
                }
            }
            else if(row.tag == "Tile")
            {
                save = save + "11";
            }
            else if(row.tag == "Hit")
            {
                save = save + "1" + row.GetChild(0).GetComponent<Text>().text;
            }
            save = save +"*";
        }
        save = save + "#";//'#'을 추가한다.
        for(int i =0;i<subPanel.transform.childCount;i++)
        //반복문을 통해 서브타일의 목록을 저장한다.(한가지 타일로 되어있기 때문에 그대로 저장하면 된다.)
        {
            save = save+subPanel.transform.GetChild(i).GetChild(0).GetComponent<Text>().text;
        }
        save = save +"#";//'#' 추가
        save = save + TouchManager.Inst.emptyTiles.transform.childCount;
        //empty 타일은 플레이어가 보기에서 메인 함수로 타일을 옮기거나 커서를 통해 빈 칸을 채운다면, 채우고 난 후 원래
        //메인 패널에 설정되어 있는 흰색 패널이다. 이 흰색 패널은 비활성화된 후, emptyTiles의 자식 오브젝트가 된다.
        save = save +"#";
        foreach(var row in wordLists)
        {
            save = save + row.word+"*";
        }
        //wordlist의 정보를 입력한다.(위 정보는 다시 split되기 때문에 '*'로 추가하였다.)
        save = save + "#";
        foreach(var row in wordLists)
        {
            if(row.direction)
                save = save +"1"+"*";
            else
                save = save +"0"+"*";
        }
        //단어의 정보를 입력한다.(단어의 방향)
        save = save + "#";
        foreach(var row in wordLists)
        {
            save = save + row.x.ToString()+"*";
        }
        //단어의 정보를 입력한다.(단어의 시작 x좌표)
        save = save + "#";
        foreach(var row in wordLists)
        {
            save = save + row.y.ToString()+"*";
        }
        //단어의 정보를 입력한다.(단어의 시작 y좌표)
        save = save + "#";
        save = save + hint.ToString() + "#";
        //남은 힌트의 갯수를 저장
        PlayerPrefs.SetString("save",save);
        //PlayerPrefs에 저장(위 데이터는 앱을 종료해도 남아있게 된다.)
        Debug.Log(save);
    }
    public void LoadGame()
    //save했던 것이 존재했을 때 동작(loadgame이 동작하거나, save가 없다면 새로 생성)
    {
        empty.RemoveRange(0,empty.Count);
        wordLists.RemoveRange(0, wordLists.Count);
        string load = PlayerPrefs.GetString("save");
        string[] loads = load.Split('#');
        string[] loadTiles = loads[1].Split('*');
        startTime = ((int)Time.time) - int.Parse(loads[0]);
        SetMainPanel();
        for(int i=0;i<index; i++)
        {
            for(int j=0;j<index;j++)
            {
                if (loadTiles[index * i + j][0] == '1')
                {
                    if (loadTiles[index * i + j][1] == '1')
                    {
                        tiles[i,j].transform.GetChild(0).GetComponent<Text>().text = "";
                        tiles[i,j].GetComponent<Image>().sprite = emptyTile[Random.Range(0, emptyTile.Count())];
                    }
                    else
                    {
                        tiles[i,j].transform.GetChild(0).GetComponent<Text>().text = loadTiles[index*i+j][1].ToString();
                        tiles[i,j].GetComponent<Image>().sprite = hitTile[Random.Range(0, hitTile.Count())];
                        tiles[i,j].GetComponent<BoxCollider2D>().enabled = true;
                        tiles[i,j].tag = "Hit";
                    }
                }
                else if (loadTiles[index * i + j][0] == '2')
                {
                    tiles[i,j].transform.GetChild(0).GetComponent<Text>().text = loadTiles[index*i+j][1].ToString();
                    tiles[i,j].GetComponent<Image>().sprite = existTile[Random.Range(0, existTile.Count())];
                    tiles[i,j].GetComponent<BoxCollider2D>().enabled = false;
                    tiles[i,j].tag = "Done";
                }
                else if (loadTiles[index * i + j][0] == '3')
                {
                    tiles[i,j].transform.GetChild(0).GetComponent<Text>().text = "";
                    tiles[i,j].GetComponent<Image>().sprite = nullTile[Random.Range(0, nullTile.Count())];
                    tiles[i,j].GetComponent<BoxCollider2D>().enabled = false;
                    tiles[i,j].tag = "Done";
                }
                tiles[i,j].transform.DOMoveZ(0.0f, 0f);
            }
        }

        for (int i = 0; i < loads[2].Length; i++)
        {
            var tile = Instantiate(tilePrefab);
            tile.transform.parent = subPanel.transform;
            tile.transform.localScale = new Vector3(1.0f, 1.0f, 1.0f);
            tile.transform.GetChild(0).GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, (900 / 8) - 8);
            tile.transform.GetChild(0).GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, (900 / 8) - 8);
            tile.transform.GetChild(0).GetComponent<Text>().text = loads[2][i].ToString();
            tile.GetComponent<Image>().sprite = hitTile[Random.Range(0, hitTile.Count())];
            tile.tag = "Hit";
            subTiles.Add(tile);
            tile.transform.DOMoveZ(0.0f,0f);
        }
        subPanel.GetComponent<GridLayoutGroup>().cellSize = new Vector2(105f, 105f);
        for(int i =0;i<loads[3].Length;i++)
        {
            var tile = Instantiate(tilePrefab);
            tile.transform.parent = TouchManager.Inst.emptyTiles.transform;
            tile.transform.localScale = new Vector3(1.0f, 1.0f, 1.0f);
            tile.GetComponent<Image>().sprite = emptyTile[Random.Range(0, emptyTile.Count())];
            tile.SetActive(false);
        }
        string[] words = loads[4].Split('*');
        string[] direction = loads[5].Split('*');
        string[] xPos = loads[6].Split('*');
        string[] yPos = loads[7].Split('*');
        for(int i =0;i<words.Length-1;i++)
        {
            wordInfo wordinfo = new wordInfo();
            wordinfo.len = words[i].Length;
            wordinfo.word = words[i];
            wordinfo.x=int.Parse(xPos[i]);
            wordinfo.y=int.Parse(yPos[i]);
            if(direction[i]=="1")
                wordinfo.direction = true;
            else
                wordinfo.direction = false;
            wordLists.Add(wordinfo);
        }
        hint = int.Parse(loads[8]);
        hintText.text="힌트 : "+hint.ToString();
        PlayerPrefs.DeleteKey("save");
        done = true;
        CheckClear();
    }

9.플레이

플레이할 때는 서브패널에서 드래그를 통해 메인패널에 옮겨 넣는 방식과 커서를 옮겨가며 서브패널의 타일을 터치하여 옮겨 넣는 방식이 있다. 두 방식 모두 타일의 스크립트와 Touch 스크립트를 통해 동작한다.

1)MouseDown

    public void MouseDown(GameObject gameObject)
    //마우스로 클릭을 시작하였을 때 동작하는 함수이다.(tile에서 호출)
    {
        if (gameObject.transform.parent.gameObject == mainPanel.gameObject)
        //메인패널의 tile을 터치하였을 때 
        {
            int childIndex = gameObject.transform.GetSiblingIndex();
            //몇번째 index였는지 확인
            gameObject.transform.parent = tiles.transform;
            //tiles는 움직이고 있는 tile의 부모 오브젝트이다.(터치하면 이 오브젝트의 자식이된다.)
            var child = emptyTiles.transform.GetChild(0);
            //emptyTiles는 tile을 메인타일로 옮겼을 때 옯긴 위치의 빈 타일이 이 오브젝트의 자식으로 옮겨진다.
            child.gameObject.SetActive(true);
            //메인타일의 tile을 옮긴 터치하면 글자가 있는 tile을 손가락 tiles의 자식이되고, emptyTiles의 자식 중 제일 
            //index가 작은 tile이 다시 터치한 위치에 오게 된다.
            child.transform.parent = mainPanel.transform;
            child.transform.SetSiblingIndex(childIndex);
        }
        else
        //서브패얼의 tile을 터치하였을 때
        {
            gameObject.transform.parent = tiles.transform;
            gameObject.transform.GetChild(0).GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, (900 / index) - 8);
            gameObject.transform.GetChild(0).GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, (900 / index) - 8); //터치하였을 경우 iindex의 키기에 따라 자식 오브젝트인 text의 크기가 작아져야 tile
            //에서 오버되지 않는다. 그러므로 크기에 맞춰 작게 만든다.
            gameObject.GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, size);
            gameObject.GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, size);
            //터치한 오브젝트 또한 메인패널과 크기가 같아야 하므로 크기를 조정한다.
        }
    }

2)MouseDrag

    public void MouseDrag(GameObject gameObject)
    //드래그는 mousedown과 mouseup 사이에 동작하는 함수이다.(tile에서 호출)
    {
        Vector3 mousePosition = new Vector3(Input.mousePosition.x,Input.mousePosition.y, 10f);
        Vector3 objPosition = Camera.main.ScreenToWorldPoint(mousePosition);
        gameObject.transform.position = objPosition;
        //마우스 포지션인 터치하고 있는 손가락을 따라다닌다
    }

3)MouseUp1

    public void MouseUp(GameObject gameObject)
    //터치를 멈출 때 두가지 동작이 있는데, 일반적인 동작으로 메인패널 위에 놓았을 때 그 위치에 빈 칸이 있을 경우와 서브패널 위에서
    //터치를 멈출 때로 나뉜다. 메인패널에서 터치를 짧게하면 서브패널로 오는 동작도 있다(MouseUp2)
    {
        gameObject.GetComponent<BoxCollider2D>().enabled = false;
        //터치하고 있는 오브젝트에 손가락과 충돌이 인식되므로 함수가 동작할 동안은 enabled을 false 해둔다.
        Vector2 touchPosition  = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        Ray2D ray = new Ray2D(touchPosition,Vector2.zero);
        RaycastHit2D hit = Physics2D.Raycast(ray.origin,ray.direction);
        //레이캐스트2D를 통해 겹친 부분을 찾는다. (손가락과)
        if(hit.collider !=null)
        //터치에 반응하는 부분이 있다면
        {
            if (hit.collider.gameObject.tag == "Tile")
            //만약 빈 타일이라면 인식된 위치로 손가락으로 드래그하고 있는 타일이 그 자리로 이동한다.
            //Tag는 Tile, Done, Hit이 있다(tile들의 Tag)
            //Tile만 Collider 2D를 활성화 시켜 충돌을 인식할 수 있다.
            {
                int childIndex = hit.collider.gameObject.transform.GetSiblingIndex();
                hit.collider.gameObject.transform.parent = emptyTiles.transform;
                gameObject.transform.parent =mainPanel.transform;
                gameObject.transform.SetSiblingIndex(childIndex);
                hit.collider.gameObject.SetActive(false);
                audioManager.GetComponents<AudioSource>()[2].Play(); // SFX 사운드 동작
            }
            else
            //만약 빈 타일이 아니라면 다시 서브 패널로 돌아간다.
            {
                gameObject.transform.parent = subPanel.transform;
                gameObject.transform.GetChild(0).GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, (900 / 8) - 8);
                gameObject.transform.GetChild(0).GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, (900 / 8) - 8);
            }
        }
        else
        //터치에 반응하는 부분이 없다면 다시 서브 패널로 돌아온다.
        {
            gameObject.transform.parent = subPanel.transform;
            gameObject.transform.GetChild(0).GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, (900 / 8) - 8);
            gameObject.transform.GetChild(0).GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, (900 / 8) - 8);
        }
        gameObject.GetComponent<BoxCollider2D>().enabled = true;
    }

4)MouseUp2

    public void MouseUp2(GameObject tile)
    //메인타일에서 살짝 터치하였을 때 동작한다.
    //터치한 tile은 서브패널로 되돌아온다.
    //Tile Tag를 가지고 있는 오브젝트에서 동작한다.
    {
        audioManager.GetComponents<AudioSource>()[3].Play();//SFX 사운드 동작
        if(subPanel.transform.childCount==0)
        //만약 서브패널이 빈 칸이라면
        {
            Vector3 tilePosition = new Vector3(-1.1f,-0.9f,0);
            //서브패널 첫번째 index의 tile 포지션
            tile.transform.GetChild(0).GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, (900 / 8) - 8);
            tile.transform.GetChild(0).GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, (900 / 8) - 8);
            tile.GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, 105f);
            tile.GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 105f);
            //서브타일 크기에 맞게 크기 줄이기
            Sequence sequence = DOTween.Sequence()
                .Append(tile.transform.DOMove(tilePosition, 0.1f)).SetEase(Ease.Unset)
                .OnComplete(() => tile.transform.parent = subPanel.transform);
            //Dotween을 통해 애니메이션을 추가하여 이동
        }
        else
        //서브패널이 빈 칸이 아니라면
        {
            Vector3 tilePosition = subPanel.transform.GetChild(subPanel.transform.childCount - 1).transform.position;
            //서브패널의 마지막 tile의 포지션
            if (tilePosition.x >= 0.9f) //마지막 tile의 옆으로 이동
                tilePosition = new Vector3(-1.1f, tilePosition.y - 0.3f, 0f);
            else //마지막 tile의 위치가 y축에서 마지막 위치라면 밑 줄 제일 첫 칸으로 이동
                tilePosition = new Vector3(tilePosition.x + 0.3f, tilePosition.y, 0f);
            tile.transform.GetChild(0).GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, (900 / 8) - 8);
            tile.transform.GetChild(0).GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, (900 / 8) - 8);
            tile.GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, 105f);
            tile.GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, 105f);
            Sequence sequence = DOTween.Sequence()
                .Append(tile.transform.DOMove(tilePosition, 0.1f)).SetEase(Ease.Unset)
                .OnComplete(() => tile.transform.parent = subPanel.transform);
            //위와 동일
        }
    }

5)SetCurser

    public void SetCurser()
    //커서를 제일 첫 단어의 빈 칸에 생성
    {
        GameObject[] tiles = new GameObject[index*index];
        for(int i =0;i<index *index;i++)
        {
            tiles[i] = mainPanel.transform.GetChild(i).gameObject;
        }
        int first = GameManager.Inst.GetFirst();
        //첫 단어부터 확인하여 제일 첫번째 빈 칸의 index
        //찾지 못할 경우 -1을 반환
        if (first != -1)
        {
            curser.SetActive(true);
            curser.GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, size + 3);
            curser.GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, size + 3);
            //메인패널의 tile 보다는 조금 크게 위치하게 한다.
            curser.transform.position = tiles[first].transform.position;
            curserX = first / index;
            curserY = first % index;
            //커서의 현재 위치를 저장 
        }
        else
        //찾지 못할 경우 제일 앞 index의 빈 칸에 위치
        {
            for (int i = 0; i < index * index; i++)
            {
                if (tiles[i].tag == "Tile")
                {
                    curser.SetActive(true);
                    curser.GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Vertical, size + 3);
                    curser.GetComponent<RectTransform>().SetSizeWithCurrentAnchors(RectTransform.Axis.Horizontal, size + 3);
                    curser.transform.position = tiles[i].transform.position;
                    curserX = i / index;
                    curserY = i % index;
                    break;
                }
            }
        }
    }

6)MoveCurser

    public void MoveCurser(GameObject gameObject,int index1)
    //터치한 곳으로 커서를 이동시킨다.
    {
        GameObject[] tiles = new GameObject[index * index];
        int moveindex = 0;
        for (int i = 0; i < index * index; i++)
        {
            tiles[i] = mainPanel.transform.GetChild(i).gameObject;
            if(gameObject == tiles[i])
                moveindex = i;
        }
        if (index1 == -1)
        {
            Sequence sequence = DOTween.Sequence()
                .Append(curser.transform.DOMove(tiles[moveindex].transform.position, 0.2f)).SetEase(Ease.Unset)
                .Append(curser.transform.DOLocalMoveZ(0, 0));
            curserX = moveindex / index;
            curserY = moveindex % index;
        }
        else
        {
            Sequence sequence = DOTween.Sequence()
                .Append(curser.transform.DOMove(tiles[index1].transform.position, 0.2f)).SetEase(Ease.Unset)
                .Append(curser.transform.DOLocalMoveZ(0, 0));
            curserX = index1 / index;
            curserY = index1 % index;
        }
    }

7)MoveTile

    public void MoveTile(GameObject gameObject)
    //서브패널에서 tile을 터치할 경우 커서의 위치로 tile을 이동시킨다.
    {
        GameObject[] Tiles = new GameObject[index * index];
        for (int i = 0; i < index * index; i++)
            Tiles[i] = mainPanel.transform.GetChild(i).gameObject;
        if(Tiles[curserX*index+curserY].tag == "Tile")
        //만약 커서의 위치의 tile이 빈 칸이라면
        {
            var emp = Tiles[curserX*index+curserY];
            Sequence sequence = DOTween.Sequence()
                .Append(gameObject.transform.DOMove(emp.transform.position, 0.1f)).SetEase(Ease.Unset)
                .OnComplete(()=>emp.transform.parent =emptyTiles.transform );
            emp.SetActive(false);
            gameObject.transform.parent = mainPanel.transform;
            gameObject.transform.SetSiblingIndex(curserX*index+curserY);
            //위의 mouseup과 동일한 방식으로 이동
        }
        else if(Tiles[curserX*index+curserY].tag == "Hit")
        //만약 커서 위치에 글자 tile이 있다면
        {
            var swap = Tiles[curserX*index+curserY];
            var swapos = swap.transform.position;
            swap.transform.parent = tiles.transform;
            MouseUp2(swap);//글자 tile은 서브패널로 되돌아온다.
            var emp = emptyTiles.transform.GetChild(0).gameObject;
            emp.transform.parent = mainPanel.transform;
            emp.transform.SetSiblingIndex(curserX*index+curserY);
            emp.SetActive(true);
            Sequence sequence = DOTween.Sequence()
                .Append(gameObject.transform.DOMove(swapos, 0.1f)).SetEase(Ease.Unset)
                .OnComplete(()=>emp.transform.parent =emptyTiles.transform);
            emp.SetActive(false);
            gameObject.transform.parent = mainPanel.transform;
            gameObject.transform.SetSiblingIndex(curserX*index+curserY);
        }
        else
        {
            MouseUp2(gameObject);
            //만약 버그로 인해 tag가 위의 두개가 아닐 경우 서브패널로 tile을 되돌린다.
        }
        StartCoroutine(AutoMoveCurser()); //자동으로 커서의 위치를 옮긴다.
    }

8)AutoMoveCurser

    public IEnumerator AutoMoveCurser()
    //커서를 통해 tile이 이동할 경우 커서는 자동적으로 커서 위치의 단어의 다른 빈칸으로 가거나
    //다음 단어의 첫 빈 칸으로 이동한다.
    {
        yield return new WaitForSeconds(0.25f); // 0.25초 뒤에 반응
        int pos = GameManager.Inst.GetPosition(curserX,curserY);
        //움직일 위치의 index를 반환
        Debug.Log(pos);
        GameObject[] Tiles = new GameObject[index * index];
        for (int i = 0; i < index * index; i++)
            Tiles[i] = mainPanel.transform.GetChild(i).gameObject;
        if(pos!=-1)
            MoveCurser(Tiles[pos],pos); //커서를 이동
    }

9)CheckClear

    public IEnumerator CheckClear()
    //타일이 이동이 있을 때마다 스테이지가 클리어 되었는지 확인
    {
        yield return new WaitForSeconds(0.1f);
        Debug.Log("Check");
        GameManager.Inst.CheckClear();
    }

10)StageClear

    IEnumerator StageClear()
    //스테이지를 클리어했는지 확인한다.
    {
        TouchManager.Inst.curser.SetActive(false);
        //커서를 비활성화
        PlayerPrefs.DeleteKey("save");
        //스테이지가 저장되어있던 것을 삭제
        int time = ((int)Time.time)-startTime;//시간 반환(초)
        Debug.Log(time+"초 걸림!");
        TouchManager.Inst.audioManager.GetComponents<AudioSource>()[4].Play();//클리어 사운드
        yield return null;
        UIManager.Inst.DoMove(clearPanel);
        clearPanel.SetActive(false);
        yield return new WaitForSeconds(0.1f);
        clearPanel.transform.GetChild(0).GetComponent<Text>().text = stageNum.ToString() +"단계 클리어!!";
        clearPanel.transform.GetChild(1).GetComponent<Text>().text = "<color=#ff0000>" + (((int)Time.time)-startTime).ToString() + "</color>"  +"초 걸림!!!";//빨간색으로
        clearPanel.SetActive(true);
        stageText.text = "";
        AddWordsMyAccount();//서버와 관련됨
        var bro = Backend.GameData.Get("Info", new Where());
        if (bro.IsSuccess())
        {
            var MyInfo = Backend.GameData.GetMyData("Info", new Where(), 10);
            int wordcnt = int.Parse(MyInfo.Rows()[0]["wordcnt"][0].ToString());
            Param param = new Param();
            param.Add("stage", stageNum + 1);
            param.Add("wordcnt", wordcnt + wordLists.Count);
            Backend.GameData.Update("Info", new Where(), param);
            Backend.URank.User.UpdateUserScore("0abc3e30-5be5-11ec-9ddb-b3b31a0d7eae", "Info", bro.GetInDate().ToString(), param);
            if(CheckAchieve(time))
            {
                StartCoroutine(AlarmAchieve());
            }
        }
        else
        {
            UIManager.Inst.DoMove(UIManager.Inst.failPanel);
        }
    }

11)Tile.cs

public class Tile : MonoBehaviour
{
    string par = "";
    int index =-1;
    public Vector3 mousePos;
    void OnMouseDown()
    //오브젝트 터치를 시작하였을 때
    {
        if(this.gameObject.tag == "Hit")
        {
            mousePos = Input.mousePosition;
            par = this.gameObject.transform.parent.name;
            if(par=="MainPanel")
            {
                index =this.transform.GetSiblingIndex();
            }
            TouchManager.Inst.MouseDown(this.gameObject);
        }
    }
    void OnMouseDrag()
    //드래그 중
    {
        if(this.gameObject.tag == "Hit")
        {
            TouchManager.Inst.MouseDrag(this.gameObject);
        }
    }
    void OnMouseUp()
    //터치를 끝냈을 때
    {
        Vector3 test = mousePos - Input.mousePosition;
        double testm = Math.Pow(test.x,2) + Math.Pow(test.y,2);
        //짧게 터치와 길게 터치를 구분할 방법을 찾다가 이동 거리를 확인하여 
        //이동거리가 특정한 상황이 되면 짧게 터치로 간주한다.
        if(this.gameObject.tag == "Hit")
        {
            if(testm<1000)
            {
                if(par=="MainPanel")
                {
                    TouchManager.Inst.MouseUp2(this.gameObject);
                    TouchManager.Inst.MoveCurser(this.gameObject,index);
                    index = -1;
                }
                else if(par=="SubPanel")
                    TouchManager.Inst.MoveTile(this.gameObject);
            }
            else
            {
                TouchManager.Inst.MouseUp(this.gameObject);   
            }
        }
        else if(this.gameObject.tag == "Tile")
            TouchManager.Inst.MoveCurser(this.gameObject,index);
        
        StartCoroutine(TouchManager.Inst.CheckClear());
    }
}