Rozdzielenie stanu świata i animacji w grze turowej

9

Jak radzisz sobie z oddzieleniem animacji od stanu świata w grze turowej? Obecnie pracuję nad grą opartą na siatce 2D. Poniższy kod jest uproszczony, aby lepiej wyjaśnić.

Kiedy aktor się porusza, chcę zatrzymać przepływ zwrotów, podczas gdy stworzenie animuje i przesuwa się do nowej pozycji. W przeciwnym razie ekran mógłby znacznie opóźnić się w stosunku do stanu na świecie, co spowodowałoby dziwny wygląd. Chcę też mieć animacje, które nie blokują przebiegu gry - efekt cząsteczkowy może się rozwijać w wielu turach bez wpływu na rozgrywkę.

Wprowadziłem dwa rodzaje animacji, które nazywam animacjami blokującymi i animacjami nieblokującymi. Gdy gracz chce się przenieść, wykonywany jest kod

class Actor {
    void move(PositionInfo oldPosition, PositionInfo newPosition) {
        if(isValidMove(oldPosition, newPosition) {
             getGame().pushBlockingAnimation(new MoveAnimation(this, oldPosition, newPosition, ....));
             player.setPosition(newPosition);
        }
    }
}

Następnie główna pętla aktualizacji wykonuje:

class Game {
    void update(float dt) {
        updateNonBlockingAnimations(dt); //Effects like particle explosions, damage numbers, etc. Things that don't block the flow of turns.
        if(!mBlockingAnimations.empty()) {
            mBlockingAnimations.get(0).update(dt);
        } else {
             //.. handle the next turn. This is where new animations will be enqueued..//
        }
        cleanUpAnimations(); // remove finished animations
    }
}

... gdzie animacja aktualizuje pozycję ekranową aktora.

Innym pomysłem, który wdrażam, jest współbieżna animacja blokująca, w której wiele animacji blokujących byłoby aktualizowanych jednocześnie, ale następna tura nie miałaby miejsca, dopóki wszystkie nie zostaną wykonane.

Czy to wydaje się rozsądnym sposobem robienia rzeczy? Czy ktoś ma jakieś sugestie, a nawet odniesienia do tego, jak inne podobne gry robią takie rzeczy.

mdkess
źródło

Odpowiedzi:

2

Jeśli twój system działa dla ciebie, nie widzę żadnego powodu, aby tego nie robić.

Cóż, jeden powód: może się bałaganić, kiedy nie można łatwo ocenić konsekwencji „player.setPosition (newPosition);” już.

Przykład (tylko zmyślanie): Wyobraź sobie, że poruszasz odtwarzaczem setPosition na pułapce. Samo wywołanie „player.setPosition” wywoła inne wywołanie do .. powiedz kod detektora pułapki, który z kolei sam popchnie blokującą animację, która wyświetla „bolesny plusk” na sobie.

Widzisz problem: rozbryzg wyświetla się jednocześnie z ruchem gracza w kierunku pułapki. (Załóżmy tutaj, że nie chcesz tego, ale chcesz, aby animacja pułapki była odtwarzana zaraz po zakończeniu ruchu;)).

Tak długo, jak jesteś w stanie pamiętać o wszystkich konsekwencjach swoich działań, które robisz po tych połączeniach „pushBlockingAnimation”, wszystko jest w porządku.

Konieczne może być rozpoczęcie zawijania logiki gry w „blokowanie czegoś” - klasy, aby można je było kolejkować w celu wykonania po zakończeniu animacji. Lub przekazujesz callback „callThisAfterAnimationFinishes” do pushBlockingAnimation i przenosisz setPosition do funkcji callback.

Innym podejściem byłyby języki skryptowe. Na przykład Lua ma „coroutine.yield”, który zwraca funkcję ze specjalnym stanem, dzięki czemu można ją później wywołać, aby kontynuować przy następnej instrukcji. W ten sposób możesz łatwo poczekać na zakończenie animacji, aby wykonać logikę gry bez zaśmiecania „normalnego” wyglądu programu. (oznacza, że ​​Twój kod nadal wyglądałby trochę jak „playAnimation (); setPosition ();” zamiast przekazywać wywołania zwrotne i zaśmiecać logikę gry przez wiele funkcji).

Unity3D (i przynajmniej jeden inny system gier, jaki znam) zawiera instrukcję C # „return return”, która symuluje to bezpośrednio w kodzie C #.

// instead of simple call to move(), you have to "iterate" it in a foreach-like loop
IEnumerable<Updatable> move(...) {
    // playAnimation returns an object that contain an update() and finished() method.
    // the caller will not iterate over move() again until the object is "finished"
    yield return getGame().playAnimation(...)
    // the next statement will be executed *after* the animation finished
    player.setPosition(newPosition);
}

Oczywiście w tym schemacie wywołanie move () powoduje teraz całą brudną robotę. Prawdopodobnie zastosowałbyś tę technikę tylko do określonych funkcji, w których dokładnie wiesz, kto je wywołuje. Przyjmij to tylko jako podstawowy pomysł, w jaki sposób zorganizowane są inne silniki - prawdopodobnie jest to zbyt skomplikowane dla twojego obecnego konkretnego problemu.

Zalecam pozostanie przy swoim pomyśle pushBlockingAnimation, dopóki nie będziesz mieć problemów w świecie rzeczywistym. Następnie zmodyfikuj to. ;)

Imi
źródło
Dziękuję bardzo za poświęcenie czasu na napisanie tej odpowiedzi, naprawdę to doceniam.
mdkess,