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(-1, 0); if (isValidGridPos()) updateGrid(); else transform.position += new Vector3(1, 0); } if (Input.GetKey(KeyCode.RightArrow)) { transform.position += new Vector3(1, 0); if (isValidGridPos()) updateGrid(); else transform.position += new Vector3(-1, 0); } // 아래 또는 마지막으로 떨어진지 1초가 지나면 if (Input.GetKey(KeyCode.DownArrow) || Time.time - lastFall >= 1) { transform.position += new Vector3(0, -1); if (isValidGridPos()) updateGrid(); else { transform.position += new Vector3(0, 1); 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(0, 1.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 |
실행화면
큰 버그없이 구현해낸것에 어느정도 만족ㅜㅜ..
저작권때문에 출시는 불가능하지만 그래도 이때까지 만든 게임중에선 그나마 완성도가 좀 높은편인 것 같다.
좌우방향키가 이동. 아래가 빨리내려가기, 위쪽 방향키가 회전, 스페이스가 바로 내려가기이다.
16:9로 만들었기 때문에 1600:900같은 16:9 비율로 실행해야 캔버스(UI)가 제 위치에 나오는듯.
'프로그래밍 공부 > Unity 프로젝트' 카테고리의 다른 글
2D 타워디펜스 - 2. 길만들기 + 몬스터생성, 이동 (2) | 2018.04.12 |
---|---|
2D 타워디펜스 - 1. 길만들기 (5) | 2018.04.12 |
2D 슈팅게임 (0) | 2018.04.11 |
캔버스를 이용한 2D 슈팅게임 (미완성) (0) | 2018.04.11 |
Space Shooter 만들어보기 (공식 예제) (0) | 2018.04.03 |