Перейти к основному содержимому

Назад: Условия

Пользовательские функции и процедуры

Общая информация о GOSUB и FUNC

Сам принцип написания игр на QSP предполагает, что игра будет состоять из отдельных блоков кода — локаций. Эти локации могут быть связаны между собой переходами с помощью операторов GOTO и XGOTO. Таким образом в QSP реализуется перемещение по игре, например, перемещение с места на место, или по главам интерактивной истории.

Однако иногда нам необходимо использовать локации не как локации мест, не как главы или страницы, а для того, чтобы иметь возможность выполнять повторяющиеся фрагменты кода. Например, мы пишем на нескольких разных локациях действие подъёма яблок. И нам нужно, чтобы это действие увеличивало число яблок у нас в рюкзаке, уменьшало на соответствующей локации и добавляло предмет "Яблоки", если мы не поднимали ещё ни одного яблока.

В принципе мы это можем сделать, просто копируя на каждую локацию вот такое действие:

act "Взять яблоко":
if яблоки['рюкзак']=0:
addobj "Яблоки"
end
яблоки['рюкзак'] += 1
яблоки[$curloc] -= 1
end

Но посмотрите, сколько строчек кода это действие занимает. Семь. Если мы скопируем такое действие два или три раза, это ещё ничего, но что, если нам придётся копировать действие сто раз? Это семь сотен строчек кода. Это попросту увеличивает объём нашей игры в десятки раз. А ведь у нас может быть не одно такое действие.

Чтобы нам не приходилось писать одинаковый код множество и множество раз, в QSP предусмотрены специальный оператор GOSUB и специальная функция FUNC, которые позволяют нам превращать любые локации в собственные процедуры и функции. В данном случае мы можем вынести код действия в отдельную локацию, назвать эту локацию "поднять_яблоко", а затем вызывать эту локацию из действия с помощью GOSUB.

Локация "поднять_яблоко":

!# поднять_яблоко
if яблоки['рюкзак']=0:
addobj "Яблоки"
end
яблоки['рюкзак']+=1
яблоки[$curloc]-=1

Действие, которое мы пишем на любой другой локации:

act "Взять яблоко":
gosub "поднять_яблоко"
end

Как видите, всё наше действие сократилось до трёх строчек кода, а сам код подъёма яблока написан нами всего один раз!

(Подробнее о том, как работает оператор GOSUB вы можете прочитать в разделе ниже).

Ну а если, предположим, нам нужно написать сразу три действия по подъёму яблок? И все три эти действия отличаются только числом яблок, которые мы поднимаем:

act "Взять одно яблоко":
if яблоки['рюкзак']=0:
addobj "Яблоки"
end
яблоки['рюкзак']+=1
яблоки[$curloc]-=1
end
act "Взять два яблока":
if яблоки['рюкзак']=0:
addobj "Яблоки"
end
яблоки['рюкзак']+=2
яблоки[$curloc]-=2
end
act "Взять пять яблок":
if яблоки['рюкзак']=0:
addobj "Яблоки"
end
яблоки['рюкзак']+=5
яблоки[$curloc]-=5
end

Вроде бы код здесь тоже повторяется, но не совсем. Что же делать?

И для таких ситуаций в QSP уже предусмотрен специальный механизм. Дело в том, что оператор GOSUB позволяет передавать на локацию, которую мы с его помощью вызываем, различные значения. В данном случае нам нужно передавать на локацию "поднять_яблоко", которую мы писали в предыдущем примере, число яблок. В действиях это будет выглядеть вот так:

act "Взять одно яблоко":
gosub "поднять_яблоко",1
end
act "Взять два яблока":
gosub "поднять_яблоко",2
end
act "Взять пять яблок":
gosub "поднять_яблоко",5
end

Через запятую после названия локации мы можем перечислять различные значения (строковые, числовые, кортежи), и эти значения будут переданы на указанную локацию. Однако где на локации "поднять_яблоко" нам искать эти переданные значения?

Всё просто. Значения, которые мы передаём на локацию таким образом, автоматически помещаются в массив ARGS, в ячейки, начиная с нулевой. И получается, что нам надо искать наше значение в ARGS[0]:

!# поднять_яблоко
if яблоки['рюкзак'] = 0:
addobj "Яблоки"
end
яблоки['рюкзак'] += args[0]
яблоки[$curloc] -= args[0]

Можно передавать до девятнадцати любых значений на локацию и искать их на этой локации в массиве ARGS соответственно в ячейках с нулевой по восемнадцатую. Вот ещё пример передачи аргументов:

Локация "Приготовить":

!# приготовить
*pl "Я должен приготовить <<$args[0]>>, но не просто <<$args[0]>>, а <<$args[0]>> <<$args[1]>>, чтобы съесть <<$args[2]>>."

А вот пример вызова этой локации из действий:

act "Завтрак":
gosub "Приготовить","яичницу","с луком","на завтрак"
end
act "Обед":
gosub "Приготовить","гуляш","с овощами","в обед"
end
act "Ужин":
gosub "Приготовить","салат","с креветками","на ужин"
end

Иногда нам нужно не только передать данные на локацию, но и получить с неё какие-то данные назад. Для этого случая нужно воспользоваться специальной функцией FUNC.

Как и GOSUB, эта функция вызывает локацию, чтобы выполнить на ней какой-то код, однако, в отличие от GOSUB, FUNC может вернуть значение из локации.

Чтобы локация, вызванная с помощью FUNC, вернула какое-то значение, мы должны на этой локации присвоить это значение переменной RESULT. Например, мы хотим написать локацию, на которую будем передавать число, и чтобы эта локация возвращала нам квадрат переданного числа (то есть число умноженное на само себя). Назовём эту локацию "sqr":

!# sqr
result = args[0]*args[0]

Как видите, на локации "sqr" мы умножаем число из ARGS[0] само на себя, а затем результат присваиваем переменной RESULT. Именно значение из переменной RESULT локация "sqr" вернёт с помощью функции FUNC на любой другой локации:

*pl func('sqr',2) & ! на экране будет 4
*pl func('sqr',3) & ! на экране будет 9
*pl func('sqr',4) & ! на экране будет 16

Во всех приведённых примерах функцией является FUNC, а оператором — GOSUB, однако для удобства допустимо называть функциями именно локации, которые мы вызываем с помощью GOSUB или FUNC. Например: «Я написал функцию "поднять_яблоко"».

Описание GOSUB

GOSUB — выполнение кода указанной локации без непосредственного перехода на неё.

Общая запись:

  GOSUB [$локация],[аргумент 0],[аргумент 1], ... ,[аргумент 18]

, где [$локация] — это название локации, код которой мы хотим выполнить без непосредственного перехода на неё. Значения [аргумент 0], [аргумент 1] и т.д. могут использоваться на этой локации, их значения автоматически помещаются в ячейки массива ARGS[0], ARGS[1], и т.д. соответственно. После обработки локации предыдущие значения ARGS восстанавливаются. Использование аргументов не обязательно.

Оператор имеет краткую форму GS:

  GS [$локация],[аргумент 0],[аргумент 1], ... ,[аргумент 18]

Допустимо помещать название локации и аргументы в скобки:

  GOSUB([$локация],[аргумент 0],[аргумент 1], ... ,[аргумент 18])

При вызове указанной локации с помощью GOSUB происходит следующее:

  • Плеер прерывает выполнение текущего кода (например, кода текущей локации), и обращается к указанной локации
  • Базовое описание и список действий указанной локации добавляются к описанию и действиям текущей локации.
  • Выполненяется код из поля Выполнить при посещении
  • Затем плеер возвращается к выполнению кода, который прервал, к команде сразу после оператора GOSUB.

На каждой локации автоматически создаётся собственный уникальный массив ARGS, поэтому значения в этом массиве для каждой локации будут свои собственные. После выполнения кода локации, вызванной по GOSUB, массив ARGS этой локации уничтожается.

Обратите внимание! Значения из массива ARGS с локации, вызванной через GOSUB, не транслируются в блоки ACT. В блоки ACT транслируются значения ARGS из текущей локации, т.е. локации, на которую был осуществлён переход с помощью операторов GOTO/XGOTO.

#начало
$args[0] = 'локация начало'
gosub 'переход', 'локация переход'
-

#переход
*pl $args[0] &! На экран выведется 'локация переход'
act 'Перейти':
goto $args[0] &! если нажмётё действие, увидите текст 'локация начало'
end
-

Другие примеры:

! обработка локации "ход"
! На локацию не переадются аргументы
! массив ARGS пуст
GS 'ход'

!обработка локации с названием из переменной $location
!Передаётся один параметр - args[0] равен 1.
GS $location,1

!обработка локации "ход" с передачей 3-х параметров.
! $args[0] = $var (значению), args[1] = 2,
! $args[2] = "данные". Обратите внимание на символы '$'.
GS 'ход',$var,2,'данные'

Описание FUNC

FUNC — выполнение кода указанной локации без непосредственного перехода на неё с возвращением значения.

Общая запись:

  FUNC([$локация],[аргумент 0],[аргумент 1], ... ,[аргумент 18])
$FUNC([$локация],[аргумент 0],[аргумент 1], ... ,[аргумент 18])
%FUNC([$локация],[аргумент 0],[аргумент 1], ... ,[аргумент 18])

, где [$локация] — это название локации, код которой мы хотим выполнить без непосредственного перехода на неё. Аргументы [аргумент 0], [аргумент 1] и т.д. могут использоваться на этой локации, их значения автоматически помещаются в ячейки массива ARGS[0], ARGS[1], и т.д. соответственно. Название локации и аргументы должны обязательно заключаться в скобки.

Чтобы FUNC вернула строковый результат, на указанной локации нужно присвоить этот результат переменной $RESULT.

Для возвращения числового результата, он должен быть присвоен переменной RESULT.

Если нужно вернуть несколько значений, используется кортеж %RESULT.

Следует помнить, что %RESULT, $RESULT и RESULT — это одна и та же переменная, но разных типов, поэтому если вы определили на локации и %RESULT, $RESULT и RESULT, функция вернёт то значение, которое было записано в эту переменную последним.

Если переменной RESULT не было присвоено никакое значение, функция ничего не вернёт. И тут есть два варианта поведения плеера.

  • Если функция FUNC стоит в каком либо выражении, вместо функции будет подставлено значение по умолчанию (пустая строка или ноль в зависимости от типа самого выражения).
  • Если функция FUNC стоит сразу после неявного оператора, такой оператор будет проигнорирован, и на экран ничего выводиться не будет. Пример:
!# локация_Н
N = 23 * 13
! ни одно значение не присваивается переменной result
'строка 1'
func('локация_Н') &! ничего не выведется на экран
'строка 2'
56 + func('локация_Н') &! функция стоит в выражении. Её значение = 0. На экране будет 56

Не обязательно, но рекомендуется, записывать ключевое слово FUNC с символом $, если функция должна вернуть текстовое значение, и с символом %, если функция должна вернуть кортеж. Это улучшит читаемость кода:

$func('срез_строки','Мы вышли из дома, когда во всех окнах погасли огни.',3,7)
func('возвести_в_степень',3,3)
$name, age, %coords = %func('get_pers', 'mjolnir')

При вызове указанной локации с помощью FUNC происходит следующее:

  • Плеер прерывает выполнение текущего кода (например, кода текущей локации), и обращается к указанной локации
  • Базовое описание и список действий указанной локации добавляются к описанию и действиям текущей локации.
  • Выполненяется код из поля Выполнить при посещении
  • Затем плеер возвращается к выполнению кода, который прервал. То есть к вычислению выражения, в котором стоит данная функция.

На каждой локации автоматически создаются свои собственные уникальные массивы ARGS и RESULT, поэтому значения в этих массивах для каждой локации будут свои собственные. После выполнения кода локации, вызванной по FUNC, массивы ARGS и RESULT этой локации уничтожаются.

Обратите внимание! Значения из массивов ARGS и RESULT с локации, вызванной через FUNC, не транслируются в блоки ACT. В блоки ACT транслируются значения ARGS и RESULT из текущей локации, т.е. локации, на которую был осуществлён переход с помощью операторов GOTO/XGOTO.

Примеры:

!Обработка локации "поднять_яблоко" как функции. 
!Массив ARGS пуст.
!Результат передается через RESULT
!и записывается в переменную яблоки
яблоки = FUNC('поднять_яблоко')

!обработка локации с названием в $name как функции. ARGS[0] равен 1.
PL FUNC($name, 1) * 78

!обработка локации с названием в $name как функции.
!$ARGS[0] содержит строку "строка", ARGS[1] равен 2.
MSG "text" + FUNC($name, "строка", 2)

Ранний выход. Оператор EXIT

Чтобы прервать выполнение текущего кода и выйти из вызванной с помощью GOSUB или FUNC локации, используйте оператор EXIT.

EXIT - завершение выполнения текущего кода (преждевременный выход из подпрограммы/обработчика какого-либо события).

Пример:

if args[0] = 0: exit

Неявный вызов функции FUNC

Вы можете не прописывать всякий раз функцию FUNC явно. Можно использовать более простую запись. Вот как это выглядит в общем виде:

@[$локация]([аргумент 0],[аргумент 1], ... ,[аргумент 18])

Здесь [$локация] — это название локации, код которой мы хотим выполнить без непосредственного перехода на неё. Аргументы [аргумент 0], [аргумент 1] и т.д. могут использоваться на этой локации, их значения автоматически помещаются в ячейки массива ARGS[0], ARGS[1], и т.д. соответственно.

То есть: пишем символ @, затем без пробелов название локации, и сразу за названием локации в скобках через запятую перечисляем аргументы.

При такой записи необходимо соблюдать несколько правил:

  • Название локации не должно содержать пробелов, символов математических операций и специальных символов (за исключением точки)
  • Название локации не должно совпадать с ключевыми словами QSP, в том числе с названиями системных переменных.
  • Аргументы, передаваемые на локацию, всегда должны заключаться в скобки
  • Если на локацию не нужно передавать аргументы, после её названия можно поставить пустые скобки, а можно не ставить.
  • Нельзя подставить вместо названия локации значение переменной!

Примеры:

*pl @sqr(2) & ! на экране будет 4
*pl @sqr(3) & ! на экране будет 9
*pl @sqr(4) & ! на экране будет 16

! функция не принимает аргументов
*pl @PI() &! можно указать пустые скобки
*pl @PI &! можно не указывать пустые скобки

степень = @возвести_в_степень(3,3)

! пример вложения функций и участие их в выражениях
point = @queue.addPoint($next[i],new_cost+@hevr.est($next[i],$args[0]))

Если подобный неявный вызов сочетается с использованием неявного оператора, а на локации при этом НЕ присваивается значение переменной RESULT, можно использовать неявный вызов функции вместо оператора GOSUB:

Локация "поднять_яблоко":

!# поднять_яблоко
if яблоки['рюкзак']=0:
addobj "Яблоки"
end
яблоки['рюкзак']+=1
яблоки[$curloc]-=1

Действие, которое мы пишем на любой другой локации:

act "Взять яблоко":
@поднять_яблоко & ! в данном случае поведение функции будет идентично поведению GOSUB
end

Локальные переменные

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

См. раздел "Локальные переменные" в разделе Переменные.

Заключение

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

Так же см. большую статью по операторам, функциям и аргументам.

Вперёд: Переходы внутри локации