script.md 14 KB


title: Компонент Script

brief: В этом руководстве описано, как добавить игровую логику с помощью компонента Script.

Компонент Script

Компонент Script позволяет создавать игровую логику, используя язык программирования Lua.

Типы скриптов

В Defold существует три типа Lua-скриптов, и для каждого из них доступны разные библиотеки Defold.

Скрипты игровых объектов : Расширение .script. Эти скрипты добавляются к игровым объектам точно так же, как и любой другой компонент, и Defold будет выполнять Lua-код как часть функций жизненного цикла движка. Скрипты игровых объектов обычно используются для управления игровыми объектами и логикой, объединяющей игру воедино: загрузкой уровней, игровыми правилами и так далее. Скрипты имеют доступ к функциям GO и ко всем библиотечным функциям Defold, за исключением GUI и Render.

Скрипты GUI : Расширение _.guiscript. Выполняются компонентами GUI и обычно содержат логику, необходимую для отображения элементов GUI, таких как интерфейс, меню и т.п. Defold выполняет Lua-код как часть функций жизненного цикла движка. Скрипты GUI имеют доступ к функциям GUI и ко всем библиотечным функциям Defold, за исключением GO и Render.

Скрипты рендера : Расширение _.renderscript. Выполняются в рамках пайплайна рендеринга и содержат логику, необходимую для отрисовки всей графики приложения/игры в каждом кадре. Скрипт рендера занимает особое место в жизненном цикле вашей игры. Подробнее см. в документации по жизненному циклу приложения. Скрипты рендера имеют доступ к функциям Render и ко всем библиотечным функциям Defold, за исключением GO и GUI.

Выполнени Script, колбэки и self

Defold выполняет Lua-скрипты как часть жизненного цикла движка и раскрывает этот жизненный цикл через набор предопределенных функций обратного вызова. При добавлении компонента скрипта к игровому объекту скрипт становится частью жизненного цикла игрового объекта и его компонента(ов). Скрипт оценивается в Lua-контексте при его загрузке, затем движок выполняет следующие функции и передает ссылку на текущий экземпляр компонента скрипта в качестве параметра. Эту ссылку self можно использовать для хранения состояния в экземпляре компонента.

::: important self --- это объект типа userdata, который действует как Lua-таблица, однако его нельзя итерировать с помощью pairs() или ipairs() и нельзя распечатить с помощью pprint(). :::

init(self)

: Вызывается при инициализации компонента.

  function init(self)
      -- Эти переменные доступны в течение всего времени существования экземпляра компонента
      self.my_var = "something"
      self.age = 0
  end

final(self)

: Вызывается при удалении компонента. Это полезно для очистки, например, если были порождены игровые объекты, которые должны быть удалены при удалении компонента.

  function final(self)
      if self.my_var == "something" then
          -- провести чистку
      end
  end

update(self, dt)

: Вызывается один раз в каждом кадре. dt содержит дельту времени с момента последнего кадра.

  function update(self, dt)
      self.age = self.age + dt -- increase age with the timestep
  end

fixed_update(self, dt)

: Обновление, не зависящее от частоты кадров. dt содержит дельту времени с момента последнего обновления. Эта функция вызывается, когда включен параметр engine.fixed_update_frequency (!= 0). Полезна, когда вы хотите управлять физическими объектами с регулярными интервалами для достижения стабильной симуляции физики при включенном параметре physics.use_fixed_timestep в game.project.

  function fixed_update(self, dt)
      msg.post("#co", "apply_force", {force = vmath.vector3(1, 0, 0), position = go.get_world_position()})
  end

on_message(self, message_id, message, sender)

: При отправке сообщений компоненту Script через msg.post() движок вызывает эту функцию компонента-приемника. За подробностями обращайтесь к руководству по передаче сообщений.

```lua
function on_message(self, message_id, message, sender)
    if message_id == hash("increase_score") then
        self.total_score = self.total_score + message.score
    end
end
```

on_input(self, action_id, action)

: Если этот компонент получил фокус ввода (см. acquire_input_focus), движок вызывает эту функцию когда ввод зарегистрирован. За подробностями обращайтесь к руководству по вводу.

```lua
function on_input(self, action_id, action)
    if action_id == hash("touch") and action.pressed then
        print("Touch", action.x, action.y)
    end
end
```

on_reload(self)

: Эта функция вызывается при перезагрузке скрипта с помощью горячей перезагрузки в редакторе (Edit ▸ Reload Resource). Очень удобно для отладки, тестирования и оптимизации. За подробностями обращайтесь к руководству по горячей перезагрузке.

  function on_reload(self)
      print(self.age) -- вывести возраст данного игрового объекта
  end

Реактивная логика

Игровой объект с компонентом Script реализует некоторую логику. Часто эта логика зависит от какого-либо внешнего фактора. Вражеский AI может реагировать на то, что игрок находится в определенном радиусе от него; дверь может отомкнуться и открыться в результате взаимодействия с игроком и т.д. и т.п.

Функция update() позволяет реализовать комплексное поведение, определяемое как механизм состояний, запускаемый каждый кадр --- иногда это вполне адекватный подход. Однако каждый вызов update() сопряжен с определенными затратами. Если функция в действительности не нужна, ее следует удалить и вместо этого попытаться построить логику реактивно. Выгоднее пассивно ждать какого-либо сообщения с последующей реакцией, чем активно исследовать игровой мир в поисках данных для ответа. Более того, проектирование реактивным способом также часто приводит к более чистому и надежному дизайну и реализации.

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

function init(self)
    -- Счетчик для учета времени.
    self.counter = 0
    -- Это необходимо, чтобы следить за состоянием.
    self.state = "first"
end

function update(self, dt)
    self.counter = self.counter + dt
    if self.counter >= 2.0 and self.state == "first" then
        -- отправить сообщение через 2 секунды
        msg.post("some_object", "some_message")
        self.state = "waiting"
    end
    if self.counter >= 5.0 and self.state == "second" then
        -- отправить сообщение через 5 секунд после получения "ответа"
        msg.post("another_object", "another_message")
        -- Nil the state so we don’t reach this state block again.
        self.state = nil
    end
end

function on_message(self, message_id, message, sender)
    if message_id == hash("response") then
        -- “first” state done. enter next
        self.state = "second"
        -- zero the counter
        self.counter = 0
    end
end

Даже в этом довольно простом случае мы получаем довольно запутанную логику. Это можно улучшить с помощью горутин в модуле (см. ниже), но давайте вместо этого попробуем сделать это реактивным и использовать встроенный механизм синхронизации.

local function send_first()
	msg.post("some_object", "some_message")
end

function init(self)
	-- Подождать 2 секунды, затем вызвать send_first()
	timer.delay(2, false, send_first)
end

local function send_second()
	msg.post("another_object", "another_message")
end

function on_message(self, message_id, message, sender)
	if message_id == hash("response") then
		-- Подождать 5 секунды, затем вызвать send_second()
		timer.delay(5, false, send_second)
	end
end

Так чище и проще для понимания. Мы избавляемся от внутренних переменных состояния, которые часто трудно проследить через логику, и которые могут привести к трудноуловимым ошибкам. Мы также полностью избавляемся от функции update(). Это освобождает движок от необходимости вызывать скрипт 60 раз в секунду, даже если он простаивает.

Препроцессинг

Можно использовать Lua-препроцессор и специальную разметку для условного включения кода в зависимости от варианта сборки. Пример:

-- Используйте одно из следующих ключевых слов: RELEASE, DEBUG или HEADLESS
--#IF DEBUG
local lives_num = 999
--#ELSE 
local lives_num = 3
--#ENDIF

Препроцессор доступен как расширение сборки. Подробнее о его установке и использовании см. на странице расширения на GitHub.

Поддержка со стороны редактора

Редактор Defold поддерживает редактирование Lua-скриптов с подсветкой синтаксиса и автодополнением. Чтобы автоматически подставить имена функций Defold, нажмите Ctrl+Space — появится список функций, соответствующих набираемому тексту.

Auto completion