Jump to content
  • Advertisement

Win Animation

Daniel Ricci

951 views

In my last post, I presented the Autocomplete feature that I implemented. This feature lets you right-click anywhere on the board, and all face-up top-most cards will attempt to put themselves in one of the four Foundation piles above the board. I also mentioned a handful of bug fixes that were done.

This next and final feature that I will demonstrate here is the `Win Animation` feature that occurs when you win the game or push the Alt+Shift+2 key combination. 

The `Win Animation` is an animation that is played when you win the game. That is when all the cards are properly situated on each of the four Foundation piles above the board. The animation starts at the left-most pile, ending at the rightmost pile for each group of cards (Kings first, then Queen, Jack, 10, etc).

The path that the card follows is similar to a sine/cosine wave. Each card has a wave randomly generated that fits within a specified period. These choices are limited to a set number of choices based on my observations of the original game, and there are quite a few randomly generated choices that can occur.

I originally started prototyping this functionality using vanilla trigonometry, however, I found an implementation online that I felt was much simpler to implement, and that had much better readability for those not so fluent in the way of mathematical formulas. The implementation that I retrofitted into my game is the one from `Mr. Doob`, you can find the link to his code here.

This is what the win animation looks like in my game (The animation is slow because of the recording software that I use,:( it is about twice as fast as this in RL)

WinAnim.gif.61477869af1ecd893444c9357a38cd3a.gif

There are a few parts to the code to make this work, and I will explain the major players of this process in snippets below. 

For me to implement this animation I first created a class called `WinAnimationHelper` to manage the animation process. This class has a static method called `processCards` that gets called when someone wants to process all the cards on the Foundation piles (called when the game winning condition has been met).   

    /**
     * Process all the cards held by the foundation views
     */
    public static void processCards() {
        // Get the list of foundation piles
        List<FoundationPileView> foundationsList = AbstractFactory.getFactory(ViewFactory.class).getAll(FoundationPileView.class);
        
        // Reverse the list so that we start with the left-most pile.
        Collections.reverse(foundationsList);

        // Initialize this helper class
        initialize();
        
        // Populate the queue of items to be processed
        _foundations.addAll(foundationsList);
    }

Of interest is the `initialize()` method that I call above. This is a private static method that creates a timer that processes a field called _foundations at a rate of 80 times per second. After I perform a call to add the available foundations to the queue this timer will start to process them in a first come first serve fashion. For each foundation pile that it processes, it will grab the top-most card and create a WinAnimationHelper object, passing in the card that it received.  

Here is what the constructor of this class looks like.

    /**
     * The change in `x` over time
     */
    private double _deltaX = Math.floor(Math.random() * 6 - 3) * 2;
    
    /*
     * The change in `y` over time
     */
    private double _deltaY = -Math.random() * 16;

    /**
     * Constructs a new instance of this class type
     * 
     * @param cardView The card view to animate
     */
    private WinAnimationHelper(CardView cardView) {
        _cardView = cardView;
        Point position = cardView.getParentIView().getContainerClass().getLocation();
        _x = position.getX();
        _y = position.getY();

        if(_deltaX == 0) {
            _deltaX = 1;
        }
    }

The above code will first initialize the deltas for this card. For the rate of change on the X-Axis, the domain of available values are from [-6, 4]. For the rate of change on the Y-Axis, the range of available values are from (-16.0, 0]. I also handle an edge case where if the change in X is 0, I set it to 1 so that there is at least some movement along the X-Axis.

After the object is constructed, my timer that created the object calls the method `update()` for each tick until the update can no longer occur because the card is out of bounds of the canvas dimensions.  Here is what the method looks like.   

    /**
     * Performs an update by performing both a next step point calculation and a draw routine
     *
     * @return TRUE if the operation was successful, false otherwise
     */
    private boolean update() {

        Point point = calculateNextStep();
        if(point == null) {
            return false;
        }
        
        draw(point);
        return true;    
    }

The above code is very straight forward, calculate the next location of the card, and then draw to that point.

Here is the `calculateNextStep()` method.

    /**
     * Calculates the next position that the currently set card will be at
     *
     * @return The position associated with the next step where the card would be at 
     */
    private Point calculateNextStep() {

        // Take the change in X and the change in Y and apply them respectively
        _x += _deltaX;
        _y += _deltaY;

        // If you are outside the left or right canvas limits then the card should not 
        // longer be positioned anywhere relevant so do not return any position
        if(_x < -CardView.CARD_WIDTH || _x > _canvasWidth) {
            return null;
        }
        
        // If the position is outside canvas height (with respect to the bottom of the card)
        if(_y > _canvasHeight - CardView.CARD_HEIGHT) {
            
            // Normalize the position of the card by placing it on the theoretical bottom of the canvas
            _y = _canvasHeight - CardView.CARD_HEIGHT;
            
            // Take the change in `y` inverse it, this along will cause the card to bounce upwards
            // Take only a small percentage of the delta so that it bounces less
            _deltaY = -_deltaY * 0.85;
        } 

        _deltaY += 0.98;

        return new Point((int)_x, (int)_y);
    }

The above code takes the currently computed deltas and adds them to the current x and y positions that were recorded by the card. If the `x` location is outside the bounds of the canvas then there is no more computation to be performed, this is our exit case.  If this is not the case then I check to see if the `y` position is outside the lower bounds of the canvas. If it is, I position the card at the absolute bottom of the canvas and then I apply an inverse linear force to the current deltaY. This will cause the card to move upwards, however, only upwards by a certain percentage. This is an ever decreasing number that will simulate a `bounce` and that will eventually flatline itself with the y-axis if this statement is executed many times. Finally, I update the deltaY with a constant to ensure that the change in `y` counteracts the `bounce` effect.

Once this is computed I perform a draw. Here is what the draw call looks like.

    /**
     * Draws the currently set card view to the specified position
     *
     * @param point The position to draw to
     */
    private void draw(Point point) {
        CardView cardView = CardView.createLightWeightCard(_cardView);
        cardView.render();
        
        ViewFactory viewFactory = AbstractFactory.getFactory(ViewFactory.class);
        GameView gameView = viewFactory.get(GameView.class);
        gameView.add(cardView, gameView.getComponentZOrder(viewFactory.get(StatusBarView.class)));
        cardView.setBounds(new Rectangle(point.x, point.y, _cardView.getWidth(), _cardView.getHeight()));        
    }

This was tough because I really wanted to just change the position of a single card and reuse the same draw buffer for performing the draw call just like in the original game. Doing this however was not possible because the layout manager that I am using is a Swing manager called GridBagLayout, and it was showing many artifacts that were making this functionality look horrible. 

So instead, for each point that I calculate I create a lightweight representation of the specified card view which is in layman terms a JPanel with an image. I take this card and I update it to the position that I calculated previously. I also make sure that it is added to the GameView and that it is positioned at the proper z-position.

The finishing touches for this feature were to make sure that clicking anywhere during the animation or pressing on any key stops the animation and asks you if you want to play again, this is in line with the original game.

So that is how I implemented the win animation.

I also fixed a few more bugs, that I will outline below

1. performing an undo doesn't undo the score, it just subtracts 2
https://github.com/danielricci/solitaire/issues/164

716234573_1.performinganundodoesntundothescoreitjustsubtracts2.gif.ca4c6b8158fa10e7559c691a27bb0596.gif

2. Performing an automove no longer updates the score
https://github.com/danielricci/solitaire/issues/155

1969642661_2.Performinganautomovenolongerupdatesthescore.gif.ce9fab28bcd7d9d3fe9c2e85cc2d2d82.gif

3. Cannot perform an undo after doing an Autocomplete
https://github.com/danielricci/solitaire/issues/153

859152244_3.CannotperformanundoafterdoinganAutocomplete.gif.64faeae1ab94ec1c7cbf4e7b8ed79ecd.gif

The next thing on my list is to normalize the UI and I have a couple of bug fixes left to do.

You can always follow my progress by following the game located at https://github.com/danielricci/solitaire, and if you have any questions I will do my best to answer them.

Take care, until my next blog post.



0 Comments


Recommended Comments

There are no comments to display.

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!