DM Guide 6
Это 6 глава перевода оригинального руководства по Dream Maker от разработчиков
Остальные главы руководства и статьи о программировании на коде BYOND от любителей на русском.
Оригинальная 6 глава руководства на английском языке
Глава 6: Действия Proc
Только он потер лампу, перед ним возник джин и спросил: 'Чего ты хочешь?'. — Аладдин и волшебная лампа. Арабские ночи.
Язык ДМ поддерживает несколько видов процедур. Первый - Verb, видимые игроком команды. Второй, не видимый игрокам, называется proc. В остальном эти процедуры можно назвать идентичными.
Создание процедуры
Также процедуры можно использовать для того чтобы избежать копирования кода.
К примеру, в вашей игре может быть несколько ситуаций, в которых игрок может получить урон. Разумно в каждой такой ситуации проверять, не убил ли этот удар игрока. Для того чтобы не писать отдельную проверку в каждой такой ситуации (и случайно не забыть написать эту проверку), вы можете написать для этого специальную процедуру.
mob var/life = 100 proc/HurtMe(D) life = life - D if(life < 0) view() << "[src] dies!" del src
Этот пример описывает процедуру с именем НанесиМнеУрон (HurtMe), которая принимает один аргумент - полученный урона. Полученный урон вычитается из текущего числа жизней персонажа и происходит проверка, остался ли он в живых. Если число жизней оказалось меньше нуля, персонаж удалится.
Оператор if, используемое в примере выше, это одна из множества процедур (инструкций), которые будут описанные в следующих частях руководства. Сейчас достаточно знать, идущий после if (с большим отступом), будет выполнен только в том случае, если условие, идущее в скобках после if - истинно.
Выполнение процедуры
Синтаксис выполнения процедуры схож с доступом к переменной, но, при выполнении, в скобках добавляются передаваемые в процедуру аргументы. В следующем примере, мы выполним процедуру HurtMe, когда персонаж выпьет отравленное зелье.
obj/poison
name = "can of soda" verb/drink() set src in view(0) usr.HurtMe(25) //OUCH! del src //one use only (please recycle)
Чаще всего вместо фразы 'Выполнить процедуру' используют фразу 'Вызвать процедуру'. Эти фразы означают одно и тоже.
Подобный синтаксис используется при вызове процедур обоих видов: Proc и Verb, но чаще verb вызывается игроками. Хорошим тоном считается наименование процедур Proc словами, начинающимися с заглавной буквы, а verb - с маленькой. Этот ход, кроме визуального разделения, позволяет избежать ситуации, когда имя процедуры proc совпадет с именем verb. Такие совпадения вызывают ошибку при компиляции, поскольку программа не сможет понять, вызываете вы процедуру вида proc или verb.
Когда вы вызываете процедуру, компьютер выполняет каждую команду по очереди (одну за раз), начиная с верху. Некоторые операторы, как if из примера выше, могут приказать компьютеру пропустить часть команд или же наоборот выполнить часть из них несколько раз подряд. Но, за исключением этих особых случаев, все команды выполняются последовательно. Когда все команды будут выполнены, процедура будет закончена. Программисты называют это возвратом, потому, что продолжится выполнение кода в той точке из которой была вызвана процедура (если таковая существует).
Наследование процедур
Так же, как в ситуации с verb, процедуры proc могут быть переопределены во время наследования. Синтаксис переопределения разных видов процедур совпадает. Исходная процедура отличается от переопределений наличием слова proc. После этого она может быть переопределена, для переопределения не требуется слова proc.
Это позволяет, на пример, создать несколько персонажей, которые по-разному будут реагировать на получаемый урон.
mob/DM var/vulnerable verb/set_vulnerability(v as num) vulnerable = v HurtMe(N) if(vulnerable) ..()
Этот код позволяет DM быть уязвимым или не уязвимым по желанию. В переопределенном HurtMe, первым делом происходит проверка на неуязвимость. Если DM в данный момент является уязвимым (для теста или чего-то подобного), будет вызвана родительская процедура, в данном случае, исходная версия процедуры HurtMe.
Гибкость Аргументов
Когда вы вызываете процедуру, вы можете передать в нее столько аргументов, сколько вы хотите. Всем аргументам, не переданным в процедуру при вызове, будет установлено значение равное null. Гибкость позволяет вам, к примеру, добавить несколько аргументов в переопределении процедуры. Если при вызове процедуры, не будут переданы эти добавленные аргументы, их значение станет равным null.
Вы также можете убрать часть аргументов, в переопределенной процедуре. Это, обычно, не влияет на код и делается только для удобства, в случае, когда часть аргументов не используется процедурой. Например, HurtMe объекта DM может быть переписана следующим образом:
mob/DM/HurtMe()
if(vulnerable) ..()
Поскольку, мы не используем переменную N (полученный урон), мы можем вовсе не объявлять этот аргумент. Тем не менее, вызовы этой процедуры с аргументом так же будут работать. Что более важно, родительская процедура будет получать переданные аргументы, даже если они не были объявлены в дочерней процедуре. Это происходит по тому, что вызов ..() без аргументов, передает в родительскую процедуру, аргументы переданные дочерней процедуре.
Глобальные процедуры
Некоторые процедуры могут не иметь ничего общего с каким-либо объектом. Они могут быть объявлены в верхнем уровне, для повсеместного доступа к ним. Такие процедуры, обычно, выполняют различные автономные (не зависящие от объектов) расчеты.
Язык ДМ имеет множество предопределенных процедур (такие как view() или locate()), которые возвращают многократно используемые результаты. Для того, чтобы отличать их от определенных вами процедур, их часто называют инструкциями.
Объявление глобальных процедур
Игра, в которой, к примеру, знаки зодиака играют важную роль, может иметь процедуры подобные следующей:
proc/Constellation(day)
//day should be 1 to 365 if(day > 354) return "Capricorn" if(day > 325) return "Sagittarius" if(day > 295) return "Scorpio" if(day > 265) return "Libra" if(day > 234) return "Virgo" if(day > 203) return "Leo" if(day > 172) return "Cancer" if(day > 141) return "Gemini" if(day > 110) return "Taurus" if(day > 79) return "Aries" if(day > 50) return "Pisces" if(day > 20) return "Aquarius" return "Capricorn" //day 1 to 20
Может потребоваться другая процедура, которая может конвертировать дни определенного месяца, в дни года. Этот код, в зависимости от даты, будет возвращать знак зодиака. Чуть позже мы рассмотрим детали реализации этого кода.
Вызов глобальных процедур
Глобальные процедуры вызываются также как и любые другие. Если такая процедура возвращает значение, оно может использоваться в любом месте, где требуется выражение.
Термин выражение означает, любую часть кода, которая в результате своего выполнения возвращает некое значение. Простейшим примером выражения можно считать константу, такую как число или текст. Более сложными выражениями являются переменные, операторы, вызовы процедур и т.д.
Это пример того, как вызвать процедуру и использовать, то, что она вернет.
var/day_of_year = 1 mob/DM/verb/set_date(day as num) set desc = "Enter the day of the year (1-365)." day_of_year = day world << "The sign of [Constellation(day)] is in force."
Этот verb дает DM возможность изменять текущую дату, после чего, всем будет сообщено об изменении текущего знака зодиака. Вызов процедуры, в данном случае, просто встраивается в текст, как и любое другое выражение.
Язык процедур
В следующей части мы изучем предопределенные процедуры. Сейчас вы знаете, как переопределять процедуры и объявлять новые созданные вами, это позволит вам создавать объект более подходящие под ваши нужды. Но, перед тем, как заняться процедурами всерьез, вы должны выучить их язык (вы можете сделать это и в "боевых" условиях, но цена ошибки будет совсем другой). В зависимости от ваших предпочтений в языках программирования (особенно если это С или близкие к нему языки), вы можете сами выбрать то, насколько тщательно вам нужно вчитываться в следующий материал.
Операторы
Основной единицей процедуры является оператор - команда, которая говорит компьютеру, что нужно сделать. До этого момента вы уже видели операторы, которые присваивают переменные, выводят данные и вызывают процедуры.
Операторы обычно располагаются в линию. Хотя они могут быть сгруппированы в одну строку, если поставить ; (точка с запятой) между ними. Также можно разбить оператор на несколько строк, если в конце каждой, кроме последней из них ставить \ (обратный слеш).
Statement 1 Statement 2; Statement 3; ... Statement 4 (begin) \ Statement 4 (end)
Кроме таких простых операторов существуют составные, такие как if, которые могут комбинировать несколько простых выражение в один составной. Дальше вы увидите все возможные виды операторов используемых в языке DM.
Возврат
Все виды процедур возвращают что-либо. Даже действия verb, что-то возвращают, но это редко используется. Если процедура завершается без возврата какого-либо явного значения, то возвращается специальное значение null.
Оператор return
Вы уже видели пример возврата значения с использованием выражения return. Основной формат записи возврата следующий:
return [expression]
Оператор return приказывает процедуре прекратить выполнение. Если возвращаемое выражение определено, оно возвращается в точку вызова процедуры. Выражение означает любой блок кода, создающий число. Это может быть константа, математическое вычисление или результат вызова другой процедуры.
Переменная . (точка)
Если процедура завершается без оператора return, или return без следующего за ним выражения, возвращаемая величина берется из переменной . (точка). Этой переменной (как и любой другой) может быть присвоено значение. По умолчанию в . (точке) хранится null. Поэтому процедура завершившаяся без явного указания возвращаемой величины возвращает null.
Выбор между использованием return и точкой - вопрос удобства. Но иногда, возвращаемая величина бывает рассчитана до завершения процедуры (не все процессы завершены). Тогда удобнее будет использовать точку. Либо, если вы захотите определить другое возвращаемое по умолчанию значение.
Такое имя для переменной было выбрано для того, чтобы создать ассоциацию с текущей процедурой, по аналогии с использованием точки во множестве файловых систем для обозначения текущей директории. Это также хорошо сочетается, с двумя подряд идущими точками, для обозначения родительской процедуры (и родительской директории в файловых системах). Эта аналогия идет и дальше, вы сможете заметить это во время дискуссии о типах.
Оператор if
Для выполнения блока кода в зависимости от состояния некого выражения, используется оператор if. Основной его синтаксис:
if (expression) Statement1 Statement2 . . .
Или
if(expression) Statement
Первый формат, позволяет одновременно блокировать/разрешать выполнение для нескольких операторов, в блоке с отступом (относительно if). Второй - уплотненный формат, для одного. Все составные операторы поддерживают оба описанных выше формата. Для краткости, дальше все примеры будут приводиться в плотном (втором) формате, поэтому важно понять, что любой одиночный оператор в них может быть заменен блоком кода с отступом.
Существует еще один путь группировки нескольких операторов в один - установка фигурных скобок вокруг операторов. После их также можно расположить на одной строке.
if(expression) {Statement1; Statement2; ...}
Операторы внутри оператора if называются его телом. Они будут выполняться, только если выражение в круглых скобках будет соответствовать истине. DM не имеет специального типа данных для определения понятий ИСТИНА и ЛОЖЬ. Вместо этого, каждый тип имеет свое представление истины, связанное с его значением. Вы скоро увидите, как это работает.
Дополнение else
Перед тем, как начать рассматривать преобразования типов, мы должны разобраться с else. Если выражение в if будет ложным, альтернативное тело оператора будет выполнено. Объединив несколько операторов, мы сможем рассмотреть все состояния условий. Основной синтаксис для это представлен ниже:
if(expression1) Statement1 else if(expression2) Statement2 . . . else Statement3
Рассматривая эту конструкцию сверху-вниз, каждое выражение будет протестировано. До тех пор, пока не будет найдена истина, соответствующие тела операций будут выполняться. Обратите внимание, что если любое условие будет истинным, то остальные операторы else будут проигнорированы. Если ни одно из условий не будет истинным последнее тело else будет выполнено.
Логическое выражение
Любое выражение, стоящее в условии оператора if может быть интерпретировано как истина или ложь, программисты называют это булевым значением. В ДМ существует три случая, когда выражение интерпретируется как ложь: если выражение равно null, 0 или "" (пустая сторка). Во всех других случаях выражение интерпретируется, как истина.
Скорее всего, вам понадобятся логические константы, обычно для этого используют числа 1 и 0. Также часто, вместо константы используют флаговую переменную (как opacity) или значение возвращаемое процедурой. Также вы можете объявить константы TRUE (ИСТИНА) и FALSE (ЛОЖЬ), если не хотите использовать их числовые эквиваленты.
Логические операторы
Сущестует несколько операторов для работы с логическими выражениями, все они возвращают результат в виде логического значения (0 или 1) в зависимости от аргументов этих операторов. Логические опараторы, как и другие (не логические) по большей части совпадают с таковыми в языке С (и основовашихся на нем С++ и Java).
( Оператор это симовол, как = или !, означающий некоторое действие или вычисление. Операторы, которые ставятся перед аргументом принято называть prefix/префиксными, ставящиеся после аргумента - postfix/постфиксными. Те же операторы, с обеих сторон которых стоят аргументы, называются infix/инфиксными. )
Оператор логического отрицания ! (НЕ)
Оператор ! вычисляет логическое отрицание аргумента следующего за ним. Проще говоря, если вырожение стоящее после ! истино оператор вернел 0, если ложно оператор вернет 1.
!expression
Следующий пример показывает использование оператора ! для изменения логического значения. Под "изменение значения", в данном случае, понимается простая замена истины на ложь и наоборот.
mob/verb/intangible() density = !density if(density) usr << "Вы материализовались." else usr << "Вы дематериализовались."
Вот так это и работает.
Оператор логического умножения && (И)
Оператор && вычисляет логическое И от двух выражений. Если аргументы с обеих сторон от оператора будут истинны, оператор так же вернет 1, в любых других случаях оператор вернет 0. Для уменьшения числа рассчетов часто используется такая особенность этого оператора: второй аргумент не будет расчитываться, если первый аргумент ложь. Это часто применятеся, в случаях, когда второе вырожение это процедура с побочными эффектами (занимает много ресурсов, времени для выполнения), которая не должна вычисляться если определенное выражение ложно. Этот прием называется short-circuiting (укороченная оценка). Значение оператора И равно значению последнего вычисленного аргумента. Конструкция Short-circuiting была заимствована из C. Тем не менее, в отличии от С, операторы && и || возвращают именно последний вычисленный аргумент, а не 1 или 0. Эта фича была взята из Perl.
expression1 && expression2
Пример ниже демонстрирует использование оператора &&, для того чтобы убедиться, что игрок и предмет материальны и могут взаимодействовать.
mob/verb/touch(mob/M) if(density && M.density) view() << "[usr] коснулся [M]!" else view() << "Рука [usr] прошла сквозь [M]!"
Оператор логического сложения (ИЛИ)
Оператор || вычисляет логическое ИЛИ двух выражений. Если один из аргументов оператора истинен, оператор вернет 1, в любых других случаях, он вернет 0. Как и в случае с оператором &&, будет расчитанно только минимальное необходимое количество аргументов. Если первый аргумен - истина, второй аргумент не будет расчитан. Оператор вернет значение последнего расчитанного аргумента.
expression1 || expression2
Пример использования short-circuit для отображения стандартного текста, если описание предмета отсутствует.
mob/verb/look() set src in view() usr << (desc || "Ничего необычного.")
Оператор сравнения == (эквивалентно)
Оператор == сравнивает два значения. Если они идентичны, оператор вернет 1, иначе 0. Стоит иметь в виду, что оператор = - это оператор присваивания, который имеет совсем другое происхождение. В отличии от языка С, ДМ не позволяет вам использовать = в выражениях (вроде if), поэтому вы можете не волноваться о том, что случайно используете не тот оператор.
Когда оператор == применяется с числами, результат это строгое сравнение чисел. В случае, когда оператор используется со ссылками, сравниваются ссылки, а не объект на который они указывают. Так что, если два объекта созданы идентичными во всех отношениях, но, это всеже разные объекты, результатом сравнения будет ЛОЖЬ. Учитывая тот факт, что одинаковые строки будут сложены в один объект для сохранения памяти, сравнение строк будет происходить путем посимвольного сравнения.
expression1 == expression2
Это пример использования оператора == для проверки, смеется ли персонаж над собой или над кем-то другим.
mob/verb/laugh(M as mob|null)
if(!M) view() << "[usr] смеется." else if(M == usr) view() << "[usr] смеется над собой." else view() << "[usr] смеется над [M]."
Математические операторы
Язык ДМ поддерживает операторы для всех основных математических операций. На их основе можно воссоздать любые более сложные операции. Все рассчеты в ДМ производятся над числами с плавующей точкой, если не указано другого. Любой не числовой аргумент приравнивается к нулю
Кроме описанных выше операторов, в ДМ встроены несколько полезных математических функций (например функция эмитирующая бросок игральных костей). Более подробно они будут рассмативаться в главе 16.
Арифметические операторы
Арифметические операторы - это +, -, * и /. Они выполняют сложение, вычитание, умножение и деление. Оператор "-" также может использоваться (в префиксной форме) для отрицания.
expression1 + expression2 20pt (addition) expression1 - expression2 (subtraction) expression1 * expression2 (multiplication) expression1 / expression2 (division) - expression (negation)
Программисты на языке С должны обратить внимание на то, что деление дает полный результат с плавающей точкой, а не только целочисленную часть. Например, 3/2 дает ожидаемый результат 1,5.
** оператор возведения
Оператор ** возводит левое значение в степень правой части. Не используйте по ошибке для этой цели оператор ^, так как он имеет совершенно другое значение (описано ниже).
expression1 ** expression2
% оператор остатка
Оператор % используется для нахождения остатка от деления. Выражение "A % B" читается как «A по модулю B» и равно остатку от деления A на B. Этот оператор работает только с целочисленными аргументами.
expression1 % expression2
Этот оператор часто используется в циклических событиях. Например, вы можете определить процедуру, которая заставляет солнце вставать, предупреждая людей об особенно неблагоприятных днях.
var day_count day_of_week //0 is Sunday
proc/NewDay() day_count = day_count + 1 day_of_week = day_count % 7 //0 through 6
if(day_of_week == 1) world << "It's Monday!" else world << "A new day dawns."
Увеличение и уменьшение
Сложение и вычитание 1 из переменной - настолько распространенные операции, что для них существуют специальные операторы. Увеличение(increment) ++ прибавляет 1 к переменной. Уменьшение(decrement) -- вычитает 1 из переменной.
Каждый из этих операторов имеет префиксную и постфиксную форму. От того, какая из них используется, зависит, будет ли значение выражения в целом взято из переменной до или после изменения ее значения. Префиксная форма изменяет переменную и возвращает результат. Постфиксная форма изменяет переменную, но при этом возвращает ее первоначальное значение.
++expression --expression expression++ expression--
В предыдущем примере с восходом солнца можно использовать оператор увеличения(increment).
day_count = day_count + 1 //long-hand day_count++ //short-hand ++day_count //or even this
В данном случае неважно, какую версию мы использовали - префиксную или постфиксную, потому что мы не использовали значение результирующего выражения. Важен только побочный эффект увеличения переменной day_count, а он одинаков в любом случае.
Мы даже можем объединить увеличение(increment) day_count со следующей строкой, использующей его, следующим образом:
day_count = day_count + 1 //increment day_of_week = day_count % 7 //use incremented value
day_of_week = ++day_count % 7 //increment and use it
Обратите внимание, что мы использовали префикс увелечения(increment). Это потому, что мы хотели сначала увеличить значение day_count, а затем использовать его для вычисления дня недели. Постфиксный вариант увелечения(increment) использовал бы существующее значение day_count для вычисления дня недели, а затем увеличил бы его. Таким образом, оба результата оказались бы рассинхронизированы на один день. Конечно, в данном примере это не имеет большого значения, но в некоторых ситуациях это может быть важно.
Порядок выполнения математических операций
Как и булевы операторы, математические символы обрабатываются в определенном порядке. Если порядок по умолчанию не подходит, можно использовать круглые скобки для формирования меньших выражений, которые оцениваются первыми.
На рисунке 6.10 представлен порядок операций с математическими символами от высшего к низшему. Операторы, расположенные в одной строке, имеют одинаковый приоритет и поэтому оцениваются по мере их появления в выражении слева направо.
Рисунок 6.10: Порядок выполнения математических операций
( ) ++ -- -(negation) ** * / % + -
Битовые операции
Иногда полезно упаковать несколько флагов в одну переменную. Примером этого является переменная mob.sight. Каждый флаг представлен одним битом включения/выключения в значении. Например, в случае с mob.sight возможны следующие значения:
(Не слишком привязывайтесь к специфике этой переменной. Тому это не нравится, и у меня есть ощущение, что он может устроить еще одно восстание, чтобы подавить его. Бессердечный демагог! Без него DM так и остался бы простым языком ассемблера.)
#define BLIND 1 //binary 00001 #define SEEINVIS 2 //binary 00010 #define SEEMOBS 4 //binary 00100 #define SEEOBJS 8 //binary 01000 #define SEETURFS 16 //binary 10000
Каждое значение является степенью двойки, что позволяет нам генерировать уникальные числа путем их комбинации.
Чтобы упростить работу с отдельными битами, существует ряд побитовых операторов (унаследованных от C). При использовании этих операторов аргументами должны быть 16-битные целые числа (в диапазоне от 0 до 65535). (65535 - это просто шестнадцать 1 в двоичном исчислении; это самое большое 16-битное число). Все, что выходит за пределы этого диапазона, будет обрезано.
~ побитовое NOT (НЕ)
Оператор ~ выполняет побитовое НЕ для своего аргумента. Для каждого из 16 битов аргумента, если этот бит равен 1, соответствующий бит в результате будет равен 0, и наоборот. Это очень похоже на оператор !, только в последнем случае неважно, какие биты включены - важно, чтобы хотя бы один бит был включен. Оператор ! также работает с другими значениями, кроме 16-битных целых чисел.
~ expression
& побитовое AND (И)
Оператор & выполняет побитовое AND своих аргументов. Для каждой пары битов в аргументах соответствующий бит в результате будет равен 1, если они оба равны 1, и 0 в противном случае. Обратите внимание, что это аналог логического оператора &&, за исключением того, что он обрабатывает каждый бит по отдельности, а не значение в целом.
expression1 & expression2
Оператор & чаще всего используется для проверки того, установлен ли определенный битовый флаг. Например, mob.sight & SEEINVIS будет ненулевым (т. е. истинным), если флаг SEEINVIS установлен, и 0 в противном случае.
| побитовое OR (ИЛИ)
Оператор | выполняет побитовое ИЛИ своих аргументов. Для каждой пары битов в аргументах соответствующий бит в результате будет равен 1, если один из них равен 1, и 0 в противном случае. Обратите внимание, что этот оператор аналогичен логическому оператору ||, за исключением того, что он обрабатывает каждый бит по отдельности, а не значение в целом.
expression1 | expression2
Оператор | чаще всего используется для объединения нескольких битовых флагов. Например, mob.sight может быть установлен в SEEMOBS | SEEOBJS, чтобы дать кому-то рентгеновское видение предметов сквозь стены. На самом деле, для этой цели можно использовать и +, если не включать один и тот же флаг более одного раза.
^ побитовый XOR (исключающее ИЛИ)
Оператор ^ выполняет побитовое исключающее ИЛИ своих аргументов. Для каждой пары битов в аргументах соответствующий бит в результате будет равен 1, если один из них равен 1, и 0 в противном случае.
expression1 ^ expression2
Оператор ^ чаще всего используется для переключения битового флага. Например, mob.sight = mob.sight ^ SEEINVIS включит флаг SEEINVIS, если он выключен, и наоборот.
Сдвиг битов
Операторы << и >> выполняют сдвиг битов влево и вправо. Они почти никогда не используются в программах DM, но включены, поскольку являются стандартными операторами языка C. При использовании вне выражения (в операторе) эти операторы имеют совсем другое значение (унаследованное от C++); в этом случае они служат операторами ввода/вывода. Вы почти всегда будете использовать их в этом виде.
Порядок побитовых операций
Порядок, в котором оцениваются побитовые операторы, приведен на рисунке 6.11 - от старшего к младшему. Операторы, расположенные в одной строке, имеют одинаковый приоритет и поэтому оцениваются слева направо по мере их появления в выражении.
Рисунок 6.11: Порядок побитовых операций
( ) ~ << >> & ^ |
Операторы присваивания
Оператор = приводит к тому, что левая переменная присваивается правому выражению. Как отмечалось ранее, это совершенно не похоже на символ ==, который выполняет сравнение.
(Операторы = и == имеют такое же значение в языке C. Однако, в отличие от языка C, присваивание в DM само по себе не является выражением. Это предотвращает легко совершаемую ошибку, когда в условном выражении используется =, а на самом деле нужно было ==.)
При присваивании числа и ссылки просто копируются в указанную переменную. Фактическое содержимое ссылки не дублируется - дублируется только сама ссылка. См. раздел 5.6 для обсуждения ссылок и данных переменных.
expression1 = expression2
Комбинирование других операций с присваиванием
Когда вы хотите добавить что-то в переменную, вы можете сделать это следующим образом:
variable = variable + 26
Однако DM предлагает удобное сокращение для этого, поскольку это очень распространенная операция. Вместо этого вы можете просто набрать:
variable += 26
Это не просто особый случай для оператора +. Он работает для всех них. В общем случае следующие два утверждения эквивалентны.
1.10pt expression1 = expression1 (operator) expression2 2.10pt expression1 (operator)= expression2
Условный оператор ?
Оператор ? проверяет булево выражение. При этом указываются два дополнительных выражения: одно из них вступает в силу, если булево выражение было истинным, а другое - если ложным. Для эффективности из двух выражений оценивается только требуемое.
boolean expression ? true expression : false expression
В следующем примере вместо оператора if используется оператор ?
mob/verb/intangible() density = !density usr << (density ? "You materialize." : "You dematerialize.")
Для любого человека, кроме программиста на языке С (или грека), это выглядит как греческий язык. Тем не менее, как только вы натренируете свой глаз читать это, вы сможете ходить с чувством превосходства над всеми остальными.
Оператор разыменования
Если ссылка на объект хранится в одной переменной, то доступ к собственным переменным и процедурам объекта можно получить с помощью операторов, описанных в следующих разделах. Такая операция называется разыменованием переменной, поскольку она требует от компьютера доступа к данным, на которые указывает ссылка.
«строгая» отсылка
Оператор "." (точка) используется для доступа к переменной или процедуре, принадлежащей объекту. Для этого необходимо, чтобы ссылка на объект хранилась в переменной соответствующего типа. Тип не обязательно должен быть полностью определен - достаточно, чтобы перейти к определению переменных и процедур, к которым будет осуществляться доступ.
object.variable 20pt Or 20pt object.procedure()
В отличие от большинства других операторов DM, пробел не допускается по обе стороны от оператора точки. Имена переменных и процедур должны располагаться по обе стороны от него без разделения.
Требование, чтобы объект принадлежал к известному типу, просто позволяет компилятору лучше проверять ошибки. Он не позволит вам попытаться получить доступ к переменной или процедуре, которая не принадлежит к указанному типу, и поэтому известен как «строгий» оператор разыменования.
Это во время компиляции. Во время выполнения вы можете присвоить переменную объекту, который даже не имеет того же типа (например, mob в переменной obj). Это не страшно. Фактически, оператор dot будет работать и в этом случае, если запрашиваемая переменная существует для данного объекта. В этом случае мы говорим, что объект имеет совместимый интерфейс.
Если во время выполнения выясняется, что у объекта нет запрошенной переменной, процедура немедленно прекращает выполнение. Это называется крахом процедуры. (Еще хуже - мировой крах, при котором погибает весь мир). Наиболее распространенный случай - нулевой объект. При этом будет выдан отладочный вывод, описывающий точную причину, чтобы помочь вам отследить проблему. Более подробно методы отладки будут рассмотрены в главе 19.
Следующие четыре verb иллюстрируют различные свойства оператора dot.
mob/verb summon1(M as mob) M.loc = loc //compile-time error! summon2(mob/M) M.loc = loc //this works summon3(obj/M as mob) M.loc = loc //this works summon4(mob/M as mob|null) M.loc = loc //could be run-time error!
Первая версия verb/summon не компилируется. Входной тип M был определен как mob, но тип переменной остался неопределенным, поэтому компилятор не знает, что в M есть переменная loc.
Вторая версия заботится о типе переменной и использует тот факт, что тип ввода по умолчанию вычисляется из типа переменной.
Третья версия странная, но она работает. Мы сказали компилятору, что это переменная obj, и сказали клиенту принимать мобов в качестве входных данных. Поскольку и obj, и mob имеют переменную loc, и компилятор, и сервер довольны. Вы, конечно, не захотите делать все именно так, но вы можете изменить тип ввода на obj|mob, и это будет более логично.
Четвертая версия рискует привести к аварийному завершению работы proc. Вместо этого следует проверять, не является ли M нулем, и не разыменовывать его в этом случае. Другой способ - присвоить M значение по умолчанию (например, usr).
«слабая» отсылка
Оператор ":" позволяет сделать еще один шаг вперед, сделав проверку валидности во время компиляции еще менее строгой. Он работает так же, как оператор dot, только полный тип объекта указывать не нужно. Пока хотя бы один объектный тип, производный от указанного, содержит запрашиваемую переменную, компилятор будет ее разрешать. Вам остается следить за тем, чтобы во время выполнения использовались только совместимые объекты (иначе произойдет сбой).
variable:variable 20pt Or 20pt variable:procedure()
Как и оператор dot, оператор ":" не может иметь пробелов между собой и своими аргументами.
Чаще всего этот оператор используется, когда тип объекта вообще не определен. В этом случае компилятор проверяет только то, что хотя бы один объектный тип во всем дереве имеет указанную переменную или процедуру. Эту технику следует использовать не из лени, а когда у вас есть законные причины оставить тип объекта неопределенным. Например, если переменная может содержать ссылки на объекты типов, не имеющих общего предка (например, obj и mob). Однако в большинстве случаев лучше использовать возможности компилятора по проверке типов.
Следующие verb демонстрируют два способа расширить вышеупомянутую команду summon, чтобы она могла принимать в качестве аргументов как мобов, так и объекты.
mob/verb summon1(obj/M as obj|mob) M.loc = loc summon2(M as obj|mob) M:loc = loc
Первый пример радует компилятор тем, что он лжет о типе переменной. M может быть obj, но может быть и mob. В любом случае у них обоих есть переменная loc, так что во время выполнения все будет работать.
Во втором случае компилятор остается доволен, используя оператор ":" для нестрогой проверки типов. Поскольку для M не объявлен тип, компилятор просто проверяет, есть ли у какого-то типа объекта переменная loc. Поскольку таких типов несколько (примеры - obj, mob и turf), проверка проходит успешно.
Суть в том, что ни строгий, ни нестрогий оператор не влияют на значение самой переменной. Они просто управляют тем, как компилятор выполняет проверку типов. Во время выполнения все, что имеет значение, - это наличие у объекта запрашиваемой переменной.
Оператор пути
Операторы . и : также могут использоваться в выражениях пути наряду с обычным разделителем /, который вы уже видели. Их значение в этом контексте напоминает то, как они ведут себя в выражениях разыменования. Все операторы пути объединяет то, что они должны находиться непосредственно рядом со своими аргументами без промежутка.
path-expression/path-expression path-expression.path-expression path-expression:path-expression
Пути используются в DM в двух контекстах. Первый - в определениях объектов. В этом случае путь используется для создания узлов в дереве кода. Второй контекст - в выражениях, где значения пути ссылаются на типы объектов. В этом случае ссылка на тип всегда должна начинаться с оператора path, чтобы быть правильно распознанной. Это легко запомнить, потому что почти всегда нужно начинать с /, как вы скоро увидите.
/ разделитель между родителями и детьми
Оператор / используется в пути между родительским и дочерним объектами. В контексте определения объекта это эквивалентно новой строке и дополнительному уровню отступа.
В начале пути этот оператор дает особый эффект, начиная с корня (или верхнего уровня) кода. Обычно путь относят к той позиции в коде, где он используется. Например, если вы находитесь внутри узла obj и определяете scroll, то на самом деле вы определяете /obj/scroll, и именно так вы будете ссылаться на этот тип в других местах кода. Путь, начинающийся с /, называется абсолютным путем, чтобы отличить его от других относительных путей.
. оператор пути поиска
Оператор . (точка) в пути ищет указанного ребенка, начиная с текущего узла, затем его родителя, родителя его родителя и так далее. Именно за такое восходящее поведение поиска мы называем его оператором "look-up". Очевидно, что для его работы узел, который вы ищете, должен уже существовать.
Чаще всего этот оператор используется для указания типа объекта-предка. Например, можно определить группы мобов, которые всегда приходят друг другу на помощь в бою.
mob var/species_alignment dragon species_alignment = .dragon red green black species_alignment = .black snake species_alignment = .snake cobra winged species_alignment = .dragon pit_viper species_alignment = .dragon/black
В этом примере переменная species_alignment предназначена для указания того, к какой группе существ данный тип мобов относится как к союзникам. Для этого в переменной хранится тип объекта. Два моба с одинаковыми значениями species_alignment будут друзьями.
В этом примере драконы выровнены друг с другом, кроме черного, который выровнен самостоятельно. Змеи выровнены друг с другом, кроме крылатых, которые выровнены с драконами, и ядовитых гадюк, которые выровнены с черным драконом.
Используя оператор dot, мы избежали использования абсолютных путей. Это не только более компактно, но и менее подвержено риску стать недействительным при внесении определенных изменений в код (например, при перемещении дракона и змеи в /mob/npc/dragon и /mob/npc/snake).
: оператор пути с видом вниз
Оператор : ищет дочерний узел, начиная с текущего узла, а затем, если необходимо, во всех его дочерних узлах. По этой причине его называют оператором пути с поиском вниз. В начале пути он заставляет поиск вестись от корня.
Предыдущий пример можно изменить, заменив оператор точки на : . Например, .dragon/black можно заменить на :black или :dragon:black или /mob:dragon:black, в зависимости от того, насколько неоднозначно название `black'. Если существуют и /mob/dragon/black, и /obj/potion/black, то вам нужно будет включить достаточно информации, чтобы отличить черного дракона от черного зелья. В противном случае может быть выбрано не то, что нужно.
Операторы . dot и : path схожи по смыслу при работе с путями и переменными. Оператор dot обращается либо к узлу, либо к переменной, определенной в указанной позиции или выше. Оператор : обращается к узлу или переменной, определенной в указанной позиции или ниже.
Одним из эффективных способов использования различных операторов пути является изменение определения объекта из другого места в коде. Это иногда полезно при работе с большими проектами, разделенными на несколько файлов. Подробнее об этом будет рассказано в главе 19. Пока же это шуточный пример:
obj/corpse icon = 'corpse.dmi'
mob dragon icon = 'dragon.dmi'
:corpse //add to corpse definition var/dragon_meat
В этом примере переменная добавляется в /obj/corpse из определения mob/dragon. Предполагается, что затем она будет каким-то образом использоваться кодом дракона. Например, когда дракон умирает и создает труп, переменная dragon_meat может быть установлена в 1. Другой дракон, наткнувшись на такой труп, может защитить его от падальщиков. Можно было бы найти и другие способы добиться того же самого, но суть в том, что мы смогли поместить определение переменной рядом с единственным местом в коде, где она будет использоваться - хороший способ организации вещей.
Очередность операций
В одном операторе могут использоваться булевы, побитовые, математические, условные операторы и операторы присваивания. Когда скобки не используются для явного контроля порядка операций, необходимо знать, какой порядок будет использоваться компилятором.
На рисунке 6.12 перечислены все операторы DM от самого высокого до самого низкого порядка работы. Каждая строка содержит операторы с одинаковым приоритетом. Они будут оцениваться в порядке слева направо по мере их появления в выражении.
Рисунок 6.12: Порядок выполнения всех операций
. : /(path) ( ) ! ~ ++ -- -(negation) ** * / % + - > < >= <= << >> == != <> & ^ | && || ? = += -= etc.
Операторы циклов
Существует множество способов выполнить один и тот же блок кода несколько раз в последовательности. Это называется циклом, потому что точка выполнения перемещается вниз по блоку кода, а затем прыгает обратно наверх, чтобы выполнить все сначала. Любая форма цикла включает в себя некоторый способ его завершения; в противном случае он будет продолжаться вечно. Обычно это делается в виде булева условия, которое должно выполняться при каждом повторении цикла.
Каждый вид цикла удобен в разных ситуациях. Синтаксис и применение каждого вида будут описаны в следующих разделах.
for цикл списка
Одна из очень распространенных задач в программировании DM - выполнение некоторой операции над каждым элементом списка. Для этой цели предназначен цикл for.
for(variable as input-type in list) Statement
Операторы внутри цикла for являются его телом. Синтаксис для определения тела цикла такой же, как и для оператора if. Одно утверждение может быть включено в одну строку или несколько утверждений могут быть помещены в блок с отступом под утверждением for.
Каждый элемент указанного списка, имеющий указанный тип ввода, поочередно присваивается заданной переменной. После каждого присваивания выполняется тело цикла for. Один проход по циклу for (или любому другому) называется итерацией. Весь процесс часто называют «перебором или просмотром списка».
(Математики и компьютерщики не обращают внимания на предлоги и часто просто выбирают их наугад, чтобы удовлетворить свои цели. Например, после того как я наконец-то принял утверждение «f из x - это карта из вещественной области на область f». я обнаружил, что оставшаяся часть исчисления относительно проста.)
Обратите внимание, что синтаксис цикла for очень похож на синтаксис определения аргумента verb. Вводится переменная, в которой будет храниться значение из списка, а все значения, не относящиеся к нужному типу, отфильтровываются. Единственное отличие заключается в том, что переменная цикла for не определяется в этом операторе. Она должна быть определена в предыдущем коде.
В цикле for можно использовать те же типы входных данных, что и в определении аргумента. Полный список см. в разделе 4.5.1. Несколько типов можно использовать в комбинации с помощью оператора |.
(Кстати, теперь вы должны понять, почему в этом случае используется |. Каждый тип ввода - это фактически битовый флаг, который может быть объединен побитовым ИЛИ.)
Как и в случае с определением аргументов, для типа ввода и списка задаются удобные значения по умолчанию. Если тип ввода не указан, все элементы, не соответствующие типу переменной, автоматически отсеиваются. Если список не указан, то по умолчанию используется содержимое всего мира (world.contents или просто world). Это отличается от аргументов verb, которые по умолчанию используют список view().
В теле цикла for переменная цикла, конечно же, будет использоваться каким-либо образом. Например, список инвентаря может быть выведен путем перебора каждого предмета из содержимого игрока.
mob/verb/inventory() var/obj/O usr << "You are carrying:" for(O in usr) usr << O.name
Оператор мог бы быть for(O as obj in usr), но это было бы излишним в данном случае, поскольку мы определили переменную для этого типа.
Один тонкий момент возникает, когда вы изменяете содержимое списка, по которому выполняется цикл. Например, могут возникнуть ситуации, когда вы хотите, чтобы игрок сбросил все, что есть в инвентаре.
mob/verb/strip() var/obj/O for(O in usr) O.loc = usr.loc //drop it
Это действительно будет работать, как и ожидалось. Если бы пришлось выполнять всю работу по прямому просмотру списка (что вы увидите в разделе 10.2), то в подобных случаях было бы легко ошибиться, поскольку содержимое списка меняется с каждой итерацией. DM справляется с подобными ситуациями, создавая временную копию списка в начале цикла for.
Однако есть один список, который DM считает слишком громоздким, чтобы работать с ним таким образом, - это список world.contents. Не перебирайте содержимое всего мира, если вы одновременно изменяете этот список (т. е. создаете или уничтожаете объекты). Это не всегда будет работать так, как вы ожидаете. При необходимости вы можете создать собственную временную копию списка, используя приемы, описанные в главе 10.
for условный цикл
Существует вторая форма цикла for. Вы можете считать его ручной версией; вы можете использовать его для цикла по списку, но он не будет автоматически обрабатывать этот процесс за вас, как другой синтаксис. Преимущество в том, что с его помощью вы можете делать все, что хотите, и так, как хотите.
for(initialization; condition; iteration) Statement
Цикл for состоит из трех частей: оператора инициализации, условного выражения и оператора итерации. Оператор инициализации выполняется один раз перед началом любой итерации. Затем в начале каждой итерации проверяется условие. Если оно ложно, цикл for завершается. В противном случае выполняется тело цикла for. (Это может быть блок из нескольких операторов.) Наконец, в конце тела цикла выполняется оператор итерации, проверяется условие, и процесс повторяется до тех пор, пока условие не станет ложным.
(Некоторые из вас узнают в этом цикл for в стиле C. Однако будьте осторожны, не используйте запятую, как это делается в C, чтобы упаковать несколько операторов в один управляющий оператор цикла. В DM запятая в этом контексте идентична точке с запятой. Чтобы упаковать несколько операторов вместе, их следует окружить скобками { }.)
Предположим, например, что вы хотите создать переменное количество объектов. Самым простым способом будет использование цикла for.
obj/scroll/medicine verb/cast(N as num) var/i for(i=1; i<=N; i++) new/obj/medicine(usr)
Этот пример определяет свиток медицины, который позволяет игроку создавать столько лекарств, сколько он пожелает. (Возможно, вы захотите встроить стоимость одного лекарства, вычитая из магической силы игрока или что-то в этом роде). Новая команда будет подробно описана в разделе 7.2. Она создает объект указанного типа в заданном месте. В данном случае местоположение - это инвентарь пользователя.
цикл while
Цикл while - это более простая версия цикла for. Он принимает только параметр условия и оставляет инициализацию и управление итерациями на усмотрение остальной части кода. Как и в цикле for, условие проверяется в начале каждой итерации. Если оно ложно, цикл завершается.
while(condition) Statement
Этот цикл полезен в основном в ситуациях, когда операторы инициализации и итерации не нужны или могут быть объединены с условием. Например, пример с циклом for может быть реализован с помощью простого цикла while.
obj/scroll/medicine verb/cast(N as num) while(N-- > 0) new/obj/medicine(usr)
Это дает точно такой же эффект, как и создание N объектов-лекарств, но делает это по-другому. Когда вы познакомитесь с операторами увеличения и уменьшения, компактный код, подобный этому, может показаться более привлекательным. В противном случае можно было бы, очевидно, уменьшить N в нижней части тела цикла while.
Цикл do while
Цикл do while похож на цикл while, за исключением того, что условие проверяется в конце итерации, а не в начале. Это гарантирует, что тело цикла будет выполнено хотя бы один раз. В некоторых ситуациях это как раз то, что нужно.
do Statement while(condition)
Например, можно сделать так, чтобы verb medicine работал вообще без аргументов (когда игрок спешит за лекарством).
obj/scroll/medicine verb/cast(N as num|null) do new/obj/medicine(usr) while(--N > 0)
Теперь можно просто набрать «(cast medicine)», чтобы создать один объект лекарства. То же самое можно было бы сделать и с аргументом по умолчанию, равным 1.
Перемещение по коду
Цикл и условные операторы существуют потому, что они обеспечивают структурированный синтаксис для очень распространенных операций. Однако иногда они могут не совсем соответствовать вашим требованиям. Существует несколько менее структурированных инструкций, которые помогут вам адаптировать другие операторы потока управления к любым возможным целям. Они описаны в следующих разделах.
Операторы break и continue
Операторы break и continue используются для завершения всего цикла или текущей итерации цикла, соответственно. Они полезны, когда возникает ситуация, не подходящая ни под один из простых операторов цикла. Они помещаются в тело цикла - обычно внутри оператора if, который должен быть выполнен при выполнении некоторого условия.
Можно использовать оператор continue при перечислении всех игроков в игре, чтобы не включать пользователя в список.
mob/verb/who() var/mob/M for(M in world) if(!M.key) continue //skip NPCs if(M == usr) continue //skip self
if(M.name == M.key) usr << M.name else usr << "[M.name] ([M.key])"
Это отображает всех других игроков (и их настоящее имя ключа, если они используют псевдоним). Конечно, этот пример можно переписать и без continue, изменив порядок утверждений. Однако в более сложных ситуациях использование continue и break иногда может значительно прояснить код.
оператор goto
Оператор goto заставляет выполнить переход к указанной метке в коде.
goto label . . . label
Метка - это просто узел, обозначающий точку назначения в коде, и может предшествовать или следовать за оператором goto. Аргумент оператора goto - это путь к узлу назначения. В качестве удобства перед этим путем ставится неявная точка. Это означает, что в большинстве случаев вам нужно указать только имя метки и никакой дополнительной информации о пути. См. раздел 6.15.2 об операторах пути.
Оператор goto следует использовать только тогда, когда он проясняет код. В большинстве ситуаций предпочтительнее использовать более структурированные циклы и условные операторы. Одна из ситуаций, в которой goto может оказаться полезным, - это когда перед возвратом из процедуры необходимо выполнить некоторый завершающий код, а в остальной части процедуры есть несколько точек, в которых необходимо завернуть код и вернуться. Вместо того чтобы повторять один и тот же код сворачивания везде (и, возможно, забывать сделать это в некоторых местах), вы можете поместить его в нижнюю часть процедуры с меткой перед ним.
Привести простой пример невозможно, поскольку в любой простой ситуации вы не захотите использовать goto. Однако общая структура будет выглядеть примерно так:
proc/Example() //Complex code with the occasional: goto wrapup
//Final code that goes straight down to wrapup //unless it returns.
wrapup: //Do wrapup stuff here.
Как показано в этом примере, в конце метки можно поставить необязательное двоеточие. Это помогает отличить узел от других утверждений. Кроме того, это стандартный способ объявления меток в большинстве языков программирования.
Важно пояснить, что метка в коде ничего не делает. Это просто маркер. Если выполнение достигает метки, оно сразу переходит к следующему оператору.
Метки для блоков
В предыдущем разделе вы узнали, как обозначить точку в коде процедуры и перейти к ней. Это очень гибкая техника, но ей не хватает структуры, поэтому исходный код может получиться запутанным и сложным для понимания. Иногда вы можете захотеть объединить функциональность goto с инструкциями цикла break и continue. Для этого нужно использовать метки блоков.
Метка блока - это то же самое, что и метка goto, за исключением того, что она размещается в верхней части блока кода с отступом. Как и метка goto, метка блока ничего не делает. Выполнение пропускается мимо нее и начинается с первого оператора в блоке. Метку блока можно даже использовать в качестве адресата оператора goto. Однако на самом деле она используется в операторах break и continue.
И break, и continue по умолчанию работают с самым внутренним циклом, содержащим их. Однако если указано имя блока, то они применяются к этому блоку. Оператор break приводит к переходу к концу блока; continue вызывает следующую итерацию цикла, непосредственно содержащегося в блоке.
В следующем примере используется блок с метками для кражи еды у людей.
obj/scroll/free_lunch/verb/cast() var/mob/M var/obj/food/F
victim_loop: for(M in view()) if(M == usr) continue victim_loop for(F in M) M << "Thanks for the contribution!" F.loc = usr //grab the snack continue victim_loop usr << "[M] has nothing to offer."
В этом примере есть два цикла, внешний и внутренний. Во внешнем цикле перебираются все существа, находящиеся в поле зрения пользователя. Он был обозначен как victim_loop. Первый оператор continue используется для того, чтобы не дать пользователю украсть свою еду. Он будет работать как с меткой victim_loop, так и без нее, поскольку это цикл, непосредственно содержащий оператор continue.
Внутренний цикл перебирает продукты, которые несет жертва. Обратите внимание, что это не совсем цикл, потому что в конце самой первой итерации он продолжает внешний цикл. Это обычный трюк, используемый для поиска первого элемента, полученного от заданного типа (в данном случае /obj/food). В данном случае необходимо было явно использовать метку victim_loop, поскольку в противном случае continue применялось бы к внутреннему циклу, а не к внешнему. Это привело бы к тому, что заклинание оказалось бы слишком жадным и украло бы всю еду каждой жертвы, а не только по одному предмету.
Оператор switch
Оператор switch используется для упрощения некоторых длинных цепочек операторов else if. Он принимает выражение, а затем выполняет блок кода, соответствующий этому значению. (Оператор switch в DM похож на свой аналог в C, но имеет улучшенный синтаксис, позволяющий избежать распространенных ошибок. Например, в конце одного тела if точка выполнения автоматически переходит в конец оператора switch, а не бежит в код следующего case, как это происходит в C.)
switch (expression) if(constant1) Statement if(constant2) Statement . . . else Statement
В одном из внутренних операторов if можно указать несколько констант, разделяя их запятыми. Диапазон целых чисел также может быть задан с помощью синтаксиса lower-bound до upper-bound.
Пример со звездочкой, использованный ранее в этой главе, можно эффективно переписать, используя оператор switch.
proc/Constellation(day) //day should be 1 to 365 switch(day) if(355 to 365) return "Capricorn" if(326 to 354) return "Sagittarius" if(296 to 325) return "Scorpio" if(266 to 295) return "Libra" if(235 to 265) return "Virgo" if(204 to 234) return "Leo" if(173 to 203) return "Cancer" if(142 to 172) return "Gemini" if(111 to 141) return "Taurus" if(80 to 110) return "Aries" if(51 to 79) return "Pisces" if(21 to 50) return "Aquarius" if(1 to 20) return "Capricorn" else return "Dan!"