Русский
Русский
English
Статистика
Реклама

If

Из песочницы Как я IF на Twine писал

31.10.2020 22:20:49 | Автор: admin
Привет Хабр! Это мой первый пост, и я хотел бы поделиться опытом создания IF-игры на Twine. Рассказать о преимуществах и недостатках инструмента, которые заметил во время работы, ну и немного о самой игре.

image

Началось все пять месяцев назад, когда мне предложили сделать IF-игру.

IF-игры это игры, в которых взаимодействие с игроком происходит с помощью текстовой информации. Есть 2 варианта взаимодействия: ввод текста или выбор из уже предложенных вариантов.

В качестве примера была выбрана игра Dictator, т.к. она была похожа на то, что планировалось реализовать. Dictator это компьютерная игра в жанре политический симулятор, разработанная Доном Пристли и выпущенная для компьютера ZX Spectrum, а впоследствии была портирована под разные платформы.

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

Для разработки игры я выбрал Twine. На просторах интернета можно найти и похожие инструменты, но потыкав пару вариантов, решил остановиться на Twine, т.к. он больше приглянулся по интерфейсу и был бесплатным.

Немного о реализации


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

Создание игры сводится к написанию html в параграфах: каждый параграф отдельная страница, которую в дальнейшем можно стилизовать. Как разработчик я могу выбрать, с какого именно параграфа будет начинаться история. Я добавил 2 параграфа: один для размещения переменных, которые будут отвечать за очки и счетчик месяцев, а второй для списка вопросов. И потом использовал display этих двух параграфов в начальном. Переходы между параграфами осуществляются с помощью ссылок. Далее в параграфах реализуется разметка того, как будет выглядеть страница.

В итоге, получилось примерно вот такое дерево:

image

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

О Twine


Twine открытая платформа для написания интерактивной литературы, текстовых игр и визуальных новелл. Позволяет публиковать игры в виде html страниц и не требует от автора глубоких познаний в программировании.

Можно работать с ним как в онлайн-версии, так и скачать софтину себе на компьютер.

Большим плюсом онлайн-версии является то, что после изменения и нажатия на кнопку Play уже открытая вкладка с историей перезагрузится. В десктопной же после нескольких десятков изменений и запуска истории появляется 100500 вкладок.

В Twine есть несколько движков, или как это называется в Twine, форматов игры.

image

В каждом формате игры есть различия в плане синтаксиса, а также в различиях в плане функционала. Для каждого из них есть дока, а также можно найти исходный код на github. Самым оптимальным мне показался SugarCube 2. В нем и синтаксис удобный, и дебаг неплохой.

Проект можно как экспортировать, так и импортировать. Но, если вдруг захочется изменить файл в редакторе, а потом обратно импортировать, эти изменения не сохранятся, т.к. Twine при запуске игры пересоберёт файл заново, и все добавленное просто удалится. Это создает небольшое неудобство, в случаях, когда надо добавить метаинформацию, favicon или же сторонние css и js файлы. В таком случае приходится добавлять их через JavaScript. Действие-то простое, но лучше бы было простое добавление тега в секцию head.

Еще одна из проблем в Twine отсутствие автокомплита, поэтому всю разметку, названия свойств нужно прописывать руками. И это в то время, когда во всех редакторах кода он присутствует!

Итог


Twine хорошо подходит для создания небольших проектов или прототипов. Можно быстренько запилить IF-игрушку. Лично мне хватило этого инструмента, за исключением некоторых минусов.

Из минусов технологии:

  • Есть проблемы с десктопной версией
  • Нет автокомплита
  • Не удобно работать с head секцией, приходится делать через js
  • Нет live-reloading, поэтому во время разработки приходится постоянно перезапускать игру, нажимая на кнопку Play

Плюсы:

  • Бесплатный инструмент
  • Понятный и простой
  • Есть достаточно возможностей сделать что-то интересное

Если кто-то хочет попробовать себя в роли диктатора, вытащить страну из бедности на небывалые высоты или, наоборот, ввергнуть ее в пучину гражданской войны, то милости прошу. Буду рад конструктивной критике.
Подробнее..

Из песочницы Как оптимизировать блок проверок case

16.08.2020 16:17:46 | Автор: admin
Почему в delphi блок case работает медленно и как это исправить.

Делюсь способами оптимизации и хаками.

Почему case плох


Даже начинающие delphi-программисты знают как выглядит блок case. Однако не все знают, что для нашего процессора, он выглядит как множество if блоков.

Вот что видит программист:

procedure case_test(Index:Integer);begin  case Index of    0: writeln('Hello');    1: writeln('Habr!');  end;end;

А вот что видит процессор:

procedure case_test(Index:Integer);begin  Index:=0;  if Index = 0 then     writeln('Hello')  else      if Index = 1 then     writeln('Habr!');end;

К сожалению никакой магии за словом case не оказалось. Более того, слишком активное использование case может замедлить выполнение кода. Например если у Вас не 2 варианта проверок, а 50, 250 или ещё больше. Худшим решением для вас будет блок case.

Чем заменить case


Решение у этой проблемы есть. Само строение блока case подсказывает нам, что наши варианты должны быть достаточно прибраны, чтобы поместиться в перечисляемом типе данных например: Integer, Word, Byte, Enum или Char.

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

const   Data:Array[0..1] of String = ('Hello', 'Habr!');procedure case_test(Index:Integer);begin    writeln(Data[Index]);end;

Это работает когда в действиях внутри блока case меняется только один параметр. Но что делать если параметров несколько?

Чем заменить сложный case


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

type  TMyTextWord = record    Text:String;    NeedLinebreak:Boolean;  end;const  Data:Array[0..2] of TMyTextWord = (     (Text:'Hello'; NeedLinebreak:False),     (Text:' '; NeedLinebreak:False),     (Text:'Habr!'; NeedLinebreak:True)   );procedure case_test(Index:Integer);var  MyTextWord:TMyTextWord;begin    MyTextWord:=Data[Index];  write(MyTextWord.Text);  if MyTextWord.NeedLinebreak then writeln;end;

Здесь мы заменили блок case, который мог выглядеть вот так

procedure case_test(Index:Integer);begin  case Index of    0: write('Hello');    1: write(' ');    2:     begin      write('Habr!');       writeln;       end;  end;end;

Таким образом мы сводим количество действий для выполнения любого из случаев в блоке case до минимума, одинакового для всех случаев. Хотя здесь не сильно видна разница т.к. один набор из 3-ех действий заменился другим. Но подумайте, что выполнится быстрее: 50 раз проверить, является ли переменная одним из чисел? или Получить по индексу из массива 1 параметр из 50 возможных?. Ответ очевиден.

И так мы пришли к тому что case не далеко ушёл от известного нам if.
А раз мы научились оптимизировать case почему бы не пойти дальше?

Чем заменить if


Допустим у нас case не использует настроек, которые можно было бы записать в обычный массив. Например такой case:

class procedure ActiveRecord<T>.SetFields(Fields: TArray<TField>;  Data: Pointer);var  I:Integer;  PRec:Pointer;begin  PRec:=@Data;  for I:=0 to Length(Fields)-1 do  begin    case Fields[I].Kind of      tkUString,      tkWideString:  PString(PRec)^:=PString(Fields[I].Data)^;      tkInteger:     PInteger(PRec)^:=PInteger(Fields[I].Data)^;      tkInt64:       PInt64(PRec)^:=PInt64(Fields[I].Data)^;      tkFloat:       PDouble(PRec)^:=PDouble(Fields[I].Data)^;      tkEnumeration: PWord(PRec)^:=PWord(Fields[I].Data)^;    end;    IncPtr(PRec,Fields[I].Size);  end;end;

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

Допустим у нас есть некая процедура содержащая несколько блоков кода.

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

Мы знаем заведомо простое решение этой проблемы: перечислить в массиве процедуры, вот так

procedure SetString(A,B:Pointer); inline;procedure SetInt(A,B:Pointer); inline;procedure SetInt64(A,B:Pointer); inline;procedure SetDouble(A,B:Pointer); inline;procedure SetBool(A,B:Pointer); inline;implementationprocedure SetString(A,B:Pointer); begin PString(A)^:=PString(B)^; end;procedure SetInt(A,B:Pointer); begin PInteger(A)^:=PInteger(B)^; end;procedure SetInt64(A,B:Pointer); begin PInt64(A)^:=PInt64(B)^; end;procedure SetDouble(A,B:Pointer); begin PDouble(A)^:=PDouble(B)^; end;procedure SetBool(A,B:Pointer); begin PBoolean(A)^:=PBoolean(B)^; end;type  TTypeHandlerProc = reference to procedure (A,B:Pointer);var  TypeHandlers:Array[TTypeKind] of TTypeHandlerProc;class procedure ActiveRecord<T>.SetFields(Fields: TArray<TField>;  Data: Pointer);var  I:Integer;  PRec:Pointer;begin  PRec:=@Data;  for I:=0 to Length(Fields)-1 do  begin    TypeHandlers[Fields[I].Kind](PRec, Fields[I].Data);    IncPtr(PRec,Fields[I].Size);  end;end;initialization  TypeHandlers[tkUString]:=SetString;  TypeHandlers[tkWideString]:=SetString;  TypeHandlers[tkInteger]:=SetInt;  TypeHandlers[tkInt64]:=SetInt64;  TypeHandlers[tkFloat]:=SetDouble;  TypeHandlers[tkEnumeration]:=SetBool;end.

Здесь мы создали массив с inline процедурами, т.е. вместо вызова этих процедур компилятор подставит их код в строку, из которой мы к ним обращаемся. А в остальном всё как раньше, обращаемся к массиву по индексу операции, и выполняем операцию указывая в круглых скобках параметры. Обработчик представлен в сокращенном варианте, т.к. обработка всех действий с типом TTypeKind выглядит громоздкой.

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

Если вы уверенный в себе программист и хотите максимальной оптимизации, но не хотите засорять модуль кучей примочек. Я представляю вам решение прямиком из тёмной стороны кодинга. Все кодеры вокруг говорят, что использовать goto не безопасно, что сам оператор устарел и в 90% случаях существуют решения без goto. Говорят что использование ассемблерных вставок доступно только злым хакерам. Но что будет, если мы пустимся во все тяжкие?

Я давно мечтал о таком способе переключения между блоками кода, чтобы хранить в массиве тип label, для осуществления прыжка goto. Таким образом я бы мог перемещаться между кусками одной процедуры при этом использовать для этого не case, а индекс по массиву адресов прыжка. Но возможно ли такое решение? Оказалось что да.

К сожалению в generic методах и классах нельзя использовать ассемблерные вставки, по этому решение пришлось переместить в метод другого объекта, хотя на самом решении это не отразилось. Решение представлено для x32 мода.

procedure TTestClass.Test(Fields: TArray<TField>; Data: Pointer);label  LS,LI,LI64,LF,LE,FIN;var  I:Integer;  PRec:Pointer;  ADR:Cardinal;  Types:Array[TTypeKind] of Cardinal;begin  FillChar(Types,Length(Types)*4,#0);  asm    lea EDX, [EAX].Types    mov [EAX-$4C+tkUString*4], offset LS    mov [EAX-$4C+tkWideString*4], offset LS    mov [EAX-$4C+tkInteger*4], offset LI    mov [EAX-$4C+tkInt64*4], offset LI64    mov [EAX-$4C+tkFloat*4], offset LF    mov [EAX-$4C+tkEnumeration*4], offset LE  end;  PRec:=Data;  for I:=0 to Length(Fields)-1 do  begin    ADR:=Types[Fields[I].Kind];    asm jmp ADR end;    LS:   PString(PRec)^:=PString(Fields[I].Data)^;   goto FIN;    LI:   PInteger(PRec)^:=PInteger(Fields[I].Data)^; goto FIN;    LI64: PInt64(PRec)^:=PInt64(Fields[I].Data)^;     goto FIN;    LF:   PDouble(PRec)^:=PDouble(Fields[I].Data)^;   goto FIN;    LE:   PByte(PRec)^:=PByte(Fields[I].Data)^;       goto FIN;    FIN:      IncPtr(PRec,Fields[I].Size);  end;end;

Трюк этого способа заключается в том, что компилятор delphi сам хранит адреса всех label и в ассемблерных вставках дает к ним доступ. Решение нашлось неожиданно просто[1]. Когда я искал как считать регистр EIP, в котором хранится адрес текущей исполняемой команды. Оказалось, что регистр считать нельзя, а вот адрес label'а как раз можно.

Ну а дальше всё просто, на ассемблере заполняем массив адресами нужных нам label'ов.
В цикле по массиву, берется каждый элемент. Тип элемента = индекс адреса его обработчика. Берем по индексу адрес обработчика, прыгаем на адрес, профит.

    ADR:=Types[Fields[I].Kind];    asm jmp ADR end;

Остались чисто формальности: после каждого блока ставим прыжок вконец цикла goto FIN;, чтобы не попадать на следующие label блоки.

Константа $4C это количество байт до начала блока с памятью массива, чтобы её вычислить можете записать в массив заполненный нулями mov [EAX], 1 и посмотреть в дебагере какая ячейка приняла это значение, количество ячеек от начала до неё * 4 и будет ваша константа.

Пишите своё мнение, и правки к статье в комментариях. Желаю успехов с оптимизацией кода.

References:

1. Хак с адресом EIP через label
Подробнее..
Категории: Оптимизация , Delphi , Case , If , Asm

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru