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:
    void SwitchTurn() {
        SideToPlay = SideToPlay == 'O' ? 'X' : 'O';
    }

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.

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!
Donate Button

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.


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,
Dragutin!

Source code
Donate Button

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:



  • 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
Donate Button

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
Donate Button

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:

  1. Generate board
  2. Create a shape
  3. Move shape down
  4. Collision between other blocks and edge
  5. User input
  6. Block rotation
  7. 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.





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:

  1. Edge of the board, we will mark it in board 2D array, so we can later check where is edge
  2. 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

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.
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

To actually move a shape down we will first need to spawn a shape. So in a update function:
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 down
Now the block will move down until it hits something, then it will stop marking current shape position as filled. Source code
Donate Button

- Copyright © Unity plus - Skyblue - Powered by Blogger - Designed by Johanes Djogan -