Defold posiada wbudowane wsparcie dla różnych typów animacji, których możesz używać jako źródło dla komponentów graficznych:
Animacja poklatkowa składa się z serii obrazów, które są pokazywane jeden za drugim, więc patrząc na nie sprawiają wrażenie ruchu. Technika ta jest bardzo podobna do tradycyjnej animacji używanej w kinematografii https://pl.wikipedia.org/wiki/Animacja i oferuje nieograniczoną kontrolę, ponieważ każda klatka może być modyfikowana indywidualnie. Jednakże pamięć zajmowana przez wszystkie obrazy składające się na taką animację może być duża w zależności od ilości klatek i ich wielkości. Płynność animacji zależy od liczby klatek pokazywanych w każdej sekundzie (FPS z ang. frames per second), co wymaga oczywiście większej ilości pracy. Animacje typu flip-book w Defoldzie są przechowywane albo jako indywidualne obrazy umieszczone w galerii zwanej Atlas, albo jako obrazy umieszczone w bezpośrednim sąsiedztwie, w poziomej sekwencji jak kafelki w tzw. Tile Source.
Wsparcie dla animacji typu Spine w Defoldzie umożliwia tworzenie animacji szkieletowych z obrazów 2D (https://pl.wikipedia.org/wiki/Rigging). Jest to zupełnie inna technika niż animacja flip-book i bliżej jej do płynnych ruchów, bez podzielenia obrazu na poszatkowane klatki. Przykładowo w animacjach wykorzystujących wycinanki https://en.wikipedia.org/wiki/Cutout_animation osobne części (np. korpus, ramiona, nogi, oczy itp.) są poruszane indywidualnie w każdej klatce z zachowaniem stałego szkieletu i określonych połączeń między częściami, np. przegubów. Animacja typu Spine umożliwia stworzenie właśnie takiego wirtualnego, niewidzialnego szkieletu składającaego się z hierarchii konkretnie połączonych części - kości (ang. bones) połączonych w łańcuchy kinematyczne. Taki szkielet, czy z ang. rig, jest właśnie animowany i przyczepione są do każdej z kości odpowiednie części - obrazy. Defold wspiera animacje stworzone w lub wyeksportowane do formatu Spine JSON. Animacja szkieletowa jest zwykle bardzo płynna, ponieważ silnik interpoluje położenie i orientację każdej z “kości” dla każdej klatki.
Szczegóły dotyczące importowania danych z programu Spine do komponentu “Spine model” w Defoldzie znajdziesz tutaj: Instrukcja Spine.
Animacja szkieletowa trójwymiarowych modeli jest podobna do animacji typu Spine, z tym, że działa również w trójwymiarze. Model 3D nie jest jednak podzielony na osobne części związane ze sobą w łańcuch kinematyczny, a w zamian tego, ruch “kości” deformuję siatkę wierzchołków (ang. vertices) reprezentującą obiekt 3D, a Ty masz kontrolę nad tym w jaki sposób kości wpływają na taką deformację.
Szczegóły dotyczące importowania danych 3D do komponentu “Model” w Defoldzie znajdziesz tutaj: Instrukcja Modeli 3D.
{.inline srcset=”images/animation/blender_animation@2x.png 2x”}
Wszystkie numeryczne właściwości (liczby, wektory (vector3, vector4) i kwaterniony) i stałe shader’ów mogą być animowane przez wbudowany system animacji, używając funkcji go.animate()
. Defold automatycznie “wyśrodkuje” właściwości dla Ciebie w zależności od podanego sposobu odtwarzania (ang. playback) czy funkcji “wygładzania” (ang. easing - więcej: Easing functions) predefiniowanych lub Twoich własnych.
{.inline srcset=”images/animation/property_animation@2x.png 2x”}
Sprite’y i węzły interfejsu GUI mogą odtwarzać animacje poklatkowe i masz nad tym całkowitą kontrolę w trakcie działania programu.
sprite.play_flipbook()
. Poniżej przykład.gui.play_flipbook()
. Poniżej przykład.Tryb odtwarzania (playback mode) “once ping-pong” odtworzy animację klatka po klatce do samego końca, a następnie odtworzy ją jeszcze raz w odwrotnej kolejności, od tyłu, ale do drugiej klatki animacji, a nie do pierwszej. Jest to zabieg służący łatwemu wiązaniu animacji w łańcuchy.
Załóżmy, że Twoja gra posiada możliwość uniku, która pozwala graczom nacisnąć klawisz, aby wykonać unik. Możesz wykonać 4 animacje, żeby w pełni pokazać unik:
Skrypt poniżej przedstawia potrzebną logikę:
local function play_idle_animation(self)
if self.dodge then
sprite.play_flipbook("#sprite", hash("dodge_idle"))
else
sprite.play_flipbook("#sprite", hash("idle"))
end
end
function on_input(self, action_id, action)
-- "dodge" is our input action
if action_id == hash("dodge") then
if action.pressed then
sprite.play_flipbook("#sprite", hash("start_dodge"), play_idle_animation)
-- remember that we are dodging
self.dodge = true
elseif action.released then
sprite.play_flipbook("#sprite", hash("stop_dodge"), play_idle_animation)
-- we are not dodging anymore
self.dodge = false
end
end
end
Przy wyborze obrazu i animacji dla węzła interfejsu graficznego (box, pie) tak naprawdę właśnie przypisujesz źródło obrazów (atlas lub tile source) i domyślną animację, tak samo jak w przypadku sprite’ów. Galeria taka jest statycznie przypisana do węzła, ale animacja może być zmieniona w trakcie trwania programu. Nieruchome obrazki są traktowane jako jednoklatkowa animacja, więc zamiana obrazu jest jednoznaczna z odtworzeniem innej animacji poklatkowej (flip-book) na węźle:
local function flipbook_done(self)
msg.post("#", "jump_completed")
end
function init(self)
local character_node = gui.get_node("character")
-- This requires that the node has a default animation in the same atlas or tile source as
-- the new animation/image we're playing.
gui.play_flipbook(character_node, "jump_left", flipbook_done)
end
Jako trzeci argument można podać opcjonalną funkcję, która jest wywoływana w momencie zakończenia animacji (flipbook_done) - to tzw. callback. Będzie ona wywołana po skończeniu animacji, więc tylko takich, które nie są zapętlone, czyli tryby playback: gui.PLAYBACK_ONCE_*
.
Aby odtworzyć animację na Twoim modelu, po prostu użyj funkcji spine.play_anim()
:
local function anim_done(self)
-- the animation is done, do something useful...
end
function init(self)
-- Play the "walk" animation on component "spinemodel" and blend against previous
-- animation for the first 0.1 seconds, then call callback.
local anim_props = { blend_duration = 0.1 }
spine.play_anim("#spinemodel", "run", go.PLAYBACK_LOOP_FORWARD, anim_props, anim_done)
end
Jeśli animacja jest odtwarzana w trybach go.PLAYBACK_ONCE_*
i podałeś funkcję callback w spine.play_anim()
, funkcja ta zostanie wywołana na końcu animacji. See below for information on callbacks.
Jako dodatek do spine.play_anim()
komponenty zwane Modele Spine posiadają właściwość zwaną “kursor” (z ang. cursor), którą można animować przy użyciu funkcji go.animate()
. Jest to wartość numeryczna między 0 i 1, gdzie 0 oznacza początek animacji, a 1 oznacza koniec animacji:
-- Set the animation on the spine model but don't run it.
spine.play_anim("#spinemodel", "run_right", go.PLAYBACK_NONE)
-- Set the cursor to position 0
go.set("#spinemodel", "cursor", 0)
-- Tween the cursor slowly between 0 and 1 pingpong with in-out quad easing.
go.animate("#spinemodel", "cursor", go.PLAYBACK_LOOP_PINGPONG, 1, go.EASING_INOUTQUAD, 6)
Przy uśrednianiu (tweening) lub ustawianiu kursora, wydarzenia na osi czasu mogą nie być startowane w spodziewanym czasie.
Indywidualne kości w szkielecie Spine są reprezentowane wewnętrznie przez podstawowe obiekty (game objects). W panelu Outline komponentu typu model Spine ukazana jest cała hierarchia. Możesz tu zobaczyć nazwę każdej z kości (bone) i ich miejsce w hierarchii.
Znając nazwy poszczególnych kości możesz uzyskać id danej instancji kości w czasie działania programu i dowolnie nią manipulować. Funkcja spine.get_go()
zwraca id danej kości a Ty możesz, przykładowo, przypisać inne obiekty jako dzieci danej kości - czyli np. wręczyć postaci pistolet w miejscu ręki, który będzie od tej pory za nią naturalnie “podążał”:
-- Attach pistol game object to the hand of the heroine
local hand = spine.get_go("heroine#spinemodel", "front_hand")
msg.post("pistol", "set_parent", { parent_id = hand })
Animacje szkieletowe mogą wywoływać wydarzenia w określonym czasie poprzez wysłanie wiadomości w konkretnym momencie. Jest to bardzo przydatne dla wydarzeń, które powinny być zsynchronizowane z animacją, jak na przykład odtworzenie odgłosu kroku w momencie dotknięcia stopą podłogi (np. gdy stopa jest w najniższym położeniu) czy odtworzenie w tym czasie efektu cząsteczkowego reprezentującego unoszącą się chmurę kurzu. Można w takim wydarzeniu odrzucić lub dołączyć dowolny obiekt do kości czy wywołać jakąkolwiek inną funckję.
Wydarzenia są dodawane w programie Spine i pokazane na osi czasu:
Każde wydarzenie jest określone przez identyfikator (“bump” w przykładzie powyżej) i może ponadto posiadać dodatkową informację:
Kiedy animacja jest odtwarzana i następuje dane wydarzenie to wiadomość o treści spine_event
jest wysyłana do skryptu, który tę animację uruchomił wywołaniem spine.play()
. Wiadomość ta posiada tablicę z powyższymi informacjami oraz innymi przydatnymi niejednokrotnie elementami:
t
animation_id
string
float
integer
event_id
blend_weight
-- Spine animation contains events that are used to play sounds in sync with the animation.
-- These arrive here as messages.
function on_message(self, message_id, message, sender)
if message_id == hash("spine_event") and message.event_id == hash("play_sound") then
-- Play animation sound. The custom event data contains the sound component and the gain.
local url = msg.url("sounds")
url.fragment = message.string
sound.play(url, { gain = message.float })
end
end
Models are animated with the model.play_anim()
function:
function init(self)
-- Start the "wiggle" animation back and forth on #model
model.play_anim("#model", "wiggle", go.PLAYBACK_LOOP_PINGPONG)
end
Defold wspiera obecnie tylko gotowe animacje (baked). Animacje muszą mieć macierze dla każdej z animowanych kości na każdą z klatek animacji, a nie osobno pozycję, rotację i skalę jako osobne pola.
Animacje są interpolowane liniowo. Jeśli stworzysz bardziej zaawansowaną krzywą animacji, animacje muszę być przygotowane wcześniej w programie przed eksportem (prebaked).
Klipy animacji (animation clips) w formacie Collada nie są obecnie wspierane. Aby używać wielu animacji na model, wyeksportuj je do osobnego pliku .dae i zbierz wszystkie pliki do osobnego pliku .animationset w edytorze Defold.
Kości szkieletu modelu 3D są reprezentowane wewnętrznie również jako obiekty (game objects).
Możesz uzyskać id danej instancji kości w czasie działania programu i dowolnie nią manipulować. Funkcja model.get_go()
zwraca id obiektu danej kości.
-- Get the middle bone go of our wiggler model
local bone_go = model.get_go("#wiggler", "Bone_002")
-- Now do something useful with the game object...
Tak jak powyżej dla komponentów typu Spine, modele 3D mogą być animowane przy użyciu tzw. kursora (ang. cursor
):
-- Set the animation on #model but don't start it
model.play_anim("#model", "wiggle", go.PLAYBACK_NONE)
-- Set the cursor to the beginning of the animation
go.set("#model", "cursor", 0)
-- Tween the cursor between 0 and 1 pingpong with in-out quad easing.
go.animate("#model", "cursor", go.PLAYBACK_LOOP_PINGPONG, 1, go.EASING_INOUTQUAD, 3)
Aby animować właściwości (ang. properties) obiektu lub komponentu użyj funkcji go.animate()
. Dla właściwości węzłów GUI, analogiczną funkcją jest gui.animate()
.
-- Set the position property y component to 200
go.set(".", "position.y", 200)
-- Then animate it
go.animate(".", "position.y", go.PLAYBACK_LOOP_PINGPONG, 100, go.EASING_OUTBOUNCE, 2)
Aby zatrzymać wszystkie animacje danej właściwości, wywołaj go.cancel_animations()
, a dla węzłów GUI, analogicznie: gui.cancel_animation()
lub dookreśl, które właściwości chcesz zatrzymać:
-- Stop euler z rotation animation on the current game object
go.cancel_animations(".", "euler.z")
Jeśli zatrzymasz animacje właściwości, która jest właściwością “kompozytową” (składającą się z kilku osobnych wartości, jak np. vector3 position
), osobne animacje każdego z elementów składowych danej właściwości (position.x
, position.y
i position.z
) zostaną zatrzymane.
Instrukcja do właściwości zawiera wszystkie informacje na temat dostępnych właściwości obiektów, komponentów i węzłów GUI.
Prawie każdą właściwość (ang. property) węzła GUI można animować. Możesz przykładowo ukryć węzeł poprzez ustawienie jego koloru na całkowicie przezroczysty, a następnie pokazać przez płynne pojawianie się animując kolor do wartości koloru białego (nieprzezroczystego):
local node = gui.get_node("button")
local color = gui.get_color(node)
-- Animate the color to white
gui.animate(node, gui.PROP_COLOR, vmath.vector4(1, 1, 1, 1), gui.EASING_INOUTQUAD, 0.5)
-- Animate the outline red color component
gui.animate(node, "outline.x", 1, gui.EASING_INOUTQUAD, 0.5)
-- And move to x position 100
gui.animate(node, hash("position.x"), 100, gui.EASING_INOUTQUAD, 0.5)
Animacje w Defoldzie mogą być odtwarzane w różnych trybach - np. jednokrotnie lub w pętli. Jest to określone przez tryby odtwarzania (ang. playback modes):
Tryb pingpong odtwarza animację raz w poprawnej kolejności, do przodu, a następnie od tyłu. Analogiczne tryby służą do kontroli elementów (węzłów) GUI:
Funkcje wygładzania (ang. easing) opisują w jaki sposób animowane wartości zmieniają się w czasie. Obrazy poniżej przedstawiają wykresy funkcji tych wartości w czasie:
Dla animowania wartości obiektów i komponentów przy użyciu go.animate()
:
go.EASING_LINEAR | |
go.EASING_INBACK | go.EASING_OUTBACK |
go.EASING_INOUTBACK | go.EASING_OUTINBACK |
go.EASING_INBOUNCE | go.EASING_OUTBOUNCE |
go.EASING_INOUTBOUNCE | go.EASING_OUTINBOUNCE |
go.EASING_INELASTIC | go.EASING_OUTELASTIC |
go.EASING_INOUTELASTIC | go.EASING_OUTINELASTIC |
go.EASING_INSINE | go.EASING_OUTSINE |
go.EASING_INOUTSINE | go.EASING_OUTINSINE |
go.EASING_INEXPO | go.EASING_OUTEXPO |
go.EASING_INOUTEXPO | go.EASING_OUTINEXPO |
go.EASING_INCIRC | go.EASING_OUTCIRC |
go.EASING_INOUTCIRC | go.EASING_OUTINCIRC |
go.EASING_INQUAD | go.EASING_OUTQUAD |
go.EASING_INOUTQUAD | go.EASING_OUTINQUAD |
go.EASING_INCUBIC | go.EASING_OUTCUBIC |
go.EASING_INOUTCUBIC | go.EASING_OUTINCUBIC |
go.EASING_INQUART | go.EASING_OUTQUART |
go.EASING_INOUTQUART | go.EASING_OUTINQUART |
go.EASING_INQUINT | go.EASING_OUTQUINT |
go.EASING_INOUTQUINT | go.EASING_OUTINQUINT |
Dla animowania wartości węzłów GUI przy użyciu gui.animate()
:
gui.EASING_LINEAR | |
gui.EASING_INBACK | gui.EASING_OUTBACK |
gui.EASING_INOUTBACK | gui.EASING_OUTINBACK |
gui.EASING_INBOUNCE | gui.EASING_OUTBOUNCE |
gui.EASING_INOUTBOUNCE | gui.EASING_OUTINBOUNCE |
gui.EASING_INELASTIC | gui.EASING_OUTELASTIC |
gui.EASING_INOUTELASTIC | gui.EASING_OUTINELASTIC |
gui.EASING_INSINE | gui.EASING_OUTSINE |
gui.EASING_INOUTSINE | gui.EASING_OUTINSINE |
gui.EASING_INEXPO | gui.EASING_OUTEXPO |
gui.EASING_INOUTEXPO | gui.EASING_OUTINEXPO |
gui.EASING_INCIRC | gui.EASING_OUTCIRC |
gui.EASING_INOUTCIRC | gui.EASING_OUTINCIRC |
gui.EASING_INQUAD | gui.EASING_OUTQUAD |
gui.EASING_INOUTQUAD | gui.EASING_OUTINQUAD |
gui.EASING_INCUBIC | gui.EASING_OUTCUBIC |
gui.EASING_INOUTCUBIC | gui.EASING_OUTINCUBIC |
gui.EASING_INQUART | gui.EASING_OUTQUART |
gui.EASING_INOUTQUART | gui.EASING_OUTINQUART |
gui.EASING_INQUINT | gui.EASING_OUTQUINT |
gui.EASING_INOUTQUINT | gui.EASING_OUTINQUINT |
Możesz tworzyć własne funkcje wygładzania zdefiniowane jako specjalny vector
ze zbiorem odpowiednich, kolejnych wartości i użyć go zamiast predefiniowanych stałych przedstawionych powyżej. Wektor ten reprezentuje krzywą zmiany wartości numerycznej od wartości startowej (0
) do wartości końcowej (1
). Silnik interpoluje w czasie działania programu te wartości liniowo.
Na przykład, wektor:
local values = { 0, 0.4, 0.2, 0.2, 0.5. 1 }
local my_easing = vmath.vector(values)
stworzy następującą krzywą:
W poniższym przykładzie wartość y pozycji obiektu skacze między aktualną pozycją startową, a pozycją docelową 200:
local values = { 0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0, 0, 0, 0, 0, 0,
1, 1, 1, 1, 1, 1, 1, 1 }
local square_easing = vmath.vector(values)
go.animate("go", "position.y", go.PLAYBACK_LOOP_PINGPONG, 200, square_easing, 2.0)
a krzywa ruchu wygląda jak sygnał prostokątny:
Wszystkie funkcje animacji (go.animate()
, gui.animate()
, gui.play_flipbook()
, gui.play_spine_anim()
, sprite.play_flipbook()
, spine.play_anim()
i model.play_anim()
) przyjmują opcjonalną funkcję zwaną callback jako ostatni argument. Ta funkcja jest wywoływana w momencie zakończenia animacji. Nie jest więc nigdy wywoływana, dla animacji, które są zapętlone, ani w przypadku, gdy animacja jest ręcznie zatrzymana przez funkcje rodzaju go.cancel_animations()
. Callback może być użyty do uruchomienia wydarzeń, które powinny wystartować w momencie końca animacji, bez wiedzy programisty o czasie jej trwania lub do uruchomienia następnej animacji, zaraz po zakończeniu obecnej.
Dokładna sygnatura funkcji callback różni się nieco dla różnych funkcji animacji (różne argumenty). Sprawdź dokumentację API dla funkcji, którą chcesz użyć.
local function done_bouncing(self, url, property)
-- We're done animating. Do something...
end
function init(self)
go.animate(".", "position.y", go.PLAYBACK_ONCE_FORWARD, 100, go.EASING_OUTBOUNCE, 2, 0, done_bouncing)
end
Did you spot an error or do you have a suggestion? Please let us know on GitHub!
GITHUB