Идея может показаться стёбом, но вполне заслуживает права на жизнь. Речь здесь пойдет о копировании событий из виртуальной реальности вовне, т. е. в направлении, обратном привычной репликации реальности в виртуальность. Я называю это «дополненной реальностью наоборот». Суть идеи заключается в отправке HTTP-запросов на Ethernet-шилд Arduino. Из UT.
Intro
Пожалуй, каждый покупатель Ethernet модуля или шилда делал из *дуино сервер включения светодиода, и многие рано или поздно пришивали его к какому-либо клиенту вроде смартфона на Android. Недавно меня посетила мысль — почему бы не использовать в роли этого клиента Unreal Tournament? UT — прекрасная визуализационная среда, а кроме того, сам движок очень удобен и гибок.
Знания редактора на уровне интерфейса достаточно. К исходникам на UnrealScript будут даны пояснения по ходу написания нашего клиента, а вообще UnrealScript имеет обычный сишный синтаксис со вполне понятными названиями функций. Тем, кто видит Unreal Editor в первый раз, но всё равно хочет попробовать, рекомендую прочесть статью VBKesha Знакомство с UnrealEngine. Часть 1, и честно говоря, я и сам очень жду продолжения этого курса, ибо в сетевой репликации, к примеру, я полный дундук. Всё моё оружие, сделанное для UT99, правильно работает только в режиме сингла/оффлайна.
Сервер
Вообще, предполагается, что читателю уже извествно всё, что касается вопроса создания web-сервера на Arduino. Тем не менее, повторение — мать учения, поэтому на всякий случай привожу один из вариантов этого примера.
Плата ENC28J60 | Arduino |
SI | MOSI (D11) |
SO | MISO (D12) |
SCL | SCK (D13) |
RST | RESET |
5V | 5V |
GND | GND |
CS | D10 |
На D6 выводе Arduino светодиод (через резистор), анодом (плюсом) в сторону дуины, катодом в сторону земли.
Скетч у меня выглядит следующим образом:
#include "EtherShield.h"
#include "ETHER_28J60.h"
int outputPin = 6;
static uint8_t mac[6] = {0xDE, 0xAD, 0xBE, 0xEF, 0xFE, 0xED};
static uint8_t ip[4] = {192, 168, 0, 40};
static uint16_t port = 80;
ETHER_28J60 e;
void setup(){
e.setup(mac, ip, port);
pinMode(outputPin, OUTPUT);
}
void loop(){
char* params;
if (params = e.serviceRequest()){
e.print("<H1>Web Remote</H1>");
if (strcmp(params, "?cmd=on") == 0) digitalWrite(outputPin, HIGH);
else if (strcmp(params, "?cmd=off") == 0) digitalWrite(outputPin, LOW);
e.print("LED state: <br>");
if(digitalRead(outputPin)==1) e.print("on");
else e.print("off");
e.print(".<br>");
e.print("<A HREF='?cmd=on'>Turn on</A><br>");
e.print("<A HREF='?cmd=off'>Turn off</A>");
e.respond();
}
}
Болванка
Для выполнения запроса нам потребуется класс клиента, которому можно передавать свои параметры во время выполнения. К счастью, такой класс там уже есть, называется он UBrowser.UBrowserHTTPClient. В UT2003/2004 пака UBrowser нет, зато есть UWeb.WebApplication и его субклассы. Рассмотрим отправку запроса из UT99, а под более старшие версии можно будет заточить по аналогии. UBrowserHTTPClient умеет делать только GET-запрос, но можно научить его и POST. Собственно, можно вообще любым образом изменять HTTP-заголовки, и реализовать не только протокол HTTP, но и какой-нибудь другой.
UBrowserHTTPClient не имеет визуального отображения в редакторе (меша или текстуры-спрайта) — если добавить его в уровень, он там появится, но редактор это никак не покажет. Поэтому создадим для него более удобную «обёртку», заодно наделив парой переменных, которые можно будет править на уровне. В качестве родителя (суперкласса) используем класс триггера.
Теперь продолжим то, о чём не было речи в статье VBKesha. Над рабочей областью с просмотрами (viewports) есть кнопки вызова обозревателей (выглядят они так: ). Нас интересует Actor Classes, открывается кнопкой, похожей на пешку (самая первая). Если в этом окне нет нижнего поля с чекбоксами, оно включается командой View -> Show packages из меню. Включить надо, оно понадобится, т. к. мы собираемся создать свой собственный пак со скриптами.
Чтобы создать тестовый триггер, который позволит нам управлять запросом, открываем в этом окне группу Triggers (принцип — как в редакторе реестра Windows), затем выделяем Trigger и нажимаем кнопку . Верхняя строка появившегося окна — имя файла с паком для создаваемого класса, нижняя — имя класса. Т. о., если нажать Enter, получится класс MyPackage.MyTrigger. Свой я назвал нехитро stdHTTPTrig.stdHTTPTrigger. Далее, пишем собственно код:
class stdHTTPTrigger expands Trigger;
var() string BrowseURL,BrowseArg;
function Touch(actor Other){
local UBrowserHTTPClient uhc;
foreach allactors(class'UBrowserHTTPClient',uhc) uhc.destroy();
uhc=spawn(class'UBrowserHTTPClient');
if(uhc != none) uhc.Browse(browseurl,browsearg);
}
Редактор сценариев (куда писать исходник) открывается двойным щелчком по классу, и автоматически при создании класса; компиляция делается кнопкой (Compile changed scripts) в верхней части окна. Нажимать Compile ALL не советую — сначала UE будет долго думать, потом пожалуется на то, что не найдены некоторые классы (из-за того что прогружено только необходимое), а потом, скорее всего, сделает GPF и вывалится, либо наглухо повиснет и его понадобится снимать тремя пальцами. После компиляции пак нужно сохранить, для этого находим его имя (то, что писали в строке Package name) в нижней части окна Actor Classes, ставим напротив него флаг, и нажимаем кнопку . Кстати, если надо быстро подправить исходник и сохранить, удобно открывать окно обозревателя классов кнопкой , не вылезая из редактора сценариев (если последний развернут на весь экран).
Как видно, рассмотренный триггер действует достаточно грубо: уничтожает вообще всех акторов клиента в уровне (просто в целях экономии памяти), затем создает новый, и просматривает им указанный ресурс. После вызова browse() клиент не уничтожается, т. к. для работы ему требуется некоторое время. Метод browse() имеет два необязательных параметра: TCP порт и таймаут. Используя последний, можно отслеживать успешность запроса, оценивая таким образом доступность ресурса. Также, у этого клиента есть callback-функции для возврата ответа сервера и обработки ошибок. Чтобы задействовать эту возможность, требуется переопределить эти функции, то есть создать свой субкласс, родителем которого будет UBrowserHTTPClient.
Тестовая комната состоит из двух триггеров, одного PlayerStart'а и источников света. Свойства BrowseURL у обоих одинаковы — это IP-адрес сервера. BrowseArg — параметр, состоящий из имени ресурса (косая черта /), и данных (знак вопроса и переменные), то есть у одного — /?cmd=on, у другого — /?cmd=off. Окно свойств открывается ПКМ на объекте -> properties, либо по F4. Если F4 не нажимается, надо нажать левой кнопкой мыши по любому просмотру. Нарисовав комнату, жмём F6, в открывшемся окне (свойства уровня) находим LevelInfo, там — DefaultGameType. Меняем на singleplayer и нажимаем Enter (подставится UnrealShare.SinglePlayer), иначе объявится орава ботов и будет мешать. Затем строим уровень (Rebuild geometry), сохраняем, запускаем. Палим из бластера по триггерам (по дефолту их радиус действия 40), наслаждаемся эффектом.
Получение ответа сервера
Теперь, заценив идею в общих чертах, можно переходить к разработке на чистовую. Если планируется назвать пак с новым классом так же, а старый «дубовый» триггер не нужен, выходим из редактора и делаем что-нибудь с файлом packname.u в каталоге %ut99%system (packname — то, как мы назвали пак с классом, %ut99% — путь к Unreal Tournament): перемещаем в каталог old, например. Или сразу в корзину, благо он нам не понадобится. Запускаем редактор заново.
События на уровне
Движок U не имеет иного механизма создания событий, кроме обхода по очереди всех акторов с подходящими свойствами, и вызова надлежащих функций. Во всех разновидностях триггеров для этого используется свойство Tag — переменная типа name (имя объекта) и функции Trigger(), Untrigger(). Untrigger() предназначена для эффекта «уничтожения» события, например в раздвижных дверях. Посередине проёма ставится триггер, создающий событие, указанное в свойстве Tag муверов (Mover, или Moveable brush — перемещаемый браш, или кисть), образующих дверь, причем в свойстве InitialState (в группе Object) указывается TriggerControl — режим, специально предусмотренный для такого поведения. При вхождении игрока в радиус действия триггера у муверов вызывается функция Trigger(), при покидании радиуса — Untrigger(). В результате двери будут открываться только в то время, пока игрок находится в зоне действия триггера. Мы поступим аналогичным образом, преследуя 2 цели: удобство использования при постройке уровня, и совместимость.
В данном примере, фактически, проверяется только состояние включено/выключено. Делается это при помощи условия и функции нахождения подстроки в строке. Но ничто не мешает развить эту систему до вытаскивания из ответа сервера некого значения, скажем уровней PWM для RGB светодиодов. Однако следует учесть, что RGB светодиод требует данные в виде RGB, а для Unreal цвет придётся пересчитывать в HSB.
Новый триггер будет иметь 8 образцов строк, которые следует искать в ответе сервера, и столько же имён для создания/уничтожения событий в случае, если строка найдена/не найдена. При желании можно увеличить, убрать, и т. д. Для HTTP-ошибок предусмотрено только создание событий — IMHO, использование Untrigger в этом случае не нужно. Опять же, никто не запрещает добавить.
Открываем обозреватель классов (Actor Classes), в нем открываем группу Actor Info InternetInfo InternetLink TcpLink UBrowserBufferedTcpLink, выделяем класс UBrowserHTTPClient, нажимаем кнопку (New script). Указываем имя класса и пак, пишем код:
class stdHTTPBrowser expands UBrowserHTTPClient;
var string SearchRespData[8];
var name SearchRespEvent[8];
var name SearchNotInRespEvent[8];
var name SearchRespCancelEvent[8];
var name SearchNotInRespCancelEvent[8];
var int HTTPErrCode[8];
var name HTTPErrEvent[8];
event HTTPReceivedData(string Data){
local int i;
local actor a;
for(i=0;i<=7;i++){
if(SearchRespData[i] != ""){
if(InStr(Caps(Data),Caps(SearchRespData[i])) != -1){
if(SearchRespEvent[i] != '')
foreach allactors(class'Actor',a,SearchRespEvent[i])
a.Trigger(self,self.instigator);
if(SearchRespCancelEvent[i] != '')
foreach allactors(class'Actor',a,SearchRespCancelEvent[i])
a.Untrigger(self,self.instigator);
}else{
if(SearchNotInRespEvent[i] != '')
foreach allactors(class'Actor',a,SearchNotInRespEvent[i])
a.Trigger(self,self.instigator);
if(SearchNotInRespCancelEvent[i] != '')
foreach allactors(class'Actor',a,SearchNotInRespCancelEvent[i])
a.Untrigger(self,self.instigator);
}
}
}
}
event HTTPError(int Code){
local int i;
local actor a;
for(i=0;i<=7;i++){
if(HTTPErrCode[i] == Code && HTTPErrEvent[i] != '')
foreach allactors(class'Actor',a,HTTPErrEvent[i])
a.Trigger(self,self.instigator);
}
}
Тип name, IMHO, мало чем отличается от string, разве что есть localized string для строк на китайском, русском и т. п. Тем не менее, крайне важно: в условиях при проверке пустые имена равны '' (в одиночных кавычках), пустые строки — "" (в двойных кавычках). Ключевое слово event — то же самое что callback в сишных исходниках для Windows API. Короче, обратный вызов, т. е. функцию запускает не сценарий класса, а внешнее событие.
Предчувствуя комментарии сродни магазинчику Бо «во забрался в дыру», обрадую тем, что именно этот класс добавлять в уровень командой Add YourHTTPClientClass here мы не будем. За нас это будет делать функция spawn() в триггере. На мой взгдяд, это самый подходящий для таких вещей класс. Поэтому закрываем окно редактора сценариев, и в обозревателе классов открываем группу Actor Triggers и выделяем Trigger. Жмём New script и пишем код триггера:
class stdHTTPTriggerX expands Trigger;
var() enum EHTTPTriggerLaunchType{ // launch type
HTTLT_Touch,
HTTLT_Trigger
} HTTPTriggerLaunchType;
var() string SearchRespData[8]; // search template in HTTP response
var() name SearchRespEvent[8]; // events generated on match
var() name SearchNotInRespEvent[8]; // events generated if no match
var() name SearchRespCancelEvent[8]; // events cancelled on match
var() name SearchNotInRespCancelEvent[8]; // events cancelled if no match
var() int HTTPErrCode[8]; // HTTP error templates
var() name HTTPErrEvent[8]; // events generated if defined HTTP error(s) ocurrs
var() string BrowseURL,BrowseArg; // URL
var() bool bAutoExec; // enable auto launch
var stdHTTPBrowser hClient; // client handle
function CreateClientRequest(){
local int i;
if(hClient != none) hClient.destroy(); // destroy previous request
hClient=spawn(class'stdHTTPBrowser'); // create
if(hClient != none){
for(i=0;i<=7;i++){
hClient.SearchRespData[i]=self.SearchRespData[i]; // transfer configs
hClient.SearchRespEvent[i]=self.SearchRespEvent[i];
hClient.SearchNotInRespEvent[i]=self.SearchNotInRespEvent[i];
hClient.SearchRespCancelEvent[i]=self.SearchRespCancelEvent[i];
hClient.SearchNotInRespCancelEvent[i]=self.SearchNotInRespCancelEvent[i];
hClient.HTTPErrCode[i]=self.HTTPErrCode[i];
hClient.HTTPErrEvent[i]=self.HTTPErrEvent[i];
}
hClient.Browse(browseurl,browsearg); // launch
}
}
function Trigger(actor Other,pawn EventInstigator){ // triggered launch
if(HTTPTriggerLaunchType == HTTLT_Trigger) CreateClientRequest();
}
function Touch(actor Other){ // proximity launch
local actor a;
if(IsRelevant(Other) && HTTPTriggerLaunchType == HTTLT_Touch){
if(ReTriggerDelay > 0){
if(Level.TimeSeconds-TriggerTime < ReTriggerDelay) return;
TriggerTime=Level.TimeSeconds;
}
CreateClientRequest();
if(Message != "") Other.Instigator.ClientMessage(Message);
if(bTriggerOnceOnly) SetCollision(False);
}
}
function PostBeginPlay(){ // autoexec launch
if(bAutoExec) CreateClientRequest();
}
Здесь уничтожается только тот объект клиента, который был создан этим триггером. Это обеспечивает одновременную работу нескольких клиентов. Ключевое слово none — пустое имя объекта, позволяет выяснить — создался объект или нет; self — имя собственного объекта, позволяет обратиться к самому себе. Выражение self. перед именем переменной можно не писать, но это делает код менее понятным. Скобки () после var обозначают, что эту переменную надо показывать в окне редактора свойств (Actor properties). То есть, класс клиента, хоть эти свойства имеет, но редактировать их можно только из сценария — у нас этим занимается цикл в CreateClientRequest(). IsRelevant() позаимствовано из класса Engine.Trigger для обеспечения совместимости с его свойствами. Условие с ReTriggerDelay позволяет сделать ограничение на срабатывание не чаще указанного интервала.
Компилируем (Compile changed scripts в редакторе сценариев), сохраняем пак в обозревателе.
Описание переменных
HTTPTriggerLaunchType | Тип запуска |
SearchRespData[] | Шаблоны для поиска |
SearchRespEvent[] | События, вызываемые при нахождении шаблона |
SearchNotInRespEvent[] | События, вызываемые при отсутствии шаблона |
SearchRespCancelEvent[] | События, уничтожаемые при нахождении шаблона |
SearchNotInRespCancelEvent[] | События, уничтожаемые при отсутствии шаблона |
HTTPErrCode[] | Коды HTTP ошибок |
HTTPErrEvent[] | События, вызываемые при HTTP ошибках |
BrowseURL | Адрес сервера |
BrowseArg | Ресурс и параметры |
bAutoExec | Автоматическое выполнение запроса при загрузке уровня |
Если тип запуска HTTLT_Touch, триггер запустит CreateClientReques() при вхождении определенного класса в свой радиус. При этом учитывается значение TriggerType (в группе Trigger): TT_AnyProximity — любой класс, TT_PawnProximity — любая пешка (игрок, бот или монстр), TT_PlayerProximity — только игрок, TT_ClassProximity — определённый класс (указывается в свойстве ClassProximityType), TT_Shoot — снаряд (субкласс Projectile) или попадание из оружия с моментальным действием, т. е. вызов функции TakeDamage() этого триггера. Порог ущерба (насколько больно сделать триггеру, чтобы тот сработал) указывается в свойстве DamageThreshold.
Если тип запуска HTTLT_Trigger, он запускается только по вызову функции Trigger, например при возникновении события, указанного в свойстве Tag (в группе Events).
Если bAutoExec равно true, триггер автоматически сделает запрос после начала игры. Это используется для первоначальной установки триггеров в соответствии с состоянием сервера, т. е. для синхронизации.
Источник света в комнате с PlayerStart — обычный Light. Источник под потолком в комнате с триггерами — TriggerLight. Он дублирует состояние светодиода. Два других источника в этой же комнате просто подсвечивают панели. Сперва разместим TriggerLight. Выделяем его в Actor classes (находится в Actor Light), добавляем нажатием ПКМ по текстуре потолка и выбором Add TriggerLight here. У меня в обозревателе классов их почему-то два, работают оба, так что выбираем любой.
Свойства группы TriggerLight не трогаем, bInitiallyActive равно false, так и нужно. Открываем группу Object и меняем InitialState на TriggerControl. Если по щелчку на свойстве выпадающая менюшка не показывается (возможный баг под Win 7), мотаем стрелками влево/вправо с клавиатуры. Далее открываем группу Events, меняем Tag с None на какое-нибудь слово, у меня было lighton.
Триггеры добавляются аналогично (находится в Actor Triggers Trigger); панели можно не рисовать — я сделал, чтобы на видео было понятнее. Текстуры из Mine.utx.
Свойства триггеров приведены в таблице.
HTTPTrigger0 | HTTPTrigger1 | HTTPTrigger2 | |
bAutoExec | False | False | True |
BrowseArg | /?cmd=on | /?cmd=off | / |
BrowseURL | 192.168.0.40 | 192.168.0.40 | 192.168.0.40 |
HTTPTriggerLaunchType | HTTLT_Touch | HTTLT_Touch | HTTLT_Trigger |
SearchNotInRespCancelEvent[0] | lighton | lighton | lighton |
SearchRespEvent[0] | lighton | lighton | lighton |
SearchRespData[0] | LED state: <br>on | LED state: <br>on | LED state: <br>on |
Если планируется ловить ошибки сервера, просто пишем в свойствах HTTPErrCode[] их номера, например 404, 403, 302 и т. д.
Результат
Братюня уже один коммент под видео написал, просто сопровождал весь процесс. Видимо, ради хохмы решил нарисовать квартиру и привязать управление умным домом к любимому рубилову. Но разумеется, это не единственное применение GET-запроса. Можно, к примеру, сделать UT-сервак, который будет грузить для игроков карты с нужными атмосферными явлениями, в зависимости от показаний датчика. В общем, теперь можно сделать web-интерфейс по-настоящему красочным.
Автор: stdamit