3일동안 테트리스를 만들어보았다.


첫날은 어느정도 틀잡고 기본만 구현하였는데 자꾸 버그터져서 잡는데 시간을 보냄.

둘째날은 결국 인터넷에 있는 자료를 참고하여 다시만들기로 결정. 기본을 완성했다.

(참고한 사이트는 여기 → https://noobtuts.com/unity/2d-tetris-game/)

셋째날은 고스트만들기, 스페이스바입력시 바로 내려가기, 다음블럭 보여주기, 스코어, 콤보 등을 만들었다.


어느정도 기본은 완성된 테트리스가 만들어진 것 같다.





전체적인 스크립트는 아래와 같다. 그렇게 많지않아서 스크립트를 올림.


Grid 클래스

- 2차원 배열에 화면에 보이는 블럭 위치대로 저장하여 그 데이터를 이용해 블럭위에 블럭이 쌓이게,

한줄이 다차면 삭제하고 내려오게, 화면밖으로 블럭이 나가지 못하게 등등을 관리한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
public class Grid : MonoBehaviour {
 
    public static int w = 10;
    public static int h = 17;
    public static Transform[,] grid = new Transform[w, h];
 
    public static Vector2 roundVec2(Vector2 v)
    {
        return new Vector2(Mathf.Round(v.x), Mathf.Round(v.y));
    }
 
    // 테두리 안에 있는지 확인
    public static bool insideBorder(Vector2 pos)
    {
        // x좌표가 0~마지막 안에 있고 y가 0보다 같거나 커야 true
        return ((int)pos.x >= 0 && (int)pos.x < w && (int)pos.y >= 0);
    }
    
    // 한줄을 지우는 함수
    public static void deleteRow(int y)
    {
        for(int x=0; x<w; ++x)
        {
            Destroy(grid[x, y].gameObject);
            grid[x, y] = null;
        }
 
        var GM = GameObject.FindGameObjectWithTag("GameController").GetComponent<GameManager>();
 
        // 줄지울때마다 스코어업
        GM.scoreUP();
    }
 
    // 한줄을 당기는(내리는) 함수
    public static void decreaseRow(int y)
    {
        for(int x=0; x<w; ++x)
        {
            if(grid[x,y] != null)
            {
                grid[x, y - 1= grid[x, y];
                grid[x, y] = null;
 
                grid[x, y - 1].position += new Vector3(0-1);
            }
        }
    }
 
    // y값부터 위의 모든 줄을 내리는 함수
    public static void decreaseRowAbove(int y)
    {
        for (int i = y; i < h; ++i)
            decreaseRow(i);
    }
 
    // 한줄이 다찼는지 확인하는 함수
    public static bool isRowFull(int y)
    {
        for(int x=0; x<w; ++x)
        {
            if (grid[x, y] == null)
                return false;
        }
        return true;
    }
 
    // 전체를 돌면서 다 찬 줄을 삭제하고 내리는 함수
    public static void deleteFullRows()
    {
        bool combocheck = false;
        for(int y=0; y<h; ++y)
        {
            // 한줄이 다찼으면
            if(isRowFull(y))
            {
                // 그줄을 삭제하고
                deleteRow(y);
                // 그다음줄부터 전부 한줄씩 당김
                decreaseRowAbove(y + 1);
                // --y하여 다음번에 다시 그 줄부터 검사
                --y;
 
                combocheck = true;
            }
        }
 
        var GM = GameObject.FindGameObjectWithTag("GameController").GetComponent<GameManager>();
        // 한줄이라도 지우면 콤보업
        if (combocheck)
            GM.comboUP();
        // 아니면 콤보 초기화
        else
            GM.comboInit();
    }
 
    // Use this for initialization
    void Start () {
        
    }
    
    // Update is called once per frame
    void Update () {
        
    }
}
cs



Shape 클래스

- 키입력과 블럭의 이동, 고스트삭제, 오브젝트 삭제 등을 관리한다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
public class Shape : MonoBehaviour {
 
    // 연속 움직임 제한시간
    private float moveTime = 0;
    // 마지막으로 떨어진 시간(1초마다 떨어지게)
    private float lastFall = 0;
 
    // 회전 중점 받음 
    public GameObject pivot = null;
 
    // Use this for initialization
    void Start () {
        lastFall = Time.time;
 
        if(!isValidGridPos())
        {
            // 게임오버 변수 true
            var GM = GameObject.FindGameObjectWithTag("GameController").GetComponent<GameManager>();
            GM.gameover = true;
 
            // 고스트 삭제
            var BS = GameObject.FindGameObjectWithTag("Spawner").GetComponent<Spawner>();
            Destroy(BS.currentGhost);
 
            // 부모만남은 블럭 삭제
            DeleteParent();
 
            // 디버그남기고 현재 블럭 삭제
            Debug.Log("Game OVER");
            Destroy(gameObject);
        }
    }
 
    // Update is called once per frame
    void Update()
    {
        // 게임이 안 끝났을때만
        var GM = GameObject.FindGameObjectWithTag("GameController").GetComponent<GameManager>();
        if (GM.gameover == false)
        {
            // 위방향키를 누르면 pivot 기준으로 90도 회전
            if (Input.GetKeyDown(KeyCode.UpArrow))
            {
                transform.RotateAround(pivot.transform.position, Vector3.forward, 90.0f);
 
                if (isValidGridPos())
                    updateGrid();
                else
                    transform.RotateAround(pivot.transform.position, Vector3.forward, -90.0f);
            }
 
            var BS = GameObject.FindGameObjectWithTag("Spawner").GetComponent<Spawner>();
 
            // 왼 오 아래 방향키를 누르고있으면 0.05초마다 이동
            moveTime += Time.deltaTime;
            if (moveTime > 0.05f)
            {
                if (Input.GetKey(KeyCode.LeftArrow))
                {
                    transform.position += new Vector3(-10);
 
                    if (isValidGridPos())
                        updateGrid();
                    else
                        transform.position += new Vector3(10);
                }
                if (Input.GetKey(KeyCode.RightArrow))
                {
                    transform.position += new Vector3(10);
 
                    if (isValidGridPos())
                        updateGrid();
                    else
                        transform.position += new Vector3(-10);
                }
                // 아래 또는 마지막으로 떨어진지 1초가 지나면
                if (Input.GetKey(KeyCode.DownArrow) || Time.time - lastFall >= 1)
                {
                    transform.position += new Vector3(0-1);
 
                    if (isValidGridPos())
                        updateGrid();
                    else
                    {
                        transform.position += new Vector3(01);
                        updateGrid();
 
                        // 처음부터 끝까지 돌면서 비어있는칸을 전부 지우고 한줄씩 내려줌
                        Grid.deleteFullRows();
 
                        // 고스트 및 다음블럭 삭제하고  새로운 블럭을 생성
                        Destroy(BS.currentGhost);
                        Destroy(BS.nextBlock);
                        BS.MakeBlock();
 
                        // 자식이 다 사라진 오브젝트들을 삭제
                        DeleteParent();
 
                        // 활성화를 멈춤
                        enabled = false;
                    }
                    lastFall = Time.time;
                }
                moveTime = 0;
            }
 
            // 스페이스바를 누르면 바로 떨어짐
            if (Input.GetKeyDown(KeyCode.Space))
            {
                // 블럭의 위치를 고스트의 위치로 옮김
                transform.position = BS.currentGhost.transform.position;
 
                // Grid를 업데이트
                updateGrid();
 
                // 처음부터 끝까지 돌면서 비어있는칸을 전부 지우고 한줄씩 내려줌
                Grid.deleteFullRows();
 
                // 고스트 및 다음블럭 삭제하고  새로운 블럭을 생성
                Destroy(BS.currentGhost);
                Destroy(BS.nextBlock);
                BS.MakeBlock();
 
                // 자식이 다 사라진 오브젝트들을 삭제
                DeleteParent();
 
                // 활성화를 멈춤
                enabled = false;
            }
        }
    }
 
 
    // 이동이 정상적인지를 확인
    bool isValidGridPos()
    {
        foreach (Transform child in transform)
        {
            // 소수점을 반올림하여 딱맞게해줌
            Vector2 v = Grid.roundVec2(child.position);
 
            // Pivot을 제외한 나머지만 검사
            if (child.tag != "Pivot")
            {
                // 화면밖으로 나가면 false
                if (!Grid.insideBorder(v))
                    return false;
 
                // grid에 들어있는애가 널이 아니고 내거가 아니면 false
                if (Grid.grid[(int)v.x, (int)v.y] != null
                    && Grid.grid[(int)v.x, (int)v.y].parent != transform)
                    return false;
            }
        }
        return true;
    }
 
    // 이동마다 새로운 좌표로 저장함
    void updateGrid()
    {
        for (int y = 0; y < Grid.h; ++y)
        {
            for (int x = 0; x < Grid.w; ++x)
            {
                if (Grid.grid[x, y] != null)
                {
                    // grid에 저장된애의 부모가 나면 즉 지금 이동하는 객체만 전부 null로 만듬
                    if (Grid.grid[x, y].parent == transform)
                        Grid.grid[x, y] = null;
                }
            }
        }
 
        // 이후 현재 위치에 다시 입력
        foreach (Transform child in transform)
        {
            if (child.tag != "Pivot")
            {
                Vector2 v = Grid.roundVec2(child.position);
                Grid.grid[(int)v.x, (int)v.y] = child;
            }
        }
    }
 
    // Pivot만 남은 부모오브젝트를 삭제
    public static void DeleteParent()
    {
        // 블럭들을 찾아줌
        GameObject[] Blocks = GameObject.FindGameObjectsWithTag("Line");
        
        // 블럭의 자식이 다 없어지면 부모도 삭제
        foreach (GameObject child in Blocks)
        {
            int count = 0;
            foreach (Transform t in child.transform)
            {
                count++;
            }
            // Pivot만 남으면
            if (count == 1)
                Destroy(child);
        }
    }
}
cs



Spawner 클래스

- 블럭을 생성하고, 다음블럭을 미리 보여줌

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public class Spawner : MonoBehaviour {
 
    public GameObject[] Blocks = new GameObject[7];
    public GameObject[] GhostBlocks = new GameObject[7];
    public GameObject[] NextBlocks = new GameObject[7];
 
    public GameObject currentBlock = null;
    public GameObject currentGhost = null;
    public GameObject nextBlock = null;
 
    private int nextBlocknum = 0;
    
    // Use this for initialization
    void Start () {
       
    }
    
    // Update is called once per frame
    void Update () {
        var GM = GameObject.FindGameObjectWithTag("GameController").GetComponent<GameManager>();
        if (GM.gamestart == true)
        {
            MakeBlock();
            GM.gamestart = false;
        }
    }
 
    // 블럭을 생성하는 함수
    public void MakeBlock()
    {
        // 게임이 안끝났을때만 생성
        var GM = GameObject.FindGameObjectWithTag("GameController").GetComponent<GameManager>();
        if (GM.gameover == false)
        {
            // 다음블럭이 없을때(즉 처음 시작은 랜덤으로 둘다 생성)
            if (nextBlock == null)
            {
                var blocknum = Random.Range(0, Blocks.Length);
                currentBlock = Instantiate(Blocks[blocknum], transform.position, Quaternion.identity);
                currentGhost = Instantiate(GhostBlocks[blocknum], transform.position, Quaternion.identity);
            }
            // 다음블럭이 있다면 다음블럭의 번호를 가져와 생성
            else
            {
                currentBlock = Instantiate(Blocks[nextBlocknum], transform.position, Quaternion.identity);
                currentGhost = Instantiate(GhostBlocks[nextBlocknum], transform.position, Quaternion.identity);
            }
 
            // 다음 블럭을 만듬
            MakeNextBlock();
        }
    }
 
    // 다음 블럭숫자를 정하고 생성
    public void MakeNextBlock()
    {
        nextBlocknum = Random.Range(0, NextBlocks.Length);
        Vector3 nextBlockPos = new Vector3(0.1f, 17.6f);
        if (nextBlocknum == 0)
            nextBlockPos += new Vector3(0.2f, 0.2f);
        else if (nextBlocknum == 1)
            nextBlockPos += new Vector3(0.2f, 0.0f);
 
        nextBlock = Instantiate(NextBlocks[nextBlocknum], nextBlockPos, Quaternion.identity);
    }
}
 
 
cs



Ghost 클래스

- 어디로 떨어질지 미리 보여주는 고스트를 관리하는 클래스

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public class Ghost : MonoBehaviour {
 
    // 회전 중점 받음 
    public GameObject pivot = null;
 
    // Use this for initialization
    void Start () {
        //if (!isValidGridPos())
        //{
        //    Debug.Log("Game OVER");
        //    Destroy(gameObject);
        //}
        
        var SP = GameObject.FindGameObjectWithTag("Spawner");
        var cBlock = SP.GetComponent<Spawner>().currentBlock;
 
        // 고스트의 회전값은 블럭과 똑같게
        transform.rotation = cBlock.transform.rotation;
        // 고스트의 위치도 블럭과 똑같게
        transform.position = cBlock.transform.position;
    }
    
    // Update is called once per frame
    void Update () {
        // 게임이 안 끝났을때만
        var GM = GameObject.FindGameObjectWithTag("GameController").GetComponent<GameManager>();
        if (GM.gameover == false)
        {
            var SP = GameObject.FindGameObjectWithTag("Spawner");
            var cBlock = SP.GetComponent<Spawner>().currentBlock;
 
            // 고스트의 회전값은 블럭과 똑같게
            transform.rotation = cBlock.transform.rotation;
            // 고스트의 위치도 블럭과 똑같게
            transform.position = cBlock.transform.position;
 
            // 갈수있는 곳이면 고스트의 포지션의 y값을 -1하여 못가는곳까지 내림
            while (isValidGridPos())
            {
                transform.position += new Vector3(0-1.0f);
            }
            // 기준치보다 1칸 더내려가므로 1칸 다시올림
            transform.position += new Vector3(01.0f);
        }
    }
 
    // 이동이 정상적인지를 확인
    bool isValidGridPos()
    {
        foreach (Transform child in transform)
        {
            // 소수점을 반올림하여 딱맞게해줌
            Vector2 v = Grid.roundVec2(child.position);
 
            // Pivot을 제외한 나머지만 검사
            if (child.tag != "Pivot")
            {
                // 화면밖으로 나가면 false
                if (!Grid.insideBorder(v))
                    return false;
 
                var SP = GameObject.FindGameObjectWithTag("Spawner");
                var cBlock = SP.GetComponent<Spawner>().currentBlock;
 
                // grid에 들어있는애가 널이 아니면 false
                if (Grid.grid[(int)v.x, (int)v.y] != null
                    && Grid.grid[(int)v.x, (int)v.y].parent != cBlock.transform)
                    return false;
            }
        }
        return true;
    }
}
cs



GameManager클래스

- 게임시작과 게임오버, 리스타트, 스코어와 콤보를 관리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class GameManager : MonoBehaviour {
 
    private int score = 0;
    private int combo = 0;
 
    public bool gamestart = false;
    public bool gameover = false;
    public bool restart = false;
 
    public GameObject ReStart = null;
    // Use this for initialization
    void Start() {
 
    }
 
    // Update is called once per frame
    void Update() {
        if (gameover == true)
        {
            if (restart == false)
            {
                // 리스타트 버튼 생성
                Instantiate(ReStart, new Vector3(4.5f, 10.0f), Quaternion.identity);
                restart = true;
            }
        }
        else
            restart = false;
    }
 
    // 스코어 업
    public void scoreUP()
    {
        score += 1000;
        scoreUpdate();
    }
 
    // 스코어텍스트 업데이트
    private void scoreUpdate()
    {
        var scoretext = GameObject.FindGameObjectWithTag("Score");
        scoretext.GetComponent<Text>().text = Convert.ToString(score);
    }
 
    // 스코어 0으로 초기화
    public void scoreInit()
    {
        score = 0;
        scoreUpdate();
    }
 
    // 콤보 업 + 콤보에 따른 스코어업
    public void comboUP()
    {
        if (combo >= 1)
        {
            score += combo * 500;
            scoreUpdate();
        }
 
        combo++;
        comboUpdate();
    }
 
    // 콤보 0으로 초기화
    public void comboInit()
    {
        combo = 0;
        comboUpdate();
    }
 
    // 콤보텍스트 업데이트
    private void comboUpdate()
    {
        var combotext = GameObject.FindGameObjectWithTag("Combo");
        combotext.GetComponent<Text>().text = Convert.ToString(combo);
    }
}
 
cs



Play 클래스

- 시작버튼을 누르면 게임이 시작하게

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Play : MonoBehaviour {
 
    // Use this for initialization
    void Start () {
        
    }
    
    // Update is called once per frame
    void Update () {
        
    }
 
    private void OnMouseDown()
    {
        var GM = GameObject.FindGameObjectWithTag("GameController").GetComponent<GameManager>();
        GM.gamestart = true;
        Destroy(gameObject);
    }
}
cs



Restart 클래스

- 리스타트 버튼을 누르면 점수, 콤보를 초기화하고, 배열초기화 및 오브젝트 삭제등을 함

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class ReStart : MonoBehaviour {
 
    // Use this for initialization
    void Start () {
        
    }
    
    // Update is called once per frame
    void Update () {
        
    }
 
    private void OnMouseDown()
    {
        // 게임스타트 변수 true
        var GM = GameObject.FindGameObjectWithTag("GameController").GetComponent<GameManager>();
        GM.gameover = false;
        GM.gamestart = true;
 
        // 점수 및 콤보 초기화
        GM.scoreInit();
        GM.comboInit();
 
        // Grid 초기화, 및 오브젝트 전부삭제
        for (int y = 0; y < Grid.h; ++y)
        {
            for(int x=0; x< Grid.w; ++x)
            {
                Grid.grid[x, y] = null;
            }
        }
 
        // 필드에 블럭 전부삭제
        GameObject[] Blocks = GameObject.FindGameObjectsWithTag("Line");
        // 블럭의 자식전부 삭제
        foreach (GameObject child in Blocks)
        {
            Destroy(child);
        }
 
        // Restart 버튼 삭제 
        Destroy(gameObject);
    }
}
cs







실행화면

Tetris.mp4

큰 버그없이 구현해낸것에 어느정도 만족ㅜㅜ..

저작권때문에 출시는 불가능하지만 그래도 이때까지 만든 게임중에선 그나마 완성도가 좀 높은편인 것 같다.



Tetris.z01

Tetris.zip

좌우방향키가 이동. 아래가 빨리내려가기, 위쪽 방향키가 회전, 스페이스가 바로 내려가기이다.

16:9로 만들었기 때문에 1600:900같은 16:9 비율로 실행해야 캔버스(UI)가 제 위치에 나오는듯.

Posted by misty_
,