반응형
테트리스의 역사

테트리스

출처 : 나무위키

1984년 소련의 프로그래머 알렉세이 파지노프(Alexey Leonidovich Pajitnov)가 만들었고 21년 6월 6일에 37주년을 맞았다. 전통 퍼즐 게임인 '펜토미노(Pentomino)'를 개량하여 만든것. 하지만 5개의 정사각형으로 조합된 도형들을 이용하던 '펜토미노'는 1984년 당시의 기술로는 게임화하기에 다소 복잡해서 4개의 사각형을 조합한 '테트로미노(Tetromino)'를 사용, 개량하면서 이름도 그리스어 접두사 'Tetra~(4개의)'와 개발자가 좋아하던 스포츠인 '테니스(Tennis)'의 끝자리를 따와서 붙였다.

 

퍼즐게임의 대표주자이자, 세계에서 8번째로 많이 팔린 게임 시리즈. 배우긴 쉽지만 마스터하는건 어려운 게임. 저작권을 관리하는 테트리스 컴퍼니가 2003년 3월 한국에 상륙하여 테트리스의 저작권을 주장하여 국내 테트리스 게임이 대거 서비스가 중단되는 테트리스 대란이 일어났다. 대부분의 회사들은 소송을 감수하고 서비스를 계속할 실익이 없다고 판단하고 서비스를 중단하거나, 돈을 주고 서비스를 계속하는 길을 선택하였다. 아직 한국에는 테트리스 게임을 따와서 배포할때 저작권을 침해하는 지에 대한 판례는 없다. 다만 한국에서도 테트리스 컴퍼니 측이 '테트리스' 명칭에 대한 상표권을 취득했기 때문에 테트리스 컴퍼니가 아닌 곳에서 '테트리스'라는 명칭을 사용해 상업적인 게임을 출시할 수는 없다고 한다. ㅠㅠ

계약을 하더라도 테트리스 컴퍼니의 동의 없이는 게임 룰을 변경할 수 없게 하고 있다. 한게임에서 계약한 서비스를 했었지만 2013년 이용자 수 부족으로 서비스 종료. 모바일 버전은 컴투스가 꾸준히 내다가 라리선스가 EA로 넘어갔으나, 2020년 4월에 서비스가 종료. 현재는 중국 산하 N3TWORK에서 모바일 라이선스를 취득해 스마트폰용 테트리스가 출시되었으며, Primetime(프라임타임)이라는 기간을 정해 테트리스 대호를 개최하고 있는데, 상금까지 걸려있으므로 자신이 고수라고 생각한다면 참가해 볼 만 하다.

 

테트로미노(Tetromino), 폴리오미노(Polyomino)에 대해

테트로미노(Tetromino)

또는 4-오미노(4-omino)는 4개의 정사각형으로 이루어진 폴리오미노이다. 5가지의 자유테트로미노, 7가지의 단면 테트로미노, 19가지의 고정 테트로미노가 있다. 단면 테트로미노 7가지는 테트리스에서 이용된다.

 

폴리오미노(Polyomino)

하버드 대학교의 솔로몬 골롬(Solomon W. Golomb) 박사가 수학 강의 중에서 처음 사용한, n개의 정사각형들이 서로 최소한 1개의 변을 공유하여 만들어지는 다각형들을 총칭하며 위 표의 형태를 말한다. n이 몇개인가에 따라 부르는 이름이 달라지는데 두개일 경우 우리가 많이 먹는 도미노피자 이름이 있는걸 알수 있다.

 

 

 

테트로미노 Prefab 제작

2DSprite 1x1사이즈를 만들고 이를 기본으로 4개의 블록을 가지는 7가지 테트로미노 프리팹을 제작한다.

회전을 고려 하여 축을 설정 한다.

 

 

그리드 설정

좌측 최하단 을 0,0으로 10x20의 그리드를 생성한다. 포토샾으로 미리 제작해서 가져와도 되지만 여기서는 유니티에서 직접 2DSprite를 사용해 레이아웃을 그렸다.

 

 

 

테트리스 컨트롤 정의 및 구현

기본적으로 각 테트로미노프리팹에 TetrisBlock.cs 스크립이 붙게 되고 아래 설명되는 코드들은 모두 이 스크립트에 들어간다.

 

1. 움직임 정의

왼쪽, 오른쪽, 아래, 한번에 내리기, 회전을 할 수 있는 기능을 넣을 예정이고 아래 키에 맵핑한다.

2. 유동범위 체크

블록이 움직일 수 있는 유동 가능한 번위를 체크한다.

유동범위체크 스크립트

public static int Height = 20;
public static int Width = 10;

// 블록들을 저장하고 체크하기 위한 10x20 그리드 데이터
private static Transform[,] grid = new Transform[Width, Height]; 

bool ValidMove()
{
	foreach (Transform children in transform) // 4씩 존재함.
	{
        int roundedX = Mathf.RoundToInt(children.transform.position.x);
        int roundedY = Mathf.RoundToInt(children.transform.position.y);

        if (roundedX < 0 || roundedX >= Width || roundedY < 0 || roundedY >= Height)
        return false; // 그리드 좌표를 벗어나면 false

        if (grid[roundedX, roundedY] != null) // 그리드에 블록이 있으면 false
        return false;
	}
	return true;
}

 

3. 아래로 움직이기

키가 눌리지 않아도 자동으로 밑으로 움직여야 하고 키가 길게 눌리면 빠르게 이동된다.

public float FallTime = 0.8f;

if (Time.time - previousTime > (Input.GetKey(KeyCode.DownArrow) ? FallTime / 14 : FallTime))
{
     moveDown();
}

void moveDown()
{
     // Down
     transform.position += new Vector3(0, -1, 0);
     if (!ValidMove())
     {
          transform.position -= new Vector3(0, -1, 0);
          AddToGrid();
          checkForLines();
          this.enabled = false;
          if (!isGameOver)
               spawTetromino.NewTetromino();
     }
     previousTime = Time.time;
}

 

4. 왼쪽, 오른쪽 이동

if ((Input.GetKey(KeyCode.LeftArrow) && Time.time - previousTimeLeft > (Input.GetKey(KeyCode.LeftArrow) ? FallTime / 8 : FallTime)))
     moveLeft();
else if ((Input.GetKey(KeyCode.RightArrow) && Time.time - previousTimeRight > (Input.GetKey(KeyCode.RightArrow) ? FallTime / 8 : FallTime)))
     moveRight();

void moveLeft()
{
     // Left
     transform.position += new Vector3(-1, 0, 0);
     if (!ValidMove())
          transform.position -= new Vector3(-1, 0, 0);
     previousTimeLeft = Time.time;
}
void moveRight()
{
     // Right
     transform.position += new Vector3(1, 0, 0);
     if (!ValidMove())
          transform.position -= new Vector3(1, 0, 0);
     previousTimeRight = Time.time;
}

 

5. 회전

if (Input.GetKeyDown(KeyCode.UpArrow))
     rotateBlock();

void rotateBlock()
{
     transform.RotateAround(transform.TransformPoint(RotationPoint), new Vector3(0, 0, 1), 90);
     if (!ValidMove())
          transform.RotateAround(transform.TransformPoint(RotationPoint), new Vector3(0, 0, 1), -90);
     // 터치커맨드 리셋 연타방지용
     touchButtons.MyCommand = Command.None;
}

 

 

 

바닥에 닿을 때 처리 할 것들

기본적으로 10x20의 그리드의 정보를 처리 할 수 있는 Array[,]를 만들어 두고 데이터를 관리한다. 해당 그리드에 블록이 있는지 없는지 체크하고 삭제하거나 입력한다.

 

1. 그리드에 해당 블록 추가

블록이 내려와 바닥에 닿거나 블록이 있어서 더이상 내려갈 수 없을 때 해당 그리드에 블록을 추가한다.

private static Transform[,] grid = new Transform[Width, Height]; // 블록들을 저장하고 체크하기 위한 10x20 그리드 데이터

void AddToGrid()
{
     foreach (Transform children in transform)
     {
          int roundedX = Mathf.RoundToInt(children.transform.position.x);
          int roundedY = Mathf.RoundToInt(children.transform.position.y);

          if (roundedY < 20)
               grid[roundedX, roundedY] = children; // 해당 좌표를 그리드에 입력한다.
          else
               gameOver();
     }
}

 

2. 가로라인 블록 체크

그리드의 행을 기준으로 블록이 가득 차있는지 체크한다.

void checkForLines()
{
     int cnt = 0;
     for (int i = Height - 1; i >= 0; i--)
     {
          if (hasLine(i)) // 채워진 라인이 있는지 확인하고 있다면 삭제한 후 아래로 내려준다.
          {
               DeleteLine(i);
               RowDown(i);
               cnt++;
          }
     }
}

bool hasLine(int i) // 해당 라인이 모두 채워졌는지 확인하여 true, false 반환
{
     for (int j = 0; j < Width; j++)
          if (grid[j, i] == null)
               return false;
     return true;
}

 

3. 라인 삭제 및 블록 아래로 이동

해당 라인에 블록이 모두 채워졌을 때 삭제하고 해당 라인 위쪽 블록들을 아래로 내려준다.

void DeleteLine(int i)
{
     for (int j = 0; j < Width; j++)
     {
          Instantiate(sparkPref, grid[j, i].gameObject.transform.position, Quaternion.identity); // 이펙트효과 추가
          Destroy(grid[j, i].gameObject);
          grid[j, i] = null;
     }
}

void RowDown(int i)
{
     for (int y = i; y < Height; y++)
          for (int j = 0; j < Width; j++)
               if (grid[j, y] != null)
               {
                   grid[j, y - 1] = grid[j, y];
                   grid[j, y] = null;
                   grid[j, y - 1].transform.position -= new Vector3(0, 1, 0);
               }
}

 

 

 

 

Spawn 생성과 관리

 

SpawnTetromino.cs파일을 생성하고 작성 후 같은 이름으로 하이어라키에 위치시킨다.

 

1. Spawn(생성)

7개의 테트로미노중 랜덤으로 하나의 테트로미노가 생성되고 다음 테트로미노도 보이게 한다.

void createTetromino()
{
     if (isFirst)
     {
          isFirst = false;
          targetSpawn = Instantiate(Tetrominoes[Random.Range(0, Tetrominoes.Length)], transform.position, Quaternion.identity);
          ListTetrominoes.Add(targetSpawn);
          nextSpawn = Instantiate(Tetrominoes[Random.Range(0, Tetrominoes.Length)], transform.position + new Vector3(4, 1f, 0), Quaternion.identity);
          nextSpawn.GetComponent<TetrisBlock>().enabled = false;
          nextSpawn.transform.localScale = new Vector3(0.5f, 0.5f, 0.5f);
     } else {
          targetSpawn = nextSpawn;
          targetSpawn.transform.position = transform.position;
          targetSpawn.transform.localScale = Vector3.one;
          targetSpawn.GetComponent<TetrisBlock>().enabled = true;
          ListTetrominoes.Add(targetSpawn);
          nextSpawn = null;
          nextSpawn = Instantiate(Tetrominoes[Random.Range(0, Tetrominoes.Length)], transform.position + new Vector3(4, 1f, 0), Quaternion.identity);
          nextSpawn.GetComponent<TetrisBlock>().enabled = false;
          nextSpawn.transform.localScale = new Vector3(0.5f, 0.5f, 0.5f);
          destoroyCheck();
     }
}

 

2. 삭제된 테트로미노 관리

테트로미노 안쪽 4개의 블록 오브젝트가 모두 파괴되면 나머지 오브젝트도 체크해서 삭제 처리한다.

public List<GameObject> ListTetrominoes;

void destoroyCheck()
{
     if (ListTetrominoes.Count > 0)
     {
          for (int i = 0; i < ListTetrominoes.Count; i++)
          {
               if (ListTetrominoes[i].transform.childCount == 0)
               {
                   Destroy(ListTetrominoes[i]);
                   ListTetrominoes.RemoveAt(i);
               }
          }
     }
}

 

 

전체스크립트 및 하이어라키 구조

 

GameData.cs

게임데이터를 관리하기 위한 스크립트이다. 하이어라키에 해당이름으로 빈게임오브젝트를 만들고 해당 스크립트를 얹어준다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameData : MonoBehaviour
{
    public int Score;
    public int PlayTime;
    public int PlayLevel;
    public int PlayCombo;
}

 

GameOver.cs

하이어라키에 게임오버 화면을 미리 만들고 해당 스크립트를 얹어준다. 

using UnityEngine.SceneManagement;
using UnityEngine;
using UnityEngine.UI;

public class GameOver : MonoBehaviour
{
    public Button Btn_Restart;
    void Start()
    {
        Btn_Restart.onClick.AddListener(Reset);
    }

    void Reset()
    {
        SceneManager.LoadScene("Main");
    }
}

 

SelfDestroy.cs

블록이 사라질 때 파티클 오브젝트를 사라지게 하기 위한 스크립트이다. 파티클 프리팹에 해당 스크립트를 붙여주면 해당 시간이 지난 후 알아서 자체적으로 디스트로이 된다.

using UnityEngine;

public class SelfDestroy : MonoBehaviour
{
    public float LifeTime = 3;
    void Start()
    {
        Invoke("destroyMe", LifeTime);
    }

    void destroyMe()
    {
        Destroy(this.gameObject);
    }
}

 

SoundController.cs

효과음을 관리하는 파일이다. 마찬가지로 하이어라키에 위치시켜준다. 각 효과음을 미리 준비하여 mp3파일 형태로 만들어두고 인스펙터에 해당 효과음을 넣어준다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum SoundClip
{
    Drop,
    Explo,
    Explo4,
    GameStart,
    GameOver
}
public class SoundController : MonoBehaviour
{
    public AudioClip A_Drop, A_Explo, A_Explo4lines, A_GameStart, A_GameOver;
    public AudioSource myAudio;

    public void PlaySound(SoundClip audioType)
    {
        switch (audioType)
        {
            case SoundClip.Drop:
                myAudio.clip = A_Drop;
                break;
            case SoundClip.Explo:
                myAudio.clip = A_Explo;
                break;
            case SoundClip.Explo4:
                myAudio.clip = A_Explo4lines;
                break;
            case SoundClip.GameStart:
                myAudio.clip = A_GameStart;
                break;
            case SoundClip.GameOver:
                myAudio.clip = A_GameOver;
                break;
        }

        myAudio.Play();
    }
}

 

SpawnTetromino.cs

위에 설명한 생성관리를 해주는 스크립트이다. 마찬가지로 하이어라키에 아래와 같이 위치시켜주고 7개의 만들어둔 테트로미노 프리팹을 넣어준다.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class SpawnTetromino : MonoBehaviour
{
    public GameObject[] Tetrominoes;
    public List<GameObject> ListTetrominoes;
    GameObject nextSpawn;
    GameObject targetSpawn;
    bool isFirst = true;

    //-------------
    SoundController soundController;

    void Start()
    {
        if (soundController == null)
            soundController = FindObjectOfType<SoundController>();
        Application.targetFrameRate = 60;
        ListTetrominoes = new List<GameObject>();
        soundController.PlaySound(SoundClip.GameStart);
        NewTetromino();
    }

    public void NewTetromino()
    {
        Invoke("createTetromino", 0.5f);
    }

    void createTetromino()
    {
        if (isFirst)
        {
            isFirst = false;
            targetSpawn = Instantiate(Tetrominoes[Random.Range(0, Tetrominoes.Length)], transform.position, Quaternion.identity);
            ListTetrominoes.Add(targetSpawn);

            nextSpawn = Instantiate(Tetrominoes[Random.Range(0, Tetrominoes.Length)], transform.position + new Vector3(4, 1f, 0), Quaternion.identity);
            nextSpawn.GetComponent<TetrisBlock>().enabled = false;
            nextSpawn.transform.localScale = new Vector3(0.5f, 0.5f, 0.5f);
        }
        else
        {
            targetSpawn = nextSpawn;
            targetSpawn.transform.position = transform.position;
            targetSpawn.transform.localScale = Vector3.one;
            targetSpawn.GetComponent<TetrisBlock>().enabled = true;
            ListTetrominoes.Add(targetSpawn);

            nextSpawn = null;
            nextSpawn = Instantiate(Tetrominoes[Random.Range(0, Tetrominoes.Length)], transform.position + new Vector3(4, 1f, 0), Quaternion.identity);
            nextSpawn.GetComponent<TetrisBlock>().enabled = false;
            nextSpawn.transform.localScale = new Vector3(0.5f, 0.5f, 0.5f);

            destoroyCheck();
        }
    }

    void destoroyCheck()
    {
        if (ListTetrominoes.Count > 0)
        {
            for (int i = 0; i < ListTetrominoes.Count; i++)
            {
                if (ListTetrominoes[i].transform.childCount == 0)
                {
                    Destroy(ListTetrominoes[i]);
                    ListTetrominoes.RemoveAt(i);
                }
            }
        }
    }
}

 

TetrisBlock.cs

가장 핵심이 되는 코드 이다. 위에 설명한 부분들이 모두 들어가 있으며 7개의 테트로미노 프리팹에 각각 붙여준다.

using UnityEngine;

public class TetrisBlock : MonoBehaviour
{
    public Vector3 RotationPoint;
    public float FallTime = 0.8f;
    public static int Height = 20;
    public static int Width = 10;
    private float previousTime, previousTimeLeft, previousTimeRight;
    private static Transform[,] grid = new Transform[Width, Height];

    bool isGameOver;

    // other commponent ----------
    TouchButtons touchButtons;
    GameData gameData;
    SpawnTetromino spawTetromino;
    SoundController soundController;

    // vfx ----------
    [SerializeField] GameObject sparkPref;

    void Update()
    {
        if (touchButtons == null)
            touchButtons = FindObjectOfType<TouchButtons>();
        if (gameData == null)
            gameData = FindObjectOfType<GameData>();
        if (spawTetromino == null)
            spawTetromino = FindObjectOfType<SpawnTetromino>();
        if (soundController == null)
            soundController = FindObjectOfType<SoundController>();


        //=================== 키보드일 경우

        if ((Input.GetKey(KeyCode.LeftArrow) && Time.time - previousTimeLeft > (Input.GetKey(KeyCode.LeftArrow) ? FallTime / 8 : FallTime)))
            moveLeft();
        else if ((Input.GetKey(KeyCode.RightArrow) && Time.time - previousTimeRight > (Input.GetKey(KeyCode.RightArrow) ? FallTime / 8 : FallTime)))
            moveRight();
        else if (Input.GetKeyDown(KeyCode.UpArrow))
            rotateBlock();

        if (Time.time - previousTime > (Input.GetKey(KeyCode.DownArrow) ? FallTime / 14 : FallTime))
            moveDown();

        if (Input.GetKeyDown(KeyCode.Space)) // 터치
            dropBlock();

        //================== 터치일 경우
        if (touchButtons)
        {
            if (touchButtons.MyCommand == Command.Left && Time.time - previousTimeLeft > (touchButtons.MyCommand == Command.Left ? FallTime / 8 : FallTime))
                moveLeft();
            else if (touchButtons.MyCommand == Command.Right && Time.time - previousTimeRight > (touchButtons.MyCommand == Command.Right ? FallTime / 8 : FallTime))
                moveRight();
            else if (touchButtons.MyCommand == Command.Rotate)
                rotateBlock();

            if (touchButtons.MyCommand == Command.MoveDown)
                moveDown();

            if (touchButtons.MyCommand == Command.Down)
                dropBlock();
        }
    }

    void moveLeft()
    {
        // Left
        transform.position += new Vector3(-1, 0, 0);
        if (!ValidMove())
            transform.position -= new Vector3(-1, 0, 0);
        previousTimeLeft = Time.time;
    }
    void moveRight()
    {
        // Right
        transform.position += new Vector3(1, 0, 0);
        if (!ValidMove())
            transform.position -= new Vector3(1, 0, 0);
        previousTimeRight = Time.time;
    }
    void rotateBlock()
    {
        // Rotate!
        transform.RotateAround(transform.TransformPoint(RotationPoint), new Vector3(0, 0, 1), 90);
        if (this.tag == "Mino_I" || this.tag == "Mino_S" || this.tag == "Mino_Z") // 도는 범위가 넓어서 다시 잡아줌. 90도로 돌아갔다가 다시 원래 위치로
        {
            if (this.transform.localEulerAngles.z < 0 || this.transform.localEulerAngles.z > 90)
                this.transform.localEulerAngles = Vector3.zero;
        }
        else if (this.tag == "Mino_O") // 돌릴필요 없음
            this.transform.localEulerAngles = Vector3.zero;

        if (!ValidMove())
            transform.RotateAround(transform.TransformPoint(RotationPoint), new Vector3(0, 0, 1), -90);

        // 4개의 블록의 부모가 돌아가니 그림자가 돌아가는데 해당 블록들은 항상 그대로 있게 만들어준다.
        for (int i = 0; i < transform.childCount; i++)
            transform.GetChild(i).transform.rotation = Quaternion.identity;

        // 터치커맨드 리셋 연타방지용 
        touchButtons.MyCommand = Command.None;
    }
    void moveDown()
    {
        // Down
        transform.position += new Vector3(0, -1, 0);
        if (!ValidMove())
        {
            transform.position -= new Vector3(0, -1, 0);
            AddToGrid();
            checkForLines();
            this.enabled = false;
            if (!isGameOver)
                spawTetromino.NewTetromino();
        }
        previousTime = Time.time;
    }
    void dropBlock()
    {
        touchButtons.MyCommand = Command.None;
        FallTime = 0;
    }

    /// <sammary>
    /// 10x20 그리드 전체 라인을 체크해서 가로라인이 만들어 질 경우 해당 라인을 삭제하고 아래로 내려준다. 그리고 몇줄의 라인이 만들어 졌는지 체크해서 점수를 준다.
    /// </sammary>
    void checkForLines()
    {
        int cnt = 0;
        for (int i = Height - 1; i >= 0; i--)
        {
            if (hasLine(i)) // 채워진 라인이 있는지 확인하고 있다면 삭제한 후 아래로 내려준다.
            {
                DeleteLine(i);
                RowDown(i);
                cnt++;
            }
        }
        if (cnt == 0)
        {
            soundController.PlaySound(SoundClip.Drop);
            gameData.Score += 10;
        }
        else
        {
            Debug.Log("deleted lines : " + cnt);
            // Score
            gameData.Score += cnt * 100;

            if (cnt == 4)
                soundController.PlaySound(SoundClip.Explo4);
            else
                soundController.PlaySound(SoundClip.Explo);
        }
    }

    /// <sammary>
    /// 해당 가로라인에 블록이 모두 있는지 없는지를 체크한다.
    /// </sammary>
    bool hasLine(int i)
    {
        for (int j = 0; j < Width; j++)
            if (grid[j, i] == null)
                return false;
        return true;
    }

    /// <sammary>
    /// 해당 라인을 삭제한다.
    /// </sammary>
    void DeleteLine(int i)
    {
        for (int j = 0; j < Width; j++)
        {
            Instantiate(sparkPref, grid[j, i].gameObject.transform.position, Quaternion.identity);
            Destroy(grid[j, i].gameObject);
            grid[j, i] = null;
        }
        // soundController.PlaySound(SoundClip.Explo);
    }

    /// <sammary>
    /// 해당 가로 라인이 비어있을 경우 아래로 내려준다
    /// </sammary>
    void RowDown(int i)
    {
        for (int y = i; y < Height; y++)
            for (int j = 0; j < Width; j++)
                if (grid[j, y] != null)
                {
                    grid[j, y - 1] = grid[j, y];
                    grid[j, y] = null;
                    grid[j, y - 1].transform.position -= new Vector3(0, 1, 0);
                }
    }

    /// <sammary>
    /// 10x20 그리드에 해당 블록을 채워 넣어줌 채울 수 없을 경우 게임 오버
    /// <sammary>
    void AddToGrid()
    {
        foreach (Transform children in transform)
        {
            int roundedX = Mathf.RoundToInt(children.transform.position.x);
            int roundedY = Mathf.RoundToInt(children.transform.position.y);

            if (roundedY < 20)
                grid[roundedX, roundedY] = children;
            else
                gameOver();

        }
    }

    /// <summary>
    /// 게임오버가 됐을 경우 띄우는 화면
    /// </summary>
    void gameOver()
    {
        isGameOver = true;
        Debug.Log("Game Over@@");
        GameObject.FindWithTag("GameOver").gameObject.transform.GetChild(0).gameObject.SetActive(true);
        soundController.PlaySound(SoundClip.GameOver);
    }

    /// <summary>
    /// 10x20 그리드 안쪽에서 움직이고 있는지 아닌지를 판단
    /// </summary>
    bool ValidMove()
    {
        foreach (Transform children in transform)
        {
            int roundedX = Mathf.RoundToInt(children.transform.position.x);
            int roundedY = Mathf.RoundToInt(children.transform.position.y);

            if (roundedX < 0 || roundedX >= Width || roundedY < 0 || roundedY >= Height)
                return false;

            if (grid[roundedX, roundedY] != null)
                return false;
        }
        return true;
    }
}

인스펙터에 떨어지는 속도와 없어질때 사용 할 VFX효과 Prefab이 입력되어있다.

 

TouchButtons.cs

모바일 플랫폼을 위해 터치컨트롤 스크립트이다.

using UnityEngine;


public enum Command
{
    Left,
    Right,
    MoveDown,
    Down,
    Rotate,
    None
}

public class TouchButtons : MonoBehaviour
{

    public Command MyCommand;
    [SerializeField] TouchHandler BtnLeft, BtnRight, BtnMoveDown, BtnDown, BtnRotate;

    void Start()
    {
        BtnLeft.onTouchDown.AddListener(() => SetCommand(Command.Left));
        BtnRight.onTouchDown.AddListener(() => SetCommand(Command.Right));
        BtnRotate.onTouchDown.AddListener(() => SetCommand(Command.Rotate));
        BtnMoveDown.onTouchDown.AddListener(() => SetCommand(Command.MoveDown));
        BtnDown.onTouchDown.AddListener(() => SetCommand(Command.Down));

        BtnLeft.onTouchUp.AddListener(ResetCommand);
        BtnRight.onTouchUp.AddListener(ResetCommand);
        BtnRotate.onTouchUp.AddListener(ResetCommand);
        BtnMoveDown.onTouchUp.AddListener(ResetCommand);
        BtnDown.onTouchUp.AddListener(ResetCommand);
    }

    void SetCommand(Command type)
    {
        MyCommand = type;
        Debug.Log("type : " + type);
    }
    void ResetCommand()
    {
        Debug.Log("type : " + MyCommand);
        MyCommand = Command.None;
    }
}

화면과 같이 버튼을 위치 시켜주고 인스펙터의 My Command를 기본 None으로 선택해준다.

 

TouchHandler.cs는 사용하지 않음.

 

아직 완성까지는 아니지만 어느정도 완성된 플레이 영상이다.

 

 

반응형

'unity C#' 카테고리의 다른 글

[Unity] mov파일 만들어 webm파일 사용하기  (0) 2022.02.04
[Unity] MQTT 통신 구현  (0) 2021.10.19
[Unity] 스도쿠게임 만들기  (0) 2021.06.15
[Unity] UDP 통신  (0) 2021.05.04
[Unity] Partial class  (0) 2021.05.04
반응형

만들어진 앱은 앱스토어 및 플레이스토어에 업로드.

앱스토어 : https://apps.apple.com/us/app/simple-sudoku-puzzle/id1567879220

 

‎Simple Sudoku Puzzle

‎Thousands of Sudoku puzzles to explore. Install to get started now! Whether you want to relax or keep your mind alive-have a good time! Choose the level you want. Play easier levels to challenge your brain, or try the expert level to get a real workout

apps.apple.com

플레이스토어 : https://play.google.com/store/apps/details?id=com.hnine.Sudoku 

 

Sudoku - Google Play 앱

언제 어디서나 스도쿠 퍼즐을 즐기십시오.

play.google.com

 

 

하나의 클래스로 처음에 작업 했다가 코드가 길어져 Partial class를 사용 하여 3개의 파일로 분리 정리함.

Partial class에 대해 궁금하다면 요기를 클릭.

 

BoardHandler.cs 작성

기본적으로 게임 View에 해당하는 전반적인 모든 것들을 연결하고 기능을 담고 있는 파일.

using System;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using DG.Tweening;

public enum Status
{
    PlayMode,
    MemoMode
}

public partial class BoardHandler : MonoBehaviour
{
    public static string HISTORY = "HISTORY";
    [SerializeField] HistoryManger historyManger;
    [SerializeField] UnityAdsManager adsManager;

    [Header("Theme Color")]
    [SerializeField] List<ColorSet> ListColor;
    [SerializeField] List<Image> Img_BGs;
    [SerializeField] List<Image> Img_Grids;
    [SerializeField] List<Image> ListButtons;
    [SerializeField] List<Image> ListButtonIcons;
    [SerializeField] List<Text> ListButtonsTexts;
    [SerializeField] List<Image> ListToastBGs;
    [SerializeField] List<Text> ListToastTexts;

    [Header("Game")]
    [SerializeField] Status MyStatus;
    [SerializeField] Dificulty dificulty;
    [SerializeField] int Life = 3;
    [SerializeField] int _hint = 3;
    [SerializeField] int ThemeNum = 0;
    public static bool isPlay;
    [SerializeField] float playTime;
    [SerializeField] GridLayoutGroup GLG_Holder;
    [SerializeField] VerticalLayoutGroup VLG;
    [SerializeField] HorizontalLayoutGroup HLG;
    [SerializeField] GameObject PrefabItem;
    [SerializeField] CellItem PrefabCellItem;
    int[,] solvedGrid = new int[9, 9];
    string s;
    [SerializeField] float spacing, padding, round;
    [SerializeField] List<CellItem> List_CellItem;
    int hideCount = 30;
    CellItem targetCellItem = null;

    [Space(10)]
    [Header("UI")]
    [SerializeField] Button Btn_Memo;
    [SerializeField] GameObject GO_TxtMemo, GO_TxtPlay, GO_AD;
    [SerializeField] Image Img_MemoBG;
    [SerializeField] Image[] Imgs_HolderBG;
    [SerializeField] Button[] Btns_Nums;
    [SerializeField] CanvasGroup CG_SelectDificult;
    [SerializeField] Button Btn_Normal, Btn_Medium, Btn_Hard, Btn_Expert, Btn_NewGame, Btn_Hint, Btn_Del, Btn_Theme, Btn_Pause;
    [SerializeField] CanvasGroup CG_Board;
    [SerializeField] RectTransform RT_Borad, RT_Top, RT_Bot, RT_BotUIHolder, RT_TopUIHolder;
    [SerializeField] Text Txt_Level, Txt_Mistake, Txt_PlayTime, Txt_Hint, Txt_Record;
    [SerializeField] UnityEngine.EventSystems.EventSystem eventSystem;
    [SerializeField] List<Text> ListNumCountTxt;
    float screenW, screenH;
    float boardW;
    Vector2 screenSize;

    static float calNum = Mathf.PI / 180;
    public static AnimationCurve SineInOut90 = new AnimationCurve(
        new Keyframe(0, 0, Mathf.Tan(5 * calNum), Mathf.Tan(5 * calNum)),
        new Keyframe(0.12f, 0.06f, Mathf.Tan(52 * calNum), Mathf.Tan(52 * calNum)),
        new Keyframe(0.4f, 0.72f, Mathf.Tan(50 * calNum), Mathf.Tan(50 * calNum)),
        new Keyframe(1f, 1f, Mathf.Tan(1 * calNum), Mathf.Tan(1 * calNum))
    );

    int usedHint = 0;
    public int SetHint
    {
        get
        {
            return _hint;
        }
        set
        {
            _hint = value;

            if (_hint == 0)
            {
                GO_AD.SetActive(true);
                Txt_Hint.gameObject.SetActive(false);
            }
            else
            {
                GO_AD.SetActive(false);
                Txt_Hint.gameObject.SetActive(true);
                Txt_Hint.text = _hint.ToString();
            }
        }
    }

    public int SetLife
    {
        get
        {
            return Life;
        }
        set
        {
            Life = value;
            Txt_Mistake.text = "Mistake : " + (3 - Life).ToString() + "/3";
        }
    }


    [Space(10)]
    [Header("Complete")]
    [SerializeField] CanvasGroup CG_Complete;
    [SerializeField] Button Btn_CompleteReGame;

    [Space(10)]
    [Header("Game Over")]
    [SerializeField] CanvasGroup CG_GameOver;
    [SerializeField] Button Btn_GameOverNewGame, Btn_GameOverRePlay, Btn_GameCompleteRePlay;

    [Space(10)]
    [Header("Toast Popup")]
    [SerializeField] ToastPopupHandler Toast;

    [Space(10)]
    [Header("New Game")]
    [SerializeField] NewGamePopupHandler NewGamePopup;


    void Start()
    {
        Application.targetFrameRate = 30;
        screenSize = new Vector2(Screen.width, Screen.height);
        resetAll();
        ThemeNum = PlayerPrefs.GetInt("ThemeNum");
        setTheme();
        Btn_Memo.onClick.AddListener(() =>
        {
            if (MyStatus == Status.PlayMode)
                toMemoMode();
            else if (MyStatus == Status.MemoMode)
                toPlayMode();
        });
        for (int i = 0; i < Btns_Nums.Length; i++)
        {
            int idx = i + 1;
            Btns_Nums[i].transform.GetChild(0).GetComponent<Text>().text = idx.ToString();
            Btns_Nums[i].onClick.AddListener(() => checkNumber(idx));
        }
        // Btn_Easy.onClick.AddListener(() => selectDificult(0));
        Btn_Normal.onClick.AddListener(() => selectDificult(1));
        Btn_Medium.onClick.AddListener(() => selectDificult(2));
        Btn_Hard.onClick.AddListener(() => selectDificult(3));
        Btn_Expert.onClick.AddListener(() => selectDificult(4));
        Btn_NewGame.onClick.AddListener(NewGamePopup.Open);
        Btn_CompleteReGame.onClick.AddListener(resetAll);
        Btn_GameOverNewGame.onClick.AddListener(resetAll);
        Btn_Hint.onClick.AddListener(HintOpen);
        Btn_Del.onClick.AddListener(resetCell);
        Btn_Theme.onClick.AddListener(() => changeTheme());
        Btn_Pause.onClick.AddListener(pauseCheck);

        NewGamePopup.Btn_NewGame.onClick.AddListener(NewGamePopupOpen);

        NewGamePopup.Btn_RePlay.onClick.AddListener(() => openFullAD("rePlay"));
        Btn_GameCompleteRePlay.onClick.AddListener(() => openFullAD("rePlay"));
        Btn_GameOverRePlay.onClick.AddListener(() => openFullAD("rePlay"));
    }

    // 전면광고 호출
    void openFullAD(string type = "hint")
    {
        // rePlay // hint
        adsManager.AdsShow(type);
    }

    void pauseCheck()
    {
        if (isPlay)
        {
            isPlay = false;
            Btn_Pause.gameObject.transform.GetChild(0).gameObject.SetActive(true);
            Btn_Pause.gameObject.transform.GetChild(1).gameObject.SetActive(false);
            for (int i = 0; i < List_CellItem.Count; i++)
            {
                int idx = i;
                List_CellItem[i].RT.DOLocalRotate(new Vector3(0, 90, 0), 0.25f).SetEase(Ease.InCubic).SetDelay(i * 0.005f).OnComplete(() =>
                {
                    List_CellItem[idx].Txt.gameObject.SetActive(false);
                    List_CellItem[idx].GO_Memo.SetActive(false);
                    List_CellItem[idx].RT.DOLocalRotate(new Vector3(0, 180, 0), 0.25f).SetEase(Ease.OutCubic);
                });
            }
        }
        else
        {
            isPlay = true;
            Btn_Pause.gameObject.transform.GetChild(0).gameObject.SetActive(false);
            Btn_Pause.gameObject.transform.GetChild(1).gameObject.SetActive(true);
            for (int i = 0; i < List_CellItem.Count; i++)
            {
                int idx = i;
                List_CellItem[i].RT.DOLocalRotate(new Vector3(0, 90, 0), 0.25f).SetEase(Ease.InCubic).SetDelay(i * 0.005f).OnComplete(() =>
                {
                    if (List_CellItem[idx].IsOpen)
                        List_CellItem[idx].Txt.gameObject.SetActive(true);
                    if (!List_CellItem[idx].IsOpen)
                        List_CellItem[idx].GO_Memo.SetActive(true);
                    List_CellItem[idx].RT.DOLocalRotate(new Vector3(0, 0, 0), 0.25f).SetEase(Ease.OutCubic);
                });
            }
        }
        setTheme();
    }

    public void NewGamePopupOpen()
    {
        // resetAll();
        adsManager.ShowNewGameAd();
    }

    // 레벨에 따라 주어지는 힌트갯수를 세팅함
    void setHint()
    {
        if (dificulty == Dificulty.Easy) { }
        else if (dificulty == Dificulty.Normal)
            SetHint = 3;
        else if (dificulty == Dificulty.Medium)
            SetHint = 3;
        else if (dificulty == Dificulty.Hard)
            SetHint = 5;
        else if (dificulty == Dificulty.Expert)
            SetHint = 7;
    }

    // 시도했던 게임을 다시 할 경우
    public void RePlayGame()
    {
        Debug.Log("RePlay");
        isPlay = false;
        SetLife = 3;
        setHint();
        playTime = 0;
        CG_Complete.gameObject.SetActive(false);
        CG_GameOver.gameObject.SetActive(false);
        for (int i = 0; i < List_CellItem.Count; i++)
        {
            int idx = i;
            List_CellItem[i].RT.DOLocalRotate(new Vector3(0, 90, 0), 0.25f).SetEase(Ease.InCubic).SetDelay(i * 0.005f).OnComplete(() =>
            {
                if (!List_CellItem[idx].IsOrigin == true)
                {
                    List_CellItem[idx].Txt.gameObject.SetActive(false);
                    List_CellItem[idx].IsOpen = false;
                }
                else
                {
                    List_CellItem[idx].Txt.gameObject.SetActive(true);
                }
                List_CellItem[idx].GO_Memo.SetActive(false);
                List_CellItem[idx].RT.DOLocalRotate(new Vector3(0, 0, 0), 0.25f).SetEase(Ease.OutCubic);
                isPlay = true;
                setTheme();// reset Theme
                toPlayMode();
            });
            List_CellItem[i].Txt.color = ListColor[ThemeNum].Clr_CellTextOrigin;
        }

    }

    // 새로 게임을 진행 할 경우
    public void resetAll()
    {
        // Debug.Log("ResetAll");
        toPlayMode();
        isPlay = false;
        SetLife = 3;
        SetHint = 3;
        playTime = 0;
        CG_Complete.gameObject.SetActive(false);
        CG_GameOver.gameObject.SetActive(false);
        CG_SelectDificult.gameObject.SetActive(true);
        CG_SelectDificult.alpha = 0;
        CG_SelectDificult.DOFade(1, 0.3f).SetEase(SineInOut90);
        for (int i = 0; i < List_CellItem.Count; i++)
        {
            List_CellItem[i].Btn.onClick.RemoveAllListeners();
            List_CellItem[i].List_MemoNums.Clear();
            Destroy(List_CellItem[i].RT.gameObject);
        }
        List_CellItem.Clear();
    }

    // 메모 모드로 집입 할 경우 처리
    void toMemoMode()
    {
        MyStatus = Status.MemoMode;

        // 버튼 상태 변경
        GO_TxtMemo.SetActive(false);
        GO_TxtPlay.SetActive(true);
        Img_MemoBG.color = ListColor[ThemeNum].Clr_MemoMode;

        // 모드를 구분하기 위한 이미지 컬러 조절
        for (int i = 0; i < Imgs_HolderBG.Length; i++)
            Imgs_HolderBG[i].color = ListColor[ThemeNum].Clr_MemoMode;
    }

    // 노멀(플레이모드)모드로 진입 할 경우 처리
    void toPlayMode()
    {
        MyStatus = Status.PlayMode;

        // 버튼 상태 변경
        GO_TxtMemo.SetActive(true);
        GO_TxtPlay.SetActive(false);
        Img_MemoBG.color = ListColor[ThemeNum].Clr_ButtonBG;

        // 모드를 구분하기 위한 이미지 컬러 조절
        for (int i = 0; i < Imgs_HolderBG.Length; i++)
            Imgs_HolderBG[i].color = ListColor[ThemeNum].Clr_PlayMode;
    }


    // 해당 셀을 터치하거나 마우스로 클릭했을 경우
    void clickedItem(CellItem item)
    {
        if (item.IsOpen)
        {
            for (int i = 0; i < List_CellItem.Count; i++)
            {
                if (List_CellItem[i].IsOpen && List_CellItem[i].MyNum == item.MyNum)                    // 셀의 숫자가 열려있는 경우 클릭하게 되면?,
                    List_CellItem[i].Img.color = ListColor[ThemeNum].Clr_FocusedSameNum;
                else                                                                                    // 셀의 숫자가 열려있는 경우 클릭한 셀을 제외한 나머지 셀들은,
                    List_CellItem[i].Img.color = ListColor[ThemeNum].Clr_UnFocused;
            }
        }
        else                                                                                            // 클릭한 셀이 열려있지 않은 경우(가려진 경우) 눌리게 되면?
        {
            for (int i = 0; i < List_CellItem.Count; i++)
                List_CellItem[i].Img.color = ListColor[ThemeNum].Clr_UnFocused;
        }
        item.Img.color = ListColor[ThemeNum].Clr_Focused;
        targetCellItem = item;

        // 클릭된 셀을 기준으로 가로세로로 포커싱되게 처리.
        int cnt = 0;
        for (int i = 0; i < 9; i++)
        {
            for (int j = 0; j < 9; j++)
            {
                if (targetCellItem.MyPos.x == i && targetCellItem.MyPos.y == j) { }                     // 클릭된 카드는 제외하고 나머지 셀들은,
                else
                {
                    if (targetCellItem.MyNum != List_CellItem[cnt].MyNum)                               // 클릭된 셀의 숫자와 다른 나머지 셀들은,
                    {
                        if (i == targetCellItem.MyPos.x || j == targetCellItem.MyPos.y)                 // 클릭된 셀과 가로나 세로가 줄이 맞는 셀이면,
                            List_CellItem[cnt].Img.color = ListColor[ThemeNum].Clr_FocusLine;
                        else                                                                            // 클릭된 셀과 가로나 세로가 맞지 않는 셀이면,
                            List_CellItem[cnt].Img.color = ListColor[ThemeNum].Clr_UnFocused;
                    }
                }
                cnt++;
            }
        }
    }

    // 힌트를 클릭했을 때
    public void HintOpen()
    {
        if (MyStatus == Status.PlayMode)
        {
            if (targetCellItem != null && !targetCellItem.IsOpen)
            {
                if (_hint > 0)
                {
                    SetHint--;
                    usedHint++;
                    checkNumber(targetCellItem.MyNum);
                }
                else
                {
                    // Toast.Popup("No more hint");
                    openFullAD("hint");
                }
            }
            else
            {
                Toast.Popup("Select blank cell.");
            }
        }
        else
        {
            Toast.Popup("change to play mode.");
        }
    }

    // 숫자키를 입력받았을 때, 처리되는 부분
    void checkNumber(int idx)
    {
        if (MyStatus == Status.PlayMode) // 노멀모드에서 처리
        {
            if (targetCellItem != null && !targetCellItem.IsOpen)                                       // 선택한 셀이 있고 열려있지 않을 경우
            {
                if (idx != targetCellItem.MyNum)                                                        //! 입력받은 숫자와 선택한 셀의 숫자가 맞지 않은경우 (틀렸을 경우)
                {
                    // 숫자가 틀렸을 경우 생명이 하나 줄고 틀린숫자가 빨강으로 표기된다.
                    Life--;
                    if (Life == 0)
                    {
                        Txt_Mistake.text = "Mistake : " + (3 - Life).ToString() + "/3";
                        gameOver();
                    }
                    else
                    {
                        Txt_Mistake.text = "Mistake : " + (3 - Life).ToString() + "/3";
                        Toast.Popup("Wrong Number T.T");
                        targetCellItem.Txt.text = idx.ToString();
                        targetCellItem.Txt.gameObject.SetActive(true);
                        targetCellItem.Txt.DOColor(ListColor[ThemeNum].Clr_CellRed, 0.3f).SetEase(SineInOut90);
                    }
                }
                else                                                                                    //? 맞았을 경우
                {
                    // 숫자가 맞았을 경우 해당 숫자가 열린다.
                    targetCellItem.RT.eulerAngles = new Vector3(0, 180, 0);
                    targetCellItem.RT.DOLocalRotate(new Vector3(0, 90, 0), 0.25f).SetEase(Ease.InCubic).OnComplete(() =>
                    {
                        targetCellItem.IsOpen = true;
                        targetCellItem.Txt.text = idx.ToString();
                        targetCellItem.Txt.gameObject.SetActive(true);
                        targetCellItem.Txt.fontStyle = FontStyle.Bold;
                        targetCellItem.Txt.DOColor(ListColor[ThemeNum].Clr_CellText, 0.3f).SetEase(SineInOut90);
                        targetCellItem.RT.DOLocalRotate(new Vector3(0, 0, 0), 0.25f).SetEase(Ease.OutCubic);
                        setNumberCount();

                        // 열린 숫자와 같은 숫자를 표기한다.
                        clickedItem(targetCellItem);

                        // 선택한 셀에 힌트로 적었던 메모를 모두 삭제한다.
                        targetCellItem.GO_Memo.SetActive(false);

                        memoNumberReset_AfterOpenCell(idx);

                        // 오픈되지 않은 남은 셀의 갯수 체크!!
                        int cnt = 0;
                        for (int i = 0; i < List_CellItem.Count; i++)
                            if (!List_CellItem[i].IsOpen)
                                cnt++;
                        if (cnt == 0) // 셀이 모두 열렸을 경우 게임 끝~~~
                            gameComplete();
                    });
                }
            }
            else
            {
                Toast.Popup("This block is already open.\nPlease select a blank.");
            }
        }
        else if (MyStatus == Status.MemoMode)                                                           // 메모 모드에서 숫자를 입력했을 경우 처리.
        {
            if (targetCellItem != null && !targetCellItem.IsOpen)
            {
                if (targetCellItem.List_MemoNums[idx - 1].activeInHierarchy)
                    targetCellItem.List_MemoNums[idx - 1].SetActive(false);
                else
                    targetCellItem.List_MemoNums[idx - 1].SetActive(true);
            }
            else
            {
                Toast.Popup("This block is already open.\nPlease select a blank.");
            }
        }
    }

    // 클릭된 셀을 기준으로 메모해둔 숫자를 사라지도록 처리하기 위한 부분
    void memoNumberReset_AfterOpenCell(int idx)
    {
        int cnt = 0;
        for (int i = 0; i < 9; i++)
        {
            for (int j = 0; j < 9; j++)
            {
                if (targetCellItem.MyPos.x == i && targetCellItem.MyPos.y == j) { }                     // 클릭된 카드는 제외하고 나머지 셀
                else
                {
                    if (targetCellItem.MyNum != List_CellItem[cnt].MyNum)                               // 클릭된 셀의 숫자와 다른 나머지 셀들은,
                        if (i == targetCellItem.MyPos.x || j == targetCellItem.MyPos.y)                 // 클릭된 셀과 가로나 세로가 줄이 맞는 셀이면, 메모해둔 숫자중에 해당 숫자는 사라지도록 처리
                            List_CellItem[cnt].List_MemoNums[idx - 1].SetActive(false);
                }
                cnt++;
            }
        }
        // 클릭된 셀의 숫자가 오픈 될 경우 같은 그룹에 있는 셀에 메모해둔 숫자가 오픈된 숫자와 같을 경우
        for (int i = 0; i < List_CellItem.Count; i++)
            if (targetCellItem.MyGroup == List_CellItem[i].MyGroup)
                List_CellItem[i].List_MemoNums[idx - 1].SetActive(false);
    }

    // 모든 셀의 숫자를 맞추고 게임이 완료되는 상황
    void gameComplete()
    {
        print("Complete!");
        isPlay = false;
        for (int i = 0; i < List_CellItem.Count; i++)
        {
            int ii = i;
            List_CellItem[ii].Img.DOColor(ListColor[ThemeNum].Clr_Focused, 1f).SetDelay(0.005f * i).SetEase(SineInOut90);
            List_CellItem[ii].RT.DOLocalRotate(new Vector3(0, 360, 0), 1f, RotateMode.FastBeyond360).SetDelay(0.005f * i).SetEase(SineInOut90).OnComplete(() =>
            {
                List_CellItem[ii].Img.DOColor(ListColor[ThemeNum].Clr_UnFocused, 1f).SetEase(SineInOut90);
            });
        }
        Invoke("viewRecord", 2.3f);
        eventSystem.enabled = false;
    }

    void viewRecord()
    {
        eventSystem.enabled = true;
        CG_Complete.gameObject.SetActive(true);
        CG_Complete.alpha = 0;
        CG_Complete.DOFade(1, 0.3f).SetEase(SineInOut90);

        string _dificult = dificulty.ToString() + "_record";
        Debug.Log("난이도 : " + _dificult);
        int _history = PlayerPrefs.GetInt(_dificult);

        if (_history > 0)
        {
            Debug.Log("my history time : " + _history);

            if (_history >= Mathf.RoundToInt(playTime))
            {
                Txt_Record.text = "<size=50>Level : " + dificulty.ToString() + "</size>" + "\n<size=90>Game Clear!!</size>\n\n<color=#00aeff>=== New Record ===</color>\n" + GetTimeStr(Mathf.RoundToInt(playTime));
                PlayerPrefs.SetInt(_dificult, Mathf.RoundToInt(playTime));
            }
            else
            {
                Txt_Record.text = "<size=50>Level : " + dificulty.ToString() + "</size>" + "\n<size=90>Game Clear!!</size>\n\n<color=orange>=== Best Record ===</color>\n" + GetTimeStr(Mathf.RoundToInt(_history)) + "\n\n<color=#00aeff>=== My Record ===</color>\n" + GetTimeStr(Mathf.RoundToInt(playTime));
            }
        }
        else
        {
            Txt_Record.text = "<size=50>Level : " + dificulty.ToString() + "</size>" + "\n<size=90>Game Clear!!</size>\n\n<color=#00aeff>=== New Record ===</color>\n" + GetTimeStr(Mathf.RoundToInt(playTime));
            PlayerPrefs.SetInt(_dificult, Mathf.RoundToInt(playTime));
        }
        historyManger.SaveHistory(dificulty.ToString(), GetTimeStr(Mathf.RoundToInt(playTime)), (3 - Life).ToString(), usedHint.ToString());
    }

    // 생명이 다 되서 게임오버가 되는 상황
    void gameOver()
    {
        isPlay = false;
        CG_GameOver.gameObject.SetActive(true);
        CG_GameOver.alpha = 0;
        CG_GameOver.DOFade(1, 0.3f).SetEase(SineInOut90);
    }

    // 선택 된 셀의 숫자를 지우는 함수.
    void resetCell()
    {
        if (targetCellItem != null && !targetCellItem.IsOpen)
        {
            targetCellItem.IsOpen = false;
            targetCellItem.Txt.gameObject.SetActive(false);
        }
        else if (targetCellItem == null)
            Toast.Popup("Select a box.");
        else if (targetCellItem != null && targetCellItem.IsOpen)
            Toast.Popup("Already open.");
    }



    // for layout size
    float tempSpacing, tempPadding, tempRound;
    void Update()
    {
        setResize();
        setKeyBoard();
        updateTime();
    }

    void updateTime()
    {
        if (isPlay)
        {
            playTime += Time.deltaTime;
            Txt_PlayTime.text = GetTimeStr(Mathf.RoundToInt(playTime));
        }
    }

    void setKeyBoard()
    {
        // 각 숫자패드가 눌릴 경우 처리. 나중에 소프트키도 만들어 동일하게 처리해야 함.
        if (Input.GetKeyDown(KeyCode.Alpha1))
        {
            checkNumber(1);
        }
        else if (Input.GetKeyDown(KeyCode.Alpha2))
        {
            checkNumber(2);
        }
        else if (Input.GetKeyDown(KeyCode.Alpha3))
        {
            checkNumber(3);
        }
        else if (Input.GetKeyDown(KeyCode.Alpha4))
        {
            checkNumber(4);
        }
        else if (Input.GetKeyDown(KeyCode.Alpha5))
        {
            checkNumber(5);
        }
        else if (Input.GetKeyDown(KeyCode.Alpha6))
        {
            checkNumber(6);
        }
        else if (Input.GetKeyDown(KeyCode.Alpha7))
        {
            checkNumber(7);
        }
        else if (Input.GetKeyDown(KeyCode.Alpha8))
        {
            checkNumber(8);
        }
        else if (Input.GetKeyDown(KeyCode.Alpha9))
        {
            checkNumber(9);
        }
        else if (Input.GetKeyDown(KeyCode.Escape) || Input.GetKeyDown(KeyCode.Backspace))
        {
            resetCell();
        }
    }

    string GetTimeStr(int totalTime)
    {
        int min = (int)totalTime / 60;
        int sec = totalTime % 60;
        string str = min.ToString("00") + ":" + sec.ToString("00");
        return str;
    }
}

 

BoardHandler.Initialize.cs 작성

게임에 가장 핵심적인 부분이다. 9x9의 블럭에 들어가는 숫자를 랜덤으로 생성하고 섞어주는 역할을 하고 게임의 난이도 등도 관리한다.

알고리즘을 참고한 사이트 링크 : https://citylock77.tistory.com/86

using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using DG.Tweening;
using Random = UnityEngine.Random;

[Serializable]
public class CellItem
{
    public RectTransform RT;
    public int MyGroup;
    public FreeModifier FM;
    public Button Btn;
    public Text Txt;
    public Image Img;
    public int MyNum;
    public bool IsOpen;
    public Vector2 MyPos;
    public List<GameObject> List_MemoNums;
    public GameObject GO_Memo;
    public bool IsOrigin;
}

public enum Dificulty
{
    Easy = 0,
    Normal = 1,
    Medium = 2,
    Hard = 3,
    Expert = 4
}

public partial class BoardHandler : MonoBehaviour
{
    void selectDificult(int idx)
    {
        switch (idx)
        {
            case 0:
                dificulty = Dificulty.Easy;
                break;
            case 1:
                dificulty = Dificulty.Normal;
                break;
            case 2:
                dificulty = Dificulty.Medium;
                break;
            case 3:
                dificulty = Dificulty.Hard;
                break;
            case 4:
                dificulty = Dificulty.Expert;
                break;
        }
        resetAll();
        init();
    }

    void init()
    {
        CG_SelectDificult.gameObject.SetActive(false);
        // 9x9 그리드 데이터를 생성.
        initGrid(solvedGrid);

        // 생성한 그리드 데이터 숫자를 섞어줌.
        shuffleGrid(solvedGrid, 25);
        debugGrid(solvedGrid);

        // 생성한 그리드 데이터를 기반으로 View 생성.
        initView(solvedGrid);
        reSizeView();

        // AdsManager ad = GameObject.FindWithTag("AdsManager").gameObject.GetComponent<AdsManager>();
        // ad.AdsInit();
        setTheme();
    }

    // 처음 화면이 만들어지는 곳.
    void initView(int[,] grid)
    {
        CG_Board.alpha = 0;
        PrefabItem.SetActive(false);
        List_CellItem = new List<CellItem>();

        PrefabItem.GetComponent<Image>().color = ListColor[ThemeNum].Clr_Cell;
        PrefabItem.transform.GetChild(0).GetComponent<Text>().color = ListColor[ThemeNum].Clr_CellText;

        for (int i = 0; i < 9; i++)
        {
            for (int j = 0; j < 9; j++)
            {
                GameObject go = Instantiate(PrefabItem);
                go.transform.SetParent(PrefabItem.transform.parent);
                go.transform.localScale = Vector3.one;
                go.transform.localPosition = Vector3.zero;
                go.SetActive(true);
                Transform t_Memo = go.transform.GetChild(1);
                List<GameObject> list_MemoNums = new List<GameObject>();
                for (int k = 0; k < t_Memo.childCount; k++)
                {
                    t_Memo.GetChild(k).gameObject.SetActive(false);
                    list_MemoNums.Add(t_Memo.GetChild(k).gameObject);
                }
                CellItem item = new CellItem
                {
                    RT = go.GetComponent<RectTransform>(),
                    FM = go.GetComponent<FreeModifier>(),
                    Btn = go.GetComponent<Button>(),
                    Txt = go.transform.GetChild(0).GetComponent<Text>(),
                    Img = go.GetComponent<Image>(),
                    MyNum = grid[i, j],
                    IsOpen = true,
                    GO_Memo = t_Memo.gameObject,
                    List_MemoNums = list_MemoNums
                };

                item.Txt.text = grid[i, j].ToString();
                item.MyPos = new Vector2(i, j);
                item.Btn.onClick.AddListener(() => { clickedItem(item); });

                // 9개 영역으로 구문하기 위한 넘버링 : 숫자가 열렸을 경우 메모해 두었던 숫자도 리셋해주기 위해 구분 함.
                if (i > 2 && i < 6 && j < 3)                // 1번 그룹 9개
                    item.MyGroup = 1;
                else if (i > 2 && i < 6 && j < 3)           // 2번 그룹 9개
                    item.MyGroup = 2;
                else if (i > 5 && j < 3)                    // 3번 그룹 9개
                    item.MyGroup = 3;
                else if (i < 3 && j > 2 && j < 6)           // 4번 그룹 9개
                    item.MyGroup = 4;
                else if (i > 2 && i < 6 && j > 2 && j < 6)  // 5번 그룹 9개
                    item.MyGroup = 5;
                else if (i > 5 && j > 2 && j < 6)           // 6번 그룹 9개
                    item.MyGroup = 6;
                else if (i < 3 && j > 5)                    // 7번 그룹 9개
                    item.MyGroup = 7;
                else if (i > 2 && i < 6 && j > 5)           // 8번 그룹 9개
                    item.MyGroup = 8;
                else if (i > 5 && j > 5)                    // 9번 그룹 9개
                    item.MyGroup = 9;

                List_CellItem.Add(item);
            }
        }

        //! Hide Random Cell from Dificulty
        if (dificulty == Dificulty.Easy)
        {
            Txt_Level.text = "Easy";
            hideCount = 10;
        }
        else if (dificulty == Dificulty.Normal)
        {
            Txt_Level.text = "Normal";
            hideCount = 27;//28
        }
        else if (dificulty == Dificulty.Medium)
        {
            Txt_Level.text = "Medium";
            hideCount = 37;//38;
        }
        else if (dificulty == Dificulty.Hard)
        {
            Txt_Level.text = "Hard";
            hideCount = 45;//46;
        }
        else if (dificulty == Dificulty.Expert)
        {
            Txt_Level.text = "Expert";
            hideCount = 55;//56;
        }
        setHint();

        // 난이도에 따라 숨겨질 셀을 램덤으로 처리
        List<int> listHideNum = new List<int>();
        for (int i = 0; i < hideCount; i++)
        {
            int ran = getRandomNumber(listHideNum, List_CellItem.Count);
            listHideNum.Add(ran);
        }

        for (int i = 0; i < listHideNum.Count; i++)
        {
            for (int j = 0; j < List_CellItem.Count; j++)
            {
                if (listHideNum[i] == j)
                {
                    List_CellItem[j].IsOpen = false;
                    List_CellItem[j].Txt.gameObject.SetActive(false);
                }
            }
        }
        for (int i = 0; i < List_CellItem.Count; i++)
        {
            if (List_CellItem[i].IsOpen)
            {
                List_CellItem[i].IsOrigin = true;
                // List_CellItem[i].Txt.fontStyle = FontStyle.Bold;
                List_CellItem[i].Txt.color = ListColor[ThemeNum].Clr_CellTextOrigin;
            }
        }
        CG_Board.DOFade(1, 0.5f).SetEase(SineInOut90);
        isPlay = true;

        setNumberCount();
    }

    // 하단 숫자버튼에 남은 수 표기 및 해당 버튼의 숫자가 0일 경우 비활성화
    void setNumberCount()
    {
        List<int> counts = new List<int>() { 0, 0, 0, 0, 0, 0, 0, 0, 0 };

        for (int i = 0; i < List_CellItem.Count; i++)
        {
            if (!List_CellItem[i].IsOpen)
            {
                if (List_CellItem[i].MyNum == 1)
                    counts[0]++;
                else if (List_CellItem[i].MyNum == 2)
                    counts[1]++;
                else if (List_CellItem[i].MyNum == 3)
                    counts[2]++;
                else if (List_CellItem[i].MyNum == 4)
                    counts[3]++;
                else if (List_CellItem[i].MyNum == 5)
                    counts[4]++;
                else if (List_CellItem[i].MyNum == 6)
                    counts[5]++;
                else if (List_CellItem[i].MyNum == 7)
                    counts[6]++;
                else if (List_CellItem[i].MyNum == 8)
                    counts[7]++;
                else if (List_CellItem[i].MyNum == 9)
                    counts[8]++;
            }
        }

        for (int i = 0; i < ListNumCountTxt.Count; i++)
        {
            ListNumCountTxt[i].color = ListColor[ThemeNum].Clr_CellText;
            if (counts[i] != 0)
            {
                ListNumCountTxt[i].gameObject.transform.parent.transform.GetChild(0).gameObject.GetComponent<Text>().color = ListColor[ThemeNum].Clr_CellText;
                ListNumCountTxt[i].gameObject.transform.parent.GetComponent<Button>().interactable = true;
                ListNumCountTxt[i].text = counts[i].ToString();
            }
            else
            {
                Color _cor = ListColor[ThemeNum].Clr_CellText;
                ListNumCountTxt[i].gameObject.transform.parent.transform.GetChild(0).gameObject.GetComponent<Text>().color = new Vector4(_cor.r, _cor.g, _cor.b, 0.2f);
                ListNumCountTxt[i].gameObject.transform.parent.GetComponent<Button>().interactable = false;
                ListNumCountTxt[i].text = "";
            }
        }
    }

    // 그리드 배열이 처음 생성되는 곳. 9x9의 갯수로 숫자가 섞이지 않고 일단 생성만 된다.
    void initGrid(int[,] grid)
    {
        for (int i = 0; i < 9; i++)
        {
            for (int j = 0; j < 9; j++)
            {
                grid[i, j] = (i * 3 + i / 3 + j) % 9 + 1;
            }
        }
    }

    // 디버깅용.
    void debugGrid(int[,] grid)
    {
        s = "";
        int sep = 0;

        for (int i = 0; i < 9; i++)
        {
            s += "|";
            for (int j = 0; j < 9; j++)
            {
                s += grid[i, j].ToString();

                sep = j % 3;
                if (sep == 2)
                    s += "|";
            }
            s += "\n";
        }
        Debug.Log("debugGrid : " + s);
    }

    // 전체 셀의 숫자들을 섞어준다.
    void shuffleGrid(int[,] grid, int shuffleAmount)
    {
        for (int i = 0; i < shuffleAmount; i++)
        {
            int value1 = Random.Range(1, 10);
            int value2 = Random.Range(1, 10);

            mixTwoGridCells(grid, value1, value2);
        }
        for (int i = 0; i < shuffleAmount; i++)
            shuffleGridHorizontal();
    }

    void shuffleGridHorizontal()
    {
        int ran = Random.Range(0, 8);
        int target = ran + 3;
        if (target > 8)
            target = target - 9;

        int small = 0, big = 0;
        if (ran > target)
        {
            small = target;
            big = ran;
        }
        else
        {
            small = ran;
            big = target;
        }
        Debug.Log(small + " change " + big);
        int changeNum = 0;
        for (int x = 0; x < 9; x++)
        {
            for (int y = 0; y < 9; y++)
            {
                if (y == small)
                {
                    changeNum = solvedGrid[x, y];
                }
                if (y == big)
                {
                    solvedGrid[x, small] = solvedGrid[x, y];
                    solvedGrid[x, y] = changeNum;
                }
            }
        }
    }

    void mixTwoGridCells(int[,] grid, int value1, int value2)
    {
        int x1 = 0, x2 = 0, y1 = 0, y2 = 0;

        for (int i = 0; i < 9; i += 3)
        {
            for (int j = 0; j < 9; j += 3)
            {
                for (int k = 0; k < 3; k++)
                {
                    for (int l = 0; l < 3; l++)
                    {
                        if (grid[i + k, j + l] == value1)
                        {
                            x1 = i + k;
                            y1 = j + l;
                        }

                        if (grid[i + k, j + l] == value2)
                        {
                            x2 = i + k;
                            y2 = j + l;
                        }
                    }
                }
                grid[x1, y1] = value2;
                grid[x2, y2] = value1;
            }
        }
    }

    // 겹치지 않는 랜덤 숫자를 가져온다.
    private int getRandomNumber(List<int> seleced, int max)
    {
        var range = Enumerable.Range(0, max).Where(i => !seleced.Contains(i));
        int index = Random.Range(0, max - seleced.Count);
        return range.ElementAt(index);
    }
}

 

BoardHandler.LayoutColor.cs 작성

게임의 컬러테마를 관리하는 파일.

using System;
using UnityEngine;
using UnityEngine.UI;

[Serializable]
public class ColorSet
{
    public Color Clr_BG;
    public Color Clr_Grid;
    public Color Clr_Cell, Clr_CellIsNotOpen;
    public Color Clr_CellText;
    public Color Clr_CellMemoText;
    public Color Clr_CellRed;
    public Color Clr_CellTextOrigin;
    public Color Clr_ButtonBG;
    public Color Clr_ButtonText;
    //======
    public Color Clr_Focused, Clr_UnFocused, Clr_FocusedSameNum, Clr_FocusLine, Clr_MemoMode, Clr_PlayMode;
    //======
    public Color Clr_ToastBg, Clr_ToastText;
}

public partial class BoardHandler : MonoBehaviour
{
    void changeTheme()
    {
        ThemeNum++;
        if (ThemeNum > ListColor.Count - 1)
            ThemeNum = 0;
        setTheme();
        PlayerPrefs.SetInt("ThemeNum", ThemeNum);
    }
    void setTheme()
    {
        for (int i = 0; i < Img_BGs.Count; i++)
            Img_BGs[i].color = ListColor[ThemeNum].Clr_BG;

        for (int i = 0; i < List_CellItem.Count; i++)
        {
            if (List_CellItem[i].IsOpen)
                List_CellItem[i].Img.color = ListColor[ThemeNum].Clr_Cell;
            else
                List_CellItem[i].Img.color = ListColor[ThemeNum].Clr_CellIsNotOpen;
            if (List_CellItem[i].Txt.gameObject.activeInHierarchy)
            {
                if (List_CellItem[i].IsOpen)
                {
                    if (!List_CellItem[i].IsOrigin)
                        List_CellItem[i].Txt.color = ListColor[ThemeNum].Clr_CellText;
                    else
                        List_CellItem[i].Txt.color = ListColor[ThemeNum].Clr_CellTextOrigin;
                }
                else
                    List_CellItem[i].Txt.color = ListColor[ThemeNum].Clr_CellRed;
            }

            for (int j = 0; j < List_CellItem[i].List_MemoNums.Count; j++)
                List_CellItem[i].List_MemoNums[j].GetComponent<Text>().color = ListColor[ThemeNum].Clr_CellMemoText;
        }

        for (int i = 0; i < Img_Grids.Count; i++)
            Img_Grids[i].color = ListColor[ThemeNum].Clr_Grid;

        if (MyStatus == Status.MemoMode)
            toMemoMode();
        else
            toPlayMode();

        for (int i = 0; i < ListButtons.Count; i++)
            ListButtons[i].color = ListColor[ThemeNum].Clr_ButtonBG;

        for (int i = 0; i < ListButtonIcons.Count; i++)
            ListButtonIcons[i].color = ListColor[ThemeNum].Clr_ButtonText;

        for (int i = 0; i < ListButtonsTexts.Count; i++)
            ListButtonsTexts[i].color = ListColor[ThemeNum].Clr_ButtonText;

        for (int i = 0; i < ListToastBGs.Count; i++)
            ListToastBGs[i].color = ListColor[ThemeNum].Clr_ToastBg;

        for (int i = 0; i < ListToastTexts.Count; i++)
            ListToastTexts[i].color = ListColor[ThemeNum].Clr_ToastText;

        for (int i = 0; i < PrefabCellItem.List_MemoNums.Count; i++)
            PrefabCellItem.List_MemoNums[i].GetComponent<Text>().color = ListColor[ThemeNum].Clr_CellMemoText;

        setNumberCount();
    }

    // 레이아웃의 사이즈가 달라질 경우 레이아웃을 다시 잡아줌.
    void reSizeView()
    {
        boardW = RT_Borad.rect.width;
        float width = (boardW - spacing * 8 - padding * 2) / 9;
        GLG_Holder.cellSize = new Vector2(width, width);
        GLG_Holder.spacing = new Vector2(spacing, spacing);
        GLG_Holder.padding.left = (int)padding;
        GLG_Holder.padding.right = (int)padding;

        VLG.spacing = width * 3 + spacing * 3 - 10;
        HLG.spacing = width * 3 + spacing * 3 - 10;

        screenW = GetComponent<RectTransform>().rect.width;
        screenH = GetComponent<RectTransform>().rect.height;
        float topHeight = (screenH - screenW) / 2 - 200;
        float botHeight = (screenH - screenW) / 2 + 200;
        float topPosY = screenW / 2 + RT_Top.rect.height / 2 + 200;
        float botPosY = -(screenW / 2 + RT_Bot.rect.height / 2) + 200;
        float gap = 0;
        if (topHeight < 160)
        {
            gap = topHeight - 160;
            Debug.Log(gap);
            RT_Borad.sizeDelta = new Vector2(1080 + gap, 1080 + gap);
            topHeight = 160;
            RT_BotUIHolder.localScale = new Vector3(RT_Borad.rect.width / 1080, RT_Borad.rect.width / 1080, RT_Borad.rect.width / 1080);
            RT_TopUIHolder.localScale = new Vector3(RT_Borad.rect.width / 1080, RT_Borad.rect.width / 1080, RT_Borad.rect.width / 1080);
        }
        else
        {
            RT_Borad.sizeDelta = new Vector2(1080, 1080);
            RT_BotUIHolder.localScale = Vector3.one;
        }
        RT_Top.sizeDelta = new Vector2(screenW, topHeight);
        // RT_Bot.sizeDelta = new Vector2(screenW, botHeight);
        // RT_Top.anchoredPosition = new Vector2(0, screenW / 2 + RT_Top.rect.height / 2 + 200);
        RT_Bot.anchoredPosition = new Vector2(0, -(screenW / 2 + RT_Bot.rect.height / 2) + 200);
    }

    // 레이아웃 사이즈를 자동 조절
    void setResize()
    {
        if (tempPadding != padding || tempSpacing != spacing || screenSize.x != Screen.width || screenSize.y != Screen.height)                                           // 패딩값이나 간격값이 바뀌게 되면 자동으로 레이아웃을 적용하도록 처리
        {
            tempPadding = padding;
            tempSpacing = spacing;
            reSizeView();
        }
        if (tempRound != round)
        {
            tempRound = round;
            for (int i = 0; i < List_CellItem.Count; i++)
                List_CellItem[i].FM.Radius = new Vector4(round, round, round, round);
        }
    }
}

 

HistoryManger.cs 작성

게임을 플레이하고 클리어 했던 기록을 볼 수 있도록하는 스크립트.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using DG.Tweening;

[Serializable]
public class HistoryData
{
    public string date;
    public string dificult;
    public string time;
    public string mistake;
    public string hint;
}
public class HistoryItem
{
    public GameObject Go_Me;
    public Text Txt_Date, Txt_Level, Txt_Time, Txt_Mis, Txt_Hint;
}

public class HistoryManger : MonoBehaviour
{
    [SerializeField] string myHistoryTxt;
    [SerializeField] List<HistoryData> listHistory;
    [SerializeField] Button btn_History, btn_Reset;
    [SerializeField] ToastPopupHandler toastPopup;
    [SerializeField] List<HistoryItem> List_HistoryItem;
    [SerializeField] GameObject go_Prefab;
    [SerializeField] Transform t_Content;
    [SerializeField] CanvasGroup cg_View;
    [SerializeField] RectTransform rt_View;
    [SerializeField] Button btn_Close, btn_OpenRecords;
    [SerializeField] GameObject go_NoData;

    void Start()
    {
        rt_View.localScale = new Vector3(0.9f, 0.9f, 0.9f);
        cg_View.alpha = 0;
        cg_View.blocksRaycasts = false;

        List_HistoryItem = new List<HistoryItem>();
        // PlayerPrefs.SetString(BoardHandler.HISTORY, "");
        if (PlayerPrefs.GetString(BoardHandler.HISTORY).Length > 5)
            myHistoryTxt = PlayerPrefs.GetString(BoardHandler.HISTORY);
        else
            myHistoryTxt = "";
        btn_History.onClick.AddListener(openHistory);
        btn_OpenRecords.onClick.AddListener(openHistory);
        btn_Reset.onClick.AddListener(resetHistory);


        // Debug.Log(getDateStr());
        btn_Close.onClick.AddListener(closeHistory);
    }

    void openHistory()
    {
        LoadHistory();
        viewHistory();
    }

    void resetHistory()
    {
        PlayerPrefs.SetString(BoardHandler.HISTORY, "");
        toastPopup.Popup("Deleted all history");
        if (List_HistoryItem != null)
        {
            for (int i = 0; i < List_HistoryItem.Count; i++)
            {
                Destroy(List_HistoryItem[i].Go_Me);
            }
        }
        myHistoryTxt = "";
        listHistory.Clear();
        List_HistoryItem.Clear();
    }


    public void SaveHistory(string _dificult, string _time, string _mistake, string _hint)
    {
        string _date = getDateStr();
        string newHistory = _date + ";" + _dificult + ";" + _time + ";" + _mistake + ";" + _hint;
        if (myHistoryTxt.Length > 5)
            myHistoryTxt = myHistoryTxt + "," + newHistory;
        else
            myHistoryTxt = newHistory;
        PlayerPrefs.SetString(BoardHandler.HISTORY, myHistoryTxt);
    }

    public void LoadHistory()
    {
        listHistory = new List<HistoryData>();
        string[] words = myHistoryTxt.Split(',');
        foreach (var word in words)
        {
            HistoryData item = new HistoryData();
            string[] datas = word.Split(';');
            for (int i = 0; i < datas.Length; i++)
            {
                if (i == 0)
                    item.date = datas[i];
                else if (i == 1)
                    item.dificult = datas[i];
                else if (i == 2)
                    item.time = datas[i];
                else if (i == 3)
                    item.mistake = datas[i];
                else if (i == 4)
                    item.hint = datas[i];
            }
            listHistory.Add(item);
        }
    }

    void viewHistory()
    {
        if (List_HistoryItem != null)
        {
            for (int i = 0; i < List_HistoryItem.Count; i++)
            {
                Destroy(List_HistoryItem[i].Go_Me);
            }
        }
        List_HistoryItem.Clear();

        for (int i = 0; i < listHistory.Count; i++)
        {
            GameObject go = Instantiate(go_Prefab);
            go.SetActive(true);
            go.transform.SetParent(t_Content);
            go.transform.localScale = Vector3.one;
            go.transform.localPosition = Vector3.zero;
            HistoryItem item = new HistoryItem
            {
                Txt_Date = go.transform.GetChild(0).GetComponent<Text>(),
                Txt_Level = go.transform.GetChild(1).GetComponent<Text>(),
                Txt_Time = go.transform.GetChild(2).GetComponent<Text>(),
                Txt_Mis = go.transform.GetChild(3).GetComponent<Text>(),
                Txt_Hint = go.transform.GetChild(4).GetComponent<Text>(),
                Go_Me = go
            };
            item.Txt_Date.text = listHistory[i].date;
            item.Txt_Level.text = listHistory[i].dificult;
            item.Txt_Time.text = listHistory[i].time;
            item.Txt_Mis.text = listHistory[i].mistake;
            item.Txt_Hint.text = listHistory[i].hint;
            List_HistoryItem.Add(item);
        }
        if (myHistoryTxt.Length > 0)
        {
            go_NoData.SetActive(false);
        }
        else
        {
            go_NoData.SetActive(true);
        }
        cg_View.blocksRaycasts = true;
        cg_View.DOFade(1, 0.35f).SetEase(BoardHandler.SineInOut90);
        rt_View.DOScale(1, 0.35f).SetEase(BoardHandler.SineInOut90);
    }

    void closeHistory()
    {
        cg_View.blocksRaycasts = false;
        cg_View.DOFade(0, 0.35f).SetEase(BoardHandler.SineInOut90);
        rt_View.DOScale(0.9f, 0.35f).SetEase(BoardHandler.SineInOut90);
    }

    string getDateStr()
    {
        return DateTime.Now.ToString("yyyy-MM-dd\\ HH:mm");
    }
}

 

MenuHandler.cs 작성

게임화면에서 메뉴화면을 호출하기 위한 파일.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using DG.Tweening;

public class MenuHandler : MonoBehaviour
{
    [SerializeField] RectTransform RT_Menu;
    RectTransform rt_me;
    [SerializeField] Button Btn_Open, Btn_Close, Btn_CloseX;
    [SerializeField] CanvasGroup CG_Me;
    bool isOpen;
    float height;

    void Start()
    {
        Btn_Open.onClick.AddListener(openMenu);
        Btn_Close.onClick.AddListener(closeMenu);
        Btn_CloseX.onClick.AddListener(closeMenu);
        rt_me = GetComponent<RectTransform>();
        height = RT_Menu.rect.height;
        // Debug.Log(height);
        isOpen = false;
        CG_Me.blocksRaycasts = false;
        CG_Me.alpha = 0;
        RT_Menu.anchoredPosition = new Vector2(-1080, 0);
    }

    void openMenu()
    {
        BoardHandler.isPlay = false;
        isOpen = true;
        CG_Me.blocksRaycasts = true;
        CG_Me.alpha = 0;
        CG_Me.DOFade(1, 0.2f);
        RT_Menu.DOAnchorPosX(0, 0.35f).SetEase(BoardHandler.SineInOut90);
    }
    void closeMenu()
    {
        BoardHandler.isPlay = true;
        isOpen = false;
        CG_Me.blocksRaycasts = false;
        RT_Menu.DOAnchorPosX(-1080, 0.35f).SetEase(BoardHandler.SineInOut90);
        CG_Me.DOFade(0, 0.35f);
    }
}

 

NewGamePopupHandler.cs 작성

새로운 게임을 하기위해 버튼을 누를 경우 새로운 게임을 할 수 있게 하는 팝업을 호출 하기 위한 파일.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using DG.Tweening;

public class NewGamePopupHandler : MonoBehaviour
{
    [SerializeField] CanvasGroup CG;
    public Button Btn_Cancel, Btn_RePlay, Btn_NewGame;
    void Start()
    {
        Btn_Cancel.onClick.AddListener(Close);
        Btn_NewGame.onClick.AddListener(Close);
        Btn_RePlay.onClick.AddListener(Close);
        gameObject.SetActive(false);
    }

    void Close()
    {

        CG.blocksRaycasts = false;
        CG.DOFade(0, 0.3f).SetEase(BoardHandler.SineInOut90).OnComplete(() =>
        {
            gameObject.SetActive(false);
        });
    }

    public void Open()
    {
        gameObject.SetActive(true);
        CG.blocksRaycasts = true;
        CG.DOFade(1, 0.3f).SetEase(BoardHandler.SineInOut90);
    }
}

 

ToastPopupHandler.cs 작성

게임 중에 간단한 팝업 문구를 띄우기 위한 스크립트.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using DG.Tweening;

public class ToastPopupHandler : MonoBehaviour
{
    [SerializeField] Text Txt_Msg;
    [SerializeField] CanvasGroup CG;
    [SerializeField] Button Btn_Dim;
    public void Popup(string msg)
    {
        CG.DOKill();
        CG.alpha = 0;
        CG.DOFade(1, 0.3f).SetEase(Ease.OutQuart);
        CG.blocksRaycasts = true;
        Txt_Msg.text = msg;
        CancelInvoke("Hide");
        Invoke("Hide", 2f);
    }

    void Start()
    {
        Hide();
        Btn_Dim.onClick.AddListener(() =>
        {
            CancelInvoke("Hide");
            Hide();
        });
    }

    void Hide()
    {
        CG.blocksRaycasts = false;
        CG.DOFade(0, 0.3f).SetEase(Ease.OutQuart);
    }
}

 

UnityAdsManager.cs 작성

유니티에서 제공하고 있는 광고를 띄우기 위한 스크립트.

광고에 대한 부분은 추가로 포스팅 예정.

using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using UnityEngine.Advertisements;

public class UnityAdsManager : MonoBehaviour
{
    public BoardHandler Board;
    public ToastPopupHandler Toast;
    public string bannerId = "banner";
    public string rewardedId = "rewardedVideo";
    public string videoId = "video";
    public bool testMode = true;

#if UNITY_IOS 
    public const string gameID = "IOS용 광고아이디";
#elif UNITY_ANDROID
    public const string gameID = "Android용 광고아이디"; 
#elif UNITY_EDITOR
    public const string gameID = "";
#endif

    void Start()
    {
        Advertisement.Initialize(gameID, testMode);
        ShowBanner();
    }

    public void ShowBanner()
    {

        StartCoroutine(ShowBannerWhenReady());
    }

    IEnumerator ShowBannerWhenReady()
    {
        while (!Advertisement.isInitialized)
        {
            yield return new WaitForSeconds(0.5f);
        }
        Advertisement.Banner.Show(bannerId);
    }

    static string myTarget;
    public void AdsShow(string target = "hint")
    {
        myTarget = target;
        ShowRewardedAd();
    }
    public void AdsShowNewGame()
    {

    }

    /// === New Game Ads ==========

    public void ShowNewGameAd()
    {
        if (Advertisement.IsReady(videoId))
        {
            var options = new ShowOptions { resultCallback = HandleNewGameAdShowResult };
            Advertisement.Show(videoId, options);
        }
    }

    private void HandleNewGameAdShowResult(ShowResult result)
    {
        switch (result)
        {
            case ShowResult.Finished:
                Board.resetAll();
                break;
            case ShowResult.Skipped:
                Board.resetAll();
                break;
            case ShowResult.Failed:
                Toast.Popup("Ads Load failed.");
                break;
        }
    }


    /// === Reward Ads ===========

    public void ShowRewardedAd()
    {
        if (Advertisement.IsReady(rewardedId))
        {
            // 광고가 끝난 뒤 콜백함수 "HandleShowResult" 호출 
            var options = new ShowOptions { resultCallback = HandleShowResult };
            Advertisement.Show(rewardedId, options);
        }
    }

    // 광고가 종료된 후 자동으로 호출되는 콜백 함수 
    private void HandleShowResult(ShowResult result)
    {
        switch (result)
        {
            case ShowResult.Finished:
                // 광고를 성공적으로 시청한 경우 보상 지급 
                if (myTarget == "hint")
                {
                    Board.SetHint = 1;
                    Board.Invoke("HintOpen", 0.5f);
                }
                else if (myTarget == "rePlay")
                {
                    Board.RePlayGame();
                }
                break;
            case ShowResult.Skipped:
                // 스킵 되었다면 뭔가 그런짓을 하면 보상을 줄 수 없잖아! 같은 창을 띄워야곘죠?
                Toast.Popup("Skiped Ads...");
                break;
            case ShowResult.Failed:
                Toast.Popup("Skiped Ads or Load failed");
                break;
        }
    }
}

 

신을 만들고 세팅하는 부분은 따로 시간 나면 정리 예정...

반응형

'unity C#' 카테고리의 다른 글

[Unity] MQTT 통신 구현  (0) 2021.10.19
[Unity] Tetris 게임 만들기  (1) 2021.06.25
[Unity] UDP 통신  (0) 2021.05.04
[Unity] Partial class  (0) 2021.05.04
[Unity] Sprite Atlas  (0) 2021.04.13
반응형

UDP(User Datagram Protocol)는 TCP와 같이 IP에 기반한 전송 프로토콜이다.

TCP는 송수신 전에 반드시 서버-클라이언트 연결이 전제되어야 하는 반면, UDP는 별도의 연결이 필요없다.


유니티에서 UDP를 사용하기 위해서는 System.Net.Sockets 네임스페이스 안의 UdpClient 클래스Socket 클래스를 사용한다. TCP와 달리 UDP는 별도의 UDP 서버 클래스가 없으며, 서버도 UdpClient 클래스를 사용한다.

 

 

*UDP 클라이언트 구현

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using static System.Console;



(1) UdpClient 객체 성성

UDP 통신을 위해 System.Net.Sockets 네임스페이스의 UdpClient 객체를 생성한다. 데이터를 내보낼 때 서버와 포트가 필요한데, UdpClient 생성자에서 잡아주는 경우도 있지만, 만약 하나의 UdpClient 객체로 여러 서버에 데이터를 보낼 경우는 Send() 메서드에서 서버와 포트를 지정해준다. 

UdpClient cli = new UdpClient();



(2) 데이터 송신

UdpClient 객체의 Send() 메서드를 사용하여 데이터를  IP와 포트번호에 맞게 보낸다. 네트워크 데이터 송수신은 바이트 데이터를 사용하기 때문에, 문자열을 보낼 경우 먼저 바이트로 인코딩한 후 보내게 된다.

msg = "안녕하세요";
byte[] datagram = Encoding.UTF8.GetBytes(msg);
cli.Send(datagram, datagram.Length, "127.0.0.1", 7777);
WriteLine("[Send] 127.0.0.1:7777 로 {0} 바이트 전송", datagram.Length);



(3) 데이터 수신

UDP에서 데이타를 수신할 경우는 UdpClient 객체의 Receive() 메서드를 사용한다. Receive() 메서드는 특히 수신 데이타와 함께 상대 컴퓨터의 종단점(IP주소와 포트) 정보도 같이 전달받는데, 이를 위해 IPEndPoint 객체를 ref 파라미터로 전달한다. 이를 통해 데이타가 수신되면 누가 그 데이타를 전송했는지 알 수 있다. 

IPEndPoint epRemote = new IPEndPoint(IPAddress.Any, 0);
byte[] bytes = cli.Receive(ref epRemote);
WriteLine("[Receive] {0} 로부터 {1} 바이트 수신", epRemote.ToString(), bytes.Length);

 

 

(4) UdpClient 객체 닫기

마지막으로 UdpClient 객체를 닫는다.

cli.Close();

 

 

*UDP 서버 구현

 

UDP 서버는 포트를 열고 클라이언트로부터 들어오는 데이타그램을 수신하게 된다. 즉, UDP 서버는 통상 UDP 포트를 Listening하고 있으면서 루프 안에서 계속 데이타 송수신을 처리하는 형태로 구현된다. UDP 클라이언트로부터 데이타그램을 직접 받아 처리하면 된다. UDP 서버는 UDP 클라이언트와 같이 거의 동일한 기능을 갖기 때문에 별도의 UDP 서버 클래스가 없고 UdpClient 클래스를 사용한다.

using System;
using System.Net;
using System.Net.Sockets;

 

 

(1) UdpClient 객체 성성

UDP 클라이언트로부터 데이타를 받아들이기 위해 먼저 Listening할 포트를 지정하며 UdpClient 객체를 생성한다.

// 포트 7777 에서 Listening
UdpClient srv = new UdpClient(7777);



(2) 데이터 수신

UDP 에서 데이타를 수신하기 위해 UdpClient 객체의 Receive() 메서드를 사용한다. Receive() 메서드는 특히 수신 데이타와 함께 상대 UDP 클라이언트 종단점(IP주소와 포트) 정보도 같이 전달받는데, 이를 위해 IPEndPoint 객체를 ref 파라미터로 전달한다. 데이타 수신 후, ref 파라미터를 체크하면 데이타를 보낸 UDP 클라이언트의 IP 주소와 포트를 알 수 있다.

// 클라이언트 IP를 담을 변수
IPEndPoint remoteEP = new IPEndPoint(IPAddress.Any, 0);

while(true) {
    byte[] dgram = srv.Receive(ref remoteEP);
    Console.WriteLine("[Receive] {0} 로부터 {1} 바이트 수신", remoteEP.ToString(), dgram.Length);
}

 

 

(3) 데이타 송신

UdpClient 객체의 Send() 메서드를 사용하여 데이타를 UDP 클라이언트로 전달한다. 이 때 클라이언트 IP는 위의 Receive() 메서드에서 받아온 IP 주소를 사용한다.

srv.Send(dgram, dgram.Length, remoteEP);
Console.WriteLine("[Send] {0} 로 {1} 바이트 송신", remoteEP.ToString(), dgram.Length);

 

 

*샘플 스크립트

 

udp_connector.cs

using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;
using UnityEngine.UI;


public class UDPConnector : MonoBehaviour {
    UdpClient ReceivePort, SendPort;
    IPEndPoint remoteEndPoint;

    public class MyUDPEvent : UnityEvent<string> { }
    public MyUDPEvent OnReceiveMessage = new MyUDPEvent ();


    void Start () {
        OnReceiveMessage.AddListener (receiveMsg);
    }

    void Init () {
        ReceivePort = new UdpClient (Int32.Parse (IF_ReceivePort.text));
        ReceivePort.BeginReceive (OnReceive, null);
        Send ();
    }

    public void Send (string msg = "receive from Unity") {
        SendPort = new UdpClient ();

        string remoteIP = IF_SendIP.text;
        int remotePort = Int32.Parse (IF_SendPort.text);
        remoteEndPoint = new IPEndPoint (IPAddress.Parse (remoteIP), remotePort);

        byte[] data = System.Text.Encoding.UTF8.GetBytes (msg);
        SendPort.Send (data, data.Length, remoteEndPoint);
    }

    void OnReceive (IAsyncResult ar) {
        try {
            IPEndPoint ipEndPoint = null;
            byte[] data = ReceivePort.EndReceive (ar, ref ipEndPoint);
            message = System.Text.Encoding.UTF8.GetString (data);
        } catch (SocketException e) { }

        ReceivePort.BeginReceive (OnReceive, null);
    }

    void receiveMsg (string msg) {
        Txt_Message.text = Txt_Message.text + msg + "\n";
        // Debug.Log(message);
    }

    void Update () {
        OnReceiveMessage.Invoke (message);
    }

    public void ShutDown () {
        OnReceiveMessage.RemoveAllListeners ();
        if (ReceivePort != null)
            ReceivePort.Close ();
        if (SendPort != null)
            SendPort.Close ();
        ReceivePort = null;
        SendPort = null;
    }
}

 

communication_handler.cs

public class CommunicationHandler : MonoBehaviour {
    public UDPConnector UDP;

    void Start () {
        UDP.OnReceiveMessage.AddListener (ReceiveMsg);
    }

    void ReceiveMsg (string msg) {
    
    }

    public void SendMsg (string msg) {
        UDP.Send (msg);
    }

    void OnApplicationQuit () {
        UDP.ShutDown ();
    }
}

 

반응형

'unity C#' 카테고리의 다른 글

[Unity] Tetris 게임 만들기  (1) 2021.06.25
[Unity] 스도쿠게임 만들기  (0) 2021.06.15
[Unity] Partial class  (0) 2021.05.04
[Unity] Sprite Atlas  (0) 2021.04.13
[Unity] vscode .net framework C# 에러문제  (1) 2020.08.28
반응형

거대해진 클래스를 쪼갤 수 있는 방법

코드를 작성하다보면 몇백줄? 몇천줄? 이상 되는 클래스를 만들때가 있는데 이를 쪼개서 관리하기 편하도록 하는 기능이다.

참조1, 참조2 


터치핸들러를 만든다고 가정하고 예제를 작성해본다.

1. 터치 다운, 2. 드래그, 3. 터치 업 세가지 기능으로 만들어보았다.

 

TouchHandler.cs

using UnityEngine;
using UnityEngine.EventSystems;

public partial class TouchHandler : MonoBehaviour, IPointerDownHandler, IDragHandler, IPointerUpHandler
{
    Vector2 movedPos;

    void Start()
    {

    }

    void OnEnable()
    {

    }
    
    void Update()
    {

    }
}

 

TouchHandler.Down.cs

using UnityEngine.EventSystems;

public partial class TouchHandler
{
    public void OnPointerDown(PointerEventData e)
    {

    }
}

 

TouchHandler.Drag.cs

using UnityEngine.EventSystems;

public partial class TouchHandler
{
    public void OnDrag(PointerEventData e)
    {
        movedPos = e.pressPosition - e.position;
    }
}

 

TouchHandler.Up.cs

using UnityEngine.EventSystems;

public partial class TouchHandler
{
    public void OnPointerUp(PointerEventData e)
    {

    }
}

 

파일은 총 4개로 쪼개진 파일에 각 기능별로 구성을 만들면 빌드시에 알아서 하나의 파일로 합쳐주게 된다.

첫번째 파일에 movedPos라는 Variable을 만들게 되면 나머지 파일에서도 접근이 가능한데 결국 하나의 파일이라고 생각하면서 작업하면 된다.

 

끝.

반응형

'unity C#' 카테고리의 다른 글

[Unity] 스도쿠게임 만들기  (0) 2021.06.15
[Unity] UDP 통신  (0) 2021.05.04
[Unity] Sprite Atlas  (0) 2021.04.13
[Unity] vscode .net framework C# 에러문제  (1) 2020.08.28
[Unity] EventSystem설정 터치 감도 조절  (0) 2020.06.30
반응형

출처 : skuld2000.tistory.com/28

 

[Unity] 이제 Sprite Packer 는 그만. Sprite Atlas 를 사용하자.

유니티에서 아틀라스(Atlas) 는 여러 개의 스프라이트(Sprite)를 한 장의 큰 텍스쳐에 모아놓은 것이라고 볼 수 있다. 게임에는 UI와 배경, 캐릭터, 이펙트 등등에 수많은 텍스쳐 들이 사용되는데 각

skuld2000.tistory.com

사용 방법은 매우매우 간단하다고는 하지만 ...

개인적으로는 예전 방식이 더 괜찮았던 것 같다.

한꺼번에 선택하고 패킹태그만 붙이면되는데...

바뀐 방식은 일일이 하나씩 혹은 여러개 선택해서

넣어 줘야 하는 번거로움?이 좀 있다고 생각하던 중

폴더째 드래그 해서 넣어도 된다고 한다.

결론은 좋아진게 확실하니 한번 써보자.

 

📌사용방법

👉 Sprite Atlas 생성

 

👉 Sprite 옵션 설정

 

👉 Tight Packing을 할 경우에 가장자리 투명도가 있는 이미지의 여백을 무시하고 합쳐주기 때문에 문제가 생길 수가 있다. 문제가 되는 이미지는 해당 이미지 파일을 선택하여 FullRect옵션으로 바꿔주고 Apply 눌러주면 간단히 해결된다.

다른 방법으로는 Sprite Editor를 사용해 커스텀라인을 만들고 Use Sprite Mesh옵션을 켜서 해당 이미지가 그려 질 때 커스텀라인 안쪽으로만 그려주게 하는 방법도 있지만 몇장 안되는 이미지라면 큰 차이가 없어보인다. 자세한 건 링크를 확인하자.

 

👉 일단 아틀라스로 묶어놓게 되면 타겟플랫폼별 이미지 옵션을 설정할 때 한방에 처리할 수 있다.

 

👉 무엇보다 큰 장점이라고 하면 위에 보이는것 처럼 폴더째로 넣어줘도 된다는게 맘에 든다.

 

끝.

반응형

'unity C#' 카테고리의 다른 글

[Unity] UDP 통신  (0) 2021.05.04
[Unity] Partial class  (0) 2021.05.04
[Unity] vscode .net framework C# 에러문제  (1) 2020.08.28
[Unity] EventSystem설정 터치 감도 조절  (0) 2020.06.30
[Unity] Animation Curve DOTween 커스터마이징  (0) 2020.06.29
반응형

[info]: OmniSharp.Stdio.Host
        Starting OmniSharp on MacOS 10.15.6 (x64)
[info]: OmniSharp.Services.DotNetCliService
        DotNetPath set to dotnet
[info]: OmniSharp.MSBuild.Discovery.MSBuildLocator
        Located 1 MSBuild instance(s)
            1: StandAlone 16.8.0 - "/Users/jangbeom-seok/.vscode/extensions/ms-dotnettools.csharp-1.23.1/.omnisharp/1.37.0/omnisharp/.msbuild/Current/Bin"
[info]: OmniSharp.MSBuild.Discovery.MSBuildLocator
        MSBUILD_EXE_PATH environment variable set to '/Users/jangbeom-seok/.vscode/extensions/ms-dotnettools.csharp-1.23.1/.omnisharp/1.37.0/omnisharp/.msbuild/Current/Bin/MSBuild.exe'
[info]: OmniSharp.MSBuild.Discovery.MSBuildLocator
        Registered MSBuild instance: StandAlone 16.8.0 - "/Users/jangbeom-seok/.vscode/extensions/ms-dotnettools.csharp-1.23.1/.omnisharp/1.37.0/omnisharp/.msbuild/Current/Bin"
            CscToolExe = csc.exe
            MSBuildToolsPath = /Users/jangbeom-seok/.vscode/extensions/ms-dotnettools.csharp-1.23.1/.omnisharp/1.37.0/omnisharp/.msbuild/Current/Bin
            CscToolPath = /Users/jangbeom-seok/.vscode/extensions/ms-dotnettools.csharp-1.23.1/.omnisharp/1.37.0/omnisharp/.msbuild/Current/Bin/Roslyn
            BypassFrameworkInstallChecks = true
            MSBuildExtensionsPath = /Users/jangbeom-seok/.vscode/extensions/ms-dotnettools.csharp-1.23.1/.omnisharp/1.37.0/omnisharp/.msbuild
[info]: OmniSharp.Cake.CakeProjectSystem
        Detecting Cake files in '/Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS'.
[info]: OmniSharp.Cake.CakeProjectSystem
        Could not find any Cake files
[info]: OmniSharp.MSBuild.ProjectSystem
        Detecting projects in '/Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS/Owao_IOS.sln'.
[info]: OmniSharp.MSBuild.ProjectManager
        Queue project update for '/Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS/Assembly-CSharp.csproj'
[info]: OmniSharp.MSBuild.ProjectManager
        Queue project update for '/Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS/Assembly-CSharp-firstpass.csproj'
[info]: OmniSharp.MSBuild.ProjectManager
        Queue project update for '/Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS/Assembly-CSharp-Editor.csproj'
[info]: OmniSharp.MSBuild.ProjectManager
        Queue project update for '/Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS/Assembly-CSharp-Editor-firstpass.csproj'
[info]: OmniSharp.Script.ScriptProjectSystem
        Detecting CSX files in '/Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS'.
[info]: OmniSharp.Script.ScriptProjectSystem
        Could not find any CSX files
[info]: OmniSharp.WorkspaceInitializer
        Invoking Workspace Options Provider: OmniSharp.Roslyn.CSharp.Services.CSharpFormattingWorkspaceOptionsProvider, Order: 0
[info]: OmniSharp.MSBuild.ProjectManager
        Loading project: /Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS/Assembly-CSharp.csproj
[info]: OmniSharp.WorkspaceInitializer
        Invoking Workspace Options Provider: OmniSharp.Roslyn.CSharp.Services.RenameWorkspaceOptionsProvider, Order: 100
[info]: OmniSharp.WorkspaceInitializer
        Invoking Workspace Options Provider: OmniSharp.Roslyn.CSharp.Services.ImplementTypeWorkspaceOptionsProvider, Order: 110
[info]: OmniSharp.WorkspaceInitializer
        Invoking Workspace Options Provider: OmniSharp.Roslyn.CSharp.Services.BlockStructureWorkspaceOptionsProvider, Order: 140
[info]: OmniSharp.WorkspaceInitializer
        Configuration finished.
[info]: OmniSharp.Stdio.Host
        Omnisharp server running using Stdio at location '/Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS' on host 20273.
[fail]: OmniSharp.MSBuild.ProjectLoader
        The reference assemblies for .NETFramework,Version=v4.7.1 were not found. To resolve this, install the Developer Pack (SDK/Targeting Pack) for this framework version or retarget your application. You can download .NET Framework Developer Packs at https://aka.ms/msbuild/developerpacks
[warn]: OmniSharp.MSBuild.ProjectManager
        Failed to load project file '/Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS/Assembly-CSharp.csproj'.
/Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS/Assembly-CSharp.csproj
/Users/jangbeom-seok/.vscode/extensions/ms-dotnettools.csharp-1.23.1/.omnisharp/1.37.0/omnisharp/.msbuild/Current/Bin/Microsoft.Common.CurrentVersion.targets(1178,5): Error: The reference assemblies for .NETFramework,Version=v4.7.1 were not found. To resolve this, install the Developer Pack (SDK/Targeting Pack) for this framework version or retarget your application. You can download .NET Framework Developer Packs at https://aka.ms/msbuild/developerpacks

[info]: OmniSharp.MSBuild.ProjectManager
        Loading project: /Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS/Assembly-CSharp-firstpass.csproj
[fail]: OmniSharp.MSBuild.ProjectLoader
        The reference assemblies for .NETFramework,Version=v4.7.1 were not found. To resolve this, install the Developer Pack (SDK/Targeting Pack) for this framework version or retarget your application. You can download .NET Framework Developer Packs at https://aka.ms/msbuild/developerpacks
[warn]: OmniSharp.MSBuild.ProjectManager
        Failed to load project file '/Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS/Assembly-CSharp-firstpass.csproj'.
/Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS/Assembly-CSharp-firstpass.csproj
/Users/jangbeom-seok/.vscode/extensions/ms-dotnettools.csharp-1.23.1/.omnisharp/1.37.0/omnisharp/.msbuild/Current/Bin/Microsoft.Common.CurrentVersion.targets(1178,5): Error: The reference assemblies for .NETFramework,Version=v4.7.1 were not found. To resolve this, install the Developer Pack (SDK/Targeting Pack) for this framework version or retarget your application. You can download .NET Framework Developer Packs at https://aka.ms/msbuild/developerpacks

[info]: OmniSharp.MSBuild.ProjectManager
        Loading project: /Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS/Assembly-CSharp-Editor.csproj
[fail]: OmniSharp.MSBuild.ProjectLoader
        The reference assemblies for .NETFramework,Version=v4.7.1 were not found. To resolve this, install the Developer Pack (SDK/Targeting Pack) for this framework version or retarget your application. You can download .NET Framework Developer Packs at https://aka.ms/msbuild/developerpacks
[warn]: OmniSharp.MSBuild.ProjectManager
        Failed to load project file '/Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS/Assembly-CSharp-Editor.csproj'.
/Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS/Assembly-CSharp-Editor.csproj
/Users/jangbeom-seok/.vscode/extensions/ms-dotnettools.csharp-1.23.1/.omnisharp/1.37.0/omnisharp/.msbuild/Current/Bin/Microsoft.Common.CurrentVersion.targets(1178,5): Error: The reference assemblies for .NETFramework,Version=v4.7.1 were not found. To resolve this, install the Developer Pack (SDK/Targeting Pack) for this framework version or retarget your application. You can download .NET Framework Developer Packs at https://aka.ms/msbuild/developerpacks

[info]: OmniSharp.MSBuild.ProjectManager
        Loading project: /Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS/Assembly-CSharp-Editor-firstpass.csproj
[fail]: OmniSharp.MSBuild.ProjectLoader
        The reference assemblies for .NETFramework,Version=v4.7.1 were not found. To resolve this, install the Developer Pack (SDK/Targeting Pack) for this framework version or retarget your application. You can download .NET Framework Developer Packs at https://aka.ms/msbuild/developerpacks
[warn]: OmniSharp.MSBuild.ProjectManager
        Failed to load project file '/Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS/Assembly-CSharp-Editor-firstpass.csproj'.
/Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS/Assembly-CSharp-Editor-firstpass.csproj
/Users/jangbeom-seok/.vscode/extensions/ms-dotnettools.csharp-1.23.1/.omnisharp/1.37.0/omnisharp/.msbuild/Current/Bin/Microsoft.Common.CurrentVersion.targets(1178,5): Error: The reference assemblies for .NETFramework,Version=v4.7.1 were not found. To resolve this, install the Developer Pack (SDK/Targeting Pack) for this framework version or retarget your application. You can download .NET Framework Developer Packs at https://aka.ms/msbuild/developerpacks

[fail]: OmniSharp.MSBuild.ProjectManager
        Attempted to update project that is not loaded: /Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS/Assembly-CSharp.csproj
[fail]: OmniSharp.MSBuild.ProjectManager
        Attempted to update project that is not loaded: /Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS/Assembly-CSharp-firstpass.csproj
[fail]: OmniSharp.MSBuild.ProjectManager
        Attempted to update project that is not loaded: /Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS/Assembly-CSharp-Editor.csproj
[fail]: OmniSharp.MSBuild.ProjectManager
        Attempted to update project that is not loaded: /Users/jangbeom-seok/Desktop/Project/Project_Owao/Owao_IOS/Assembly-CSharp-Editor-firstpass.csproj

유니티를 새로 설치하고 이런에러가 뜨면서 코드힌트라든지 레퍼런스가 뜨지 않는 경우가 생긴다.

구글링하다보니 Mono설정문제인듯 하다.

참고 : github.com/OmniSharp/omnisharp-vscode/issues/3063

 

세팅 검색창에 mono라고 검색해 보면 Omnisharp: Use Global Mono 라는 부분이 나오는데 auto에서 always로 바꿔주면 에러가 사라진다.

재시작 해주면 정상 작동 한다.

반응형
반응형

private const float inchToCm = 2.54f; 
[SerializeField] private EventSystem eventSystem = null; 
[SerializeField] private float dragThresholdCM = 0.5f; //For drag Threshold 
private void SetDragThreshold() { 
    if (eventSystem != null) { 
        eventSystem.pixelDragThreshold = (int)(dragThresholdCM * Screen.dpi / inchToCm); 
    } 
} 

void Awake() { 
    SetDragThreshold(); 
}

Unity로 안드로이드 앱을 개발하다보면 스크롤 안에 버튼이 들어갈 경우가 종종 있다.

PC에서 테스트 할 때는 마우스로 잘 눌리던 버튼이 스마트 폰에 넣어서 테스트 해보면 간혹 잘 눌리지 않는 경우가 있다. 이럴때 EventSystem 설정을 바꿔줘야하는데 다음과 같이 세팅 하면 적당하다.

 

반응형
반응형

커스텀 애니메이션커브를 담은 스크립(HnineCurves.cs)을 만들고 AnimationCurve를 적당한 이름으로 선언해 줍니다.

인스펙터에 가보면 이처럼 에디트할 수있는 창을 띄울 수 있겠죠.

클릭해서 원하는 모양의 이지값을 아래처럼 만들어 줍니다.

이렇게 만든 커브를 다음과 같이 DOTween에 사용할 수 있습니다.

반응형

+ Recent posts