Unity3D и MySQL

от автора


Сегодня мы разберёмся как подключить проект на Unity3D к БД MySQL.

Если точнее, то мы разберём как сохранять в БД состояния игровых объектов, а затем загружать их из БД. За основу взят гайд Джонатана Вуда для Unity 3.0.0f5 и MySQL 5.2.28. Он дополнен и адаптирован для 4-й версии Юнити.

Нам понадобятся:
1. Unity3D 4.x.
2. Доступ к БД MySQL.
3. Библиотека MySql.Data.dll.

В папке с проектом необходимо создать каталог Libraries, в который необходимо поместить библиотеку MySQL.Data.dll. Её можно скачать из этого архива, но если у вас уже установлен MySQL, то можете скопировать из c:\Program Files (x86)\MySQL\MySQL Connector Net 6.0.3\Assemblies\.

Чтобы избежать проблем совместимости версий сборок, а также сэкономить себе несколько часов времени и нервов (спасибо Джоэлю) установите уровень API совместимости Edit —> Project Settings —> Player —>Optimization—>Api Compatibility Level = .NET 2.0.

Предварительная работа:

1. Объекты состояние, которых необходимо сохранять помечаем тэгом «Savable».

2. Создаём таблицу в БД, в которой будет храниться информация об объектах.

Чтобы пользоваться новой библиотекой добавьте в начало скрипта:

using MySql.Data;  using MySql.Data.MySqlClient;  

А также не забудьте прописать параметры подключения в переменную:

	// MySQL настройки  	string constr = "Server=localhost;Database=demo;User ID=demo;Password=demo;Pooling=true;CharSet=utf8;";  

Создаём структуру, в которой будет храниться информация об объектах:

	// Названия синхронизируемых полей таблицы в БД 	string ID, Name, levelname, objectType;  	float posx, posy, posz, tranx, trany, tranz;  	// описание структуры синхронизируемых данных 	public struct data  	{  		public int UID;  		public string ID, Name, levelname, objectType;  		public float posx, posy, posz, tranx, trany, tranz;  	}  	// коллекция в которую будут записываться данные о синхронизируемых объектах 	List<data> _GameItems; 

При запуске скрипт подключается к БД, а при выключении разрывает соединение:

	void Awake()  	{  		try  		{  			// установка элемента соединения  			con = new MySqlConnection(constr);    			// посмотрим, сможем ли мы установить соединение  			con.Open(); 	 			Debug.Log("Connection State: " + con.State);  		}  		catch (IOException ex)  {Debug.Log(ex.ToString());}  	}   void OnApplicationQuit()  	{  		Debug.Log("killing con");  		if (con != null)  		{  			// Конечно, правильнее использовать: 			// if (con.State != ConnectionState.Closed)  			// но из-за проблем с версиями сборок приходится использовать костыли 			if (con.State.ToString()!="Closed") 			con.Close();  			con.Dispose();  		}  	} 

Есть две процедуры, отвечающие за работу со объектами на сцене:

prepDate — готовит данные для записи в БД

/// Этот метод подготавливает данные для записи в БД  void prepData()  	{  		bodies = GameObject.FindGameObjectsWithTag("Savable");  		_GameItems = new List<data>();  		data itm;  		foreach (GameObject body in bodies)  		{  			itm = new data();  			itm.ID = body.name + "_" + body.GetInstanceID();  			itm.Name = body.name;  			itm.levelname = Application.loadedLevelName;  			itm.objectType = body.name.Replace("(Clone)", "");  			itm.posx = body.transform.position.x;  			itm.posy = body.transform.position.y;  			itm.posz = body.transform.position.z;  			itm.tranx = body.transform.rotation.x;  			itm.trany = body.transform.rotation.y;  			itm.tranz = body.transform.rotation.z;  			_GameItems.Add(itm);  		} 		Debug.Log("Items in collection: " + _GameItems.Count);  	} }  

loadDate — загружает объекты на сцену

/// Этот метод устанавливает загруженные объекты на сцене  void loadData()  	{  		// Удаляем все лишние объекты со сцены 		bodies = GameObject.FindGameObjectsWithTag("Savable"); 		DestroyObject(bodies); 		// Создаём сохранённые в БД объекты 		if (_GameItems != null) 		{ 			if (_GameItems.Count > 0) 			{ 				foreach (data itm in _GameItems) 				{ 					if (itm.objectType="CUBE")  					{Instantiate(mCUBE, new Vector3(itm.posx, itm.posy , itm.posz),Quaternion.Euler(itm.tranx, itm.trany, itm.tranz));} 					else if (itm.objectType="SPHERE")  					{Instantiate(mSPHERE, new Vector3(itm.posx, itm.posy , itm.posz),Quaternion.Euler(itm.tranx, itm.trany, itm.tranz));} 					else if (itm.objectType="TRIANGLE")  					{Instantiate(mTRIANGLE, new Vector3(itm.posx, itm.posy , itm.posz),Quaternion.Euler(itm.tranx, itm.trany, itm.tranz));} 				} 			} 		}  	} }  

А также четыре основных процедуры, отвечающие за работу с БД:

InsertEntries — вставка новой записи в таблицу.

// Вставка новой записи в таблицу void InsertEntries()  	{  	prepData();  	string query = string.Empty;  	// Вылавливаем ошибки  	try  	{  		query = "INSERT INTO demo_table (ID, Name, levelname, objectType, posx, posy, posz, tranx, trany, tranz) VALUES (?ID, ?Name, ?levelname, ?objectType, ?posx, ?posy, ?posz, ?tranx, ?trany, ?tranz)"; 		if (con.State.ToString()!="Open") 		con.Open();  		using (con)  		{ 		foreach (data itm in _GameItems)  			{  			using (cmd = new MySqlCommand(query, con))  				{  					MySqlParameter oParam = cmd.Parameters.Add("?ID", MySqlDbType.VarChar);  					oParam.Value = itm.ID;  					MySqlParameter oParam1 = cmd.Parameters.Add("?Name", MySqlDbType.VarChar);  					oParam1.Value = itm.Name;  					MySqlParameter oParam2 = cmd.Parameters.Add("?levelname", MySqlDbType.VarChar);  					oParam2.Value = itm.levelname;  					MySqlParameter oParam3 = cmd.Parameters.Add("?objectType", MySqlDbType.VarChar);  					oParam3.Value = itm.objectType;  					MySqlParameter oParam4 = cmd.Parameters.Add("?posx", MySqlDbType.Float);  					oParam4.Value = itm.posx;  					MySqlParameter oParam5 = cmd.Parameters.Add("?posy", MySqlDbType.Float);  					oParam5.Value = itm.posy;  					MySqlParameter oParam6 = cmd.Parameters.Add("?posz", MySqlDbType.Float); 					oParam6.Value = itm.posz;  					MySqlParameter oParam7 = cmd.Parameters.Add("?tranx", MySqlDbType.Float);  					oParam7.Value = itm.tranx;  					MySqlParameter oParam8 = cmd.Parameters.Add("?trany", MySqlDbType.Float);  					oParam8.Value = itm.trany;  					MySqlParameter oParam9 = cmd.Parameters.Add("?tranz", MySqlDbType.Float);  					oParam9.Value = itm.tranz;  					cmd.ExecuteNonQuery();  				} 			} 		} 	} 	catch (IOException ex)  	{  		Debug.Log(ex.ToString());  	} 	finally {}  	} 

UpdateEntries- обновление существующих записей в таблице.

	// Обновление существующих записей в таблице  	void UpdateEntries()  	{  	prepData();  	string query = string.Empty;  	// Вылавливаем ошибки 	try  	{ 		query = "UPDATE demo_table SET ID=?ID, Name=?Name, levelname=?levelname, objectType=?objectType, posx=?posx, posy=?posy, posz=?posz, tranx=?tranx, trany=?trany, tranz=?tranz WHERE iddemo_table=?UID"; 		if (con.State.ToString()!="Open") 		con.Open();  		using (con)  		{ 		foreach (data itm in _GameItems)  			{ 				using (cmd = new MySqlCommand(query, con))                      		{ 					MySqlParameter oParam = cmd.Parameters.Add("?ID", MySqlDbType.VarChar);  					oParam.Value = itm.ID;  					MySqlParameter oParam1 = cmd.Parameters.Add("?Name", MySqlDbType.VarChar);  					oParam1.Value = itm.Name;  					MySqlParameter oParam2 = cmd.Parameters.Add("?levelname", MySqlDbType.VarChar);  					oParam2.Value = itm.levelname;  					MySqlParameter oParam3 = cmd.Parameters.Add("?objectType", MySqlDbType.VarChar);  					oParam3.Value = itm.objectType;  					MySqlParameter oParam4 = cmd.Parameters.Add("?posx", MySqlDbType.Float);  					oParam4.Value = itm.posx;  					MySqlParameter oParam5 = cmd.Parameters.Add("?posy", MySqlDbType.Float);  					oParam5.Value = itm.posy;  					MySqlParameter oParam6 = cmd.Parameters.Add("?posz", MySqlDbType.Float); 					oParam6.Value = itm.posz;  					MySqlParameter oParam7 = cmd.Parameters.Add("?tranx", MySqlDbType.Float);  					oParam7.Value = itm.tranx;  					MySqlParameter oParam8 = cmd.Parameters.Add("?trany", MySqlDbType.Float);  					oParam8.Value = itm.trany;  					MySqlParameter oParam9 = cmd.Parameters.Add("?tranz", MySqlDbType.Float);  					oParam9.Value = itm.tranz;  					cmd.ExecuteNonQuery();  				}  			} 		} 	} 	catch (IOException ex) {Debug.Log(ex.ToString());}  	finally {}  	} 

DeleteEntries- удаление записей из таблицы.

// Удаляем запись из таблицы  void DeleteEntries()  	{  	string query = string.Empty;  	// Вылавливаем ошибки 	try  	{  		// лучше всего если вы знаете ID записи, которую необходимо удалить 		//-----------------------------------------------------------------------  		// query = "DELETE FROM demo_table WHERE iddemo_table=?UID";  		// MySqlParameter oParam = cmd.Parameters.Add("?UID", MySqlDbType.Int32);  		// oParam.Value = 0;  		//-----------------------------------------------------------------------  		query = "DELETE FROM demo_table WHERE iddemo_table";  		if (con.State.ToString()!="Open") 		con.Open();  		using (con)  		{  			using (cmd = new MySqlCommand(query, con))  			{  				cmd.ExecuteNonQuery();                  	}  		} 	} 	catch (IOException ex) {Debug.Log(ex.ToString());} 	finally {} 	} 

ReadEntries- чтение всех записей из таблицы.

// Чтение всех записей из таблицы  void ReadEntries()  	{ 		string query = string.Empty;  		if (_GameItems == null)  		_GameItems = new List<data>();  		if (_GameItems.Count > 0)  		_GameItems.Clear();  		// Отлавливаем ошибки  		try 		{ 			query = "SELECT * FROM view_demo";  			if (con.State.ToString()!="Open") 			con.Open();  			using (con)  			{ 				using (cmd = new MySqlCommand(query, con))  				{ 					rdr = cmd.ExecuteReader();  					if(rdr.HasRows)  					while (rdr.Read())  					{ 						data itm = new data();  						itm.UID = int.Parse(rdr["iddemo_table"].ToString());  						itm.ID = rdr["ID"].ToString();  						itm.levelname = rdr["levelname"].ToString();  						itm.Name = rdr["Name"].ToString();  						itm.objectType = rdr["objectType"].ToString();  						itm.posx = float.Parse(rdr["posx"].ToString());  						itm.posy = float.Parse(rdr["posy"].ToString()); 						itm.posz = float.Parse(rdr["posz"].ToString());  						itm.tranx = float.Parse(rdr["tranx"].ToString());  						itm.trany = float.Parse(rdr["trany"].ToString());  						itm.tranz = float.Parse(rdr["tranz"].ToString());  						_GameItems.Add(itm);  					} 					rdr.Dispose();  				} 			} 		} 	catch (IOException ex) {Debug.Log(ex.ToString());} 	finally {} 	} 

Остаётся только сделать пару кнопок для загрузки и сохранения сцены:

	// Кнопки загрузки и сохранения  	void OnGUI()  	{ 	if (GUI.Button(new Rect(10, 70, 50, 30), "Save") && !saving)  		{  			saving = true;  			// Для начала очистим таблицу  			DeleteEntries();  			// теперь сохраним информацию о сцене 			InsertEntries();  			// можно также использовать обновление если известен ID уже сохранённого элемента  			saving = false;  		} 	if (GUI.Button(new Rect(10, 110, 50, 30), "Load") && !loading)  		{  			loading = true;  			// считаем информацию из БД 			ReadEntries();  			// создадим загруженные объекты 			loadData(); 			// теперь отобразим информацию из лога 			LogGameItems();  			loading = false;  		} 	}  

Вот в общем-то и всё. В комментах приветствуются примечания и дополнения.

Весь скрипт

using UnityEngine;  using MySql.Data;  using MySql.Data.MySqlClient;  using System.IO;  using System.Collections;  using System.Collections.Generic;    public class MySQLCS : MonoBehaviour  {  	public GameObject mCUBE; 	public GameObject mSPHERE; 	public GameObject mTRIANGLE;  	bool saving = false;  	bool loading = false;   	// MySQL настройки  	string constr = "Server=localhost;Database=demo;User ID=demo;Password=demo;Pooling=true;CharSet=utf8;";  	// соединение  	MySqlConnection con = null;  	// команда к БД 	MySqlCommand cmd = null;  	// чтение 	MySqlDataReader rdr = null;  	// ошибки 	MySqlError er = null;   	// массив синхронизируемых игровых объектов 	GameObject[] bodies;   	// Названия синхронизируемых полей таблицы в БД 	string ID, Name, levelname, objectType;  	float posx, posy, posz, tranx, trany, tranz;  	// описание структуры синхронизируемых данных 	public struct data  	{  		public int UID;  		public string ID, Name, levelname, objectType;  		public float posx, posy, posz, tranx, trany, tranz;  	}  	// коллекция в которую будут записываться данные о синхронизируемых объектах 	List<data> _GameItems; 	void Awake()  	{  		try  		{  			// установка элемента соединения  			con = new MySqlConnection(constr);    			// посмотрим, сможем ли мы установить соединение  			con.Open(); 	 			Debug.Log("Connection State: " + con.State);  		}  		catch (IOException ex)  {Debug.Log(ex.ToString());}  	}   void OnApplicationQuit()  	{  		Debug.Log("killing con");  		if (con != null)  		{  			// Конечно, правильнее использовать: 			// if (con.State != ConnectionState.Closed)  			// но из-за проблем с версиями сборок приходится использовать костыли 			if (con.State.ToString()!="Closed") 			con.Close();  			con.Dispose();  		}  	}  	// Use this for initialization  	void Start()  	{   	}   	// Update is called once per frame  	void Update()  	{  	}     	// Кнопки загрузки и сохранения  	void OnGUI()  	{ 	if (GUI.Button(new Rect(10, 70, 50, 30), "Save") && !saving)  		{  			saving = true;  			// Для начала очистим таблицу  			DeleteEntries();  			// теперь сохраним информацию о сцене 			InsertEntries();  			// можно также использовать обновление если известен ID уже сохранённого элемента  			saving = false;  		} 	if (GUI.Button(new Rect(10, 110, 50, 30), "Load") && !loading)  		{  			loading = true;  			// считаем информацию из БД 			ReadEntries();  			// создадим загруженные объекты 			loadData(); 			// теперь отобразим информацию из лога 			LogGameItems();  			loading = false;  		} 	}   // Вставка новой записи в таблицу void InsertEntries()  	{  	prepData();  	string query = string.Empty;  	// Вылавливаем ошибки  	try  	{  		query = "INSERT INTO demo_table (ID, Name, levelname, objectType, posx, posy, posz, tranx, trany, tranz) VALUES (?ID, ?Name, ?levelname, ?objectType, ?posx, ?posy, ?posz, ?tranx, ?trany, ?tranz)"; 		if (con.State.ToString()!="Open") 		con.Open();  		using (con)  		{ 		foreach (data itm in _GameItems)  			{  			using (cmd = new MySqlCommand(query, con))  				{  					MySqlParameter oParam = cmd.Parameters.Add("?ID", MySqlDbType.VarChar);  					oParam.Value = itm.ID;  					MySqlParameter oParam1 = cmd.Parameters.Add("?Name", MySqlDbType.VarChar);  					oParam1.Value = itm.Name;  					MySqlParameter oParam2 = cmd.Parameters.Add("?levelname", MySqlDbType.VarChar);  					oParam2.Value = itm.levelname;  					MySqlParameter oParam3 = cmd.Parameters.Add("?objectType", MySqlDbType.VarChar);  					oParam3.Value = itm.objectType;  					MySqlParameter oParam4 = cmd.Parameters.Add("?posx", MySqlDbType.Float);  					oParam4.Value = itm.posx;  					MySqlParameter oParam5 = cmd.Parameters.Add("?posy", MySqlDbType.Float);  					oParam5.Value = itm.posy;  					MySqlParameter oParam6 = cmd.Parameters.Add("?posz", MySqlDbType.Float); 					oParam6.Value = itm.posz;  					MySqlParameter oParam7 = cmd.Parameters.Add("?tranx", MySqlDbType.Float);  					oParam7.Value = itm.tranx;  					MySqlParameter oParam8 = cmd.Parameters.Add("?trany", MySqlDbType.Float);  					oParam8.Value = itm.trany;  					MySqlParameter oParam9 = cmd.Parameters.Add("?tranz", MySqlDbType.Float);  					oParam9.Value = itm.tranz;  					cmd.ExecuteNonQuery();  				} 			} 		} 	} 	catch (IOException ex)  	{  		Debug.Log(ex.ToString());  	} 	finally {}  	}  	// Обновление существующих записей в таблице  	void UpdateEntries()  	{  	prepData();  	string query = string.Empty;  	// Вылавливаем ошибки 	try  	{ 		query = "UPDATE demo_table SET ID=?ID, Name=?Name, levelname=?levelname, objectType=?objectType, posx=?posx, posy=?posy, posz=?posz, tranx=?tranx, trany=?trany, tranz=?tranz WHERE iddemo_table=?UID"; 		if (con.State.ToString()!="Open") 		con.Open();  		using (con)  		{ 		foreach (data itm in _GameItems)  			{ 				using (cmd = new MySqlCommand(query, con))                      		{ 					MySqlParameter oParam = cmd.Parameters.Add("?ID", MySqlDbType.VarChar);  					oParam.Value = itm.ID;  					MySqlParameter oParam1 = cmd.Parameters.Add("?Name", MySqlDbType.VarChar);  					oParam1.Value = itm.Name;  					MySqlParameter oParam2 = cmd.Parameters.Add("?levelname", MySqlDbType.VarChar);  					oParam2.Value = itm.levelname;  					MySqlParameter oParam3 = cmd.Parameters.Add("?objectType", MySqlDbType.VarChar);  					oParam3.Value = itm.objectType;  					MySqlParameter oParam4 = cmd.Parameters.Add("?posx", MySqlDbType.Float);  					oParam4.Value = itm.posx;  					MySqlParameter oParam5 = cmd.Parameters.Add("?posy", MySqlDbType.Float);  					oParam5.Value = itm.posy;  					MySqlParameter oParam6 = cmd.Parameters.Add("?posz", MySqlDbType.Float); 					oParam6.Value = itm.posz;  					MySqlParameter oParam7 = cmd.Parameters.Add("?tranx", MySqlDbType.Float);  					oParam7.Value = itm.tranx;  					MySqlParameter oParam8 = cmd.Parameters.Add("?trany", MySqlDbType.Float);  					oParam8.Value = itm.trany;  					MySqlParameter oParam9 = cmd.Parameters.Add("?tranz", MySqlDbType.Float);  					oParam9.Value = itm.tranz;  					cmd.ExecuteNonQuery();  				}  			} 		} 	} 	catch (IOException ex) {Debug.Log(ex.ToString());}  	finally {}  	}  // Удаляем запись из таблицы  void DeleteEntries()  	{  	string query = string.Empty;  	// Вылавливаем ошибки 	try  	{  		// лучше всего если вы знаете ID записи, которую необходимо удалить 		//-----------------------------------------------------------------------  		// query = "DELETE FROM demo_table WHERE iddemo_table=?UID";  		// MySqlParameter oParam = cmd.Parameters.Add("?UID", MySqlDbType.Int32);  		// oParam.Value = 0;  		//-----------------------------------------------------------------------  		query = "DELETE FROM demo_table WHERE iddemo_table";  		if (con.State.ToString()!="Open") 		con.Open();  		using (con)  		{  			using (cmd = new MySqlCommand(query, con))  			{  				cmd.ExecuteNonQuery();                  	}  		} 	} 	catch (IOException ex) {Debug.Log(ex.ToString());} 	finally {} 	}  // Чтение всех записей из таблицы  void ReadEntries()  	{ 		string query = string.Empty;  		if (_GameItems == null)  		_GameItems = new List<data>();  		if (_GameItems.Count > 0)  		_GameItems.Clear();  		// Отлавливаем ошибки  		try 		{ 			query = "SELECT * FROM view_demo";  			if (con.State.ToString()!="Open") 			con.Open();  			using (con)  			{ 				using (cmd = new MySqlCommand(query, con))  				{ 					rdr = cmd.ExecuteReader();  					if(rdr.HasRows)  					while (rdr.Read())  					{ 						data itm = new data();  						itm.UID = int.Parse(rdr["iddemo_table"].ToString());  						itm.ID = rdr["ID"].ToString();  						itm.levelname = rdr["levelname"].ToString();  						itm.Name = rdr["Name"].ToString();  						itm.objectType = rdr["objectType"].ToString();  						itm.posx = float.Parse(rdr["posx"].ToString());  						itm.posy = float.Parse(rdr["posy"].ToString()); 						itm.posz = float.Parse(rdr["posz"].ToString());  						itm.tranx = float.Parse(rdr["tranx"].ToString());  						itm.trany = float.Parse(rdr["trany"].ToString());  						itm.tranz = float.Parse(rdr["tranz"].ToString());  						_GameItems.Add(itm);  					} 					rdr.Dispose();  				} 			} 		} 	catch (IOException ex) {Debug.Log(ex.ToString());} 	finally {} 	}  /// Запишем в лог всё что было считано из БД void LogGameItems()  	{  		if (_GameItems != null)  		{ 			if (_GameItems.Count > 0)  			{ 				foreach (data itm in _GameItems)  				{ 					Debug.Log("UID: " + itm.UID);  					Debug.Log("ID: " + itm.ID);  					Debug.Log("levelname: " + itm.levelname);  					Debug.Log("Name: " + itm.Name);  					Debug.Log("objectType: " + itm.objectType);  					Debug.Log("posx: " + itm.posx);  					Debug.Log("posy: " + itm.posy);  					Debug.Log("posz: " + itm.posz);  					Debug.Log("tranx: " + itm.tranx);  					Debug.Log("trany: " + itm.trany);  					Debug.Log("tranz: " + itm.tranz);  				} 			} 		} 	}  /// Этот метод подготавливает данные для записи в БД  void prepData()  	{  		bodies = GameObject.FindGameObjectsWithTag("Savable");  		_GameItems = new List<data>();  		data itm;  		foreach (GameObject body in bodies)  		{  			itm = new data();  			itm.ID = body.name + "_" + body.GetInstanceID();  			itm.Name = body.name;  			itm.levelname = Application.loadedLevelName;  			itm.objectType = body.name.Replace("(Clone)", "");  			itm.posx = body.transform.position.x;  			itm.posy = body.transform.position.y;  			itm.posz = body.transform.position.z;  			itm.tranx = body.transform.rotation.x;  			itm.trany = body.transform.rotation.y;  			itm.tranz = body.transform.rotation.z;  			_GameItems.Add(itm);  		} 		Debug.Log("Items in collection: " + _GameItems.Count);  	} }   /// Этот метод устанавливает загруженные объекты на сцене  void loadData()  	{  		// Удаляем все лишние объекты со сцены 		bodies = GameObject.FindGameObjectsWithTag("Savable"); 		DestroyObject(bodies); 		// Создаём сохранённые в БД объекты 		if (_GameItems != null) 		{ 			if (_GameItems.Count > 0) 			{ 				foreach (data itm in _GameItems) 				{ 					if (itm.objectType="CUBE") {Instantiate(mCUBE, new Vector3(itm.posx, itm.posy , itm.posz),Quaternion.Euler(itm.tranx, itm.trany, itm.tranz));} 					else if (itm.objectType="SPHERE") {Instantiate(mSPHERE, new Vector3(itm.posx, itm.posy , itm.posz),Quaternion.Euler(itm.tranx, itm.trany, itm.tranz));} 					else if (itm.objectType="TRIANGLE") {Instantiate(mTRIANGLE, new Vector3(itm.posx, itm.posy , itm.posz),Quaternion.Euler(itm.tranx, itm.trany, itm.tranz));} 				} 			} 		}  	} }   

ссылка на оригинал статьи http://habrahabr.ru/post/185594/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *