//
// Triple Play - utilities for use in PlayN-based games
// Copyright (c) 2011-2014, Three Rings Design, Inc. - All rights reserved.
// http://github.com/threerings/tripleplay/blob/master/LICENSE
package tripleplay.game;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import playn.core.util.Clock;
import static playn.core.PlayN.graphics;
import static playn.core.PlayN.pointer;
import tripleplay.game.trans.FlipTransition;
import tripleplay.game.trans.PageTurnTransition;
import tripleplay.game.trans.SlideTransition;
import tripleplay.util.Paintable;
import static tripleplay.game.Log.log;
/**
* Manages a stack of screens. The stack supports useful manipulations: pushing a new screen onto
* the stack, replacing the screen at the top of the stack with a new screen, popping a screen from
* the stack.
*
* <p> Care is taken to preserve stack invariants even in the face of errors thrown by screens when
* being added, removed, shown or hidden. Users can override {@link #handleError} and either simply
* log the error, or rethrow it if they would prefer that a screen failure render their entire
* screen stack unusable. </p>
*/
public class ScreenStack
implements Paintable
{
/** Implements a particular screen transition. */
public interface Transition {
/** Direction constants, used by transitions. */
enum Dir { UP, DOWN, LEFT, RIGHT; }
/** Allows the transition to pre-compute useful values. This will immediately be followed
* by call to {@link #update} with an elapsed time of zero. */
void init (Screen oscreen, Screen nscreen);
/** Called every frame to update the transition
* @param oscreen the outgoing screen.
* @param nscreen the incoming screen.
* @param elapsed the elapsed time since the transition started (in millis if that's what
* your game is sending to {@link ScreenStack#update}).
* @return false if the transition is not yet complete, true when it is complete.
*/
boolean update (Screen oscreen, Screen nscreen, float elapsed);
/** Called when the transition is complete. This is where the transition should clean up
* any temporary bits and restore the screens to their original state. The stack will
* automatically destroy/hide the old screen after calling this method. Also note that this
* method may be called <em>before</em> the transition signals completion, if a new
* transition is started and this transition needs be aborted. */
void complete (Screen oscreen, Screen nscreen);
}
/** Used to operate on screens. See {@link #remove(Predicate)}. */
public interface Predicate {
/** Returns true if the screen matches the predicate. */
boolean apply (Screen screen);
}
/** Simply puts the new screen in place and removes the old screen. */
public static final Transition NOOP = new Transition() {
public void init (Screen oscreen, Screen nscreen) {} // noopski!
public boolean update (Screen oscreen, Screen nscreen, float elapsed) { return true; }
public void complete (Screen oscreen, Screen nscreen) {} // noopski!
};
/** The x-coordinate at which screens are located. Defaults to 0. */
public float originX = 0;
/** The y-coordinate at which screens are located. Defaults to 0. */
public float originY = 0;
/** Creates a slide transition. */
public SlideTransition slide () { return new SlideTransition(this); }
/** Creates a page turn transition. */
public PageTurnTransition pageTurn () { return new PageTurnTransition(); }
/** Creates a flip transition. */
public FlipTransition flip () { return new FlipTransition(); }
/**
* {@link #push(Screen,Transition)} with the default transition.
*/
public void push (Screen screen) {
push(screen, defaultPushTransition());
}
/**
* Pushes the supplied screen onto the stack, making it the visible screen. The currently
* visible screen will be hidden.
* @throws IllegalArgumentException if the supplied screen is already in the stack.
*/
public void push (Screen screen, Transition trans) {
if (_screens.isEmpty()) {
addAndShow(screen);
} else {
final Screen otop = top();
transition(new Transitor(otop, screen, trans) {
@Override protected void onComplete() { hide(otop); }
});
}
}
/**
* {@link #push(Iterable,Transition)} with the default transition.
*/
public void push (Iterable<? extends Screen> screens) {
push(screens, defaultPushTransition());
}
/**
* Pushes the supplied set of screens onto the stack, in order. The last screen to be pushed
* will also be shown, using the supplied transition. Note that the transition will be from the
* screen that was on top prior to this call.
*/
public void push (Iterable<? extends Screen> screens, Transition trans) {
if (!screens.iterator().hasNext()) {
throw new IllegalArgumentException("Cannot push empty list of screens.");
}
if (_screens.isEmpty()) {
for (Screen screen : screens) add(screen);
justShow(top());
} else {
final Screen otop = top();
Screen last = null;
for (Screen screen : screens) {
if (last != null) add(last);
last = screen;
}
transition(new Transitor(otop, last, trans) {
@Override protected void onComplete() { hide(otop); }
});
}
}
/**
* {@link #popTo(Screen,Transition)} with the default transition.
*/
public void popTo (Screen newTopScreen) {
popTo(newTopScreen, defaultPopTransition());
}
/**
* Pops the top screen from the stack until the specified screen has become the
* topmost/visible screen. If newTopScreen is null or is not on the stack, this will remove
* all screens.
*/
public void popTo (Screen newTopScreen, Transition trans) {
// if the desired top screen is already the top screen, then NOOP
if (top() == newTopScreen) return;
// remove all intervening screens
while (_screens.size() > 1 && _screens.get(1) != newTopScreen) {
justRemove(_screens.get(1));
}
// now just pop the top screen
remove(top(), trans);
}
/**
* {@link #replace(Screen,Transition)} with the default transition.
*/
public void replace (Screen screen) {
replace(screen, defaultPushTransition());
}
/**
* Pops the current screen from the top of the stack and pushes the supplied screen on as its
* replacement.
* @throws IllegalArgumentException if the supplied screen is already in the stack.
*/
public void replace (Screen screen, Transition trans) {
if (_screens.isEmpty()) {
addAndShow(screen);
} else {
final Screen otop = _screens.remove(0);
// log.info("Removed " + otop + ", new top " + top());
transition(new Transitor(otop, screen, trans) {
@Override protected void onComplete () {
hide(otop);
wasRemoved(otop);
}
});
}
}
/**
* {@link #remove(Screen,Transition)} with the default transition.
*/
public boolean remove (Screen screen) {
return remove(screen, defaultPopTransition());
}
/**
* Removes the specified screen from the stack. If it is the currently visible screen, it will
* first be hidden, and the next screen below in the stack will be made visible.
*
* @return true if the screen was found in the stack and removed, false if the screen was not
* in the stack.
*/
public boolean remove (Screen screen, Transition trans) {
if (top() != screen) return justRemove(screen);
if (_screens.size() > 1) {
final Screen otop = _screens.remove(0);
// log.info("Removed " + otop + ", new top " + top());
transition(new Untransitor(otop, top(), trans) {
@Override protected void onComplete () {
hide(otop);
wasRemoved(otop);
}
});
} else {
hide(screen);
justRemove(screen);
}
return true;
}
/**
* {@link #remove(Predicate,Transition)} with the default transition.
*/
public void remove (Predicate pred) {
remove(pred, defaultPopTransition());
}
/**
* Removes all screens that match the supplied predicate, from lowest in the stack to highest.
* If the top screen is removed (as the last action), the supplied transition will be used.
*/
public void remove (Predicate pred, Transition trans) {
// first, remove any non-top screens that match the predicate
if (_screens.size() > 1) {
Iterator<Screen> iter = _screens.iterator();
iter.next(); // skip top
while (iter.hasNext()) {
Screen screen = iter.next();
if (pred.apply(screen)) {
iter.remove();
wasRemoved(screen);
// log.info("Pred removed " + screen + ", new top " + top());
}
}
}
// last, remove the top screen if it matches the predicate
if (_screens.size() > 0 && pred.apply(top())) remove(top(), trans);
}
/**
* Returns the top screen on the stack, or null if the stack contains no screens.
*/
public Screen top () {
return _screens.isEmpty() ? null : _screens.get(0);
}
/**
* Searches from the top-most screen to the bottom-most screen for a screen that matches the
* predicate, returning the first matching screen. {@code null} is returned if no matching
* screen is found.
*/
public Screen find (Predicate pred) {
for (Screen screen : _screens) if (pred.apply(screen)) return screen;
return null;
}
/**
* Returns true if we're currently transitioning between screens.
*/
public boolean isTransiting () {
return _transitor != null;
}
/**
* Returns the number of screens on the stack.
*/
public int size () {
return _screens.size();
}
/**
* Called from your game's {@code update} method. Calls {@link Screen#update} on top screen.
*/
public void update (int delta) {
if (_transitor != null) _transitor.update(delta);
else if (!_screens.isEmpty()) top().update(delta);
}
/**
* Called from your game's {@code paint} method. Calls {@link Screen#paint} on top screen.
*/
public void paint (Clock clock) {
if (_transitor != null) _transitor.paint(clock);
else if (!_screens.isEmpty()) top().paint(clock);
}
protected Transition defaultPushTransition () {
return NOOP;
}
protected Transition defaultPopTransition () {
return NOOP;
}
protected void add (Screen screen) {
if (_screens.contains(screen)) {
throw new IllegalArgumentException("Cannot add screen to stack twice.");
}
_screens.add(0, screen);
// log.info("Added " + screen + ", new top " + top());
try { screen.wasAdded(); }
catch (RuntimeException e) { handleError(e); }
}
protected void addAndShow (Screen screen) {
add(screen);
justShow(screen);
}
protected void justShow (Screen screen) {
graphics().rootLayer().addAt(screen.layer, originX, originY);
try { screen.wasShown(); }
catch (RuntimeException e) { handleError(e); }
}
protected void hide (Screen screen) {
graphics().rootLayer().remove(screen.layer);
try { screen.wasHidden(); }
catch (RuntimeException e) { handleError(e); }
}
protected boolean justRemove (Screen screen) {
boolean removed = _screens.remove(screen);
if (removed) wasRemoved(screen);
// log.info("Just removed " + screen + ", new top " + top());
return removed;
}
protected void wasRemoved (Screen screen) {
try { screen.wasRemoved(); }
catch (RuntimeException e) { handleError(e); }
}
protected void transition (Transitor transitor) {
if (_transitor != null) _transitor.complete();
_transitor = transitor;
_transitor.init();
}
/**
* A hacky mechanism to allow a game to force a transition to skip some number of frames at its
* start. If a game's screens tend to do a lot of image loading in wasAdded or immediately
* after, that will cause an unpleasant jerk at the start of the transition as the first frame
* or two have order of magnitude larger frame deltas than subsequent frames. Having those
* render as t=0 and then starting the timer after the skipped frames are done delays the
* transition by a bit, but ensures that when things are actually animating, that they are nice
* and smooth.
*/
protected int transSkipFrames () {
return 0;
}
protected class Transitor {
public Transitor (Screen oscreen, Screen nscreen, Transition trans) {
_oscreen = oscreen;
_nscreen = nscreen;
_trans = trans;
}
public void init () {
_oscreen.hideTransitionStarted();
showNewScreen();
_trans.init(_oscreen, _nscreen);
// disable pointer interactions while we transition; disallowing interaction
pointer().setEnabled(false);
// Force a complete if the Transition is a noop, so that we don't have to wait until
// the next update. We should consider checking some property of the Transition object
// rather than checking against noop, in the odd case that we have a custom 0-duration
// transition.
if (_trans == NOOP) {
complete();
}
}
public void update (int delta) {
_oscreen.update(delta);
_nscreen.update(delta);
if (_complete) complete();
}
public void paint (Clock clock) {
_oscreen.paint(clock);
_nscreen.paint(clock);
if (_skipFrames > 0) _skipFrames -= 1;
else _elapsed += clock.dt();
_complete = _trans.update(_oscreen, _nscreen, _elapsed);
}
public void complete () {
_transitor = null;
// let the transition know that it's complete
_trans.complete(_oscreen, _nscreen);
// make sure the new screen is in the right position
_nscreen.layer.setTranslation(originX, originY);
_nscreen.showTransitionCompleted();
// reenable pointer interactions
pointer().setEnabled(true);
onComplete();
}
protected void showNewScreen () {
addAndShow(_nscreen);
}
protected void onComplete () {}
protected final Screen _oscreen, _nscreen;
protected final Transition _trans;
protected int _skipFrames = transSkipFrames();
protected float _elapsed;
protected boolean _complete;
}
protected class Untransitor extends Transitor {
public Untransitor (Screen oscreen, Screen nscreen, Transition trans) {
super(oscreen, nscreen, trans);
}
@Override protected void showNewScreen () {
justShow(_nscreen);
}
}
/** Called if any exceptions are thrown by the screen calldown functions. */
protected void handleError (RuntimeException error) {
log.warning("Screen choked", error);
}
/** The currently executing transition, or null. */
protected Transitor _transitor;
/** Containts the stacked screens from top-most, to bottom-most. */
protected final List<Screen> _screens = new ArrayList<Screen>();
}