In my previous post I talked about how you can control complex transitions by animating a single control value at a constant speed from 0 to 1, and then applying various curves to derive more complex transition behaviors from that single source value.
That approach works well when you want the transition to fit exactly into a fixed period of time. Other times, though, you may not know exactly when you want things to start or stop moving. When the factors controlling your transitions get more complicated, it can be easier to switch to a physics based approach.
The basic idea here is that on each update, you calculate what state you would ideally like your object to be in, and then you apply some kind of physics simulation to gradually move it toward that state.
Again using the Game State Management sample as an example, open up MenuScreen.cs. First off we need to add some state to store the current position of each menu entry, so add this to the Fields region:
protected List<Vector2> menuPositions = new List<Vector2>();
Now find the Draw method, and add these two lines right after the start of the for loop (to avoid trying to draw entries we haven't yet computed positions for):
for (int i = 0; i < menuEntries.Count; i++) { if (i >= menuPositions.Count) // NEW! continue; // NEW! Color color; float scale;
Near the end of the Draw method, change the DrawString call to use this stored entry position:
ScreenManager.SpriteBatch.DrawString(ScreenManager.Font, menuEntries[i], menuPositions[i], color, 0, origin, scale, SpriteEffects.None, 0);
(after making this change you can take out all the other code inside Draw that computes a position based off the 0 to 1 transition state, since we are no longer using that technique)
Finally we need to add a new Update method to the MenuScreen class. This initializes new entries to zero (the top left corner of the screen). It then figures out where each entry would like to be, examining the ScreenState to decide whether they should go for their normal location or transition off toward the bottom left. Finally, it uses the Vector2.Lerp helper to move each entry gradually from its current position toward the target:
public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen) { for (int i = 0; i < menuEntries.Count; i++) { if (i >= menuPositions.Count) menuPositions.Add(Vector2.Zero); // Work out where we would like this menu entry to be. Vector2 wantedPosition; if (ScreenState == ScreenState.TransitionOn || ScreenState == ScreenState.Active) wantedPosition = new Vector2(100, 100 + i * ScreenManager.Font.LineSpacing); else wantedPosition = new Vector2(0, 500); // Move it toward the target position. menuPositions[i] = Vector2.Lerp(menuPositions[i], wantedPosition, 0.1f); } base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen); }
Go back and forth between the main menu and options screen a few times to get a sense of how this behaves. Note in particular that if you toggle back and forth very quickly, the menu entries can smoothly change direction in mid animation.
You can use all kinds of physics calculations to move user interface components around during transitions, in exactly the same way you would for any other sort of game entity. For instance we could add a velocity component to implement a spring model. Add this field to the top of the class:
protected List<Vector2> menuVelocities = new List<Vector2>();
And change the Update method to:
public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen) { for (int i = 0; i < menuEntries.Count; i++) { if (i >= menuPositions.Count) { menuPositions.Add(new Vector2(-i * 100, 0)); menuVelocities.Add(Vector2.Zero); } // Work out where we would like this menu entry to be. Vector2 wantedPosition; if (ScreenState == ScreenState.TransitionOn || ScreenState == ScreenState.Active) wantedPosition = new Vector2(100, 100 + i * ScreenManager.Font.LineSpacing); else wantedPosition = new Vector2(0, 100 + i * 150); // Move it toward the target position. menuVelocities[i] += (wantedPosition - menuPositions[i]) * 0.01f; menuPositions[i] += menuVelocities[i]; menuVelocities[i] *= 0.9f; } base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen); }
Volia! Now each menu entry does a little bounce as it settles into position.
Anything you know about game physics can be equally useful for transition effects. Spring simulations are particularly common, and a good understanding of velocity, acceleration and inertia can be a big help in coming up with something that will look cool. Don't forget about AI techniques, too. Let's say you want a "+10" bonus display to appear, zoom up really big, flash yellow, then shrink in size as it slides off to the left before disappearing in a puff of smoke. Coordinating an effect like that is exactly the same problem as coordinating the behaviors of an AI character, and exactly the same solutions will apply: finite state machines to the rescue!
The really cool thing about this style of transition is how well they react to changing situations. Because you are simply deciding where the object would like to be and then applying physics to move toward that location, the transition effect will kick in any time the object changes state, even if that wasn't something you originally planned for.
As a very contrived and somewhat buggy example, open up OptionsMenuScreen.cs and change the Update method to:
public override void Update(GameTime gameTime, bool otherScreenHasFocus, bool coveredByOtherScreen) { base.Update(gameTime, otherScreenHasFocus, coveredByOtherScreen); int i = 0; if (gameTime.TotalGameTime.Seconds % 10 < 5) { if (MenuEntries.Count < 5) { MenuEntries.Insert(0, string.Empty); menuPositions.Insert(0, Vector2.Zero); menuVelocities.Insert(0, Vector2.Zero); } MenuEntries[i++] = "Preferred ungulate: " + currentUngulate.ToString(); } else { if (MenuEntries.Count > 4) { MenuEntries.RemoveAt(0); menuPositions.RemoveAt(0); menuVelocities.RemoveAt(0); } } MenuEntries[i++] = "Language: " + languages[currentLanguage]; MenuEntries[i++] = "Frobnicate: " + (frobnicate ? "on" : "off"); MenuEntries[i++] = "elf: " + elf.ToString(); MenuEntries[i++] = "Back"; }
This will remove or re-add the first menu entry every five seconds, in response to which the other menu entries will automatically slide upward to fill the gap, or down to make room for it coming back again.