Tic tac toe game tutorial
Hi,
Few hours ago I tried to make a tic tac toe game for the first time. I just wanted to see how long it would take me. The result was less than 30min. Now you may wonder how I did it, so I'm going to explain it.
First create a new script call it as you like since this will be the only script used in this project.
Code:
public int SideWon = 0; public char SideToPlay = 'O'; public char[] Grid; private int AtMove = 0;
I declared this variables in the script. Side won is telling us which side has won the game. Side to play tells us which turn it is and at move variable holds a number which represents how many moves were made. Grid array will be used to represent board or tiles that you can see above. Each element in this array could be set to 'O' , 'X' or ' ' which is empty space.
In the start method initialize the grid.
Code:
void Start() { Grid = new char[9]; for (int i = 0; i < 9; i++) Grid[i] = ' '; }
Now create a method which will be used when we want to switch the turn by changing side to play value.
Code:
We could visualize the grid even now but there is still no need to since we need to write few more methods before we can actually do anything in the game.
void SwitchTurn() { SideToPlay = SideToPlay == 'O' ? 'X' : 'O'; }
We need two methods, one to make a move and the other one to undo 'any' move.
Code:
void MakeMove(int move) { Grid[move] = SideToPlay; SwitchTurn(); AtMove++; } void UndoMove(int move) { Grid[move] = ' '; SwitchTurn(); AtMove--; }
As you can see when making a move we are using move as an index and we are setting character of the current side to play and then we are switching turn and we increase the AtMove value by one.
It's almost the same thing when doing undo method. This time we set the grid at the move position or index to empty space. Then we again switch the turn and we decrease AtMove by one.
Now let's write evaluation method to check if there is match in the grid, in another words we check to see if one of the sides won the game, if so we return the int value(1 for O and -1 for X) but if there is no match we simply return the 0.
Code:
int Evaluate() { //Horizontal check if (Grid[0] != ' ' && Grid[0] == Grid[1] && Grid[1] == Grid[2]) return SideWonTheGame(); if (Grid[3] != ' ' && Grid[3] == Grid[4] && Grid[4] == Grid[5]) return SideWonTheGame(); if (Grid[6] != ' ' && Grid[6] == Grid[7] && Grid[7] == Grid[8]) return SideWonTheGame(); //Vertical check if (Grid[0] != ' ' && Grid[0] == Grid[3] && Grid[3] == Grid[6]) return SideWonTheGame(); if (Grid[1] != ' ' && Grid[1] == Grid[4] && Grid[4] == Grid[7]) return SideWonTheGame(); if (Grid[2] != ' ' && Grid[2] == Grid[5] && Grid[5] == Grid[8]) return SideWonTheGame(); //Diagonal check if (Grid[0] != ' ' && Grid[0] == Grid[4] && Grid[4] == Grid[8]) return SideWonTheGame(); //Anti diagonal check if (Grid[2] != ' ' && Grid[2] == Grid[4] && Grid[4] == Grid[6]) return SideWonTheGame(); return 0; } int SideWonTheGame() { if (SideToPlay == 'O') return 1; else return -1; }
Now as you can see this is really simple and rough check. We just make sure that the one element in the array we are checking against is not empty. Then we check that with the next element and one more time. Since there need to be 3 matched.
Now let's write the method to check if there could be made any more move in the game. To do that we need to check if the game is not already over - we will need to call the evaluate method and we will also need to make sure the whole board is not filled.
Code:
bool NoMoreMoves() { if (Evaluate() != 0) return true; for (int i = 0; i < Grid.Length; i++) { if (Grid[i] == ' ') return false; } return true; }
Now let's visualize the game. For that I used OnGUI method then I looped through each element in the grid array and for each I created a button on the screen. The button is labeled with the grid character at the given index. If that button is pressed we check if there are still moves to be made and if SideWon is still 0 (no need to do anything if you already lost the game) and we also need to check if the button that was clicked is empty. After checking all this we make a move and we evaluate the new position. If there is still possible moves and the player did not win ( which is impossible but we still check for some reason ) we make the computer move.
Code:
void OnGUI() { for (int x = 0; x < 3; x++) { for (int y = 0; y < 3; y++) { if (GUI.Button(new Rect(x*70, y*70 , 70, 70), Grid[x + y * 3].ToString()) && !NoMoreMoves() && SideWon == 0 && Grid[x+y*3] == ' ') { MakeMove(x + y * 3); SideWon = Evaluate(); if (!NoMoreMoves() && SideWon == 0) { //Computer to play here } } } } }
The hardest part is how to learn the computer to play the best move. He doesn't know how to think on his own and what's the point in the game so we will need to tell him. One possible solution is to use minimax algorithm which is how I did the AI. I won't go in details cause you can find way better explanation on the wikipedia if you want.
Rough explanation: Two players min and max they are both trying to get the best score. Computer plays all moves for the one side and after the each move before undoing the action he calls the opponent to make all of his possible moves( let's say that the opponent was MAX). Now the MAX calls MIN to play his moves and it goes over and over until no moves are possible or the game over was found. When that occurs we evaluate the position. MAX is trying to maximize the outcome and MIN is trying to minimize the outcome. While doing computer at the end assumes that if I play my best move, he'll plays his best move, I'll play my best move, he will play his best move, doing like this I'm going to get the best possible outcome. However if the player makes a mistake the outcome will be better for the one side.
So the computer in the tic tac toe where he can see all the possible game endings, He is unbeatable.
For my implementation I used negamax algorithm which is easier to write, once you understand it.
Code:
int NegateScore(int score) { if (SideToPlay == 'O') { return -score; } else return score; } int MiniMax(int atMove) { if (NoMoreMoves()) { return NegateScore(Evaluate()); //From different perspectives } int move = GetNextMove(0); //First move int bestValue = -2; //Worst score int bestMove = 0; while (move != -1) { //We have no more moves to perform MakeMove(move); int score = -MiniMax(-1); //Call opponent with -1 since atMove == AtMove will always be false if (score > bestValue) { //Beaten the worst score - must be beaten bestValue = score; bestMove = move; } UndoMove(move); move = GetNextMove(move+1); //Getting the next move } if (atMove == AtMove) //Instead of score returning best move which is also int return bestMove; return bestValue; } int GetNextMove(int last) { for (int i = last; i < Grid.Length; i++) { if (Grid[i] == ' ') return i; } return -1; }
We are getting the next move with the method GetNextMove with the offset value called last. Since we don't want to pick the moves we already picked. Once there are no more moves we return -1.
At the root node instead of the score we return the bestMove which was recorded during the process.
Now we just need to call minimax which would analyze and return the best move. I call it when the button was pressed like this:
Code:
MakeMove(MiniMax(AtMove)); SideWon = Evaluate();
Now you should have fully working tic tac toe game with unbeatable AI.
Optional task; Can you make it beatable but still smart?
Thank you for reading!
All the best,
Dragutin!
Unity chess project
Hi,
I've been working on a chess project called 'Lux Chess'. This was one of the hardest but yet amazing projects I worked on. I would encourage you to try to make your own chess engine just because there is so much great things you can learn. Anyway, after a few months working on this engine I got some pretty good results and I am satisfied with the engine strength. There is a lot of things that can be improved which I'll be hopefully doing in future.
I've been working on a chess project called 'Lux Chess'. This was one of the hardest but yet amazing projects I worked on. I would encourage you to try to make your own chess engine just because there is so much great things you can learn. Anyway, after a few months working on this engine I got some pretty good results and I am satisfied with the engine strength. There is a lot of things that can be improved which I'll be hopefully doing in future.
At the bottom of this post you can find the full source code download link. I hope you will have some fun playing against my engine and I hope you will even improve it or even make a game from it, if you do please don't hesitate to show me.
Now I'll quickly introduce you to my engine.
First thing you need to think about when writing a chess engine is how to represent the board and the pieces. There are many ways, and the first and the worst way that you probably already thought about is using arrays. This is really bad since in the chess engine it is all about the speed and arrays are slow. There are some ways to improve the speed but it's very slow when it comes to the move generation.
That's why I used something which is called bitboards. Since the largest number has a 64 bits and the chess board has a 64 squares you can see that this number can be used to represent the chess squares.
For each piece I had a ulong variable. Each bit can be set to 0 or 1. If it is 0 then that square is empty and if it is 1 then the square is occupied.
Here is the example of the 64 bit number:
0000 0000 0000 0000 0000 0000 0000 0000 = 0
Now there are a lot of useful instructions and bitwise operators that can be used on this bitboards. There is also a script in the project called Ops which stands for Operations. You can see some of the functions that helps extracting the bits from the bitboard or even finding their position or index.
Now this is the rough explanation but if you are interested in more details I might release another post which would explain more and even some other parts of the chess engine.
Also for the first time I'm going to put a donate button on my blog. Now you have a chance to help me do more amazing free projects!
Thank's for reading.
All the best,
Last wheel - Android game
Hi,
This is my own first android game published on google play. If you want to play it, here is the link:
https://play.google.com/store/apps/details?id=com.ShinyStar.LastWheel
Tell me what you think!
Match3 game
Hey there,
Today, I'd like to show you how to create a match3 style game.
We will start of by setting up our scene as following:
Once you have that you can go ahead and create gems or blocks for this match3 game:
Thank's for reading , also I'd love to hear some feedback from you :]
SOURCE CODE
Source code
Today, I'd like to show you how to create a match3 style game.
We will start of by setting up our scene as following:
- Set new Main Camera position to Vector3(5, 4.5f, -10f)
- Create two c# scripts (Board and block)
- Create new empty Game Object and assign Board script
Once you have that you can go ahead and create gems or blocks for this match3 game:
- Create new 3d object - Cube Set cube scale to Vector3(0.8f,0.8f,0.8f)
- Make a prefab and change material(this way you can have more different blocks)
- Do this for at least 4 blocks.
Board script:
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 | using UnityEngine; using System.Collections; using System.Collections.Generic; public class Board : MonoBehaviour { //Blocks used in game public GameObject[] Blocks; //Size of the board public int Width = 10; public int Height = 10; //2D grid public Block[,] Grid; //Blocks falling public bool Falling; void Start(){ //Initialize grid Grid = new Block[Width,Height*2]; } void Update(){ Falling = BlocksFalling(); //If blocks are not falling then we can spawn :] if(!Falling) Spawn(); //Make them fall down each frame Fall(); } void Spawn(){ //For each collumn for(int x=0; x<Width; x++){ int count = 0;// Count how many blocks we spawned //Go from bottom up for(int y=0; y<Height; y++){ //If we found empty block and there is empty position above playable board if(!Grid[x,y] && !Grid[x,Height+count]){ //Spawn it above the board CreateBlock(x,Height+count); //We spawned one more block count++; } } } } void Fall(){ //For each collumn for(int x=0; x<Width; x++){ int count = 0; //counter //Start from bottom and go up for(int y=0; y<Height*2; y++){ Block block = Grid[x,y];//Get block at this position //If the block exist if(block){ //If this block ID equals 1, that means that this block is currently getting destroyed if(block.ID == -1) y = Height*2;//Skip this collumn else if(block.NY != count){//If the new block position is not equal to current position....Then move it down Grid[x,y] = null;//Old position is now empty block.NY = count;//Set new block height block.CheckForMatch = true;//This block moved, so we check for match Grid[x,count] = block;//New position now equals this block } else if(block.CheckForMatch && !Falling){//If no blocks are falling then we can check for match if we need to Solve(Match (block)); //We call Solve method passing List that we found in match method block.CheckForMatch = false;//Do not check anymore } //We found one more block count++; } } } } void CreateBlock(int x, int y){ //Random ID int id = Random.Range(0,Blocks.Length); //Create block GameObject obj = Instantiate(Blocks[id], new Vector3(x,y), Quaternion.identity) as GameObject; //Block component Block block = obj.AddComponent<Block>(); //Set values Grid[x,y] = block; //We spawned block here block.ID = id; //Block have this ID block.X = x; block.Y = y; block.NY = y; block.board = this; //Just to make things clean obj.transform.parent = transform; } //This will return true if the blocks are falling and false if not bool BlocksFalling(){ for(int x=0; x<Width; x++){ for(int y=0; y<Height; y++){ if(Grid[x,y] && !Grid[x,y].IsReady()){ return true; } } } return false; } //Match3 will return List of Blocks that has been found against check block List<Block> Match(Block check){ List<Block> ToReturn = new List<Block>(); //Blocks to return List<Block> Matches = new List<Block>();//List of blocks bool linked = false; //If we found our check block //HORIZONTAL CHECK for(int x=0; x<Width; x++){//Left to right if(x==(int)check.X)//If we found our check block on this position then link matches linked = true; if(Grid[x,(int)check.Y] && Grid[x,(int)check.Y].IsReady() && Grid[x,(int)check.Y].ID == check.ID)//If this block is same as check block(id) Matches.Add(Grid[x,(int)check.Y]);//Add this block in the list else if(linked)//The block doesn't exist or ID is not equal or Its Falling....Then if linked we will stop there and we will check whatever we have in list break; else //Nothing of this is true, then just clear list and keep checking Matches.Clear(); } //We have list and blocks in it but we will return it only if we found 3 or more blocks if(Matches.Count>=3) ToReturn.AddRange(Matches); //Rese Matches.Clear(); linked = false; //VERTICAL CHECK //Same as before for(int y=0; y<Height; y++){ if(y==(int)check.Y) linked = true; if(Grid[(int)check.X, y] && Grid[(int)check.X, y].IsReady() && Grid[(int)check.X, y].ID == check.ID) Matches.Add(Grid[(int)check.X, y]); else if(linked) break; else Matches.Clear(); } if(Matches.Count>=3) ToReturn.AddRange(Matches); return ToReturn; } //Solve List of blocks void Solve(List<Block> Matches){ int count = Matches.Count; //How many blocks we have //We will destroy every block in this list, no matter how many we found...You can do stuff based on count foreach(Block block in Matches){ block.StartCoroutine("Destroy");//Start destroy } } } |
Block script:
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 | using UnityEngine; using System.Collections; public class Block : MonoBehaviour { [HideInInspector]public int ID; //ID of this block //Position of this block in the board [HideInInspector]public float X; [HideInInspector]public float Y; [HideInInspector]public float NY; // New block height [HideInInspector]public Board board; [HideInInspector]public bool CheckForMatch = false; //If we need to check for match void Update(){ //If the new height is set then it will be lower than curent height if(NY<Y){//It should fall down //Move it down Y-=0.1f; //Update block position transform.position = new Vector3(X,Y); } else{//Its not falling, and it should not Y=NY; //Round Y transform.position = new Vector3(X,Y); //Round position } } //This will destroy block when called public IEnumerator Destroy(){ ID = -1; //We update block ID so it doesn't get matched or moved float time = 0; //Change block scale by time while(time<=1){ transform.localScale = Vector3.Lerp(new Vector3(0.8f,0.8f,0.8f), Vector3.zero, time); time += Time.deltaTime/2; yield return null; } //Update grid, set empty board.Grid[(int)X, (int)Y] = null; //At the end destroy that block gameobject Destroy(gameObject); } // Determines whether this Block is ready - falling down public bool IsReady(){ return Y==NY; } } |
board.Fall(): we go from bottom up to top. Once we found solid block we update that block height to equal current number of solid blocks found. Now we increase that value by one. You can go ahead and draw this method and see how it works. Here is my drawing :)
If you need anything else explained feel free to ask.
Thank's for reading , also I'd love to hear some feedback from you :]
SOURCE CODE
Source code
Tetris game tutorial - part 2
User input
Now when we spawn a block we want to make user able to controll that shape(left, right, rotate, fall down faster).
But each time we want to move left or right or even to rotate block we will need to check will it hit something.
In update function:
void Update(){ if(spawn && shapes.Count == 4){ //If there is block //Get spawned blocks positions Vector3 a = shapes[0].transform.position; Vector3 b = shapes[1].transform.position; Vector3 d = shapes[2].transform.position; Vector3 c = shapes[3].transform.position; if(Input.GetKeyDown(KeyCode.LeftArrow)){//Move left if(CheckUserMove(a,b,c,d,true)){//Check if we can move it left a.x-=1; b.x-=1; c.x-=1; d.x-=1; pivot.transform.position = new Vector3(pivot.transform.position.x-1, pivot.transform.position.y, pivot.transform.position.z); shapes[0].transform.position = a; shapes[1].transform.position = b; shapes[2].transform.position = c; shapes[3].transform.position = d; } } if(Input.GetKeyDown(KeyCode.RightArrow)){//Move right if(CheckUserMove(a,b,c,d,false)){//Check if we can move it right a.x+=1; b.x+=1; c.x+=1; d.x+=1; pivot.transform.position = new Vector3(pivot.transform.position.x+1, pivot.transform.position.y, pivot.transform.position.z); shapes[0].transform.position = a; shapes[1].transform.position = b; shapes[2].transform.position = c; shapes[3].transform.position = d; } } if(Input.GetKey(KeyCode.DownArrow)){ //Move down fast moveDown(); }CheckUserMove function:
bool CheckUserMove(Vector3 a, Vector3 b, Vector3 c, Vector3 d, bool dir){ //Check, if we move a block left/right will it hit something if(dir){//Left if(board[Mathf.RoundToInt(a.x-1),Mathf.RoundToInt(a.y)]==1 || board[Mathf.RoundToInt(b.x-1),Mathf.RoundToInt(b.y)]==1 || board[Mathf.RoundToInt(c.x-1),Mathf.RoundToInt(c.y)]==1 || board[Mathf.RoundToInt(d.x-1),Mathf.RoundToInt(d.y)]==1){ return false; } } else{//Right if(board[Mathf.RoundToInt(a.x+1),Mathf.RoundToInt(a.y)]==1 || board[Mathf.RoundToInt(b.x+1),Mathf.RoundToInt(b.y)]==1 || board[Mathf.RoundToInt(c.x+1),Mathf.RoundToInt(c.y)]==1 || board[Mathf.RoundToInt(d.x+1),Mathf.RoundToInt(d.y)]==1){ return false; } } return true; }So when user press left arrow or right it will first check if CheckUserMove returns true, which means that we can move shape left or right. Also when user press down arrow it will call function moveDown so it goes faster...
Block rotation
The way I did block rotation is really simple but not the best way. Anyway in the update function we will check when user press the space and then we will rotate blocks around object which is called RotateAround(this object moves with shape and his position is defined for each block in SpawnShape function). Also we will check after we rotate shape does any block hit edge or other block and if so will just rotate it again in default position.
if(Input.GetKeyDown(KeyCode.Space)){ //Rotate Rotate(shapes[0].transform,shapes[1].transform,shapes[2].transform,shapes[3].transform); }
void Rotate(Transform a, Transform b, Transform c, Transform d){ //Set parent to pivot so we can rotate a.parent = pivot.transform; b.parent = pivot.transform; c.parent = pivot.transform; d.parent = pivot.transform; currentRot +=90;//Add rotation if(currentRot==360){ //Reset rotation currentRot = 0; } pivot.transform.localEulerAngles = new Vector3(0,0,currentRot); a.parent = null; b.parent = null; c.parent = null; d.parent = null; if(CheckRotate(a.position,b.position,c.position,d.position) == false){ //Set parent to pivot so we can rotate a.parent = pivot.transform; b.parent = pivot.transform; c.parent = pivot.transform; d.parent = pivot.transform; currentRot-=90; pivot.transform.localEulerAngles = new Vector3(0,0,currentRot); a.parent = null; b.parent = null; c.parent = null; d.parent = null; } } bool CheckRotate(Vector3 a, Vector3 b, Vector3 c, Vector3 d){ if(Mathf.RoundToInt(a.x)<board.GetLength(0)-1){//Check if block is in board if(board[Mathf.RoundToInt(a.x),Mathf.RoundToInt(a.y)]==1){ //If rotated block hit any other block or edge, after rotation return false; //Rotate in default position - previous } } else{//If the block is not in the board return false;//Do not rotate } if(Mathf.RoundToInt(b.x)<board.GetLength(0)-1){ if(board[Mathf.RoundToInt(b.x),Mathf.RoundToInt(b.y)]==1){ return false; } } else{ return false; } if(Mathf.RoundToInt(c.x)<board.GetLength(0)-1){ if(board[Mathf.RoundToInt(c.x),Mathf.RoundToInt(c.y)]==1){ return false; } } else{ return false; } if(Mathf.RoundToInt(d.x)<board.GetLength(0)-1){ if(board[Mathf.RoundToInt(d.x),Mathf.RoundToInt(d.y)]==1){ return false; } } else{ return false; } return true; //We can rotate }Now when you press space you should see shape rotate by 90 deegres.
Check if there is full row, destroy and move blocks down
This is the last thing we need to do. We will check for full row each time shape hit something(You can find it in moveDown function). For each row we will count how many blocks are at the height 1. If there is 10 blocks(which is full row) move blocks down and again check height 1, because there can be again full row. And if less than 10 blocks then check row above this.
And also if at the game over height is more than 1 block then just set gameOver boolean to true, and spawninig will stop.
//Check specific row for match void checkRow(int y){ GameObject[] blocks = GameObject.FindGameObjectsWithTag("Block"); //All blocks in the scene int count = 0; //Blocks found in a row for(int x=1; x<board.GetLength(0)-1; x++){//Go through each block on this height if(board[x,y]==1){//If there is any block at this position count++;//We found +1 block } } if(y==gameOverHeight && count>0){//If the current height is game over height, and there is more than 0 block, then game over Debug.LogWarning("Game over"); gameOver = true; } if(count==10){//The row is full //Start from bottom of the board(withouth edge and block spawn space) for(int cy=y; cy<board.GetLength(1)-3; cy++){ for(int cx=1; cx<board.GetLength(0)-1; cx++){ foreach(GameObject go in blocks){ int height = Mathf.RoundToInt(go.transform.position.y); int xPos = Mathf.RoundToInt(go.transform.position.x); if(xPos == cx && height == cy){ if(height == y){//The row we need to destroy board[xPos,height] = 0;//Set empty space Destroy(go.gameObject); } else if(height > y){ board[xPos,height] = 0;//Set old position to empty board[xPos,height-1] = 1;//Set new position go.transform.position = new Vector3(xPos, height-1, go.transform.position.z);//Move block down } } } } } checkRow(y); //We moved blocks down, check again this row } else if(y+1<board.GetLength(1)-3){ checkRow(y+1); //Check row above this } }We will also need to call this in moveDown function when the shape hit something.
else{ //We hit something. Stop and mark current shape location as filled in board, also destroy last pivot gameobject Destroy(pivot.gameObject); //Destroy pivot //Set ID in board board[Mathf.RoundToInt(a.x),Mathf.RoundToInt(a.y)]=1; board[Mathf.RoundToInt(b.x),Mathf.RoundToInt(b.y)]=1; board[Mathf.RoundToInt(c.x),Mathf.RoundToInt(c.y)]=1; board[Mathf.RoundToInt(d.x),Mathf.RoundToInt(d.y)]=1; //**************************************************** checkRow(1); //Check for any match checkRow(gameOverHeight); //Check for game over //**************************************************** shapes.Clear(); //Clear spawned blocks from array spawn = false; //Spawn a new block }That's it. You should be able to rotate, move shape left and right, speed down. And when the row is full it should clear that row also all blocks above will fall. Feel free to request any tutorial. I hope you enjoyed.
Full tetris script.
Source code
Tetris game tutorial - part 1
Hello,
By the Able_Elizalde request, I will start the tutorial about ,,How to create a simple tetris game".
So far I've never tried to make a tetris game. And this may not be the best way to do tetris style game.
Anyway, what we need to do:
Now we will need to generate a board as soon as the game start, so make sure to call GenBoard in a Start function.
We will generate a tetris board like this:
Now lets create all shapes that can be spawned and each shape will have pivot point(later we will rotate shape around that point).
One shape will consist out of 4 blocks. So spawn shape function will return 4 blocks.
You will also need to add a new tag ,,Block,,. Then later we can just simply find all objects with that tag.
To actually move a shape down we will first need to spawn a shape. So in a update function:
Let's now move that shape down.
By the Able_Elizalde request, I will start the tutorial about ,,How to create a simple tetris game".
So far I've never tried to make a tetris game. And this may not be the best way to do tetris style game.
Anyway, what we need to do:
- Generate board
- Create a shape
- Move shape down
- Collision between other blocks and edge
- User input
- Block rotation
- Check if there is full row, destroy and move blocks down
Before we start you can test final result.
Generate board
To generate a board, we will first need to set board width and height. Tetris board size is 10x20. But we will also add left and right edge of the board. Which is 1 + 10 + 1 = 12.
Same for the height 1 + 20 + 1 and for the block spawn +2 = 24.
In a new c# script we will define few variables we will need for this tetris game.
Same for the height 1 + 20 + 1 and for the block spawn +2 = 24.
In a new c# script we will define few variables we will need for this tetris game.
using UnityEngine; using System.Collections; using System.Collections.Generic; public class Tetris : MonoBehaviour { //Board public int[,] board; //Block public Transform block; //Spawn boolean public bool spawn; //Seconds before next block spawn public float nextBlockSpawnTime = 0.5f; //Block fall speed public float blockFallSpeed = 0.5f; //Game over level public int gameOverHeight = 22; //20 board + 2 edge //Current spawned shapes private List<Transform> shapes = new List<Transform>(); //Set true if game over private bool gameOver; //Current rotation of an object private int currentRot = 0; //Current pivot of the shape private GameObject pivot; void Start(){ //Deafult board is 10x16 //1+10+1 - Side edge //+2 - Space for spawning //+1 - Top edge //20 - Height //+1 - Down edge board = new int[12,24];//Set board width and height
Now we will need to generate a board as soon as the game start, so make sure to call GenBoard in a Start function.
We will generate a tetris board like this:
- Edge of the board, we will mark it in board 2D array, so we can later check where is edge
- Board, mark it in board 2D array as empty space
void GenBoard(){ for(int x=0; x<board.GetLength(0);x++){ for(int y=0; y<board.GetLength(1);y++){ if(x<11 && x>0){ if(y>0 && y<board.GetLength(1)-2){ //Board board[x,y]=0; GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube); cube.transform.position = new Vector3(x,y,1); Material material = new Material(Shader.Find("Diffuse")); material.color = Color.grey; cube.renderer.material = material; cube.transform.parent = transform; } else if(y<board.GetLength(1)-2){ board[x,y]=1; GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube); cube.transform.position = new Vector3(x,y,0); Material material = new Material(Shader.Find("Diffuse")); material.color = Color.black; cube.renderer.material = material; cube.transform.parent = transform; cube.collider.isTrigger = true; } } else if((y<board.GetLength(1)-2)){ //Left and right edge board[x,y]=1; GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube); cube.transform.position = new Vector3(x,y,0); Material material = new Material(Shader.Find("Diffuse")); material.color = Color.black; cube.renderer.material = material; cube.transform.parent = transform; } } } }
Create a shape
void SpawnShape(){ int shape = Random.Range(0,6);//Random shape int height = board.GetLength(1)-4; int xPos = board.GetLength(0)/2-1; //Create a new pivot pivot = new GameObject("RotateAround"); //Pivot of the shape //SShape if(shape==0){ pivot.transform.position = new Vector3(xPos,height+1, 0); shapes.Add(GenBlock(new Vector3(xPos, height,0))); shapes.Add(GenBlock(new Vector3(xPos-1, height,0))); shapes.Add(GenBlock(new Vector3(xPos, height+1,0))); shapes.Add(GenBlock(new Vector3(xPos+1, height+1,0))); Debug.Log("Spawned SShape"); } //IShape else if(shape==1){ pivot.transform.position = new Vector3(xPos+0.5f,height+1.5f, 0); shapes.Add(GenBlock(new Vector3(xPos, height,0))); shapes.Add(GenBlock(new Vector3(xPos, height+1,0))); shapes.Add(GenBlock(new Vector3(xPos, height+2,0))); shapes.Add(GenBlock(new Vector3(xPos, height+3,0))); Debug.Log("Spawned IShape"); } //OShape else if(shape==2){ pivot.transform.position = new Vector3(xPos+0.5f,height+0.5f, 0); shapes.Add(GenBlock(new Vector3(xPos, height,0))); shapes.Add(GenBlock(new Vector3(xPos+1, height,0))); shapes.Add(GenBlock(new Vector3(xPos, height+1,0))); shapes.Add(GenBlock(new Vector3(xPos+1, height+1,0))); Debug.Log("Spawned OShape"); } //JShape else if(shape==3){ pivot.transform.position = new Vector3(xPos,height+2, 0); shapes.Add(GenBlock(new Vector3(xPos, height,0))); shapes.Add(GenBlock(new Vector3(xPos+1, height,0))); shapes.Add(GenBlock(new Vector3(xPos, height+1,0))); shapes.Add(GenBlock(new Vector3(xPos, height+2,0))); Debug.Log("Spawned JShape"); } //TShape else if(shape==4){ pivot.transform.position = new Vector3(xPos,height, 0); shapes.Add(GenBlock(new Vector3(xPos, height,0))); shapes.Add(GenBlock(new Vector3(xPos-1, height,0))); shapes.Add(GenBlock(new Vector3(xPos+1, height,0))); shapes.Add(GenBlock(new Vector3(xPos, height+1,0))); Debug.Log("Spawned TShape"); } //LShape else if(shape==5){ pivot.transform.position = new Vector3(xPos,height+1, 0); shapes.Add(GenBlock(new Vector3(xPos, height,0))); shapes.Add(GenBlock(new Vector3(xPos-1, height,0))); shapes.Add(GenBlock(new Vector3(xPos, height+1,0))); shapes.Add(GenBlock(new Vector3(xPos, height+2,0))); Debug.Log("Spawned LShape"); } //ZShape else{ pivot.transform.position = new Vector3(xPos,height+1, 0); shapes.Add(GenBlock(new Vector3(xPos, height,0))); shapes.Add(GenBlock(new Vector3(xPos+1, height,0))); shapes.Add(GenBlock(new Vector3(xPos, height+1,0))); shapes.Add(GenBlock(new Vector3(xPos-1, height+1,0))); Debug.Log("Spawned ZShape"); } } //Create block at the position Transform GenBlock(Vector3 pos){ Transform obj = (Transform)Instantiate(block.transform, pos, Quaternion.identity) as Transform; obj.tag = "Block"; return obj; }
Move shape down and collision
if(!spawn && !gameOver){//If nothing spawned, if game over = false, then spawn StartCoroutine("Wait"); spawn = true; //Reset rotation currentRot = 0; }Then we will wait some time before shape is spawned.
//Wait time before next block spawn IEnumerator Wait(){ yield return new WaitForSeconds(nextBlockSpawnTime); SpawnShape(); }Now if you run the code you should see a random spawned shape, and also board in the background.
Let's now move that shape down.
void moveDown(){ //Spawned blocks positions if(shapes.Count!=4){ return; } Vector3 a = shapes[0].transform.position; Vector3 b = shapes[1].transform.position; Vector3 c = shapes[2].transform.position; Vector3 d = shapes[3].transform.position; if(CheckMove(a,b,c,d)==true){ // Will we hit anything if we move block down(true = we can move) //Move block down by 1 a = new Vector3(Mathf.RoundToInt(a.x),Mathf.RoundToInt(a.y-1.0f),a.z); b = new Vector3(Mathf.RoundToInt(b.x),Mathf.RoundToInt(b.y-1.0f),b.z); c = new Vector3(Mathf.RoundToInt(c.x),Mathf.RoundToInt(c.y-1.0f),c.z); d = new Vector3(Mathf.RoundToInt(d.x),Mathf.RoundToInt(d.y-1.0f),d.z); pivot.transform.position = new Vector3(pivot.transform.position.x, pivot.transform.position.y-1, pivot.transform.position.z); shapes[0].transform.position = a; shapes[1].transform.position = b; shapes[2].transform.position = c; shapes[3].transform.position = d; } else{ //We hit something. Stop and mark current shape location as filled in board, also destroy last pivot gameobject Destroy(pivot.gameObject); //Destroy pivot //Set ID in board board[Mathf.RoundToInt(a.x),Mathf.RoundToInt(a.y)]=1; board[Mathf.RoundToInt(b.x),Mathf.RoundToInt(b.y)]=1; board[Mathf.RoundToInt(c.x),Mathf.RoundToInt(c.y)]=1; board[Mathf.RoundToInt(d.x),Mathf.RoundToInt(d.y)]=1; shapes.Clear(); //Clear spawned blocks from array spawn = false; //Spawn a new block } } bool CheckMove(Vector3 a, Vector3 b, Vector3 c, Vector3 d){ //Check, if we move a block down will it hit something if(board[Mathf.RoundToInt(a.x),Mathf.RoundToInt(a.y-1)]==1){ return false; } if(board[Mathf.RoundToInt(b.x),Mathf.RoundToInt(b.y-1)]==1){ return false; } if(board[Mathf.RoundToInt(c.x),Mathf.RoundToInt(c.y-1)]==1){ return false; } if(board[Mathf.RoundToInt(d.x),Mathf.RoundToInt(d.y-1)]==1){ return false; } return true; }But this is never called. So let's call it in Start function, just this time we will use InvokeRepeating.
InvokeRepeating("moveDown",blockFallSpeed,blockFallSpeed); //move block downNow the block will move down until it hits something, then it will stop marking current shape position as filled. Source code