Read this manual in English

Skrypty Edytora

Możesz tworzyć niestandardowe pozycje menu oraz rozszerzać cyklu życia Edytora, używając plików Lua o specjalnym rozszerzeniu: .editor_script. Dzięki temu systemowi możesz dostosować dowolnie Edytor, aby zwiększyć swoją wydajność w procesie tworzenia gier.

Uruchamianie skryptów Edytora

Skrypty Edytora (editor scripts) działają wewnątrz Edytora, w maszynie wirtualnej Lua emulowanej przez maszynę wirtualną Java. Wszystkie skrypty współdzielą to samo środowisko, co oznacza, że mogą ze sobą współdziałać. Możesz wymagać (require) modułów Lua, tak samo jak w przypadku plików .script, ale wersja Lua uruchamiana wewnątrz Edytora jest inna, więc upewnij się, że twój współdzielony kod jest zgodny. Edytor używa wersji Lua 5.2.x, a dokładniej silnika luaj, który jest obecnie jedynym dostępnym rozwiązaniem do uruchamiania Lua w JVM. Oprócz tego istnieją pewne ograniczenia:

  • brak pakietów debug i coroutine;
  • brak funkcji os.execute — zapewniamy bardziej przyjazny i bezpieczny sposób wykonywania skryptów powłoki (shell scripts) w sekcji “akcje” - actions;
  • brak funkcji os.tmpname i io.tmpfile — obecnie skrypty Edytora mają dostęp tylko do plików wewnątrz katalogu projektu;
  • obecnie brak funkcji os.rename, choć zamierzamy ją dodać;
  • brak funkcji os.exit i os.setlocale.

Wszystkie rozszerzenia Edytora zdefiniowane w skryptach Edytora są ładowane podczas otwierania projektu. Podczas pobierania bibliotek rozszerzenia są ponownie ładowane, ponieważ w bibliotekach, od których zależysz, mogą znajdować się nowe skrypty Edytora. Podczas tego ponownego ładowania nie są wykrywane żadne zmiany w twoich własnych skryptach Edytora, ponieważ mogłeś być w trakcie ich zmian. Aby również je ponownie załadować, musisz uruchomić komendę Project → Reload Editor Scripts (Przeładuj skrypty Edytora).

Anatomia skryptu .editor_script

Każdy skrypt Edytora powinien zwracać moduł, na przykład:

local M = {}

function M.get_commands()
  -- TODO - define editor commands
end

function M.get_language_servers()
  -- TODO - define language servers
end

return M

Edytor zbiera wszystkie skrypty Edytora zdefiniowane w projekcie i bibliotekach, ładuje je do pojedynczej maszyny Lua i wywołuje je w odpowiednich momentach (więcej na ten temat w sekcjach “komendy”: commands i “haki cyklu życia”: lifecycle hooks).

Edytor API

Możesz komunikować się z Edytorem za pomocą pakietu editor, który definiuje to API:

  • editor.platform — string oznaczający platformę: "x86_64-win32" dla systemu Windows, "x86_64-macos" dla macOS lub "x86_64-linux" dla systemu Linux.
  • editor.version — string - nazwa wersji Defold, na przykład "1.4.8".
  • editor.engine_sha1 — string - SHA1 silnika Defold.
  • editor.editor_sha1 — string - SHA1 Edytora Defold.
  • editor.get(node_id, property) — pobierz wartość węzła (node) w Edytorze. Węzły w kontekście Edytora Defold to różne elementy, takie jak pliki skryptów, pliki kolekcji, obiekty gry w kolekcjach, pliki JSON wczytywane jako zasoby itp. "node_id" to userdata przekazywane do Skryptu Edytora przez sam Edytor. Możesz również podać ścieżkę zasobu zamiast identyfikatora węzła, na przykład "/main/game.script". "property" to string. Obecnie obsługiwane są tylko te właściwości:
    • "path" — ścieżka pliku od katalogu projektu dla zasobów — elementów, które istnieją jako pliki. Przykład zwracanej wartości: "/main/game.script"
    • "text" — treść tekstowa zasobu edytowalna jako tekst (na przykład pliki skryptów lub pliki JSON). Przykład zwracanej wartości: "function init(self)\nend". Należy zauważyć, że to nie jest to samo co odczytywanie pliku za pomocą io.open(), ponieważ możesz edytować plik bez zapisywania go, a te edycje są dostępne tylko podczas dostępu do właściwości "text".
    • niektóre właściwości wyświetlane w widoku Properties (Właściwości), gdy coś jest zaznaczone w panelu Outline. Obsługiwane są następujące typy właściwości:
      • string - ciągi znaków
      • boolean - zmienne logiczne
      • number - liczby
      • vec2/vec3/vec4 - wektory
      • resource - zasoby

Należy zauważyć, że niektóre z tych właściwości mogą być tylko do odczytu (read-only), a niektóre mogą być niedostępne w różnych kontekstach, więc przed ich odczytaniem powinieneś użyć editor.can_get, a przed ich zmianą - editor.can_set, które zwrócą informację, czy daną właściwość można odczytać i czy można zmienić i zapisać. Najedź wskaźnikiem myszki na właściwość w panelu Properties (właściwości), żeby zobaczyć tooltop z informacją o jej nazwie w skryptach Edytora. Możesz ustawić właściwości zasobów jako nil używając pustej wartości "".

  • editor.can_get(node_id, property) — sprawdź czy można odczytać daną właściwość w danym kontekście. Jeśli tak (true), to editor.get() nie zwróci błędu.
  • editor.can_set(node_id, property) — sprawdź czy można zmienić i zapisać daną właściwość w danym kontekście. Jeśli tak (true), to akcja "set" na tej właściwości nie zwróci błędu.

Komendy

Jeśli Skrypt Edytora definiuje funckję get_commands, to będzie one wywołana podczas przeładowania rozszerzenia i zwróci komendy możliwe do użycia w Edytorze w pasku menu lub w kontekstowym menu w panelach Assets i Outline. Przykład:

local M = {}

function M.get_commands()
  return {
    {
      label = "Remove Comments",
      locations = {"Edit", "Assets"},
      query = {
        selection = {type = "resource", cardinality = "one"}
      },
      active = function(opts)
        local path = editor.get(opts.selection, "path")
        return ends_with(path, ".lua") or ends_with(path, ".script")
      end,
      run = function(opts)
        local text = editor.get(opts.selection, "text")
        return {
          {
            action = "set",
            node_id = opts.selection,
            property = "text",
            value = strip_comments(text)
          }
        }
      end
    },
    {
      label = "Minify JSON"
      locations = {"Assets"},
      query = {
        selection = {type = "resource", cardinality = "one"}
      },
      active = function(opts)
        return ends_with(editor.get(opts.selection, "path"), ".json")
      end,
      run = function(opts)
        local path = editor.get(opts.selection, "path")
        return {
          {
            action = "shell",
            command = {"./scripts/minify-json.sh", path:sub(2)}
          }
        }
      end
    }
  }
end

return M

Edytor oczekuje, że funkcja get_commands() zwróci tablicę tablic, z których każda opisuje osobne polecenie. Opis polecenia składa się z:

  • label (wymagane) — tekst, który zostanie wyświetlony użytkownikowi jako pozycja w menu.
  • locations (wymagane) — tablica zawierająca jedno z poniższych: "Edit", "View", "Assets" lub "Outline" - określa, w jakim miejscu Edytora menu powinno być dostępne. "Edit" i "View" oznaczają pasek menu na górze, "Assets" oznacza menu kontekstowe w panelu "Assets", a "Outline" oznacza menu kontekstowe w panelu "Outline".
  • query — sposób, w jaki polecenie pyta Edytor o odpowiednie informacje i definiuje, na jakich danych operuje. Dla każdego klucza w tabeli query istnieje odpowiadający klucz w tabeli opts, który jest przekazywany jako argument do funkcji active i run. Obsługiwane klucze to:
    • selection — oznacza, że polecenie jest ważne, gdy coś w Edytorze jest zaznaczone, i działa na tym zaznaczeniu.
      • type — określa typ zaznaczonych węzłów, na które polecenie jest zainteresowane. Obecnie dozwolone są następujące rodzaje:
        • "resource" — w panelach "Assets" i "Outline" oznacza zaznaczony element, który ma odpowiadający plik. W pasku menu (Edit lub View), "resource" to aktualnie otwarty plik;
        • "outline" — coś, co może być wyświetlane w "Outline". W "Outline" to zaznaczony element, w pasku menu to aktualnie otwarty plik;
      • cardinality — określa, ile zaznaczonych elementów powinno być. Jeśli jest to "one", zaznaczenie przekazywane do funkcji obsługującej polecenie będzie zawierać tylko jeden identyfikator węzła. Jeśli jest to "many", przekazywana tablica będzie zawierać jeden lub więcej identyfikatorów węzła.
  • active - funkcja wywoływana w celu sprawdzenia, czy polecenie jest aktywne, powinna zwracać wartość logiczną. Jeśli w locations zawarte są "Assets" lub "Outline", funkcja active zostanie wywołana podczas wyświetlania menu kontekstowego. Jeśli w locations zawarte są "Edit" lub "View", funkcja active zostanie wywołana przy każdej interakcji użytkownika, takiej jak pisanie na klawiaturze lub klikanie myszą, dlatego upewnij się, że funkcja active działa stosunkowo szybko.
  • run - funkcja wywoływana, gdy użytkownik wybierze pozycję z menu, i powinna zwrócić tablicę akcji - actions.

Actions

Action (akcja) to tabela opisująca, co Edytor powinien zrobić. Każda akcja zawiera klucz action. Akcje dzielą się na dwa rodzaje: możliwe do cofnięcia (undoable) i niemożliwe do cofnięcia (non-undoable).

Akcje możliwe do cofnięcia

Undoable action - możliwa do cofnięcia akcja może zostać cofnięta po jej wykonaniu (Undo or Ctrl + Z). Jeśli polecenie zwraca wiele akcji możliwych do cofnięcia, są one wykonywane razem i cofane razem. Należy używać akcji możliwych do cofnięcia, jeśli to możliwe. Ich wadą są większe ograniczenia.

Istniejące działania możliwe do cofnięcia to:

  • "set" — ustawienie właściwości węzła w Edytorze na określoną wartość. Przykład:
    {
      action = "set",
      node_id = opts.selection,
      property = "text",
      value = "current time is " .. os.date()
    }
    

    Akcja "set" wymaga podania tych parametrów:

    • node_id — identyfikator węzła jako userdata. Alternatywnie, można użyć ścieżki zasobu zamiast identyfikatora węzła otrzymanego od Edytora, na przykład "/main/game.script";
    • property — właściwość węzła do ustawienia, obecnie obsługiwane jes tylko "text";
    • value — nowa wartość właściwości. Dla właściwości "text" powinno to być łańcuchem znaków (string).

Akcje niemożliwe do cofnięcia

Akcje możliwe do cofnięcia czyszczą historię cofnięć (undo), więc z poziomu Edytora nie można ich cofnąć i jeśli użytkownik chce to zrobić, musi użyć innych środków, np. systemów kontroli wersji.

Istniejące działania niemożliwe do cofnięcia to:

  • "shell" — wykonanie skryptu powłoki. Przykład:
    {
      action = "shell",
      command = {
        "./scripts/minify-json.sh",
        editor.get(opts.selection, "path"):sub(2) -- trim leading "/"
      }
    }
    

    Działanie "shell" wymaga parametru command, który jest tablicą polecenia, oraz jego argumentów. Główna różnica w porównaniu do os.execute polega na tym, że jest to potencjalnie niebezpieczna operacja, dlatego Edytor wyświetli okno dialogowe z pytaniem do użytkownika czy na pewno chce wywołać daną komendę. Edytor zapamięta, jeśli użytkownik już wyraził zgodę na wykonanie takiej komendy.

Łączenie akcji i efekty uboczne

Możesz łączyć akcje możliwe do cofnięcia (undoable) i akcje niemożliwe do cofnięcia (non-undoable). Akcje są wykonywane sekwencyjnie, dlatego w zależności od kolejności działań możesz stracić możliwość cofania części tego polecenia.

Zamiast zwracać akcje z funkcji, które ich oczekują, możesz po prostu czytać i zapisywać dane bezpośrednio do plików, korzystając z funkcji io.open(). Spowoduje to ponowne załadowanie zasobów, co wyczyści historię cofania (undo history).

Haki cyklu życia (Lifecycle Hooks)

Istnieje jeden, specjalnie traktowany plik Skryptu Edytora: hooks.editor_script, znajdujący się w głównym katalogu twojego projektu, w tym samym katalogu co game.project. Tylko ten plik Skryptu Edytora otrzyma zdarzenia cyklu życia od Edytora. Oto przykład takiego pliku:

local M = {}

function M.on_build_started(opts)
  local file = io.open("assets/build.json", "w")
  file:write("{\"build_time\": \"".. os.date() .."\"}")
  file:close()
end

return M

Zdecydowaliśmy się ograniczyć haki cyklu życia do jednego pliku Skryptu Edytora, ponieważ kolejność wykonywania haków budowania (build hooks) jest ważniejsza niż łatwość dodawania kolejnego kroku buildu. Polecenia są niezależne od siebie, więc nie ma znaczenia, w jakiej kolejności są wyświetlane w menu. W końcu to użytkownik wykonuje konkretne polecenie, które wybrał. Gdyby można było określać haki cyklu życia w różnych plikach Skryptu Edytora, stworzyłoby to problem: w jakiej kolejności mają się wykonywać haki? Chcesz prawdopodobnie utworzyć sumy kontrolne zawartości po jej skompresowaniu… Dlatego posiadanie jednego pliku, który ustala kolejność kroków buildu, wywołując każdą funkcję kroku, jest sposobem na rozwiązanie tego problemu.

Każdy hak cyklu życia może zwracać akcje lub zapisywać pliki w katalogu projektu.

Istniejące haki cyklu życia, które plik hooks.editor_script może określić:

  • on_build_started(opts) — wykonywane, gdy gra jest budowana w celu uruchomienia jej lokalnie lub na zdalnym, docelowym urządzeniu, używając opcji "Project Build" lub "Debug Start". Twoje zmiany, czy to zwracane akcje czy zaktualizowane zawartości pliku, pojawią się w zbudowanej grze. Wyrzucenie błędu z tego haka spowoduje przerwanie budowy. opts to tabela zawierająca obecnie następujący klucz:
    • platform — łańcuch w formacie %arch%-%os%, opisujący platformę, dla której budowana jest gra, zawsze taki sam jak editor.platform.
  • on_build_finished(opts) — wykonywane, gdy budowa zostanie zakończona, niezależnie od tego, czy zakończyła się sukcesem czy nie. opts w tym przypadku to tabela zawierająca następujące klucze:
    • platform — to samo, co w on_build_started.
    • success — czy budowa zakończyła się sukcesem, true lub false.
  • on_bundle_started(opts) — wykonywane, gdy tworzysz paczkę z grą lub budujesz wersję HTML5 gry. Podobnie jak on_build_started, zmiany wywołane przez ten hak pojawią się w paczce, a błędy spowodują przerwanie procesu pakowania (bundle). opts zawiera tutaj następujące klucze:
    • output_directory — ścieżka do katalogu wyjściowego paczki, na przykład “/path/to/project/build/default/__htmlLaunchDir”`
    • platform — platforma, dla której paczka jest tworzona. Zobacz listę możliwych wartości platform w podręczniku Boba (narzędzia do budowania i pakowania).
    • variant — wariant paczki, "debug", "release" lub "headless".
  • on_bundle_finished(opts) — wykonywane, gdy budowanie paczki (bundle) zostanie ukończone, niezależnie od tego, czy zakończyło się sukcesem. opts w tym przypadku to tabela zawierająca te same dane co opts w on_bundle_started, oraz dodatkowo klucz success, który wskazuje, czy budowa zakończyła się sukcesem.
    • on_target_launched(opts) — wykonywane, gdy użytkownik uruchomił grę i uruchomienie zakończyło się sukcesem. opts zawiera klucz url wskazujący na uruchomioną usługę silnika, na przykład "http://127.0.0.1:35405".
    • on_target_terminated(opts) — wykonywane, gdy uruchomiona gra zostaje zamknięta. opts ma te same klucze co on_target_launched.

Należy zauważyć, że haki cyklu życia są obecnie funkcją dostępną tylko w Edytorze i nie są wykonywane przez Boba podczas pakowania z wiersza poleceń.

Skrypty Edytora w bibliotekach

Możesz publikować biblioteki dla użytku przez inne osoby, które zawierają polecenia, i zostaną one automatycznie wykryte przez Edytor. Haki cyklu życia nie mogą być jednak automatycznie wykrywane, ponieważ muszą być zdefiniowane w pliku znajdującym się w głównym katalogu projektu, a biblioteki wystawiają tylko podkatalogi. Ma to na celu umożliwienie większej kontroli nad procesem budowy: nadal możesz tworzyć haki cyklu życia jako proste funkcje w plikach .lua, więc użytkownicy twojej biblioteki mogą je zaimportować i używać w swoim pliku hooks.editor_script.

Należy również zauważyć, że chociaż zależności są wyświetlane w widoku "Assets", to nie istnieją one jako pliki (są wpisami w archiwum zip), więc obecnie nie ma łatwego sposobu na wykonanie skryptu powłoki dostarczonego jako zależności (biblioteki). Jeśli jest to absolutnie konieczne, będziesz musiał wydobyć dostarczone skrypty, pobierając ich tekst za pomocą editor.get() i zapisując go gdzieś za pomocą file:write(), na przykład w katalogu build/editor-scripts/your-extension-name.

Prostszym sposobem na wydobycie niezbędnych plików jest wykorzystanie systemu wtyczek rozszerzeń natywnych (native extensions). Aby to zrobić, musisz utworzyć plik ext.manifest w katalogu twojej biblioteki, a następnie utworzyć katalog plugins/bin/${platform} w tym samym katalogu, w którym znajduje się plik ext.manifest. Pliki w tym katalogu zostaną automatycznie wydobyte do katalogu /build/plugins/${extension-path}/plugins/bin/${platform}, dzięki czemu twoje Skrypty Edytora mogą się do nich odnosić.

Serwery językowy (language servers)

Edytor obsługuje niewielki podzbiór protokołu Language Server Protocol. Chociaż zamierzamy rozwijać obsługę Edytora dla funkcji LSP w przyszłości, obecnie obsługuje on tylko wykrywanie diagnoz (lints) w edytowanych plikach.

Aby zdefiniować serwer językowy, musisz edytować funkcję get_language_servers w swoim Skrypcie Edytora, jak w poniższym przykładzie:

function M.get_language_servers()
  local command = 'build/plugins/my-ext/plugins/bin/' .. editor.platform .. '/lua-lsp'
  if editor.platform == 'x86_64-win32' then
    command = command .. '.exe'
  end
  return {
    {
      languages = {'lua'},
      watched_files = {
        { pattern = '**/.luacheckrc' }
      },
      command = {command, '--stdio'}
    }
  }
end

Edytor uruchomi serwer językowy, korzystając z określonej komendy, używając standardowego wejścia i wyjścia procesu serwera do komunikacji.

Tabela definicji serwera językowego może określać:

  • languages (wymagane) — listę języków, których serwer dotyczy, zdefiniowanych tutaj (rozszerzenia plików także działają);
  • command (wymagane) - tablicę komendy i jej argumentów
  • watched_files - tablicę tablic z kluczami pattern (glob), które będą powiadomiać serwer o zmianie plików, zgodnie z powiadomieniami o zmianie plików śledzonych.