Building a Word Game in Unity

Moving on to my next Unity tutorial, I’ll show you how to build a basic word game. A kind of endless Boggle. In the process, you’ll see how to build a basic grid game in Unity. The next couple of tutorials I have planned will also involve grid based games.

Once again I skip the detailed explanation of Unity’s interface, so again this tutorial is aimed at people who already know how to set up a project inside Unity. If you don’t know how to do it: Youtube.

CODE UPDATE

After the original post for the word game in Unity (a boggle like game) a lot of people asked about handling the input for Desktop as well as how to make the tile selection a bit smoother.
I had coded a much better input controller for the Rock Paper Scissors Lizard Spock game and so I would redirect people to that code. But some people had problems merging the logic in the two projects.
So here is the updated Word Game with desktop input handling and smoother tile selection.

The way to solve the tile selection, like in the Rock Paper… game is to use a tiny collider. So I added a trigger collider to the tiles and a second invisible trigger collider on an object the user drags around with every touch/mouse down.

Hopefully this will solve people’s problems with the code.

download the project here

I might even rework this with Reactive Extensions. It would be a nice project for it.

The Game

We’ll build a 6×8 grid made of random letters. The player can select these tiles to form a word. Unlike some versions of Boggle, the selected tiles must be connected, although diagonally connected tiles can be selected, allowing then an eight-directional selection.

The project is 2D, and has only one scene and 5 classes.

The Hierarchy

The project only has a GameView container and a canvas for the UI. The GameView game object will contain all the components we need: The InputController, the WordData, the Grid and the Game controller itself

The GridTile

This is a sprite prefab. It has a background texture and a texture for each letter. It also has the remaining class in the game: Tile, as a component.

Loading the Word Data

I used two word lists I found as part of the Guttenberg Project. One is a massive word list, and one is a list of the most common words in the English language. For word games you will almost always load two lists like these. One, the massive one, to compare generated words with, and one, the common words one, to generate anagrams or “find the words” sort of puzzles.

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
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using System.Text;
 
 
public class WordData : MonoBehaviour {
 
	public WordGame game;
 
	[HideInInspector]
	public Dictionary<char, List<string>> wordsMap;
 
	private List<string> allWords;
 
	private List<string> allWordsUnique;
 
 
	public bool IsValidWord (string word)
	{
		if (!wordsMap.ContainsKey (word [0]))
			return false;
		var list = wordsMap [word [0]];
		if (list != null) 
		{
			return list.Contains(word);
 
		}
		return false;
	}
 
	public string GetRandomWord (int len = 0)
	{
		if (len != 0) {
 
			while (true)
			{
				var i = Random.Range (0, allWordsUnique.Count);
				var w = allWordsUnique [i].TrimEnd ();
				w = w.TrimStart ();
				allWordsUnique.RemoveAt (i);
				if (w.Length == len) {
					return w;
				}
			}
		}
 
		var index = Random.Range (0, allWordsUnique.Count);
		var word = allWordsUnique [index].TrimEnd ();
		word = word.TrimStart ();
		allWordsUnique.RemoveAt (index);
		return word;
	}
 
	void Start ()
	{
		wordsMap = new Dictionary<char, List<string>> ();
		StartCoroutine ("LoadWordData");
	}
 
	IEnumerator LoadWordData() {
 
		string filePath = System.IO.Path.Combine(Application.streamingAssetsPath, "words.txt");
 
		string result = null;
 
		if (filePath.Contains("://")) {
			WWW www = new WWW(filePath);
			yield return www;
			result = www.text;
		} else
			result = System.IO.File.ReadAllText(filePath);
 
		ProcessWordSource(result);
 
 
		filePath = System.IO.Path.Combine(Application.streamingAssetsPath, "commonWords.txt");
 
		result = null;
 
		if (filePath.Contains("://")) {
			WWW www = new WWW(filePath);
			yield return www;
			result = www.text;
		} else
			result = System.IO.File.ReadAllText(filePath);
 
 
		ProcessWordData (result);
 
		game.InitGame ();
	}
 
	void ProcessWordSource (string data) {
		var words = data.Split('\n');
		foreach (var entry in words) 
		{
			var c = entry[0];
			if (!wordsMap.ContainsKey(c))
			{
				wordsMap.Add (c, new List<string>());
			}
			wordsMap[c].Add(entry.TrimEnd());
		}
	}
 
	void ProcessWordData (string data)
	{
		var words = data.Split('\n');
		allWords = new List<string> (words);
 
		ShuffleList (allWords);
 
		allWordsUnique = new List<string> ();
		allWordsUnique.AddRange (allWords);
	}
 
 
	private static System.Random random = new System.Random();
 
	public static void ShuffleList<T>(List<T> array)
	{
		int n = array.Count;
		for (int i = 0; i < n; i++)
		{
			int r = i + (int)(random.NextDouble() * (n - i));
			T t = array[r];
			array[r] = array[i];
			array[i] = t;
		}
	}
}

The lists need to be inside a folder, inside Assets, called StreamingAssets.

I generate more collections than necessary for this simple game, but it’s good to create one list you can remove indexes from, so words are not repeated and one master list. But again, it depends on what sort of game you’re building.

The Input Controller

Not unlike the generic Input Controller I used in the previous tutorial. It maps both mouse and touch input to the same handlers.

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
using UnityEngine;
using System.Collections;
 
public class InputController : MonoBehaviour {
 
	public WordGame game;
 
	void Update () {
 
		if (Input.touches.Length > 0) {
 
			Touch touch = Input.touches[0];
 
			if (touch.phase == TouchPhase.Began)
			{
				game.HandleTouchDown (touch.position);
			}
			else if (touch.phase == TouchPhase.Canceled || touch.phase == TouchPhase.Ended)
			{
				game.HandleTouchUp(touch.position);
			}
			else if (touch.phase == TouchPhase.Moved || touch.phase == TouchPhase.Stationary)
			{
				game.HandleTouchMove (touch.position);
			}
 
 
			game.HandleTouchMove (touch.position);	
 
			return;
		}
		else if (Input.GetMouseButtonDown(0) )
		{
			game.HandleTouchDown (Input.mousePosition);
		}
		else if (Input.GetMouseButtonUp(0))
		{
			game.HandleTouchUp(Input.mousePosition);
		}
		else 
			game.HandleTouchMove (Input.mousePosition);
 
	}
}

The Grid

The heart of the game. This class builds the grid.

I like to create two lists. One lists only the indexes of all the tiles in the grid, and it’s the one I can shuffle. The other list is the grid proper, with columns and rows.

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
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
 
public class Grid : MonoBehaviour {
 
	public int ROWS = 8;
	public int COLUMNS = 6;
 
	public GameObject gridTileGO;
 
	public  float GRID_OFFSET_X = 6.4f;
	public  float GRID_OFFSET_Y = 10f;
 
	[HideInInspector]
	public List<Tile> tiles;
 
	[HideInInspector]
	public List<List<Tile>> gridTiles;
 
	private string wordSource;
 
	private int wordSourceIndex;
 
	private struct Cell
	{
		public int row;
		public int column;
	}
 
	private List<Cell> gridIndexes;
 
 
	void Awake () {
		BuildSuffledIndexes ();
	}
 
	public void BuildGrid ()
	{
		var wordData = GetComponent<WordData> ();
		wordSource = wordData.GetRandomWord ();
 
		foreach (var index in gridIndexes) 
		{
			gridTiles[index.column][index.row].SetTileData(wordSource[wordSourceIndex]);
			wordSourceIndex++;
			if (wordSourceIndex == wordSource.Length)
			{
				wordSource = wordData.GetRandomWord();
				wordSourceIndex = 0;
			}
		}
	}
 
	public void CollapseGrid ()
	{
		for (int column = 0; column < COLUMNS; column++) {
 
			var columnList = gridTiles[column];
			var newColumn = new List<Tile>(ROWS);
			var removedCnt = 0;
			var row = ROWS - 1;
			var removedTiles = columnList.FindAll ( (e)=> {return (!e.gameObject.activeSelf);});
			removedTiles.Reverse();
			var totalRemoved = removedTiles.Count;
 
			for (var i = columnList.Count - 1; i >= 0; i--)
			{
				if (!columnList[i].gameObject.activeSelf)
				{
					columnList[i].row = removedCnt;
					var p = columnList[i].transform.position;
					p.y = columnList[0].transform.position.y + (totalRemoved - removedCnt) * 2.4f ;
					columnList[i].transform.position = p;
					removedCnt++;
				}
				else
				{
					columnList[i].row = row;
					row--;
					newColumn.Insert(0, columnList[i]);
				}
			}
 
			//append removed tiles
			newColumn.InsertRange (0, removedTiles);
 
			//update tiles
			foreach (var tile in newColumn)
			{
				tile.UpdateData();
			}
 
			gridTiles[column] = newColumn;
		}
	}
 
	private void BuildSuffledIndexes ()
	{
		tiles = new List<Tile> ();
		gridTiles = new List<List<Tile>> ();
 
		gridIndexes = new List<Cell> ();
		Cell indexer;
		for (int column = 0; column < COLUMNS; column++) {
 
			var columnTiles = new List<Tile>();
 
			for (int row = 0; row < ROWS; row++) {
				indexer = new Cell();
				indexer.column = column;
				indexer.row = row;
				gridIndexes.Add (indexer);
 
				var item = Instantiate (gridTileGO) as GameObject;
				var tile = item.GetComponent<Tile>();
				tile.SetTilePosition(this, column, row);
				tile.transform.parent = gameObject.transform;
				tiles.Add (tile);
				columnTiles.Add (tile);
			}
			gridTiles.Add(columnTiles);
		}
 
		WordData.ShuffleList (gridIndexes);
	}
 
}

In order for the game to be an “endless” Boggle, I need to collapse the grid. So every time a word is found, I “remove” those tiles, and add fresh ones to the grid. the CollapseGrid method takes care of that, it looks for invisible tiles, the ones I removed by making them inactive, and rearranges them.

The key thing here is the wordSource variable. I found that the best way to generate a random grid of letters so that the player can find words, reasonably easy, is to pick the letters from random words. So I fill the grid with letters from multiple words, placing them randomly in the grid. This ensures the right amount of vowels and consonants.

The Tile

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
using UnityEngine;
using System;
using System.Collections;
 
 
public class Tile : MonoBehaviour {
 
	public GameObject[] charsGO;
 
	public GameObject tileBg;
 
	[HideInInspector]
	public int row;
 
	[HideInInspector]
	public int column;
 
	[HideInInspector]
	public int type;
 
	[HideInInspector]
	public bool selected;
 
	[HideInInspector]
	public bool touched;
 
	private Vector3 tilePosition;
 
	private static char[] chars = new char[] {'a','b','c','d','e','f','g','h','i','j','k','l',
		'm','n','o','p','q','r','s','t','u','v','w','x','y','z'};
 
	private static char[] vowels = new char[] {'a','e','i','o','u'};
 
	private static char[] consonants = new char[] {'b','c','d','f','g','h','j','k','l','m','n',
		'p','q','r','s','t','v','w','x','y','z'};
 
	private bool updatePosition;
 
	public char TypeChar {
		get { return chars [type]; }
		private set{}
	}
 
	public static float size;
 
	private Grid grid;
 
	public void SetTileData (char c) 
	{
		charsGO [type].SetActive (false);
 
		var index = Array.IndexOf (chars, c);
 
		charsGO [index].SetActive (true);
 
		type = index;
 
		tileBg.GetComponent<Renderer> ().material.color =  Color.black;
 
		charsGO [index].GetComponent<Renderer> ().material.color = Color.white;
	}
 
	public void SetTilePosition (Grid grid, int column, int row)
	{
		this.grid = grid;
		size = tileBg.GetComponent<SpriteRenderer> ().bounds.size.x;
		this.column = column;
		this.row = row;
		tilePosition = new Vector3 ( (column * size) - grid.GRID_OFFSET_X, grid.GRID_OFFSET_Y + (-row * size)  , 0);
		transform.position = tilePosition;
 
		foreach (var go in charsGO) {
			go.SetActive(false);
		}
 
		Select (false);
	}
 
	public void Select (bool value)
	{
		selected = value;
 
		if (selected) {
			tileBg.GetComponent<Renderer> ().material.color = Color.white;
			charsGO [type].GetComponent<Renderer> ().material.color = Color.black;
		} else {
			tileBg.GetComponent<Renderer> ().material.color = Color.black;
			charsGO [type].GetComponent<Renderer> ().material.color = Color.white;
		}
	}
 
	public void UpdateData ()
	{
		if (!gameObject.activeSelf) 
		{
			char c;
			if (Array.IndexOf (consonants, chars [type]) == -1) {
				c = vowels [UnityEngine.Random.Range (0, vowels.Length)];
			} else {
				c = consonants [UnityEngine.Random.Range (0, consonants.Length)];
			}
			charsGO [type].SetActive (false);
			type = Array.IndexOf (chars, c);
			charsGO [type].SetActive (true);
		}
 
		if (transform.position.y != grid.GRID_OFFSET_Y + (-row * size)) 
		{
			updatePosition = true;
		}
 
		gameObject.SetActive(true);
 
	}
 
	void Update ()
	{
		if (updatePosition) {
			var targetY = grid.GRID_OFFSET_Y + (-row * size);
			var nowPosition = transform.position;
			nowPosition.y -= 0.4f;
 
			if (nowPosition.y < targetY)
			{
				nowPosition.y = targetY;
				updatePosition = false;
			}
 
			transform.position = nowPosition;
		}
	}
 
}

Each tile has the 26 textures for the alphabet. I mapped the index of that sprite with the index of the char in the alphabet. Not the safest solution ever, but it does the trick.

When I determine which leter the tile will contain, I set its type to be the index of that char in the alphabet. The same index is used to find the correct sprite I need to make visible.

When a tile is “removed” from the grid, it is reused. I update its char by simply picking a random vowel or consonant depending on what the tile contained previously. This way I keep the same optimal ratio of consonants and vowels in the grid.

If I tile is collapsed, I simply switch a boolean (updatePosition) and then move the tile to its new position based on its new row index.

The Game Controller

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
#define EIGHT_DIRECTIONAL
 
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
 
public class WordGame : MonoBehaviour {
 
	[HideInInspector]
	public Tile selectedTile;
 
	[HideInInspector]
	public List<Tile> selectedTiles;
 
	public Text statusLabel;
 
	private Grid grid;
 
	private WordData wordData;
 
	public void InitGame ()
	{
		grid = GetComponent<Grid> ();
		wordData = GetComponent<WordData> ();
 
		selectedTiles = new List<Tile> ();
		selectedTile = null;
 
		grid.BuildGrid ();
		statusLabel.text = "";
	}
 
 
	public void HandleTouchDown (Vector2 touch)
	{
		selectedTiles.Clear ();
		if (selectedTile != null)
			selectedTile.Select (false);
 
 
		selectedTile = TileCloseToPoint (touch);
 
		if (selectedTile != null) 
		{
			selectedTile.Select(true);
			if (!selectedTiles.Contains(selectedTile)) selectedTiles.Add (selectedTile);
		}
		statusLabel.text = (""+selectedTile.TypeChar).ToUpper();
	}
 
	public void HandleTouchUp (Vector2 touch)
	{
 
		if (selectedTile == null || selectedTiles.Count < 3)
			return;
 
		if (selectedTile != null) {
			selectedTile.Select(false);
			selectedTile = null;
		}
 
		char[] word = new char[selectedTiles.Count];
		for (var i = 0; i < selectedTiles.Count; i++) {
			var tile = selectedTiles[i];
			word[i] = tile.TypeChar;
			tile.Select(false);
		}
 
		var s = new string (word);
		statusLabel.text = s.ToUpper();
 
		if (wordData.IsValidWord (s)) 
		{
			for (var i = 0; i < selectedTiles.Count; i++) {
				var tile = selectedTiles [i];
				tile.gameObject.SetActive (false);
			}
			grid.CollapseGrid();
		}
 
		selectedTiles.Clear ();
 
	}
 
	public void HandleTouchMove (Vector2 touch)
	{
		if (selectedTile == null)
			return;
 
		var nextTile = TileCloseToPoint (touch);
 
		if (nextTile != null && nextTile != selectedTile) 
		{
 
			if (nextTile.row == selectedTile.row && (nextTile.column == selectedTile.column - 1 || nextTile.column == selectedTile.column + 1))
			{
				selectedTile = nextTile;
			}
			else if (nextTile.column == selectedTile.column && (nextTile.row == selectedTile.row - 1 || nextTile.row == selectedTile.row + 1))
			{
				selectedTile = nextTile;
			}
			#if EIGHT_DIRECTIONAL
			else if (Mathf.Abs (nextTile.column - selectedTile.column) == 1 &&  Mathf.Abs (nextTile.row - selectedTile.row) == 1) 
			{
				selectedTile = nextTile;
			}
			#endif
 
			selectedTile.Select(true);
			if (!selectedTiles.Contains(selectedTile)) selectedTiles.Add (selectedTile);
 
			char[] word = new char[selectedTiles.Count];
			for (var i = 0; i < selectedTiles.Count; i++) {
				var tile = selectedTiles[i];
				word[i] = tile.TypeChar;
			}
 
			statusLabel.text = new string (word).ToUpper();
		}
	}
 
	private Tile TileCloseToPoint (Vector2 point)
	{
		var t = Camera.main.ScreenToWorldPoint (point);
		t.z = 0;
 
		int c = Mathf.FloorToInt ((t.x + grid.GRID_OFFSET_X + ( Tile.size * 0.5f )) / Tile.size);
		if (c < 0)
			c = 0;
		if (c >= grid.COLUMNS)
			c = grid.COLUMNS - 1;
 
		int r =  Mathf.FloorToInt ((grid.GRID_OFFSET_Y + ( Tile.size * 0.5f ) - t.y )/  Tile.size);
		if (r < 0) r = 0;
		if (r >= grid.ROWS) r = grid.ROWS - 1;
 
		return grid.gridTiles [c] [r];
 
	}
}

This class takes care of the user’s input. The letters are selected on Down and Move, and on Up I check if the generated word is a valid one in WordData. If it is, the selected tiles are made inactive and the Grid is told to collapse. I also added a minimum of 3 letters per word.

Things to Improve

It would be nice to add points to the words being generated as well as bonus points to some of the letters (double, tripple points).

You can download the project here.

11 thoughts to “Building a Word Game in Unity”

    1. Sure. Also, make sure to use the tile selection logic from the rock paper scissor spock example. it works better with tiny tiles.

  1. I want to improve it but i’m just a beginner in unity and in c#. 🙁 can you teach me if you have time? i really want to learn. please :))) thanks again.

    1. I’m working on a book on how to make games with Unity, it will cover basics of both the interface and C#. But meanwhile, you can send me any questions and I’ll answer them as soon as I can

  2. hi… thanks for the good tutorial in your website
    I m sorry for my weakness in english
    I found some bug in the touch when I touch the screen and choose a word and this word is correct sometimes this word don’t hide and sometimes when I select whole the word, color of the all of the screen go to white and we can’t change it. can I help me for this touching problem??

    thanks a lot

    1. I will do another post on those word game samples, meanwhile if you could give me a better idea of how your game is supposed to work?

  3. My letters are button so when you click it it goes on the available blank spot to spell something and a box collider to sebd signal that theres a letter here you go to the next position but im currently stuck cause i dont know how to code it

  4. Soroosh change inputcontroller script as follow

    public WordGame game;

    private bool tapped = false;
    private bool justTouch = false;

    void Update () {

    if (Input.touches.Length > 0) {

    Touch touch = Input.touches[0];

    if (touch.phase == TouchPhase.Began)
    {
    game.HandleTouchDown (touch.position);
    }
    else if (touch.phase == TouchPhase.Canceled || touch.phase == TouchPhase.Ended)
    {
    game.HandleTouchUp(touch.position);
    }
    else if (touch.phase == TouchPhase.Moved || touch.phase == TouchPhase.Stationary)
    {
    game.HandleTouchMove (touch.position);
    }

    game.HandleTouchMove (touch.position);

    return;
    }
    else if (Input.GetMouseButtonDown(0))
    {
    tapped = true;
    game.HandleTouchDown(Input.mousePosition);

    }
    else if (Input.GetMouseButtonUp(0))
    {
    tapped = false;
    game.HandleTouchUp(Input.mousePosition);
    }
    else if (tapped)
    game.HandleTouchMove(Input.mousePosition);

    }

  5. hi sir. i want to make a word by selecting the letters that i want . how can i change the input controller. can you help me?

    1. I just finished a post updating the code. Let me know if that helps. A lot of people have been asking me for it

Comments are closed.