Dialrhea — это дисковый телефон, переделанный для игры в классический Doom по Bluetooth. Мы собрали его за два дня во время хакатона «Internet Of Shit», организованного командой Technarium в Вильнюсе. Темой хакатона было создание абсолютно бесполезных, но полностью функциональных устройств. Ниже вы можете увидеть наш Dialrhea в действии.
В тот же уикенд мы смонтировали оригинальное промо-видео, демонстрирующее возможности этой революционной дерьмовой машины.
Что дальше?
Я хочу провести мастер-класс, в котором мы соберём ещё не менее трёх таких устройств, после чего поиграем на них в Doom в режиме deathmatch. Идеальным сеттингом будет какой-нибудь Hacker Camp или аналогичное событие. Если у вас есть идеи о том, как это организовать, напишите мне.
История
Устройство мы собирали на хакатоне «Internet Of Shit». Это потрясающее событие, в 2017 и 2018 годах организованное, как я уже сказал, командой Technarium в Вильнюсе. Победителей выбирали в следующих номинациях: «Fire Means It’s Working» (горит, значит, работает), «Least Private On-line Gadget» (наименее приватный онлайн-гаджет), «This is Likely Illegal» (это наверняка нелегально), и команды упорно соперничали над воплощением этих принципов в технологические устройства.
Конкретно наше устройство выиграло награду «Least Shitty project», плюс мы получили награду «Public Prize» за самый популярный проект. Стоит отметить, что призом оказался резиновый дренажный насос, покрашенный баллончиком золотой краски.
Сама же эта гениальная идея родилась после нескольких бутылок пива, выпитых в компании с Донатасом Валиулисом и Дзюгасом Барткусом. Наша троица провернула весь процесс, в котором мы с Донатасом заботились о технических аспектах, а Дзюгас отвечал за съёмку видео и промо-материалы.
Дзюгас также снял классное триповое видео о сборке девайса и всём хакатоне в целом.
В рамках события было реализовано и много других абсурдных проектов. Кому интересно, вот часовая презентация всех проектов «Internet of Shit 2017», включая Dialrhea.
Показы
Помимо хакатона, наш девайс также стал почётным гостем множества событий, включая «Maker Faire 2017» в The Energy and Technology Museum, Hacker Camp «No Trolls Allowed», день рождения «Vilnius Tech Park», музыкальный фестиваль «Braille Satellite» и прочих.
▍ День рождения «Vilnius Tech Park 2018»
Я немало времени провёл в коворкинге недавно открытого «Sapiegos Tech Park», представляющего наполненный стартапами комплекс из старых больничных зданий посреди живописного парка в центре города. Удивительное место, где я встретил много интересных людей, посетил множество событий и услышал кучу интересных бизнес-идей, обсуждавшихся возле мойки на кухне. Когда «Sapiegos Tech Park» праздновал свой второй день рождения, я предложил запустить в качестве интерактивной инсталляции наш Dialrhea.
На событии присутствовало много интересных людей из мира бизнеса и технологий. Выше на фото вы можете видеть известного латвийского предпринимателя Валдаса Ласаса, весело тестирующего наш Dialrhea. К сожалению, он не стал вкладываться в эту революционную дерьмовую машину. Возможно, ввиду своей консервативности он просто не понимал, как правильно использовать этот высокотехнологичный гаджет, и инстинктивно пытался приложить трубку к уху, что, очевидно, не давало никаких результатов.
▍ Музыкальный фестиваль Braille Satellite 2018
В качестве довольно контрастного предыдущему мероприятия, где был представлен наш Dialrhea, стал музыкальный фестиваль «Braille Satellite». Этот фестиваль, проводившийся в удивительном парке «Mushroom Manor» в латвийской глубинке, был посвящён андерграундным направлениям электронной музыки.
На этот раз вместо предпринимателей Dialrhea испытывали всякие рейверы, хипстеры, музыканты и люди, не спавшие по три дня.
Технические детали
Сам девайс собран на Arduino и для беспроводной связи с ПК использует Bluetooth LE. Определяется он как Bluetooth-клавиатура, и его можно сопрягать с любым устройством, поддерживающим Bluetooth LE. Ниже представлены компоненты, лежащие в основе Dialrhea.
И хотя конечным применением устройства была игра в Doom, по факту оно поддерживает несколько режимов работы:
- Doom — в этом режиме девайс выступает как игровой контроллер и настроен на управление классической игрой Doom (посредством Doomsday Engine).
- Emoji — этот режим лучше всего использовать со смартфонами. Он позволяет вводить и отправлять эмодзи друзьям.
- Boring — в этом режиме Dialrhea просто выводит набранные номера (не рекомендуется).
▍ Считывание данных при наборе номера
Самым интересным во всём процессе было выяснить фактический принцип работы дискового набора с технической точки зрения. Я отношусь к поколению, которое до сих пор помнит дисковые телефоны, поэтому мне было реально любопытно понять всю простоту этого механизма и то, почему меня не всегда било током от проводов, когда я играл в механика-телефониста. Если вам любопытно, то вот видео, где этот механизм описывается.
Разобравшись с принципом работы, реализовать его на базе Arduino было уже несложно, и я не помню, чтобы возникали какие-то серьёзные сложности.
▍ Проблемы с Bluetooth
Пожалуй, самым трудным было добиться подобающей работы Bluetooth. Для этой задачи мы выбрали модуль Adafruit Bluefruit LE UART Friend, просто потому, что он у меня был, и я уже пробовал использовать его в другом проекте. Это очень функциональный модуль, но основные наши проблемы были связаны со стабильностью и надёжностью. Порой всё работало хорошо, но иногда при выполнении в точности того же кода мы получали ошибки. Мы прочитали много страниц документации о правильной реализации протоколов обмена рукопожатиями, выполнении сопряжения и так далее, но в итоге просто добавили везде циклы повтора и таймауты, чтобы у микросхемы после каждой рисковой операции было время «прийти в чувство». Ниже показан весь исходный код для Dialrhea.
/***************************************************************************
Это мозг Dialrhea — революционной дерьмовой машины — написанный за два часа на хакатоне «Internet Of Shit 2017», проводившемся организацией Technarium в Вильнюсе, Латвия. Так что код вполне ожидаемо получился тоже дерьмовым.
Автор: Giedrius Tamulaitis, giedrius@tamulaitis.lt
Version: 1.0
Код для Adafruit nRF51822 на базе модулей Bluefruit LE https://learn.adafruit.com/introducing-the-adafruit-bluefruit-le-uart-friend
***************************************************************************/
#include <Arduino.h>
#include <SPI.h>
#if not defined (_VARIANT_ARDUINO_DUE_X_) && not defined(ARDUINO_ARCH_SAMD)
#include <SoftwareSerial.h>
#endif
#include "Adafruit_BLE.h"
#include "Adafruit_BluefruitLE_UART.h"
#include "BluefruitConfig.h"
#define DEVICE_NAME "Dialrhea"
// Входной контакт дискового набора
#define ROTARY_PIN 2
// Входной контакт телефонной трубки
#define HANDSET_PIN 3
// Контакт потенциометра переключения рабочего режима
#define OPERATION_MODE_PIN A5
// Сколько ждать до отправки сообщения «keyup» для клавиш движения в режиме Gaming
#define CONTROL_KEY_HOLD_DURATION 200
// Сколько ждать до отправки сообщения «keyup» для кнопки выстрела в режиме Gaming
#define FIRE_KEY_HOLD_DURATION 200
// Сколько ждать до отправки сообщения «keyup» для клавиш, которые должны быть просто одиночными нажатиями
#define INSTANT_KEY_HOLD_DURATION 10
// Контакты для отражения состояния RGB-светодиодов
#define STATUS_LED_RED_PIN 4
#define STATUS_LED_GREEN_PIN 6
#define STATUS_LED_BLUE_PIN 5
// Константы для цветов
#define COLOR_OFF 0
#define COLOR_RED 1
#define COLOR_GREEN 2
#define COLOR_BLUE 3
// Общее число клавиш, поддерживающих хронометрированные нажатия
#define KEY_COUNT 14
// Индекс данных рычага телефонной трубки в массивах (режим Gaming, нужно два индекса, так как мы отправляем сигналы нажатия клавиш для выстрела и открытия двери)
#define KEY_GAMING_MODE_HANDSET_1_INDEX 10
#define KEY_GAMING_MODE_HANDSET_2_INDEX 11
// Индекс данных с рычага телефонной трубки в массивах (режим Emoji)
#define KEY_EMOJI_MODE_HANDSET_INDEX 12
// Индекс данных с рычага телефонной трубки в массивах (режим Boring)
#define KEY_BORING_MODE_HANDSET_INDEX 13
// Отображение значений для каждого типа набранного номера и нажатия рычага телефонной трубки
const int keyValues[KEY_COUNT] = {
0x42, // Номер 0 в режиме gaming (сейчас быстрая загрузка)
0x52, // Номер 1 в режиме gaming (сейчас стрелка «вверх»)
0x4F, // Номер 2 в режиме gaming (сейчас стрелка «вправо»)
0x50, // Номер 3 в режиме gaming (сейчас стрелка «влево»)
0x51, // Номер 4 в режиме gaming (сейчас стрелка «вниз»)
0x2A, // Номер 5 в режиме gaming (сейчас ?, следующее оружие)
0x00, // Номер 6 в режиме gaming
0x00, // Номер 7 в режиме gaming
0x00, // Номер 8 в режиме gaming
0x00, // Номер 9 в режиме gaming
0x10, // Нажатие рычага трубки в режиме Gaming (KEY_GAMING_MODE_HANDSET_1_INDEX) (на данный момент пробел)
0x2C, // Нажатие рычага трубки в режиме Gaming (KEY_GAMING_MODE_HANDSET_2_INDEX) (на данный момент 'm')
0x28, // Нажатие рычага трубки в режиме Emoji (KEY_EMOJI_MODE_HANDSET_INDEX) (на данный момент Enter)
0x29 // Нажатие рычага трубки в режиме Boring (KEY_BORING_MODE_HANDSET_INDEX) (на данный момент Esc)
};
// Продолжительность нажатия каждого вида клавиши (отображение того же содержимого, что и в массиве keyValues)
const int keyHoldDurations[KEY_COUNT] = {
INSTANT_KEY_HOLD_DURATION,
CONTROL_KEY_HOLD_DURATION,
CONTROL_KEY_HOLD_DURATION,
CONTROL_KEY_HOLD_DURATION,
CONTROL_KEY_HOLD_DURATION,
INSTANT_KEY_HOLD_DURATION,
INSTANT_KEY_HOLD_DURATION,
INSTANT_KEY_HOLD_DURATION,
INSTANT_KEY_HOLD_DURATION,
INSTANT_KEY_HOLD_DURATION,
FIRE_KEY_HOLD_DURATION,
FIRE_KEY_HOLD_DURATION,
INSTANT_KEY_HOLD_DURATION,
INSTANT_KEY_HOLD_DURATION
};
// Массив для хранения времени нажатия каждой клавиши
unsigned long keyPressTimes[KEY_COUNT];
// Массив для хранения состояния каждой клавиши
bool keyPressStates[KEY_COUNT];
// Переменные, необходимые для обработки ввода с номеронабирателя
int rotaryHasFinishedRotatingTimeout = 100;
int rotaryDebounceTimeout = 10;
int rotaryLastValue = LOW;
int rotaryTrueValue = LOW;
unsigned long rotaryLastValueChangeTime = 0;
bool rotaryNeedToEmitEvent = 0;
int rotaryPulseCount;
// Режимы работы
#define OPERATION_MODE_GAMING 0 // Управление в режиме Gaming, настроенное для лучшей игры всех времён: "Doom"
#define OPERATION_MODE_EMOJI 1 // Эмодзи + Enter
#define OPERATION_MODE_BORING 2 // Номера + Esc
// Текущий рабочий режим
int operationMode;
// Эмодзи для каждого набранного номера
const char* emojis[] = {":-O", ":poop:", ":-)", ":-(", ":-D", ":-\", ";-)", ":-*", ":-P", ">:-("};
// Переменные для обработки рычага трубки
bool isHandsetPressed = false;
unsigned long handsetPressStartTime = 0;
unsigned long handsetPressStartTimeout = 60;
// Переменная, определяющая изменение состояний клавиш во время обработки цикла (чтобы можно было отправлять команды по необходимости один раз в конце цикла)
bool keyPressStateChanged;
// Настройки конфигурации модуля Bluetooth LE
#define FACTORYRESET_ENABLE 0
#define VERBOSE_MODE false // Если установлено «true», активируется отладочный вывод
#define MINIMUM_FIRMWARE_VERSION "0.6.6"
#define BLUEFRUIT_HWSERIAL_NAME Serial1
// Объект модуля Bluetooth LE
Adafruit_BluefruitLE_UART ble(BLUEFRUIT_HWSERIAL_NAME, BLUEFRUIT_UART_MODE_PIN);
void setup(void) {
pinMode(ROTARY_PIN, INPUT);
pinMode(HANDSET_PIN, INPUT_PULLUP);
pinMode(STATUS_LED_RED_PIN, OUTPUT);
pinMode(STATUS_LED_GREEN_PIN, OUTPUT);
pinMode(STATUS_LED_BLUE_PIN, OUTPUT);
setStatusLEDColor(COLOR_GREEN);
// Ожидание установки последовательного соединения (необходимо для Flora & Micro, или когда нужно
// придержать инициализацию, пока не откроется
// монитор последовательного интерфейса
// while (!Serial);
// Даём микросхеме немного времени на разогрев
delay(1000);
initializeSerialConnection();
initializeBLEModule();
// Небольшая задержка, поскольку хорошим устройствам всегда нужно какое-то время для запуска
delay(100);
setStatusLEDColor(COLOR_BLUE);
}
void loop(void) {
keyPressStateChanged = false;
refreshOperationMode();
handleHandset();
handleRotary();
processKeyUps();
// Если состояние нажатых клавиш изменилось — отправляем новое состояние
if (keyPressStateChanged)
sendCurrentlyPressedKeys();
}
// Устанавливает цвет светодиода, отражающего состояние
void setStatusLEDColor(int colorID) {
digitalWrite(STATUS_LED_RED_PIN, colorID == COLOR_RED ? HIGH : LOW);
digitalWrite(STATUS_LED_GREEN_PIN, colorID == COLOR_GREEN ? HIGH : LOW);
digitalWrite(STATUS_LED_BLUE_PIN, colorID == COLOR_BLUE ? HIGH : LOW);
}
// Выводит сообщение об ошибке и кирпичит революционную дерьмовую машину
void error(const __FlashStringHelper*err) {
setStatusLEDColor(COLOR_RED);
Serial.println(err);
while (1);
}
// Мигает светодиодом состояния (пока поддерживается только зелёный)
void blink() {
setStatusLEDColor(COLOR_OFF);
delay(100);
setStatusLEDColor(COLOR_GREEN);
}
// Открывает последовательное соединение для отладки
void initializeSerialConnection() {
Serial.begin(9600);
Serial.println(F("Hello, I am the Dialrhea! Ready for some dialing action?"));
Serial.println(F("8-------------------------------------D"));
}
// Инициализирует модуль Bluetooth LE
void initializeBLEModule() {
// Буфер для хранения команд, отправленных модулю BLE
char commandString[64];
setStatusLEDColor(COLOR_GREEN);
Serial.print(F("Initialising the Bluefruit LE module: "));
if (!ble.begin(VERBOSE_MODE)) error(F("Couldn't find Bluefruit, make sure it's in CoMmanD mode & check wiring?"));
Serial.println( F("Easy!") );
blink();
if (FACTORYRESET_ENABLE)
{
Serial.println(F("Performing a factory reset: "));
if (!ble.factoryReset()) error(F("Couldn't factory reset. Have no idea why..."));
Serial.println(F("Done, feeling like a virgin again!"));
}
blink();
// Отключение отражения команды от Bluefruit
ble.echo(false);
blink();
Serial.println("Requesting Bluefruit info:");
ble.info();
blink();
// Изменение имени устройства — пусть весь мир знает, что это Dialrhea
Serial.print(F("Setting device name to '"));
Serial.print(DEVICE_NAME);
Serial.print(F("': "));
sprintf(commandString, "AT+GAPDEVNAME=%s", DEVICE_NAME);
if (!ble.sendCommandCheckOK(commandString)) error(F("Could not set device name for some reason. Sad."));
Serial.println(F("It's beautiful!"));
blink();
Serial.print(F("Enable HID Service (including Keyboard): "));
strcpy(commandString, ble.isVersionAtLeast(MINIMUM_FIRMWARE_VERSION) ? "AT+BleHIDEn=On" : "AT+BleKeyboardEn=On");
if (!ble.sendCommandCheckOK(commandString)) error(F("Could not enable Keyboard, we're in deep shit..."));
Serial.println(F("I'm now officially a keyboard!"));
blink();
// Сброс ПО (добавление и удаление сервисов требует сброса)
Serial.print(F("Performing a SW reset (service changes require a reset): "));
if (!ble.reset()) error(F("Couldn't reset?? Lame."));
Serial.println(F("Baby I'm ready to go!"));
Serial.println();
}
// Считывает положение потенциометра выбора режима работы и определяет текущий режим
void refreshOperationMode() {
operationMode = floor((float)analogRead(OPERATION_MODE_PIN) / 342.0);
}
// Отслеживает состояние рычага трубки
void handleHandset() {
// Игнорирует ввод, пока не пройдёт таймаут последнего действия (для исключения шума)
if (millis() - handsetPressStartTime > handsetPressStartTimeout) {
int ragelisCurrentValue = digitalRead(HANDSET_PIN);
if (!isHandsetPressed && ragelisCurrentValue == HIGH) {
isHandsetPressed = true;
handsetPressStartTime = millis();
onHandsetClicked();
}
else if (isHandsetPressed && ragelisCurrentValue == LOW) {
isHandsetPressed = false;
handsetPressStartTime = millis();
}
}
}
// Отслеживает состояние номеронабирателя
void handleRotary() {
int rotaryCurrentValue = digitalRead(ROTARY_PIN);
// Если номер не набирается, или его набор только что закончился
if ((millis() - rotaryLastValueChangeTime) > rotaryHasFinishedRotatingTimeout) {
// Если вращение номеронабирателя только остановилось, нужно отправить событие
if (rotaryNeedToEmitEvent) {
// Отправка события (мы берём количество по модулю 10, так как «0» отправляет 10 импульсов).
onRotaryNumberDialed(rotaryPulseCount % 10);
rotaryNeedToEmitEvent = false;
rotaryPulseCount = 0;
}
}
// Если значение номеронабирателя изменилось, регистрируем время, когда это произошло
if (rotaryCurrentValue != rotaryLastValue) {
rotaryLastValueChangeTime = millis();
}
// Начинаем анализировать данные, только когда сигнал стабилизируется (таймаут антидребезга проходит)
if ((millis() - rotaryLastValueChangeTime) > rotaryDebounceTimeout) {
// Это означает, что переключатель перешёл либо из состояния закрыт в открыт, либо наоборот
if (rotaryCurrentValue != rotaryTrueValue) {
// Регистрация фактического изменения значения
rotaryTrueValue = rotaryCurrentValue;
// При переходе в состояние HIGH повышаем количество импульсов
if (rotaryTrueValue == HIGH) {
rotaryPulseCount++;
rotaryNeedToEmitEvent = true;
}
}
}
// Сохраняем текущее значение в качестве последнего
rotaryLastValue = rotaryCurrentValue;
}
// Обработчик событий сработал, когда был зарегистрирован щелчок рычага трубки
void onHandsetClicked() {
// Регистрация изменения состояния рычага трубки при нажатии клавиш в зависимости от режима
if (operationMode == OPERATION_MODE_GAMING) {
if (keyPressStates[KEY_GAMING_MODE_HANDSET_1_INDEX] == false || keyPressStates[KEY_GAMING_MODE_HANDSET_2_INDEX] == false)
keyPressStateChanged = true;
keyPressStates[KEY_GAMING_MODE_HANDSET_1_INDEX] = true;
keyPressTimes[KEY_GAMING_MODE_HANDSET_1_INDEX] = millis();
keyPressStates[KEY_GAMING_MODE_HANDSET_2_INDEX] = true;
keyPressTimes[KEY_GAMING_MODE_HANDSET_2_INDEX] = millis();
} else if (operationMode == OPERATION_MODE_EMOJI) {
if (keyPressStates[KEY_EMOJI_MODE_HANDSET_INDEX] == false)
keyPressStateChanged = true;
keyPressStates[KEY_EMOJI_MODE_HANDSET_INDEX] = true;
keyPressTimes[KEY_EMOJI_MODE_HANDSET_INDEX] = millis();
} else if (operationMode == OPERATION_MODE_BORING) {
if (keyPressStates[KEY_BORING_MODE_HANDSET_INDEX] == false)
keyPressStateChanged = true;
keyPressStates[KEY_BORING_MODE_HANDSET_INDEX] = true;
keyPressTimes[KEY_BORING_MODE_HANDSET_INDEX] = millis();
}
}
// Обработчик событий срабатывает при наборе номера
void onRotaryNumberDialed(int number) {
if (operationMode == OPERATION_MODE_GAMING) {
// Установка состояния клавиши для набранной клавиши
if (keyPressStates[number] == false)
keyPressStateChanged = true;
keyPressStates[number] = true;
keyPressTimes[number] = millis();
} else if (operationMode == OPERATION_MODE_EMOJI) {
// Отправка эмодзи на радостное устройство
sendCharArray(emojis[number]);
} else if (operationMode == OPERATION_MODE_BORING) {
// Создание строки из номера и её отправка на устройство
char numberString[1];
sprintf(numberString, "%d", number);
sendCharArray(numberString);
}
}
// Отправка сырой команды модулю BLE и вывод отладочной информации
void sendBluetoothCommand(char *commandString) {
setStatusLEDColor(COLOR_OFF);
Serial.print(commandString);
ble.println(commandString);
if (ble.waitForOK()) {
Serial.println(F(" <- OK!"));
setStatusLEDColor(COLOR_BLUE);
}
else {
Serial.println(F(" <- FAILED!"));
setStatusLEDColor(COLOR_RED);
};
}
// Отправка массива символов (строка) модулю BLE
void sendCharArray(char* charArray) {
char commandString[64];
sprintf(commandString, "AT+BleKeyboard=%s", charArray);
sendBluetoothCommand(commandString);
}
// Проверка, какие клавиши сейчас нажаты, и отправка их кодов модулю BLE
void sendCurrentlyPressedKeys() {
char commandString[64] = "AT+BleKeyboardCode=00-00";
for (int i=0; i<KEY_COUNT; i++) {
if (keyPressStates[i] == true && keyValues[i] != 0x00) {
sprintf (commandString, "%s-%02X", commandString, keyValues[i]);
}
}
sendBluetoothCommand(commandString);
}
// Таймеры процессов для обнаружения, когда для каждой клавиши должны отправляться сообщения «keyup»
void processKeyUps() {
for (int i=0; i<KEY_COUNT; i++) {
if (keyPressStates[i] == true) {
if (millis() - keyPressTimes[i] > keyHoldDurations[i]) {
keyPressStates[i] = false;
keyPressStateChanged = true;
}
}
}
}
Стоит отметить, что в нашем устройстве есть очень интересный баг, который позволяет игроку совершать резкий «рывок» вперёд. Я видел такое несколько раз, обычно после того, как кто-то неистово долбил по телефону какое-то время. Я понятия не имею, почему так происходит, или как это воспроизвести. Хотя в целом мне такое поведение нравится, поэтому я решил оставить его как есть и просто считать своеобразной фишкой.
Doom
Не удивительно, что в качестве предмета для управления с помощью Dialhrea был выбран классический Doom. Я очень люблю эту игру, и в детстве был ей долгое время одержим. Мой отец принёс её однажды с работы где-то на 10 дискетах. Мне пришлось научиться самостоятельно прописывать файлы autoexec.bat и config.sys, а также загружать систему с особой дискеты, содержащей минимальную версию MS-DOS и оптимизированный драйвер мыши. Всё это только для того, чтобы оставалось достаточно памяти для запуска Doom на моей машине Intel 386 33МГц, имевшей, как я помню, всего 4МБ RAM. LH A:MOUSE.COM
была той магической строкой, которая просила DOS загрузить драйвер мыши в иначе недоступную верхнюю область памяти, выиграв тем самым дополнительные килобайты для Doom.
Очевидно, что этой игрой был одержим не только я. Лишь спустя много лет я выяснил, что Doom и его создатели, «id Software», кардинально повлияли на игровой рынок для ПК и первыми успешно внедрили модель распространения продуктов «Shareware» (условно бесплатное ПО). В одно время Doom был установлен на большем числе компьютеров, чем ОС Windows 95. У Дэвида Кушнера есть отличная книга «Masters of Doom», в которой он рассказывает историю «id Software» со множеством невероятных деталей. Очень рекомендую. Кроме того, на YouTube есть ролик, который обрисовывает довольно интересную картину, отражающую инновационные решения и драматические события внутри «id Sodtware». Я всё ещё считаю Doom лучшей игрой из когда-либо выпущенных.
Doom запускается на всём
Ещё одной причиной выбора для нашего проекта именно Doom стала его широко известная способность запускаться на практически любом железе. Подобные проекты обычно реализуют безумные фанаты Doom. Эта игра портировалась для запуска на калькуляторах, микроволновках, беговых дорожках, устройствах оплаты проезда, вейпах и даже тестах на беременность! Если в девайсе есть процессор и экран, наверняка найдётся какой-нибудь гик, который попытается запустить на нём Doom. Самая обширная из известных мне коллекций подобных устройств собрана на канале Subreddit r/itrunsdoom.
Одним из наиболее интересных проектов был реализован парнем, запустившим Doom на научном калькуляторе, запитанным от ~800 картошин. Да, именно картошин! В сравнении с ним, наш проект Dialrhea уже не кажется столь безумным.
Соперничество
Как оказалось, мы стали не единственными, кто пожелал потратить своё время на создание подобной нелепой машины. Спустя пять лет после нашей попытки, где-то в Японии парень по имени Йошино собрал аналогичный девайс. Должен признать, что его маркетинговая кампания оказалась намного успешнее нашей, и видео стало вирусным — о нём писали такие ведущие игровые СМИ, как PC Gamer, IGN и прочие. Причём его даже отметил твитом сам Джон Ромеро (один из создателей оригинального Doom).
В сравнении с Dialrhea устройство Йошино не задействует телефонную трубку, вынуждая для выстрела набирать 1
(что куда менее весело, чем неистово лупить по рычагу трубкой). Кроме того, его нужно подключать к компьютеру кабелем, в то время как Dialrhea полностью беспроводной и поддерживает Bluetooth LE. Ещё один пример того, что технологически более совершенное решение не всегда выигрывает рыночную борьбу. Вся сила в маркетинге.
Автор: Bright_Translate