VRath – animacja tarczy i próba strzału

Kolejny krokiem przy projektowaniu mojej strzelnicy będzie animacja celu, który ma zachowywać się jak festynowa tarcza oraz oczywiście obsługa zdarzeń. Nie ma na co czekać, to ostatni miesiąc zabawy.

Ostatnim razem nadałem nieco wojskowego klimatu całej scenie. Zanim zajmę się elementami ze wstępu, chciałbym podzielić elementy ze sceny do osobnych plików.



Wydzielenie komponentów

Obecnie w moim projekcie jest jeden komponent sceny, który zajmuje się absolutnie wszystkim. I mogłoby tak pozostać gdyby ilość kodu się nie powiększała, ale jeżeli chcę zaprezentować coś więcej niż statyczne pomieszczenie, utrzymanie wszystkiego w jednym pliku będzie trudne, a jeżeli zostanie czas na testy – wręcz okropne do testowania. Dlatego wydzielę poszczególne elementy na zasadzie odpowiedzialności. Na pierwszy ogień idzie komponent zarządzania zewnętrznymi zasobami, który nazwę <AssetsManager> i umieszczę w pliku assets-manager.component.jsx:

import { Entity } from "aframe-react";
import React from "react";
import floorImg from "../../images/stone.png";
import crosshairImg from "../../images/crosshair.png";
import ceilImg from "../../images/wall2.jpg";
import wallImg from "../../images/wall2.png";

const AssetsManagerComponent = () => (
    <Entity primitive="a-assets">
        <img id="floorTexture" src={floorImg} />
        <img id="crosshairTexture" src={crosshairImg} />
        <img id="ceilTexture" src={ceilImg} />
        <img id="wallTexture" src={wallImg} />
    </Entity>
);

export default AssetsManagerComponent;

Większość z moich komponentów nie będzie przechowywała stanu ani zaawansowanej logiki, więc mogą być one przedstawione w postaci prostej funkcji. Kolejnym komponentem będzie <Arena> zawierająca prostopadłościan w którym znajdzie się gracz, czyli fundamenty sceny:

import { Entity } from "aframe-react";
import React from "react";

const ArenaComponent = () => (
    <Entity>
        <Entity primitive="a-plane" material="src: #floorTexture" repeat="12.5 12.5" width="25" height="25" rotation="-90 0 0" />
        <Entity primitive="a-plane" material="src: #wallTexture" repeat="5 1" width="25" height="5" rotation="180 0 0" position="0 2.5 12.5" />
        <Entity primitive="a-plane" material="src: #wallTexture" repeat="5 1" width="25" height="5" rotation="0 0 0" position="0 2.5 -12.5" />
        <Entity primitive="a-plane" material="src: #wallTexture" repeat="5 1" width="25" height="5" rotation="0 -90 0" position="12.5 2.5 0" />
        <Entity primitive="a-plane" material="src: #wallTexture" repeat="5 1" width="25" height="5" rotation="0 90 0" position="-12.5 2.5 0" />
        <Entity primitive="a-plane" material="src: #ceilTexture" repeat="5 5" width="25" height="25" rotation="90 0 0" position="0 5 0" />
    </Entity>
);

export default ArenaComponent;

Warto byłoby się zastanowić nad wydzieleniem nazw tekstur z areny i managera zasobów, tak aby zmiana nazwy odbywała się tylko w jednym miejscu. Ale ewentualny refaktor na później, muszę skończyć przynajmniej działającą prowizorkę. W tym miejscu warto zauważyć, że encje mogą być obejmowane przez inne encje jako grupa. Gdybym zmienił pozycje encji obejmującej, zmieniłbym położenie wszystkich fundamentów jednocześnie. Kolejnym elementem będzie komponent <Camera>, który nie trzeba tłumaczyć:

import { Entity } from "aframe-react";
import React from "react";

const CameraComponent = () => (
    <Entity primitive="a-camera" wasd-controls="enabled: false" position="0 0 10">
        <Entity primitive="a-cursor" geometry="primitive: ring; radiusInner: 0.00001; radiusOuter: 0.04" material="src: #crosshairTexture" />
    </Entity>
);

export default CameraComponent;

Kamera obejmuje także kursor, czyli celownik i na ten moment nie rozdzielam tego na osobne pliki. Ostatnim elementem będą światła, które umieszczę w jednym pliku:

import { Entity } from "aframe-react";
import React from "react";

const ArenaLightsComponent = () => (
    <Entity>
        <Entity primitive="a-light" type="spot" intensity="1" penumbra="0.3" color="#fff" position="0 1 5" rotation="-15 0 0"></Entity>
        <Entity primitive="a-light" type="point" intensity="0.5" color="green" position="0 3 5"></Entity>
        <Entity primitive="a-light" type="point" intensity="0.2" color="#fff" position="0 3 12"></Entity>
    </Entity>
);

export default ArenaLightsComponent;

Gdy już wydzieliłem komponenty na osobne pliki, komponent sceny odchudzi się dość znacznie:

import React from "react";
import { Scene } from "aframe-react";
import AssetsManager from "./assets-manager.component.jsx";
import Arena from "./arena.component.jsx";
import ArenaLights from "./arena-lights.component.jsx";
import Camera from "./camera.component.jsx";

const SceneComponent = () => (
    <Scene stats>
        <AssetsManager />
        <Arena />
        <Camera />
        <ArenaLights />
    </Scene>
);

export default SceneComponent;

Animacja celu

Istotnym elementem każdej strzelnicy jest cel do którego można postrzelać, czyli po prostu tarcza. Zanim jednak powstanie tarcza z prawdziwego zdarzenia, chciałbym stworzyć prosty prototyp, który będzie się zachowywał tak jak to sobie wymarzyłem, a na końcu podmienię go na właściwy obiekt. Moją prowizorką będzie biała kula, a to co w tej chwili chciałbym z nią zrobić to animacja po osi X. Nie będzie to więc zachowanie standardowej tarczy na strzelnicy gdzie możemy przybliżać i oddalać tarcze. Będzie to coś w rodzaju tarczy festynowej, gdzie człowiek dostaje rozkalibrowaną wiatrówke i strzela do metalowej kaczki. No w każdym bądź razie coś podobnego do tego.
W A-Frame problem animacji rozwiązano za pomocą encji, którą umieszcza się wewnątrz encji, którą to z kolei chcemy animować. Czyli jeżeli chcemy aby pudełko kartonowe jeździło po podłodzie, najlepiej postawmy je na jakimś zdalnie sterowanym samochodziku. To duże uproszczenie, ale powinno zadziałać na wyobraźnie. Co ciekawe, twórcy A-Frame’a zaznaczyli w dokumentacji, że taki sposób animacji może w przyszłości zostać zastąpiony komponentem. Zgodnie z metodologią ECS to komponenty określają zachowanie encji, więc byłaby to bardziej naturalna forma animacji dla tego frameworka. Na szczęście taki komponent został już stworzony i znajduje się w rejestrze komponentów A-Frame i właśnie ten sposób chciałbym wykorzystać.

Na początek muszę dodać odpowiednią paczkę do projektu:

yarn add aframe-animation-component --dev

Następnie należy zaimportować ten komponent zanim się go użyje deklaracją:
import "aframe-animation-component";
i zrobię to bezpośrednio w komponencie sceny, dzięki czemu inne encje także będą mogły z niego skorzystać. Tak więc czas utworzyć komponent <Target>, który będzie poruszał się z lewej na prawo i z powrotem:

import { Entity } from "aframe-react";
import React from "react";

const TargetComponent = () => (
    <Entity primitive="a-sphere" radius="0.5" color="#fff" position="10 2.5 -5" animation={{
        property: "position",
        dir: "alternate",
        dur: 4000,
        easing: "easeInOutCubic", 
        loop: true,
        to: "-10 2.5 -5"
    }} />
);

export default TargetComponent;

Właściwością wartą opisania jest oczywiście animation, stanowiąca komponent zainstalowany przeze mnie przed chwilą. Przekazuje się do niego opcje dla animacji i można je oczywiście podać w postaci ciągu znaków jak pozostałe właściwości encji, ale w przypadku złożonych opcji warto pokusić się o składnie JSONa. Opcja property określa, która właściwość ma być animowana. W moim przypadku chodzi mi o przemieszczanie się obiektu czyli użycie position jest jak najbardziej na miejscu. Właściwość dir określa sposób poruszania się animacji w danym kierunku. Wykorzystałem opcję alternate, która powoduje, że animacja gdy osiągnie wartość graniczną zaczyna płynnie cofać się do początku z którego zaczynała. Opcja dur to czas trwania animacji w milisekundach, easing to sposób animacji, loop określa czy animacja ma być zapętlona, a to to finałowa pozycja animacji. Nie musiałem deklarować jawnie skąd ma się rozpoczynać animacja opcją from, ponieważ wystarczy określenie pozycji obiektu.

Jak widać używanie aframe-animation-component nie tylko daje fajne rezultaty, ale także jest bardzo proste i wręcz naturalne do wykorzystywanego frameworka. Dla ciekawskich warto dodać, że w „bebechach” wykorzystywana jest biblioteka anime.js, która zapewnia wydajność i zawiera sporo gotowych animacji.

Mały zrzucik ekranu:

Przykładowy cel

Obsługa zdarzeń

Prowizoryczny cel jest gotowy i nawet się animuje, świetnie. Pora na wejście z nim w interakcję. A-Frame umożliwia dodawanie zdarzeń do encji w bardzo podobny sposób w jaki dodaje się zdarzenia do węzłów w drzewie DOM. Wykorzystywana jest do tego metoda addEventListener, która przyjmuje nazwę zdarzenia oraz funkcję, która zostanie wywołana gdy określone zdarzenie zajdzie. A żeby określone zdarzenie zaszło, encje mogą emitować je za pomocą metody emit. W tym momencie własne zdarzenia mnie nie interesują, chciałbym zasymulować strzał w kule, a do tego będę potrzebował zwykłego kliknięcia. Na szczęście z pomocą przychodzi wyzwalacz w postaci celownika który już mam, czyli encji kursora. Encja <a-cursor> emituje zdarzenie click więc pozostaje mi jedynie nasłuchiwanie na to zdarzenie w obiekcie celu. I tutaj przychodzi kolejne ułatwienie, ponieważ aframe-react załatwia zdarzenia jeszcze prościej, poprzez właściwość events. Na razie jedynie w ramach testu wypiszę na konsoli ciąg „Hit!” gdy „postrzelony” zostanie obiekt kuli. Z efektami dźwiękowymi całość powinna wyglądać znacznie lepiej, a póki co na „sucho” komponent celu wraz ze zdarzeniem kliknięcia wygląda następująco:

import { Entity } from "aframe-react";
import React from "react";

const TargetComponent = () => (
    <Entity primitive="a-sphere" radius="0.5" color="#fff" position="10 2.5 -5" animation={{
        property: "position",
        dir: "alternate",
        dur: 4000,
        easing: "easeInOutCubic", 
        loop: true,
        to: "-10 2.5 -5"
    }} events={{
        click: () => {
            console.log("Hit!");
        }
    }} />
);

export default TargetComponent;

Podsumowanie

Udało mi się pokazać w jaki sposób w A-Frame można animować dowolne encje na scenie, a także jak obsługiwać podstawowe zdarzenia. Zanim zajmę się dopieszczaniem elementów aby przypominały te, które powinny (tarcza zamiast kuli) chciałbym pokazać wczytywanie innych mediów do sceny. Przyda się nieco dźwięku przy strzałach, a i może jakiś smaczek graficzny umieściłoby sie na scenie. Do następnego!