Генератор utf-8 json на php с поддержкой unicode 6

Разумеется, в PHP есть прекрасная функция json_encode. Но до версии 5.3 включительно те же русские символы кодируются в виде \uXXXX — в разы длиннее, чем utf-8. Чтобы уменьшить объем трафика, необходимо убрать преобразование utf-8 символов в \u-последовательности. Да, в PHP 5.4 у json_encode наконец-то появился параметр JSON_UNESCAPED_UNICODE, но многие хостеры до сих пор представляют пользователям выбор только между версиями 5.2 и 5.3.

Я бы не стал изобретать очередной велосипед, но те решения, которые мне попадались, имеют общую проблему — они корректно обрабатывают только символы базовой плоскости юникода.

Способ, в разных модификациях широко распространенный на просторах интернета, заключается в том, что результат работы ф-ции json_encode обрабатывается фильтром, заменяющим все вхождения \uXXXX на utf-8 символы. Например, так:

class Json{   static function json_encode($data){     return preg_replace_callback('/\\\\u([0-9a-f]{4})/i',       function($val){         return mb_decode_numericentity('&#'.intval($val[1], 16).';', array(0, 0xffff, 0, 0xffff), 'utf-8');       }, json_encode($data)     );   } } 

И этот код работал… До тех пор, пока не понадобилось добавить поддержку юникодных emoji (эмотиконы были добавлены в стандарте Unicode 6), большинство из которых имеет коды более 0x1F000 (первая плоскость unicode).

Дело в том, что \u-последовательности имеют кодировку utf-16: слово (2 байта) на символ с кодом от 0x0000 до 0xFFFF (исключая «окно» 0xD800-0xDFFF) и 2 слова (4 байта) с кодами 0xD800-0xDFFF для символов с кодами более 0xFFFF.

Например, исходный юникод-символ с кодом 0x1f601, имеющий utf-8 представление "\xf0\x9f\x98\x81", будет преобразован функцией json_dencode в строку "\ud83d\ude01" и результатом вышеприведенной ф-ции будет строка "\xed\xa0\xbd\xed\xb8\x81". Вместо одного 4-х байтового символа получили два 3-х байтовых.

Таким образом, для нормальной обработки символов необходим анализ кодов и отдельное преобразование 2-х символьных \u-последовательностей. Например, так:

class Json{   static public $_code;    static public function json_encode($data){     Json::$_code=0;     return preg_replace_callback('/\\\\u([0-9a-f]{4})/i',       function($val){         $val=hexdec($val[1]);           if(Json::$_code){             $val=((Json::$_code&0x3FF)<<10)+($val&0x3FF)+0x10000;             Json::$_code=0;           }elseif($val>=0xD800&&$val<0xE000){             Json::$_code=$val;             return '';           }           return html_entity_decode(sprintf('&#x%x;', $val), ENT_NOQUOTES, 'utf-8');       }, json_encode($data)     );   } } 

Данный вариант корректно преобразовывает любые utf-8 символы.

P.S. Я прекрасно понимаю, что вышеприведенный код далек от оптимального. Но он работает и с достаточной — для моих задач — производительностью. А сравнивать скорость работы всех придуманных вариантов просто лень. Вот, например, вариант, перекладывающий анализ на регулярное выражение:

class Json{   static public function json_encode($data){     return preg_replace_callback('/\\\\ud([89ab][0-9a-f]{2})\\\\ud([c-f][0-9a-f]{2})|\\\\u([0-9a-f]{4})/i', function($val){       return html_entity_decode(empty($val[3])?         sprintf('&#x%x;', ((hexdec($val[1])&0x3FF)<<10)+(hexdec($val[2])&0x3FF)+0x10000):         '&#x'.$val[3].';', ENT_NOQUOTES, 'utf-8');     }, json_encode($data));   } } 

P.P.S. Вызовы html_entity_decode вставлены в callback-функцию потому, что обрабатываемые данные могут содержать html-код, включающий служебные html-сущности (‘<‘, ‘>’, ‘&’ и т.д.), которые не должны быть преобразованы в символы.

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

Настраиваем HTTPS-сервер на nginx

Для чего я это пишу?

В последнее время в связи с кучей факторов (АНБ, DPI с рекламой и другое) у меня начала просыпаться паранойя и я подумал полностью перевести свой небольшой сайт на https. На хабре было несколько статей с техническими подробностями работы SSL/TLS, однако поискав информацию на тему настройки https-вебсервера обнаружил традиционное деление статей — либо это статьи «Делайте вот так», где просто даны настройки без каких-либо разъяснений и вариантов использования, либо это большие теоретические статьи, где обсуждаются различные схемы использования, но без практически применимых готовых вариантов. На хабре была статья о настройке, однако в ней нет информации про DH-кодировки, да и некоторые параметры не описаны. Подумал, что стоит упорядочить найденное в виде статьи, которая будет полезна тем, кто хотел бы развернуть https у себя на сервере, но не слишком углубляться в дебри SSL.

Повествование будет вестись с учетом того, что веб-сервером выступает nginx (и в одном месте будет параметр для php-fpm).

Сертификат

У меня уже был сертификат от StartSSL. О нем уже писали на хабре, так что на этом шаге задерживаться не буду. Скажу только, что в течении первых двух-трех дней браузеры, проверяющие сертификат на сервере, могут на него ругаться (у меня такое происходило с Opera 12 и Firefox), видимо у StartCom кеши валидных сертификатов обновляются не так часто. Про установку же будет сказано ниже

О вариантах настройки

Nginx из коробки в новых версиях предлагает практически актуальные, но все же требующие шлифовки параметры, однако актуальные параметры появились в стандартном конфиге не так давно, поэтому в некоторых случаях стандартный пример HTTPS-сервера в конфиге будет не актуален.

В общем случае есть два актуальных на данный момент варианта настройки — с Forward Secrecy и без него. При настройке различие только в наборе кодировок (директива ssl_ciphers), однако тут стоит задуматься, что же вы хотите от https.

О Forward Secrecy можно почитать скажем тут. В двух словах, суть заключается в том, что для актуального на данный момент алгоритма RC4 ключи сессии генерируются на основе приватного ключа сервера. Таким образом, если приватный ключ будет скомпрометирован, появится возможность расшифровать все сессии (если они были записаны). В случае же использования DH-кодировок, каждая сессия имеет свой набор ключей, которые сессии никак не зависят от приватного ключа. Однако в этом случае тратится гораздо больше процессорного времени на хендшейк, что увеличивает нагрузку и время открытия страницы.

Тут стоит задуматься, для чего нужен https конкретно у вас на сайте. При большом количестве посетителей использование DH-алгоритмов шифрования может прилично увеличить нагрузку (которая в любом случае повысится при переходе на HTTPS), в некоторых случаях придется увеличить тариф на VDS и т.п. В большинстве случаев RC4 достаточно, однако многим хочется чтобы все было «по высшему классу», так почему бы не сделать, если ресурсы позволяют?

Настройка nginx

В результате настройки у меня сформировался приблизительно такой конфиг, суть параметров поясню ниже.

В секции http необходимо добавить:

ssl_session_cache   shared:SSL:10m; ssl_session_timeout 5m; ssl_prefer_server_ciphers on; ssl_stapling on; resolver 8.8.8.8;

Секция server же получится приблизительно такая:

server {     listen       443 ssl;     server_name  www.site.ru;     .......      keepalive_timeout   60;     ssl                  on;     ssl_certificate      certificate.crt;     ssl_certificate_key  privatekey.key;     ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;     ssl_ciphers  "RC4:HIGH:!aNULL:!MD5:!kEDH";     add_header Strict-Transport-Security 'max-age=604800';  	.......     location ~ \.php$ { 	.......         fastcgi_param HTTPS on; # Для php-fpm 	.......     } }

В данном примере не используются DH-алгоритмы, т.е. нет Forward Secrecy. Из улучшений тут можно опустить поддержку SSLv3 (убрав его из ssl_ciphers), таким образом перестанет поддерживаться IE 6 и ниже, поскольку он не поддерживает TLS, а так же увеличить время STS, но об этом ниже.
Без SSLv3 такая настройка дает оценку 100-95-100-90 в тесте SSL.

Пройдемся по параметрам

ssl_session_cache shared:SSL:10m;
ssl_session_timeout 5m;

«Задаёт тип и размеры кэшей для хранения параметров сессий.» (©nginx.org) Кеш необходим для возможности повторного использования ключей сессии, таким образом при установлении нового соединения будут использоваться старые ключи, т.е. не будет повторно производиться хендшейк. Особенно актуально при использовании кодировки DHE (например в бразуере Opera 12), поскольку время загрузки страницы со всеми элементами сильно увеличивается при отсутствии кеша, а DHE еще и использует больше ресурсов и времени (относительно EECDH и RC4). Параметр shared задает общий для всех рабочих процессов nginx кеш, 10m — объем кеша (10 МБ, при этом 1 МБ~4000 сессий, таким образом при этих настройках можно хранить до 40 тысяч сессий), 5m — таймаут сессии в кеше (5 минут).

ssl_prefer_server_ciphers on;
«Указывает, чтобы при использовании протоколов SSLv3 и TLS серверные шифры были более приоритетны, чем клиентские.» (©nginx.org) — клиентские шифры (CBC) уязвимы к некоторым типам атак.

ssl_stapling on;
Позволяет серверу прикреплять OCSP-ответы, тем самым уменьшая время загрузки страниц у пользователей. ЗДесь имеются ввиду ответы о валидности сертификата (при проверке на отозванность). С точки зрения безопасности пользователя не важно, кто передает ответы — веб-сервер или сервер CA — ведь ответ в любом случае подписан и валидность ответа тоже можно проверить, а ответ включает в себя свой срок действия.
Для работы этой функции нужно указать DNS-сервер, что и делается директивой resolver.

keepalive_timeout — думаю в описании не нуждается, не стоит выключать или ставить слишком малым для уменьшения нагрузки из-за повторного установления соединения.

ssl_certificate и ssl_certificate_key указывают на файл сертфиката и файл приватного ключа для него. Так как я рассказываю на примере сертификата от StartSSL, то здесь допущу небольшой комментарий относительно инструкций StartSSL по установке сертификата — не нужно добавлять сертификат Root CA в обобщенный файл сертификата, поскольку это не имеет смысла и только увеличивает, хоть и не на много, размер передаваемых данных. Достаточно иметь в файле последовательно личный сертификат и сертификат промежуточного центра сертификации. Готовый файл сертификата для nginx (для сетификата StartSSL) можно получить следующей командой:

cat certificate.crt sub.class1.server.ca.pem > certificate.crt

Где ваш сертификат — certificate.crt, а промежуточный сертификат — www.startssl.com/certs/sub.class1.server.ca.pem

add_header Strict-Transport-Security ‘max-age=604800’;
Strict-Transport-Secutiry — заголовок, указывающий браузеру на то, что сайт доступен только по https. Это предотвращает возможность перехода обратно на http-версию для последующей атаки через незашифрованное соединение. Кстати данный параметр еще удобен тем, что при наличии в коде страницы «забытого» подключения ресурса (картинки/скрипта/стиля/…) с того же сайта по http, браузер сам пойдет на https-версию и не будет ругаться на частично незашифрованное соединение. Конечно же это не сработает для внешних ресурсов. Время — неделя. Многие рекомендуют ставить 1 год, однако в случае решения в будущем отказаться от использования https это может доставить проблемы некоторым пользователям. Время обновляется при каждой передаче этого заголовка, т.е. при каждом заходе на сайт.

ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;
Указывает поддерживаемые протоколы. Как я уже говорил, если допустимо оставить за бортом IE6 и ниже, можно убрать из этого списка SSLv3. SSLv2 не стоит ставить, т.к. есть уязвимости.

ssl_ciphers «RC4:HIGH:!aNULL:!MD5:!kEDH»;
Указывает используемые шифры. Собственно за счет изменения набора шифров и настраивается Forward Secrecy. От стандартного набора, предлагаемого nginx, отличается только параметром !kEDH,

Forward Secrecy

Для включения Forward Secrecy можно использовать например такой набор шифров:

ssl_ciphers "EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS";

Кроме того, необходимо настроить приоритет шифров OpenSSL:

openssl ciphers -V 'EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA256 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EDH+aRSA EECDH RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS'

В данном варианте не запрещается использование RC4 для сохранения совместимости с некоторыми браузерами, однако не так давно обнаружились уязвимости и в нем, хоть и практически трудно реализуемые.

Для усиления шифрования можно увеличить стойкость DH-шифров, создав файл параметров DH-шифров (создание файла займет некоторое время!), скажем длинной 4096 бит:

openssl dhparam -out dh4096.pem 4096

И добавив в конфиг nginx директиву

ssl_dhparam dh4096.pem;

Это можно делать для скажем для веб-интерфейсов управления сервером/службами, однако хендшейк будет происходить еще дольше, поэтому не стоит делать это на обычном сайте.

Про CDN-сервисы

В обсуждении инструкций по настройке Forward Secrecy было замечено, что по крайней мере CDN Amazon CloudFront не поддерживает обмен с вашим сервером в DH-кодировках, да и RC4 вроде тоже, что не радует. Возможно что и с другими CDN тоже не все идеально, но я лично пока с ними не сталкивался, поэтому ничего сказать не могу.

Полезные ссылки

Тестирование настроек https-вебсервера
Настройки Apache и nginx для Forward Secrecy

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

Автоматизированное создание отчета по тестированию

Введение

Так уж сложилось, что у нас в компании ведется учет работ по тестированию в небезызвестных GoogleDocs. Поскольку таким учетом занимаюсь я один, то это идеальный вариант. Плюс еще есть возможность без труда поделиться с наработками с коллегами и не нужно заботиться об актуальности версий — все сохраняется моментально.
Это основные причины выбранного варианта работы. Но, как известно, у любой монеты две стороны. Минус такого подхода состоит в том, что большие проекты трудно обрабатывать и составлять вменяемые отчеты по тестированию (для этих целей, безусловно, подходят системы управления тестами).
Совсем недавно я столкнулся с Google Script. Это инструмент, позволяющий намного эффективнее работать с документами в облаке. Я решил, что он подойдет для задачи автоматизированного составления отчетов по тестированию. И вот что получилось.

Структура

В документе в виде таблицы я веду описания пользовательских сценариев для тестирования приложений. Не претендую на подробное описание, однако самая критичная информация там имеется, а именно: раздел, к которому относится сценарий, название самого сценария, шаги сценария, статус пройденного сценария, комментарии и ссылка на страницу с описанием ошибке в баг-трекере. Набор статусов у нас обычно ограничен «Реализовано», «Дефект», «Отложено». На некоторых проектах могут добавляться и другие, но не суть. Выглядит это следующим образом:

image

Очевидно, что такие сценарии не удобно вести на одной странице — есть смысл разбить на несколько листов (например, по этапам или релизам).

Задача

Необходимо максимально упростить жизнь тестировщика при составлении отчетов тестирования. Идеальным будет вариант, где можно посмотреть сводку по разделам и проекту целиком, а так же не только получить список ошибок и ссылки на них, но и посмотреть на общую картину.

Решение

В качестве решения я выбрал таблицу, разделенную по разделам. Каждая строка соответствует сценарию тестирования и окрашена в соответствующий цвет (для дефектов — красный, для готовых задач — зеленый, для остального — желтый). При этом подводится статистика для каждого из разделов отдельно (сколько ошибок, сколько успешных сценариев, сколько других, то же самое в процентах) и для всего проекта целиком.
Вот такой вид отчета для сценариев:

image

Вот что получилось для статистики:

image

Как же это делать?

Для начала нам нужно создать скрипт внутри документа. Делается это буквально за несколько минут.
Сначала необходимо создать таблицу на диске Google.
image

Затем перейти в меню «Инструменты» и выбрать пункт «Редактор скриптов»
image

После этого выбираем пункт меню «Пустой проект», стираем код и начинаем писать свой.
Для начала напишем функцию onOpen:

function onOpen() {     var spreadsheet = SpreadsheetApp.getActive();   var menuItems = [     {name: 'Сгенерировать отчет по тестированию', functionName: 'generateReport_'}   ];   spreadsheet.addMenu('Отчет', menuItems); } 

Это поможет нам добавить пункт меню в панель инструментов:
image

Далее пишем функцию, которая будет вызываться при выборе этого пункта меню:

function generateReport_() {   //Список интересующих колонок на странице сценариев   var columns = ['Раздел', 'Название сценария', 'Статус', 'Комментарии', 'Тикет'];   //Список колонок для страницы отчета   var reportColumns = ['Название сценария', 'Статус', 'Комментарии', 'Тикет'];   //Функция для формирования массива с информацией о сценариях   var Data = getAllCases(columns);   //Функция создания листа с отчетом   CreateNewSheet();   //Функция сбора и вывода статистики   var stat = FormReport(Data, reportColumns);   ShowStat(stat); } 

Ну а теперь по порядку.
Функция пробегает по всем листам открытого файла и считывает всю информацию для отчета:

function getAllCases(columns) {   var sheets = SpreadsheetApp.getActive().getSheets();   var data = new Array();      for (q=0; q<sheets.length; q++)   {     SpreadsheetApp.getActive().setActiveSheet(sheets[q]);     data.push(getCases(columns));   }   return data; } 

Функция формирует массив для вывода в удобочитабельном виде. Тут есть одна особенность — если вы поменяли местами поля на разных листах, то ничего страшного не случится. Массив parts содержит списки разделов, по которым сгруппированы сценарии, а его элементы — имя раздела и список сценариев в виде массива:

function getCases(columns) {   var range = SpreadsheetApp.getActiveSheet().getDataRange().getValues();   var fields = FindFields(range[0], columns);   var parts = new Array();   for (i=1; i<range.length; i++)   {     if (range[i][fields[0]] != '')     {       parts.push(new Array());       parts[parts.length-1]['name'] = range[i][fields[0]];       parts[parts.length-1]['scen'] = new Array();     }          if (range[i][fields[1]] != '')     {       var title = range[i][fields[1]];     }          if (range[i][fields[2]] != '')     {       var scen = new Array();       scen.push(title);       for (j=2; j<fields.length; j++)       {         scen.push(range[i][fields[j]]);       }       parts[parts.length-1]['scen'].push(scen);     }       }   return parts; } 

Собственно, это и есть функция, благодаря которой не так страшно путать местами колонки на страницах сценариев:

function FindFields (data, columns) {   var fields = new Array();   for (i=0; i<columns.length; i++)   {     for (j=0; j<data.length; j++)     {       if (columns[i] == data[j])         fields.push(j);     }   }   return fields; } 

Для создания нового листа с отчетом:

function CreateNewSheet() {   SpreadsheetApp.getActive().insertSheet('Отчет');   SpreadsheetApp.setActiveSheet(SpreadsheetApp.getActive().getSheetByName('Отчет')); } 

Формирование самого отчета, сбор статистики и немного оформления:

function FormReport(data, columns) {   var doc = SpreadsheetApp.getActive();   var stat = [0, 0, 0, 0];   doc.appendRow(columns);   doc.setColumnWidth(1, 300);   doc.setColumnWidth(2, 200);   doc.setColumnWidth(3, 300);   doc.setColumnWidth(4, 300);      for (i=0; i<data.length; i++)   {     for (j=0; j<data[i].length; j++)     {       var temp = WritePart(data[i][j], columns);       stat[0] += +temp[0];       stat[1] += +temp[1];       stat[2] += +temp[2];       stat[3] += +temp[3];     }   }   return stat; } 

Функция вывода в отчет информации по одному разделу и формирование статистики по нему, а так же оформления для более приятного чтения:

function WritePart(data, columns) {   SpreadsheetApp.getActive().appendRow([data['name']]).set;      var line = SpreadsheetApp.getActive().getLastRow();   SpreadsheetApp.getActive().getRange("A"+line).setFontSize(20);      var defects = 0;   var done = 0;   var skipped = 0;   var other = 0;      for (k=0; k<data['scen'].length; k++)   {     SpreadsheetApp.getActive().appendRow(data['scen'][k]);     var line = SpreadsheetApp.getActive().getLastRow();     switch (data['scen'][k][1])     {         case 'Дефект': defects++; SpreadsheetApp.getActive().getRange("A"+line+":D"+line).setBackground('red'); break;         case 'Реализовано': done++; SpreadsheetApp.getActive().getRange("A"+line+":D"+line).setBackground('green'); break;         case 'Отложено': skipped++; SpreadsheetApp.getActive().getRange("A"+line+":D"+line).setBackground('yellow'); break;         default: other++; SpreadsheetApp.getActive().getRange("A"+line+":D"+line).setBackground('yellow');     }   }   SpreadsheetApp.getActive().appendRow(['', 'Итого по разделу:']);   SpreadsheetApp.getActive().appendRow(['', 'Реализовано:', done, (done/(done+defects+skipped+other)*100).toFixed(2) + "%"]);   line = SpreadsheetApp.getActive().getLastRow();   SpreadsheetApp.getActive().getRange("B"+line+":D"+line).setBackground('green');   SpreadsheetApp.getActive().appendRow(['', 'Дефектов:', defects, (defects/(done+defects+skipped+other)*100).toFixed(2) + "%"]);   line = SpreadsheetApp.getActive().getLastRow();   SpreadsheetApp.getActive().getRange("B"+line+":D"+line).setBackground('red');   SpreadsheetApp.getActive().appendRow(['', 'Отложено:', skipped, (skipped/(done+defects+skipped+other)*100).toFixed(2) + "%"]);   line = SpreadsheetApp.getActive().getLastRow();   SpreadsheetApp.getActive().getRange("B"+line+":D"+line).setBackground('yellow');   SpreadsheetApp.getActive().appendRow(['', 'Другое:', other, (other/(done+defects+skipped+other)*100).toFixed(2) + "%"]);   line = SpreadsheetApp.getActive().getLastRow();   SpreadsheetApp.getActive().getRange("B"+line+":D"+line).setBackground('yellow');   var stat = [defects, done, skipped, other];   return stat; } 

И, наконец, финальная статистика и много оформления:

function ShowStat(stat) {   SpreadsheetApp.getActive().appendRow(['Всего по проекту:']);   var line = SpreadsheetApp.getActive().getLastRow();   SpreadsheetApp.getActive().getRange("A"+line).setFontSize(20);   SpreadsheetApp.getActive().appendRow(['Реализовано:', stat[1], (stat[1]/(stat[0]+stat[1]+stat[2]+stat[3])*100).toFixed(2) + "%"]);   line = SpreadsheetApp.getActive().getLastRow();   SpreadsheetApp.getActive().getRange("A"+line+":C"+line).setBackground('green');   SpreadsheetApp.getActive().appendRow(['Дефектов:', stat[0], (stat[0]/(stat[0]+stat[1]+stat[2]+stat[3])*100).toFixed(2) + "%"]);   line = SpreadsheetApp.getActive().getLastRow();   SpreadsheetApp.getActive().getRange("A"+line+":C"+line).setBackground('red');   SpreadsheetApp.getActive().appendRow(['Отложено:', stat[2], (stat[2]/(stat[0]+stat[1]+stat[2]+stat[3])*100).toFixed(2) + "%"]);   line = SpreadsheetApp.getActive().getLastRow();   SpreadsheetApp.getActive().getRange("A"+line+":C"+line).setBackground('yellow');   SpreadsheetApp.getActive().appendRow(['Другое:', stat[3], (stat[3]/(stat[0]+stat[1]+stat[2]+stat[3])*100).toFixed(2) + "%"]);   line = SpreadsheetApp.getActive().getLastRow();   SpreadsheetApp.getActive().getRange("A"+line+":C"+line).setBackground('yellow'); } 

Для полноценной сборки нужны все эти функции в любой последовательности в файле скрипта.
Код не идеален — есть над чем работать, однако если кому-то это пригодится, буду рад ответить на вопросы.

Заключение

Я привел один из примеров использования скриптов от Google. Стоит признать, что это довольно мощный инструмент для работы с документами. По крайней мере, он хорошо справился с этой задачей. Есть еще примеры использования скриптов для помощи в работе, например, вот эта интересная статья. Судя по всему, Google Script может стать отличным помощником в повседневной жизни IT’шника.

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

Пишем фреймворк для разработки игр — Mechanic Framework

Добрый день, жители Хабра!
Сегодня мы будем писать фреймворк с названием Mechanic Framework для удобной разработки игр под андроид.

image

Что нам потребуется:

  • Установленные Eclipse и Android SDK
  • Приличное знание Java либо другого С-подобного языка. Лучший пример – C#
  • Терпение


Для начала создаем проект.
File – New – Other – Android Application Project

image
Появляется окошко New Android Application. Вводим любое имя (например, Mechanic), называем package своим именем, выбираем минимально возможную версию андроид для приложения и целевую версию, нажимаем Next.

image
Нажимаем Next.

image
Выбираем иконку (если вам не нравится иконка андроида, жмите Clipart – Choose и выбираем что-нибудь, либо ставим свою иконку).

image
Жмем Next.

image
Выбираем название для Activity, например, MyGame, жмем Finish.

Откроется .xml окно визуального редактирования, закрываем его.
Открываем AndroidManifest.xml и настраиваем его под свои нужды

image

Для того, чтобы устанавливать игру на карту памяти, когда это возможно, и не загрязнять внутреннюю память устройства, в поле manifest пишем

android:installLocation="preferExternal" 

Для того, чтобы приложение было доступным для отладки, пишем в поле application

android:debuggable="true" 

Для того, чтобы приложение было зафиксировано в портретном либо ландшафтном режиме (в этом случае ландшафтный режим), в поле activity пишем

android:screenOrientation="landscape" 

Для того, чтобы приложение на эмуляторе могло обрабатывать действия с клавиатурой, пишем в том же поле

android:configChanges="keyboard|keyboardHidden|orientation" 

Когда вы скачиваете приложение с Google Play, вы замечаете, что приложения требуют доступа к карте памяти/к интернету и прочим вещам, так вот, для того, чтобы получить контроль над картой памяти и предотвратить блокировку экрана при бездействии, пишем

	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> 	<uses-permission android:name="android.permission.WAKE_LOCK"/> 

Вид манифеста будет примерно такой

<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android"     package="com.frame"     android:versionCode="1"     android:versionName="1.0"     android:installLocation="preferExternal">      <uses-sdk         android:minSdkVersion="8"         android:targetSdkVersion="18" />      <application         android:allowBackup="true"         android:icon="@drawable/ic_launcher"         android:label="@string/app_name"         android:theme="@style/AppTheme"         android:debuggable="true" >         <activity             android:name="com.frame.MyGame"             android:screenOrientation="landscape"             android:configChanges="keyboard|keyboardHidden|orientation"             android:label="@string/app_name" >             <intent-filter>                 <action android:name="android.intent.action.MAIN" />                  <category android:name="android.intent.category.LAUNCHER" />             </intent-filter>         </activity>     </application>      	<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> 	<uses-permission android:name="android.permission.WAKE_LOCK"/> </manifest>  

Закрываем манифест

Теперь необходимо создать каркас фреймворка – интерфейсы, управляющие вводом, отрисовкой графики и прочим, а позже все интерфейсы реализовать.
image

Ввод

Создаем новый package с названием com.mechanic.input
Создаем интерфейс Input в этом package, и доводим его до такого вида

public interface Input { 	public static class MechanicKeyEvent 	{ 		public static final int KEY_DOWN = 0, KEY_UP = 1; 		 		public int Type; 		public int KeyCode; 		public char KeyChar; 	}  	public static class MechanicTouchEvent 	{ 		public static final int TOUCH_DOWN = 0, TOUCH_UP = 1, TOUCH_DRAGGED = 2; 		 		public int Type; 		public int X, Y; 		public int Pointer; 	} 	 	public boolean IsKeyPressed(int KeyCode); 	public boolean IsKeyPressed(char KeyChar); 	 	public boolean IsTouchDown(int pointer); 	public int GetTouchX(int pointer); 	public int GetTouchY(int pointer); 	 	public float GetAccelX(); 	public float GetAccelY(); 	public float GetAccelZ(); 	 	public List<MechanicTouchEvent> GetTouchEvents(); 	public List<MechanicKeyEvent> GetKeyEvents(); } 

GetKeyDown – булево значение, принимает код клавиши и возвращает true, если нажата кнопка
GetTouchDown – булево значение, возвращает true, если нажат экран, причем принимает эта функция номер пальца, нажавшего экран. Старые версии андроида не поддерживает Multitouch.
GetTouchX – возвращает X-координату нажатой клавиши
GetTouchY – возвращает Y-координату нажатой клавиши
Обе последние функции принимают номер пальца
GetAccelX, GetAccelY, GetAccelZ – возвращают ускорение по какой-либо координате акселерометра. Когда мы держим телефон в портретном режиме вертикально вверх, то ускорение по оси Y будет равно 9.6 м/с2, по осям X и Z 0 м/с2.

Обратите внимание на MechanicKeyEvent и MechanicTouchEvent
Первый класс хранит информацию о событии клавиши. Type всегда будет либо KEY_DOWN либо KEY_UP. KeyCode и KeyChar хранят значение клавиши в числовом и символьном типе соответсвенно.
Во втором классе X и Y – координаты пальца, нажимающего экран, Pointer – номер пальца. TOUCH_DRAGGED означает перемещение пальца.

Стоит отвлечься и сказать о том, как налажен интерфейс Input.
За акселерометр, клавиатуру и нажатия на экран отвечает не тот класс, который реализует Input, а те классы, что будут реализовывать интерфейсы Accelerometer, Keyboard и Touch соответственно. Input будет просто хранить экземпляры этих классов. Если вы знакомы с паттернами проектирования, то должны знать, что таким образом реализуется нехитрый паттерн «Фасад».

Вот эти интерфейсы

public interface Accelerometer extends SensorEventListener { 	public float GetAccelX(); 	public float GetAccelY(); 	public float GetAccelZ(); } 
public interface Keyboard extends OnKeyListener { 	public boolean IsKeyPressed(int keyCode); 	public List<KeyEvent> GetKeyEvents(); } 
public interface Touch extends OnTouchListener { 	public boolean IsTouchDown(int pointer); 	public int GetTouchX(int pointer); 	public int GetTouchY(int pointer); 	 	public List<TouchEvent> GetTouchEvents(); } 

Нетрудно догадаться, что Input просто перенаправляет методы в другие классы, а те работают честно и выкладывают результаты.

Файлы

Настало время работы с файлами. Наш интерфейс будет называться FileIO, так как класс File уже есть.
Создаем новый package com.mechanic.fileio и новый интерфейс в нем

public interface FileIO { 	public InputStream ReadAsset(String name) throws IOException; 	public InputStream ReadFile(String name) throws IOException; 	public OutputStream WriteFile(String name) throws IOException; } 

Обычно мы храним все картинки, звуки и прочие файлы в папке assets проекта. Первая функция открывает файл с указанным именем из assets, позволяя избежать лишней мороки с AssetsManager. Последние 2 функции нужны, например, для сохранения рекордов. Когда мы сохраняем данные, то записываем в хранилище устройства текстовый файл с информацией, а потом считываем его. На всякий случай постарайтесь придумать название файла пооригинальнее «file.txt», например, «.mechanicsave» — так тоже можно.

Звуки

Создаем package com.mechanic.audio и новый интерфейс Audio

public interface Audio { 	public Music NewMusic(String name); 	public Sound NewSound(String name); } 

У нас есть 2 варианта хранения и воспроизведения звука. Первый вариант – обычный, когда мы загружаем звук и проигрываем его, но такой подход в большинстве случаев годится для маленьких звуков вроде выстрелов и взрывов, а для больших звуковых файлов вроде фоновой музыки бессмысленно полностью загружать звук, поэтому мы используем в этом случае потоковое произведение звуков, динамически подгружая звуки и проигрывая их. За первый и за второй вариант отвечают соответственно интерфейсы Sound и Music. Вот их определения

public interface Sound { 	public void Play(float volume); 	public void Close(); } 
public interface Music extends OnCompletionListener { 	public void Close(); 	public boolean IsLooping(); 	public boolean IsPlaying(); 	public boolean IsStopped(); 	public void Play(); 	public void SetLooping(boolean loop); 	public void SetVolume(float volume); 	public void Stop(); } 

Графика

Создаем package com.mechanic.graphics
За графику отвечает в основном интерфейс Graphics
Вот его определение

public interface Graphics { 	public static enum ImageFormat 	{ 		ARGB_8888, ARGB_4444, RGB_565 	} 	 	public Image NewImage(String fileName); 	 	public void Clear(int color); 	public void DrawPixel(int x, int y, int color); 	public void DrawLine(int x, int y, int x2, int y2, int color); 	public void DrawRect(int x, int y, int width, int height, int color); 	 	public void DrawImage(Image image, int x, int y, int srcX, int srcY, 			int srcWidth, int srcHeight); 	 	public void DrawImage(Image image, int x, int y); 	public int GetWidth(); 	public int GetHeight(); } 

ImageFormat – перечисление, облегчающее выбор способа загрузки изображения. Вообще-то он ничего особенного не делает, но перечисление, куда надо передавать формат, имеет еще кучу ненужных методов и ненужное название Config, так что пусть будет так.
NewImage возвращает новое изображение, мы его будет сохранять в переменной и рисовать
Методы с названиями Draw… говорят сами за себя, причем первый метод DrawImage рисует только часть изображения, а второй – изображение полностью.
GetWidth и GetHeight возвращают размер «полотна», где мы рисуем картинки

Есть еще один интерфейс – для картинок

public interface Image { 	public int GetWidth(); 	public int GetHeight(); 	public ImageFormat GetFormat(); 	public void Dispose(); } 

Все достаточно красноречиво

Централизованное управление игрой

Создаем package com.mechanic.game
Остался предпоследний важный интерфейс, который будет поддерживать работу всего приложения – Game

public interface Game { 	public Input GetInput(); 	public FileIO GetFileIO(); 	public Graphics GetGraphics(); 	public Audio GetAudio(); 	public void SetScreen(Screen screen); 	public Screen GetCurrentScreen(); 	public Screen GetStartScreen(); } 

Мы просто пихаем туда интерфесы – темы прошлых глав.
Но что такое Screen?

Позвольте отвлечься. Почти каждая игра состоит из нескольких «состояний» — главное меню, меню настроек, экран рекордов, все уровни и т.д. и т.п. Немудрено, что поддержка хотя бы 5 состояний может ввергнуть нас в пучину кода. Нас спасает абстрактный класс Screen

public abstract class Screen { 	protected final Game game; 	 	public Screen(Game game) 	{ 		this.game = game; 	} 	public abstract void Update(float deltaTime); 	public abstract void Present(float deltaTime); 	public abstract void Pause(); 	public abstract void Resume(); 	public abstract void Dispose(); } 

Каждый наследник Screen (MainMenuScreen, SettingsScreen) отвечает за такое «состояние». У него есть несколько функций.
Update – обновление
Present – показ графики (введено для удобства, на самом деле эта функция вызывается так же, как предыдущая)
Pause – вызывается каждый раз, когда игра ставится на паузу (блок экрана)
Resume – продолжение игры после паузы
Dispose – освобождение всех ресурсов, к примеру, загруженных картинок

Стоит немного рассказать об deltaTime, передающихся в 2 функции.
Более искушенным геймдевелоперам известна проблема, когда скорость игры (допустим, передвижение игрока) зависит напрямую от скорости устройства, т.е. если мы будем увеличивать переменную x на 1 каждый цикл, то никогда не будет такого, чтобы игра работала одинаково и на нетбуке, и на компе с огромной оперативкой.

Таким образом, труЪ-вариант:

	@Override 	public void Update(float deltaTime) 	{  		x += 150 * deltaTime;  	} 

Не труЪ-вариант:

	@Override 	public void Update(float deltaTime) 	{  		x += 150;  	} 

Есть одна элементарная ошибка – очень часто, увеличивая x на 1.0f*deltaTime, не всегда можно заметить, что сложение целого числа с нецелым числом от 0 до 1 не дает никакого результата, засим x должен быть float

Как мы будем сменять экраны? Возвратимся к интерфейсу Game
За все отвечает функция SetScreen. Также есть функции для получения текущего и стартового экрана.

Настало время реализовать весь этот сборник!

Начинаем с ввода

Вы заметили, что в интерфейсе Input есть функции GetKeyEvents и GetTouchEvents, которые возвращают список событий, то есть по случаю какого-либо события программа создает множество объектов, которые затем чистит сборщик мусора. Скажите мне, в чем главная причина тормозов приложений для андроид? Правильно – это перегружение сборщика мусора! Нам надо как-то проконтролировать проблему. Перед тем, как продолжить, создадим класс Pool, реализуем «object pooling», способ, предложенный в прекрасной книге Марио Цехнера «Программирование игр для Android».

Его смысл заключается в том, что мы не даем сборщику мусора мешать приложению и не тратим попусту нужные ресурсы

public class Pool<T> { 	public interface PoolFactory<T> 	{ 		public T Create(); 	} 	 	private final List<T> Objects; 	private final PoolFactory<T> Factory; 	private final int MaxSize; 	 	public Pool(PoolFactory<T> Factory, int MaxSize) 	{ 		this.Factory = Factory; 		this.MaxSize = MaxSize; 		Objects = new ArrayList<T>(MaxSize); 	} 	 	public T NewObject() 	{ 		T obj = null; 		if (Objects.size() == 0) 			obj = Factory.Create(); 		else 			obj = Objects.remove(Objects.size() - 1); 		 		return obj; 	} 	 	public void Free(T object) 	{ 		if (Objects.size() < MaxSize) 			Objects.add(object); 	} } 

Допустим, у нас есть объект Pool pool. Вот так его используем

		PoolFactory<MechanicTouchEvent> factory = new PoolFactory<MechanicTouchEvent>() 		{ 			@Override 			public MechanicTouchEvent Create() 			{ 				return new MechanicTouchEvent(); 			} 		}; 		 		TouchEventPool = new Pool<MechanicTouchEvent>(factory, 100); 

Объявление пула

TouchEventPool.Free(event); 

Сохранение события в пуле

event = TouchEventPool.NewObject(); 

Получаем событие из пула. Если список пуст, то это не страшно, так как после использования события мы его помещаем в пул обратно до следующего вызова.
Очень хорошая вещь!

MechanicAccelerometer

package com.mechanic.input;  import android.content.Context; import android.hardware.Sensor; import android.hardware.SensorEvent; import android.hardware.SensorManager;  public class MechanicAccelerometer implements Accelerometer { 	float accelX, accelY, accelZ; 	 	 	public MechanicAccelerometer(Context context) 	{ 		SensorManager manager = (SensorManager) 				context.getSystemService(Context.SENSOR_SERVICE); 		 		if(manager.getSensorList(Sensor.TYPE_ACCELEROMETER).size() > 0) 		{ 			Sensor accelerometer = manager.getSensorList(Sensor.TYPE_ACCELEROMETER).get(0); 			manager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_GAME); 		} 	} 	 	@Override 	public void onAccuracyChanged(Sensor sensor, int accuracy) 	{  	}  	@Override 	public void onSensorChanged(SensorEvent event) 	{ 		accelX = event.values[0]; 		accelY = event.values[1]; 		accelZ = event.values[2]; 	}  	@Override 	public float GetAccelX() 	{ 		return accelX; 	}  	@Override 	public float GetAccelY() 	{ 		return accelY; 	}  	@Override 	public float GetAccelZ() 	{ 		return accelZ; 	} } 

Кроме Accelerometer, этот класс реализует еще SensorEventListener – он нужен для получения контроля не только над акселерометром, но и над прочими игрушками – компасом, фонариком, что-то еще. Пока что мы делаем только акселерометр.
В конструкторе мы получаем менеджер сенсоров и проверяем, есть ли доступ к акселерометру. Вообще теоретически акселерометров может быть не 1, а несколько (это же List, а не один объект), практически же он всегда один. Если число акселерометров больше 0, получаем первый из них и регистрируем его, выставляя этот класс в качестве listener’a (слушателя). onAccuracyChanged нужен, если сбилась точность сенсора, мы это не используем. onSensorChanged вызывается всегда, когда изменяется значение акселерометра, тут-то мы и снимаем показания.

MechanicTouch

package com.mechanic.input;  import java.util.ArrayList; import java.util.List;  import com.mechanic.input.Input.MechanicTouchEvent; import com.mechanic.input.Pool.PoolFactory;  import android.os.Build.VERSION; import android.view.MotionEvent; import android.view.View;  public class MechanicTouch implements Touch { 	boolean EnableMultiTouch; 	final int MaxTouchers = 20; 	boolean[] IsTouched = new boolean[MaxTouchers]; 	int[] TouchX = new int[MaxTouchers]; 	int[] TouchY = new int[MaxTouchers]; 	Pool<MechanicTouchEvent> TouchEventPool; 	List<MechanicTouchEvent> TouchEvents = new ArrayList<MechanicTouchEvent>(); 	List<MechanicTouchEvent> TouchEventsBuffer = new ArrayList<MechanicTouchEvent>(); 	float ScaleX; 	float ScaleY; 	 	public MechanicTouch(View view, float scaleX, float scaleY) 	{ 		if(Integer.parseInt(VERSION.SDK) < 5) 			EnableMultiTouch = false; 		else 			EnableMultiTouch = true; 		 		PoolFactory<MechanicTouchEvent> factory = new PoolFactory<MechanicTouchEvent>() 		{ 			@Override 			public MechanicTouchEvent Create() 			{ 				return new MechanicTouchEvent(); 			} 		}; 		 		TouchEventPool = new Pool<MechanicTouchEvent>(factory, 100); 		view.setOnTouchListener(this); 		 		this.ScaleX = scaleX; 		this.ScaleY = scaleY; 	} 	 	@Override 	public boolean onTouch(View v, MotionEvent event) 	{ 		synchronized (this) 		{ 			int action = event.getAction() & MotionEvent.ACTION_MASK; 			 			@SuppressWarnings("deprecation") 			int pointerIndex = (event.getAction() & 			MotionEvent.ACTION_POINTER_ID_MASK) 			>> MotionEvent.ACTION_POINTER_ID_SHIFT; 			 			int pointerId = event.getPointerId(pointerIndex); 			 			MechanicTouchEvent TouchEvent; 			 			switch (action) 			{ 				case MotionEvent.ACTION_DOWN: 				case MotionEvent.ACTION_POINTER_DOWN: 					TouchEvent = TouchEventPool.NewObject(); 					TouchEvent.Type = MechanicTouchEvent.TOUCH_DOWN; 					TouchEvent.Pointer = pointerId; 					TouchEvent.X = TouchX[pointerId] = (int)(event.getX(pointerIndex) * ScaleX); 					TouchEvent.Y = TouchY[pointerId] = (int)(event.getY(pointerIndex) * ScaleY); 					IsTouched[pointerId] = true; 					TouchEventsBuffer.add(TouchEvent); 				break; 				 				case MotionEvent.ACTION_UP: 				case MotionEvent.ACTION_POINTER_UP: 				case MotionEvent.ACTION_CANCEL: 					TouchEvent = TouchEventPool.NewObject(); 					TouchEvent.Type = MechanicTouchEvent.TOUCH_UP; 					TouchEvent.Pointer = pointerId; 					TouchEvent.X = TouchX[pointerId] = (int)(event.getX(pointerIndex) * ScaleX); 					TouchEvent.Y = TouchY[pointerId] = (int)(event.getY(pointerIndex) * ScaleY); 					IsTouched[pointerId] = false; 					TouchEventsBuffer.add(TouchEvent); 				break; 				 				case MotionEvent.ACTION_MOVE: 					int pointerCount = event.getPointerCount(); 					 					for (int i = 0; i < pointerCount; i++) 					{ 						pointerIndex = i; 						pointerId = event.getPointerId(pointerIndex); 						TouchEvent = TouchEventPool.NewObject(); 						TouchEvent.Type = MechanicTouchEvent.TOUCH_DRAGGED; 						TouchEvent.Pointer = pointerId; 						TouchEvent.X = TouchX[pointerId] = (int)(event.getX(pointerIndex) * ScaleX); 						TouchEvent.Y = TouchY[pointerId] = (int)(event.getY(pointerIndex) * ScaleY); 						TouchEventsBuffer.add(TouchEvent); 					} 				break; 			} 			 			return true; 		} 	}  	@Override 	public boolean IsTouchDown(int pointer) 	{ 		synchronized(this) 		{ 			if(pointer < 0 || pointer >= MaxTouchers) 				return false; 			else 				return IsTouched[pointer]; 		} 	}  	@Override 	public int GetTouchX(int pointer) 	{ 		synchronized(this) 		{ 			if (pointer < 0 || pointer >= MaxTouchers) 				return 0; 			else 				return TouchX[pointer]; 		} 	}  	@Override 	public int GetTouchY(int pointer) 	{ 		synchronized(this) 		{ 			if (pointer < 0 || pointer >= 20) 				return 0; 			else 				return TouchY[pointer]; 		} 	}  	@Override 	public List<MechanicTouchEvent> GetTouchEvents() 	{ 		synchronized (this) 		{ 			for (int i = 0; i < TouchEvents.size(); i++) 				TouchEventPool.Free(TouchEvents.get(i)); 			 			TouchEvents.clear(); 			TouchEvents.addAll(TouchEventsBuffer); 			TouchEventsBuffer.clear(); 			return TouchEvents; 		} 	} } 

Кроме Touch мы реализуем еще OnTouchListener
EnableMultiTouch нужен для определения, поддерживает ли устройство одновременное нажатие нескольких пальцев. Если VERSION.SDK меньше 5 (представлена эта переменная почему-то в виде строки), то не поддерживает.
MaxTouchers – максимальное число пальцев. Их 20, может быть больше или меньше.
В функции onTouch мы получаем номер пальца и действие (нажатие, отрыв, перемещение), которое записываем в событие и добавляем событие в список.
В GetTouchEvents мы возвращаем список событий, который после этого очищаем. За возвращение списка событий отвечает другой список.
Вы можете спросить, за что отвечает ScaleX и ScaleY? Об этом будет рассказано чуть позже, в разделе графики

MechanicKeyboard

package com.mechanic.input;  import java.util.ArrayList; import java.util.List;  import android.view.KeyEvent; import android.view.View;  import com.mechanic.input.Input.MechanicKeyEvent; import com.mechanic.input.Pool.PoolFactory; import com.mechanic.input.Pool;   public class MechanicKeyboard implements Keyboard { 	boolean[] PressedKeys = new boolean[128]; 	Pool<MechanicKeyEvent> KeyEventPool; 	 	List<MechanicKeyEvent> KeyEventsBuffer = new ArrayList<MechanicKeyEvent>(); 	List<MechanicKeyEvent> KeyEvents = new ArrayList<MechanicKeyEvent>(); 	 	public MechanicKeyboard(View view) 	{ 		PoolFactory<MechanicKeyEvent> pool = new PoolFactory<MechanicKeyEvent>() 		{ 			@Override 			public MechanicKeyEvent Create() 			{ 				return new MechanicKeyEvent(); 			} 		}; 		KeyEventPool = new Pool<MechanicKeyEvent>(pool,100); 		 		view.setOnKeyListener(this); 		view.setFocusableInTouchMode(true); 		view.requestFocus(); 	}  	public boolean IsKeyPressed(int KeyCode) 	{ 		if(KeyCode < 0 || KeyCode > 127) 			return false; 		return PressedKeys[KeyCode]; 	}  	public List<MechanicKeyEvent> GetKeyEvents() 	{ 		synchronized(this) 		{ 			for(int i = 0; i < KeyEvents.size(); i++) 				KeyEventPool.Free(KeyEvents.get(i)); 			 			KeyEvents.clear(); 			KeyEvents.addAll(KeyEventsBuffer); 			KeyEventsBuffer.clear(); 			 			return KeyEvents; 		} 	}  	@Override 	public boolean onKey(View v, int keyCode, KeyEvent event) 	{ 		if(event.getAction() == KeyEvent.ACTION_MULTIPLE) 			return false; 		 		synchronized(this) 		{ 			MechanicKeyEvent key = KeyEventPool.NewObject(); 			key.KeyCode = keyCode; 			key.KeyChar = (char)event.getUnicodeChar(); 			 			if(event.getAction() == KeyEvent.ACTION_DOWN) 			{ 				key.Type = MechanicKeyEvent.KEY_DOWN; 				if(keyCode > 0 && keyCode < 128) 					PressedKeys[keyCode] = true; 			} 			 			if(event.getAction() == KeyEvent.ACTION_UP) 			{ 				key.Type = MechanicKeyEvent.KEY_UP; 				if(keyCode > 0 && keyCode < 128) 					PressedKeys[keyCode] = false; 			} 			 			KeyEventsBuffer.add(key); 		} 		 		return false; 	} } 

Создаем массив из 128 булевых переменных, которые будут держать информацию о 128 нажатых или не нажатых клавишах. Также создаем пул объектов и 2 списка. Все просто

MechanicInput

package com.mechanic.input;  import java.util.List;  import android.content.Context; import android.view.View;   public class MechanicInput implements Input { 	MechanicKeyboard keyboard; 	MechanicAccelerometer accel; 	MechanicTouch touch; 	 	 	public MechanicInput(Context context, View view, float scaleX, float scaleY) 	{ 		accel = new MechanicAccelerometer(context); 		keyboard = new MechanicKeyboard(view); 		touch = new MechanicTouch(view, scaleX, scaleY); 	}  	@Override 	public boolean IsKeyPressed(int keyCode) 	{ 		return keyboard.IsKeyPressed(keyCode); 	} 	 	@Override 	public boolean IsKeyPressed(char keyChar) 	{ 		return keyboard.IsKeyPressed(keyChar); 	}  	@Override 	public boolean IsTouchDown(int pointer) 	{ 		return touch.IsTouchDown(pointer); 	}  	@Override 	public int GetTouchX(int pointer) 	{ 		return touch.GetTouchX(pointer); 	}  	@Override 	public int GetTouchY(int pointer) 	{ 		return touch.GetTouchY(pointer); 	}  	@Override 	public float GetAccelX() 	{ 		return accel.GetAccelX(); 	}  	@Override 	public float GetAccelY() 	{ 		return accel.GetAccelY(); 	}  	@Override 	public float GetAccelZ() 	{ 		return accel.GetAccelZ(); 	}  	@Override 	public List<MechanicTouchEvent> GetTouchEvents() 	{ 		return touch.GetTouchEvents(); 	}  	@Override 	public List<MechanicKeyEvent> GetKeyEvents() 	{ 		return keyboard.GetKeyEvents(); 	} } 

Реализуем паттерн «Фасад».

Теперь настало время поработать с файлами!

Работа с файлами

MechanicFileIO

package com.mechanic.fileio;  import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream;  import android.content.res.AssetManager; import android.os.Environment;   public class MechanicFileIO implements FileIO { 	AssetManager assets; 	String ExternalStoragePath; 	 	public MechanicFileIO(AssetManager assets) 	{ 		this.assets = assets; 		ExternalStoragePath = Environment.getExternalStorageDirectory().getAbsolutePath() +  				File.separator; 	} 	 	public InputStream ReadAsset(String name) throws IOException 	{ 		return assets.open(name); 	} 	 	public InputStream ReadFile(String name) throws IOException 	{ 		return new FileInputStream(ExternalStoragePath + name); 	} 	 	public OutputStream WriteFile(String name) throws IOException 	{ 		return new FileOutputStream(ExternalStoragePath + name); 	} } 

Мы получаем менеджер ассетов для изъятия файлов из папки assets, его использует первая функция, а вторые 2 функции берут файлы из специальной папки устройства на андроид, куда записываем и откуда считываем все данные насчет игры – рекорды, настройки, и прочее. Путь до этой папки берем в конструкторе.

Теперь создаем звуки

Работа со звуками

MechanicSound

package com.mechanic.audio;  import android.media.SoundPool;  public class MechanicSound implements Sound { 	int id; 	SoundPool pool; 	 	public MechanicSound(SoundPool pool, int id) 	{ 		this.pool = pool; 		this.id = id; 	} 	 	public void Play(float volume) 	{ 		pool.play(id, volume, volume, 0, 0, 1); 	} 	 	public void Close() 	{ 		pool.unload(id); 	} } 

В MechanicAudio для держания мелких звуковых эффектов мы используем SoundPool. В MechanicSound мы передаем номер звукового эффекта и сам объект SoundPool, от которого производим звук

MechanicMusic

package com.mechanic.audio;  import java.io.IOException;  import android.content.res.AssetFileDescriptor; import android.media.MediaPlayer;  public class MechanicMusic implements Music { 	MediaPlayer Player; 	boolean IsPrepared = false; 	 	public MechanicMusic(AssetFileDescriptor descriptor) 	{ 		Player = new MediaPlayer(); 		 		try 		{ 			Player.setDataSource(descriptor.getFileDescriptor(), 					descriptor.getStartOffset(), descriptor.getLength()); 			Player.prepare(); 			IsPrepared = true; 		} 		catch(Exception ex) 		{ 			throw new RuntimeException("Невозможно загрузить потоковую музыку"); 		} 	} 	 	public void Close() 	{ 		if(Player.isPlaying()) 			Player.stop(); 		Player.release(); 	} 	 	public boolean IsLooping() 	{ 		return Player.isLooping(); 	} 	 	public boolean IsPlaying() 	{ 		return Player.isPlaying(); 	} 	 	public boolean IsStopped() 	{ 		return !IsPrepared; 	} 	 	public void Play() 	{ 		if(Player.isPlaying()) 			return; 		 		try 		{ 			synchronized(this) 			{ 				if(!IsPrepared) 					Player.prepare(); 				Player.start(); 			} 		} 		catch(IllegalStateException ex) 		{ 			ex.printStackTrace(); 		} 		catch(IOException ex) 		{ 			ex.printStackTrace(); 		} 	} 	 	public void SetLooping(boolean loop) 	{ 		Player.setLooping(loop); 	} 	 	public void SetVolume(float volume) 	{ 		Player.setVolume(volume, volume); 	} 	 	public void Stop() 	{ 		Player.stop(); 		synchronized(this) 		{ 			IsPrepared = false; 		} 	} 	 	@Override 	public void onCompletion(MediaPlayer player) 	{ 		synchronized(this) 		{ 			IsPrepared = false; 		} 	} } 

Мы ставим звуковой файл на поток и воспроизводим его.
IsPrepared показывает, готов ли звук для произведения.
Рекомендую самому разобраться в этом классе.

Мы дошли до MechanicAudio

package com.mechanic.audio;  import java.io.IOException;  import android.app.Activity; import android.content.res.AssetFileDescriptor; import android.content.res.AssetManager; import android.media.AudioManager; import android.media.SoundPool;  public class MechanicAudio implements Audio { 	AssetManager assets; 	SoundPool pool; 	 	public MechanicAudio(Activity activity) 	{ 		activity.setVolumeControlStream(AudioManager.STREAM_MUSIC); 		this.assets = activity.getAssets(); 		pool = new SoundPool(20, AudioManager.STREAM_MUSIC, 0); 	} 	 	public Music NewMusic(String name) 	{ 		try 		{ 			AssetFileDescriptor descriptor = assets.openFd(name); 			return new MechanicMusic(descriptor); 		} 		catch(IOException ex) 		{ 			throw new RuntimeException("Невозможно загрузить потоковую музыку " + name); 		} 	} 	 	public Sound NewSound(String name) 	{ 		try 		{ 			AssetFileDescriptor descriptor = assets.openFd(name); 			int id = pool.load(descriptor, 0); 			return new MechanicSound(pool, id); 		} 		catch(IOException ex) 		{ 			throw new RuntimeException("Невозможно загрузить звуковой эффект " + name); 		} 	} } 

В конструкторе мы делаем возможность регулировать музыку устройством, берем менеджер ассетов и создаем SoundPool, который может проигрывать не более 20 звуковых эффектов за раз. Думаю, в большинстве игр этого хватит.
В создании Music мы передаем в конструктор MechanicMusic дескриптор файла, в создании Sound загружаем звук в soundPool и передаем в конструктор MechanicSound сам пул и номер звука, если что-то идет не так, делается исключение.

Делаем рисовальщик

Работа с графикой

MechanicImage

package com.mechanic.graphics;  import com.mechanic.graphics.Graphics.ImageFormat;  import android.graphics.Bitmap;  public class MechanicImage implements Image { 	Bitmap bitmap; 	ImageFormat format; 	 	public MechanicImage(Bitmap bitmap, ImageFormat format) 	{ 		this.bitmap = bitmap; 		this.format = format; 	} 	 	@Override 	public int GetWidth() 	{ 		return bitmap.getWidth(); 	} 	 	@Override 	public int GetHeight() 	{ 		return bitmap.getHeight(); 	} 	 	@Override 	public ImageFormat GetFormat() 	{ 		return format; 	} 	 	@Override 	public void Dispose() 	{ 		bitmap.recycle(); 	} } 

Этот класс – держатель изображения. Ничего особенного он не делает, введен для удобства.

MechanicGraphics

package com.mechanic.graphics;  import java.io.IOException; import java.io.InputStream;  import android.content.res.AssetManager; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.Rect;  public class MechanicGraphics implements Graphics { 	AssetManager assets; 	Bitmap buffer; 	Canvas canvas; 	Paint paint; 	Rect srcRect = new Rect(), dstRect = new Rect(); 	 	public MechanicGraphics(AssetManager assets, Bitmap buffer) 	{ 		this.assets = assets; 		this.buffer = buffer; 		this.canvas = new Canvas(buffer); 		this.paint = new Paint(); 	} 	 	@Override 	public Image NewImage(String fileName) 	{ 		ImageFormat format; 		InputStream file = null; 		Bitmap bitmap = null; 		 		try 		{ 			file = assets.open(fileName); 			bitmap = BitmapFactory.decodeStream(file); 			 			if (bitmap == null) 				throw new RuntimeException("Нельзя загрузить изображение '" 						+ fileName + "'"); 		} 		catch (IOException e) 		{ 			throw new RuntimeException("Нельзя загрузить изображение '" 					+ fileName + "'"); 		} 		finally 		{ 				try 				{ 					if(file != null) 						file.close(); 				} 				catch(IOException e) 				{ 					 				} 		} 		 		if (bitmap.getConfig() == Config.RGB_565) 			format = ImageFormat.RGB_565; 		else if (bitmap.getConfig() == Config.ARGB_4444) 			format = ImageFormat.ARGB_4444; 		else 			format = ImageFormat.ARGB_8888; 		 		return new MechanicImage(bitmap, format); 	}  	@Override 	public void Clear(int color) 	{ 		canvas.drawRGB((color & 0xff0000) >> 16, (color & 0xff00) >> 8, (color & 0xff)); 	}  	@Override 	public void DrawPixel(int x, int y, int color) 	{ 		paint.setColor(color); 		canvas.drawPoint(x, y, paint); 	}  	@Override 	public void DrawLine(int x, int y, int x2, int y2, int color) 	{ 		paint.setColor(color); 		canvas.drawLine(x, y, x2, y2, paint); 	}  	@Override 	public void DrawRect(int x, int y, int width, int height, int color) 	{ 		paint.setColor(color); 		paint.setStyle(Style.FILL); 		canvas.drawRect(x, y, x + width - 1, y + width - 1, paint); 	}  	@Override 	public void DrawImage(Image image, int x, int y, int srcX, int srcY, 			int srcWidth, int srcHeight) 	{ 		srcRect.left = srcX; 		srcRect.top = srcY; 		srcRect.right = srcX + srcWidth - 1; 		srcRect.bottom = srcY + srcHeight - 1; 		dstRect.left = x; 		dstRect.top = y; 		dstRect.right = x + srcWidth - 1; 		dstRect.bottom = y + srcHeight - 1; 		canvas.drawBitmap(((MechanicImage)image).bitmap, srcRect, dstRect, 				null); 	}  	@Override 	public void DrawImage(Image image, int x, int y) 	{ 		canvas.drawBitmap(((MechanicImage)image).bitmap, x, y, null); 	}  	@Override 	public int GetWidth() 	{ 		return buffer.getWidth(); 	} 	 	@Override 	public int GetHeight() 	{ 		return buffer.getHeight(); 	} } 

Обратите внимание! Мы не создаем объекты Paint и Rect каждый раз при отрисовке, так как это преступление против сборщика мусора.
В конструкторе мы берем Bitmap — буфер, на котором будем все рисовать, его использует canvas.
По загрузке изображения мы считываем картинку из ассетов, а потом декодируем ее в Bitmap. Бросается исключение, если загружаемый файл не картинка или если его не существует, потом файл закрывается. Под конец мы берем формат картинки и возвращаем новый MechanicImage, передавая в конструктор Bitmap и ImageFormat. Также внимание заслуживает первый метод DrawImage, который рисует часть картинки. Это применяется, когда вместо отдельных изображений картинок в игре используется группа картинок, называемая атласом. Вот пример такого атласа
image
(изображение взято из веб-ресурса interesnoe.info)
Допустим, нам потребовалось отрисовать часть картинки с 32,32 по 48,48, в позиции 1,1; тогда мы делаем так

DrawImage(image, 1, 1, 32, 32, 16, 16); 

Остальные методы легко понятны и интереса не представляют.

Настало время для интерфейсов Game и Screen!

Перед тем, как продолжать, нам нужно отрисовывать графику в отдельном потоке и не загружать пользовательский поток.
Встречайте класс SurfaceView, который предлагает в отдельном потоке рисовать графику. Создайте класс Runner

package com.mechanic.game;  import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; import android.view.SurfaceHolder; import android.view.SurfaceView;   public class Runner extends SurfaceView implements Runnable { 	MechanicGame game; 	Canvas canvas; 	Bitmap buffer; 	Thread thread = null; 	SurfaceHolder holder; 	volatile boolean running = false; 	 	public Runner(Object context, MechanicGame game, 			Bitmap buffer) 	{ 		super(game); 		this.game = game; 		this.buffer = buffer; 		this.holder = getHolder(); 	}  	public void Resume() 	{ 		running = true; 		thread = new Thread(this); 		thread.start(); 	} 	 	public void run() 	{ 		Rect dstRect = new Rect(); 		long startTime = System.nanoTime(); 		 		while(running) 		{ 			if(!holder.getSurface().isValid()) 				continue; 			 			float deltaTime = (System.nanoTime()-startTime) / 1000000000.0f; 			startTime = System.nanoTime(); 			 			game.GetCurrentScreen().Update(deltaTime); 			game.GetCurrentScreen().Present(deltaTime); 			 			canvas = holder.lockCanvas(); 			canvas.getClipBounds(dstRect); 			canvas.drawBitmap(buffer, null, dstRect, null); 			holder.unlockCanvasAndPost(canvas); 		} 	} 	 	public void Pause() 	{ 		running = false; 		 		while(true) 		{ 			try 			{ 				thread.join(); 				break; 			} 			catch (InterruptedException e) 			{  			} 		} 	} } 

Класс MechanicGame скоро будет, не волнуйтесь.
Для рисования графики не в пользовательском интерфейсе нам нужен объект SurfaceHolder. Его главные функции – lockCanvas и unlockCanvasAndPost. Первая функция блокирует Surface и возвращает Canvas, на котором можно что-нибудь рисовать (в нашем случае – буфер Bitmap, который выступает в роли холста).
В функции Resume мы запускаем новый поток с этим классом.
В функции run, пока приложение работает, берется прошедший промежуток с прошлого цикла (System.nanoTime возвращает наносекунды) и вызываются функции Update и Present текущего Screen’а приложения, после чего рисуется буфер.

Вот класс MechanicGame

package com.mechanic.game;  import com.mechanic.audio.Audio; import com.mechanic.audio.MechanicAudio; import com.mechanic.fileio.FileIO; import com.mechanic.fileio.MechanicFileIO; import com.mechanic.graphics.Graphics; import com.mechanic.graphics.MechanicGraphics; import com.mechanic.input.Input; import com.mechanic.input.MechanicInput;  import android.app.Activity; import android.content.Context; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.os.Bundle; import android.os.PowerManager; import android.os.PowerManager.WakeLock; import android.view.Window; import android.view.WindowManager;  public abstract class MechanicGame extends Activity implements Game { 	Runner runner; 	Graphics graphics; 	Audio audio; 	Input input; 	FileIO fileIO; 	Screen screen; 	WakeLock wakeLock;  	static final int SCREEN_WIDTH = 80; 	static final int SCREEN_HEIGHT = 128; 	 	@Override 	public void onCreate(Bundle savedInstanceState) 	{ 		super.onCreate(savedInstanceState); 		 		requestWindowFeature(Window.FEATURE_NO_TITLE); 		getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 				WindowManager.LayoutParams.FLAG_FULLSCREEN); 		 		boolean IsLandscape = (getResources().getConfiguration().orientation == 				Configuration.ORIENTATION_LANDSCAPE); 		 		int frameBufferWidth = IsLandscape ? SCREEN_HEIGHT : SCREEN_WIDTH; 		int frameBufferHeight = IsLandscape ? SCREEN_WIDTH : SCREEN_HEIGHT; 		 		Bitmap frameBuffer = Bitmap.createBitmap(frameBufferWidth, 				frameBufferHeight, Config.RGB_565); 		 		float scaleX = (float) frameBufferWidth / 				getWindowManager().getDefaultDisplay().getWidth(); 		float scaleY = (float) frameBufferHeight / 			getWindowManager().getDefaultDisplay().getHeight(); 		 		runner = new Runner(null, this, frameBuffer); 		graphics = new MechanicGraphics(getAssets(), frameBuffer); 		fileIO = new MechanicFileIO(getAssets()); 		audio = new MechanicAudio(this); 		input = new MechanicInput(this, runner, scaleX, scaleY); 		screen = GetStartScreen(); 		setContentView(runner); 		 		PowerManager powerManager = (PowerManager) 		getSystemService(Context.POWER_SERVICE); 		wakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, 				"Game"); 	}  	@Override 	public Input GetInput() 	{ 		return input; 	} 	 	@Override 	public FileIO GetFileIO() 	{ 		return fileIO; 	} 	 	@Override 	public Graphics GetGraphics() 	{ 		return graphics; 	} 	 	@Override 	public Audio GetAudio() 	{ 		return audio; 	} 	 	@Override 	public void SetScreen(Screen screen) 	{ 		if (screen == null) 			throw new IllegalArgumentException("Screen не может быть null"); 		 		this.screen.Pause(); 		this.screen.Dispose(); 		 		screen.Resume(); 		screen.Update(0); 		 		this.screen = screen; 	}  	@Override 	public Screen GetCurrentScreen() 	{ 		return screen; 	}  	@Override 	public Screen GetStartScreen() 	{ 		return null; 	} 	 	@Override 	public void onResume() 	{ 		super.onResume(); 		wakeLock.acquire(); 		screen.Resume(); 		runner.Resume(); 	} 	 	@Override 	public void onPause() 	{ 		super.onPause(); 		wakeLock.release(); 		runner.Pause(); 		screen.Pause(); 		 		if(isFinishing()) 			screen.Dispose(); 	} } 

У этого класса есть объекты Runner, всех наших интерфейсов и классов и объект WakeLock (нужен для того, чтобы телефон не засыпал, когда запущена игра)

Также у него есть 2 константы – SCREEN_WIDTH и SCREEN_HEIGHT, которые очень важны!
У устройств множество разрешений, и почти невозможно и бессмысленно под каждое устройство подстраивать размеры картинок, вычислять местоположение и т.д. и т.п. Представьте, что у нас есть окошко размером 80×128 пикселей (из двух вышеназванных констант). Мы в этом окошке рисуем маленькие картинки. Но вдруг размер экрана устройства не подходит по размеру этому окошку. Что делать? Все очень просто – мы берем отношение ширины и длины нашего окошка к ширине и длине устройства и рисуем все картинки, учитывая это отношение.
В итоге приложение само растягивает картинки под экран устройства.

Этот класс включает в себя Activity и у него есть методы onCreate, onResume и onPause.
В onCreate сначала приложение переходит в полноэкранный режим (чтобы не было видно зарядки и времени вверху). Потом выясняется ориентация телефона – ландшафтная или портретная (которая уже прописана в .xml файле в начале статьи). Потом создается долгожданный буфер с размером с это вот окошко 80×128 пикселей, выясняется отношение этого окошка к размеру устройства, которое передается в конструктор MechanicInput, он, в свою очередь, передает отношение в MechanicTouch. И тут – бинго! Полученные точки касания на экран умножаются на это отношение, так что координаты нажатия не зависят от размеров устройства.
Дальше создаем наши интерфейсы, регистрируем Runner и WakeLock.
В методе SetScreen мы освобождаем текущий Screen и записываем другой Screen.
Остальные методы интереса не предоставляют.

Неужели это все?

Да, господа, фреймворк уже готов!
When it’s done.

А как теперь связать фреймворк с главным классом, допустим, с MyGame?

«Главный» класс выглядит примерно так

public class MyGame extends Activity {  	@Override 	protected void onCreate(Bundle savedInstanceState) { 		super.onCreate(savedInstanceState); 		setContentView(R.layout.activity_my_game); 	}  	@Override 	public boolean onCreateOptionsMenu(Menu menu) { 		// Inflate the menu; this adds items to the action bar if it is present. 		getMenuInflater().inflate(R.menu.my_game, menu); 		return true; 	}  } 

Видоизменяем его до такого класса

package com.mechanic;  import com.mechanic.game.MechanicGame; import com.mechanic.game.Screen;  public class MyGame extends MechanicGame { 	@Override 	public Screen GetStartScreen() 	{ 		return new GameScreen(this); 	} }  

Java воспринимает этот класс как наследника от Activity, так как сам MechanicGame наследник от Activity. onCreate уже прописан, и единственное, что нам надо сделать – переопределить GetStartScreen(), так как в MechanicGame этот метод возвращает null, а это кидает ошибку.
Не забудьте реализовать класс GameScreen 🙂

package com.mechanic;  import com.mechanic.game.Game; import com.mechanic.game.Screen; import com.mechanic.graphics.Graphics; import com.mechanic.graphics.Image;  public class GameScreen extends Screen { 	Graphics g = game.GetGraphics(); 	Image wikitan; 	 	float x = 0.0f; 	 	public GameScreen(Game game) 	{ 		super(game); 		wikitan = g.NewImage("wikipetan.png"); 	}   	@Override 	public void Update(float deltaTime) 	{ 		if(game.GetInput().IsTouchDown(0)) 			x += 1.0f * deltaTime; 	}  	@Override 	public void Present(float deltaTime) 	{ 		g.Clear(0); 		g.DrawImage(wikitan, (int)x, 0); 	}  	@Override 	public void Pause() 	{  	}  	@Override 	public void Resume() 	{  	}  	@Override 	public void Dispose() 	{ 		wikitan.Dispose(); 	} } 

Это простой пример реализации Screen, который загружает изображение Википе-тан и двигает его по клику на экран.
image
(Изображение взято из веб-ресурса ru.wikipedia.org)

Результат
image

Переменная x представлена как float, так как прибавление чисел от 0 до 1 ничего не дает, идет округление.
Википе-тан рисуется c увеличением, так как размер нашего холста 80×128 пикселей

Вопросы и ответы:

— У меня неправильно отрисовывается картинка – повернутой на 90 градусов!
— Это все потому что мы дали команду в xml файле работать только в ландшафтном режиме. Для переключения режима жмите на клавишу 7 в правой части клавиатуры
— Я честно изменяю x += 1.0f * deltaTime, но картинка не двигается с места или медленно двигается. Что делать?
— Эмулятор – очень медленная штука. Проверяйте работоспособность приложения на устройстве.

Have fun!

Исходники:
rghost.ru/49052713
Литература:
developer.alexanderklimov.ru/android/
habrahabr.ru/post/109944/
Книга Марио Цехнера «Программирование игр под Android»

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

Выучить французский и остаться в Тулузе

image
Про Тулузу многие читатели Хабра должны были слышать как о штаб-квартире Аэробус и аэрокосмическом центре Европы. К этому добавлю, что до Средиземного моря отсюда меньше двух часов езды и до Атлантического океана три часа на машине. Так же в двух шагах Пиренеи и Испания. Температура воздуха сегодня 27 градусов. Если вам интересно, то я могу раскрыть легкий способ переехать сюда жить.

Сейчас работая в околоITшной сфере, могу с уверенностью сказать, что самый легкий способ это вы учиться на хорошего программиста и тогда работодатели сами оформят все необходимые бумаги, снимут жилье и оплатят переезд с семьей. Сам знаю прекрасную контору, которая сама ездит по окрестным странам в поиске талантливых разработчиков.

Я же не талантливый разработчик, а простой русский парень из провинции. Но девять лет назад мне открылось тайное (для нефранкоговорящих) знание, что в тулузском университете номер 2, под названием Мирай есть факультет DEFLE (французский язык как иностранный) и обучение на нем фактически бесплатное (300-500 евро в зависимости от того, нужна ли вам страховка).

Запись на 2014-2015 учебный год производится в январе (сразу после каникул, точную дату мне не сказали) феврале 2014го на сайте факультета.
Нужно будет перейти по ссылке «cliquer ici» и пройти предзапись. Сейчас там ничего нет, ссылка заработает только в январе. Нужно будет заполнить форму и предоставить копию и заверенный перевод аттестата о среднем образовании и университетских дипломов/сертификатов. В этом году будет даваться 200 мест. Секретариат не раскрыл мне принцип по которому будут отбирать кандидатов (скорее всего есть квоты по странам, но я могу только догадываться). Они сказали, что это не очередность предзаписи, от себя все же скажу, что при прочих равных все-таки выберут первого записавшегося. Что точно не играет роли — это знание французского. Здесь есть группа начищающая с полного ноля, с объяснения на пальцах, сам туда попал в свое время. Ограничения по возрасту, на сколько я знаю, нет. Когда я записался мне было 25, я видел студентов и за 50.

Затем в мае с вами свяжутся и подтвердят запись. С ней уже летом вы поедете в посольство получать студенческую визу и по приезду проходить окончательную запись с получением студенческого билета.

Почему это так здорово? Франция это великолепная страна, а юг Франции — это прекрасный край великолепной страны! А если серьезно, то даже если вы программист или дизайнер и ваша профессиональная квалификация вполне на уровне, требуемом здесь, то все равно скорее всего вам потребуется французский язык. Почему бы не поучить его в размеренном ритме уже находясь в теплой стране? К тому же, студенческая виза позволяет работать 20 часов в неделю (или 60% обычного времени в году, можно их выработать скопом, не размазывая на весь год). Можно продолжать фрилансить.

С временем вы уже сами решите нужна ли вам рабочая (или семейная, кто знает?) виза или вам все надоело и вы хотите обратно на Родину.

Лично я, уже на втором курсе DEFLE понял, что одного французского мне мало и записался на художественный факультет того же университета, потом узнал, что специализированная École des Beaux-Arts лучше и записался еще и туда. При этом не выписываясь из двух других мест учебы. Так и учился в трех местах и еще работал. Протянул целый семестр. Но не будем отвлекаться.

Недостатки и в Тулузе есть. За счет того, что это очень студенческий город, ну и конечно Аэробус, с жильем здесь сложно. Здесь владельцы жилья выбирают квартиросъемщиков. Франция очень бюрократическая страна, здесь высокие налоги и они очень непунктуальны. Но плюсы и минусы Франции и Тулузы — это отдельная большая тема. Если вам что-то интересно, то спрашивайте в комментариях — буду отвечать.

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