Pull to refresh

Unreal LED, или управление нагрузкой из Unreal Tournament

Reading time 11 min
Views 14K
Идея может показаться стёбом, но вполне заслуживает права на жизнь. Речь здесь пойдет о копировании событий из виртуальной реальности вовне, т. е. в направлении, обратном привычной репликации реальности в виртуальность. Я называю это «дополненной реальностью наоборот». Суть идеи заключается в отправке HTTP-запросов на Ethernet-шилд Arduino. Из UT.

Intro


Пожалуй, каждый покупатель Ethernet модуля или шилда делал из *дуино сервер включения светодиода, и многие рано или поздно пришивали его к какому-либо клиенту вроде смартфона на Android. Недавно меня посетила мысль — почему бы не использовать в роли этого клиента Unreal Tournament? UT — прекрасная визуализационная среда, а кроме того, сам движок очень удобен и гибок.

Знания редактора на уровне интерфейса достаточно. К исходникам на UnrealScript будут даны пояснения по ходу написания нашего клиента, а вообще UnrealScript имеет обычный сишный синтаксис со вполне понятными названиями функций. Тем, кто видит Unreal Editor в первый раз, но всё равно хочет попробовать, рекомендую прочесть статью VBKesha Знакомство с UnrealEngine. Часть 1, и честно говоря, я и сам очень жду продолжения этого курса, ибо в сетевой репликации, к примеру, я полный дундук. Всё моё оружие, сделанное для UT99, правильно работает только в режиме сингла/оффлайна.

Сервер


Вообще, предполагается, что читателю уже известно всё, что касается вопроса создания web-сервера на Arduino. Тем не менее, повторение — мать учения, поэтому на всякий случай привожу один из вариантов этого примера.

Описание сервера
Аппаратная часть представлена Ethernet-модулем или шилдом на ENC28J60, для постройки скетча применена библиотека Ether_28J60 от Dr. Monk. Она представляет собой обёртку для библиотеки Nuelectronics. Распаковываем архивы отсюда и отсюда в каталог %arduino%\libraries, берём пример WebRemote, меняем под свои нужды, собираем по схеме (для отдельного модуля):
Плата 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 из меню. Включить надо, оно понадобится, т. к. мы собираемся создать свой собственный пак со скриптами.

Обозреватель Actor Classes Создание класса

Чтобы создать тестовый триггер, который позволит нам управлять запросом, открываем в этом окне группу 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.

Тестовый уровень 1

Тестовая комната состоит из двух триггеров, одного 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, триггер запустит CreateClientRequest() при вхождении определенного класса в свой радиус. При этом учитывается значение TriggerType (в группе Trigger): TT_AnyProximity — любой класс, TT_PawnProximity — любая пешка (игрок, бот или монстр), TT_PlayerProximity — только игрок, TT_ClassProximity — определённый класс (указывается в свойстве ClassProximityType), TT_Shoot — снаряд (субкласс Projectile) или попадание из оружия с моментальным действием, т. е. вызов функции TakeDamage() этого триггера. Порог ущерба (насколько больно сделать триггеру, чтобы тот сработал) указывается в свойстве DamageThreshold.
Если тип запуска HTTLT_Trigger, он запускается только по вызову функции Trigger, например при возникновении события, указанного в свойстве Tag (в группе Events).
Если bAutoExec равно true, триггер автоматически сделает запрос после начала игры. Это используется для первоначальной установки триггеров в соответствии с состоянием сервера, т. е. для синхронизации.

Тестовый уровень 2

Источник света в комнате с 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-интерфейс по-настоящему красочным.
Tags:
Hubs:
+22
Comments 10
Comments Comments 10

Articles