Animation pipeline for an ASCII roguelike game

Keywords: index parameter, beam animation, current animation, current index, target start, pipeline, run, queue, sequence, protocol, architecture, screen, play. Powered by TextRank.

Animations are an important part of an immersive experience so I wanted to incorporate some combat animations into the game. Cogmind has exceptional animations and downloaded some of the animation gifs that were in the devblog and analyzed them frame by frame to get a better idea on how to create good animations. I'll go over the animations themselves in a different section but in this doc I want to outline a system that will manage the rendering of each individual animation on the screen.

Architecture

There are going to be different kinds of animations: explosion, fire, beam weapon charging, etc. All these animation will contain the procedural logic to play the animation within it self, but there needs to be a rendering pipeline that accepts these animations and plays them in sequence. The computations of the animations need to happen in a background thread and the actual rendering needs to happen it the UI thread so we are talking about async execution. This means the pipeline needs to synchronize these animations to play in sequence. This is accomplished by using DispatchGroup and a semaphore that controls the number of animations allowed to run. A simple Animating protocol defines the interface that the pipeline will need to run any given animation.

class AnimationPipeline {
    var game: Game
    var animations = [Animating]()
    private let semaphore = DispatchSemaphore(value: 1)
    private let dispatchQueue = DispatchQueue(label: "taskQueue", qos: .default)
 
    init(game: Game) {
        self.game = game
    }
    func execute() {
        let group = DispatchGroup()
        for a in animations {
            group.enter()
            self.dispatchQueue.async {
                self.semaphore.wait()
                a.run()
                self.semaphore.signal()
                group.leave()
            }
        }
        group.notify(queue: DispatchQueue.main) {
            self.game.render()
        }
    }
}

Here the various components that have animations queue up all their Animating classes in and then call execute once they are ready to have the animations played. The pipeline iterates through all the animations in the queue and waits on the semaphore until the animation is completed. Once every animation has been player it clears the screen to return to the original state.

Update 1

An shortcoming that I discovered today with the pipeline was that the current animation has to finish before the next one can start and this is not always the desired effect. When you shoot a laser it's more realistic and better looking if the target starts bursting out sparks at contact rather then waiting for the beam animation to finish. This means that the architecture had to change into a model that allows creation of a chain of animations with the running animation having some means of triggering the next animation in the and continue running. I implemented this by adding an index and a closure parameter to the run function of the Animating protocol. When the pipeline calls run on the animation it passes in the current index and a reference to a method called trigger of itself. When the animation has done its initial work (e.g the beam animation has reached the target) it calls the method reference with the index parameter plus 1 indicating the next animation should start. This signal is captured by the pipeline and the animation with the index received from the current animation is added to the running queue. There are other ways of accomplishing the same synchronization by using semaphores inn the pipeline but this approach seemed cleaner and totally non blocking.

km1.md.gif image

km2.md.gif image

Update 2

I ran into an issue with the sequence of the scheduled actors and the animations. Since these two are running in different queues some of the actors were getting scheduled to move before the explosion or destruction animations were being completed. I didn't want to use a single serial queue to synchronize the animations and scheduled actions to I implemented a DispatchGroup on the AnimationPipeline to block until the animation is completed.

km11.md.gif image


Metadata

First published on 2020-08-29

Generated on May 5, 2024, 8:47 PM

Index

Mobile optimized version. Desktop version.