001    package fang;
002    
003    import java.awt.*;
004    import javax.swing.*;
005    
006    
007    import java.net.URL;
008    import java.util.LinkedList;
009    import java.util.TreeMap;
010    
011    /**
012     * used to advance the frame in an animation using the AnimationCanvas. There
013     * are two basic parametes to the FrameAdvancer: the model frame rate and the
014     * screen frame rate. The model frame rate describes the number of frames per
015     * second which must be computed to have a consistent model. This in effect
016     * determines the maximum time for computations between displays. The screen
017     * frame rate is how often the screen is refreshed with the current Sprites. The
018     * screen rate is never higher than the model rate.
019     * 
020     * @author Jam Jenkins
021     */
022    
023    public abstract class FrameAdvancer extends GameWindow implements
024        AlarmScheduler {
025      /** keeps track of the screen refresh rate */
026      private LinkedList<Double> refreshTimes;
027    
028      /** the canvas which displays the Sprites */
029      protected AnimationCanvas canvas;
030    
031      /** the current time, used in updateInterval method */
032      private double currentTime;
033    
034      /** offset used for restarting the game */
035      private double timeOffset = 0;
036    
037      /** time since the last call to updateInterval */
038      public double timeInterval;
039    
040      /** the highest allowable time between calls to update */
041      private double maxModelTimeInterval = 1 / 10.0;
042    
043      /** all pending alarms */
044      private TreeMap<Double, LinkedList<Alarm>> alarms = new TreeMap<Double, LinkedList<Alarm>>();
045    
046      /** initializes the canvas to empty and no alarms set */
047      public FrameAdvancer() {
048        this(AnimationCanvas.DEFAULT_SIZE, AnimationCanvas.DEFAULT_BACKGROUND);
049      }
050    
051      /** initializes the canvas to size and no alarms set */
052      public FrameAdvancer(Dimension gameDimensions) {
053        this(gameDimensions, AnimationCanvas.DEFAULT_BACKGROUND);
054      }
055    
056      public FrameAdvancer(Dimension gameDimensions, Color backgroundColor) {
057        super();
058        canvas = new AnimationCanvas(gameDimensions, backgroundColor);
059        canvas.removeAllSprites();
060        alarms.clear();
061        refreshTimes = new LinkedList<Double>();
062        for (int i = 0; i < 100; i++)
063          refreshTimes.add(0.0);
064    
065      }
066    
067      /**
068       * Set the bacground color of this game. This sets the background color of
069       * both the FrameAdvancer and the contained AnimationCanvas.
070       * 
071       * @param backgroundColor
072       *          color to use for the background of the game field
073       */
074      public void setBackground(Color backgroundColor) {
075        if (canvas != null)
076          canvas.setBackground(backgroundColor);
077        super.setBackground(backgroundColor);
078      }
079    
080      /**
081       * set canvas to a new canvas
082       * 
083       * @param canvas
084       */
085      public void setCanvas(AnimationCanvas canvas) {
086        this.canvas = canvas;
087      }
088    
089      /**
090       * returns the canvas
091       * 
092       * @return canvas
093       */
094      public AnimationCanvas getCanvas() {
095        return canvas;
096      }
097    
098      /** called repeatedly between every model update */
099      public void advanceFrame(double timePassed) {
100      }
101    
102      /**
103       * returns the current time in seconds since the beginning of this game. When
104       * the game starts over, so does the time.
105       * 
106       * @return the current time in seconds
107       */
108      public double getTime() {
109        return currentTime - timeOffset;
110      }
111    
112      /**
113       * returns the screen refresh rate
114       * 
115       * @return ScreenRefreshRate
116       */
117      public double getScreenRefreshRate() {
118        return (refreshTimes.size() - 1)
119            / (refreshTimes.getLast() - refreshTimes.getFirst());
120      }
121    
122      /**
123       * sets the minimum number of frames which must be computed per second. This
124       * is separate from the screen frame rate in that more frames can be computed
125       * than displayed, but it can be affected by (and affect) the screen rate. Too
126       * high of a model frame rate can cause computation to slow down the display
127       * rate, and too low of a model frame rate can cause important events to be
128       * missed (typically the intersection of Sprites). Too high a model frame rate
129       * can slow down the advancing of time to slower than physical time. Frame
130       * rates higher than 200 Hz are not allowed.
131       * 
132       * @param framesPerSecond
133       *          the number of frames computed per second
134       */
135      public void setMinimumModelFrameRate(int framesPerSecond) {
136        if (framesPerSecond > 0) {
137          maxModelTimeInterval = Math.max(1.0 / 200, 1.0 / framesPerSecond);
138        }
139      }
140    
141      /**
142       * sets and alarm to go off relative to the current time. For example, this
143       * method can be used to set off an alarm in 5 seconds from the current time.
144       * 
145       * @param alarm
146       *          the class to call the alarm method on
147       * @param relative
148       *          the time from now in seconds to call the alarm method
149       */
150      public void scheduleRelative(Alarm alarm, double relative) {
151        scheduleAbsolute(alarm, relative + currentTime - timeOffset);
152        // double absolute=currentTime + relative;
153        // LinkedList<Alarm> existing =
154        // alarms.get(absolute);
155        // if(existing==null)
156        // existing=new LinkedList<Alarm>();
157        // existing.add(alarm);
158        // alarms.put(absolute, existing);
159      }
160    
161      public void schedule(TimedAction action, double delay) {
162        scheduleRelative(action, delay);
163      }
164    
165      /**
166       * sets and alarm to go off at a time relative to the beginning of time
167       * (zero). For example, this method can be used to set off an alarm 30 seconds
168       * from the beginning time.
169       * 
170       * @param alarm
171       *          the class to call the alarm method on
172       * @param absolute
173       *          the time in seconds to call the alarm method
174       */
175      public void scheduleAbsolute(Alarm alarm, double absolute) {
176        LinkedList<Alarm> existing = alarms.get(absolute + timeOffset);
177        if (existing == null)
178          existing = new LinkedList<Alarm>();
179        existing.add(alarm);
180        alarms.put(absolute + timeOffset, existing);
181      }
182    
183      /**
184       * removes all pending alarms on this object. If there are no alarms with this
185       * object as the target, the method call is ignored.
186       * 
187       * @param alarm
188       *          the object that is the target of a pending alarm
189       */
190      public void cancelAlarm(Alarm alarm) {
191        for (LinkedList<Alarm> list : alarms.values()) {
192          if (list.contains(alarm)) {
193            list.remove(alarm);
194          }
195        }
196      }
197    
198      public void cancel(TimedAction action) {
199        cancelAlarm(action);
200      }
201    
202      /**
203       * removes all pending alarms. If there are no pending alarms, the method call
204       * is ignored.
205       */
206      public void cancelAllAlarms() {
207        alarms.clear();
208      }
209    
210      public void cancelAllTimedActions() {
211        cancelAllAlarms();
212      }
213    
214      public TimedAction[] getAllTimedActions() {
215        LinkedList<TimedAction> all = new LinkedList<TimedAction>();
216        for (LinkedList<Alarm> list : alarms.values())
217          for (Alarm single : list)
218            if (single instanceof TimedAction)
219              all.add((TimedAction) single);
220        return all.toArray(new TimedAction[0]);
221      }
222    
223      /**
224       * gets the list of alarms scheduled to go off in the future.
225       * 
226       * @return the array of alarms in the order which they would go off
227       */
228      public Alarm[] getAlarms() {
229        LinkedList<Alarm> all = new LinkedList<Alarm>();
230        for (LinkedList<Alarm> list : alarms.values())
231          all.addAll(list);
232        return all.toArray(new Alarm[0]);
233      }
234    
235      /**
236       * updates the model between displays. This method can split the time passed
237       * if more time has passed since the last display than the maximum time
238       * interval allowable for the model frame rate.
239       * 
240       * @param time
241       *          the current absolute time
242       */
243      public void updateModel(double time) {
244        double timeInterval = time - currentTime;
245        while (timeInterval > 0) {
246          double timeToNextAlarm = Double.MAX_VALUE;
247          if (!alarms.isEmpty()) {
248            timeToNextAlarm = alarms.firstKey() - currentTime;
249          }
250          double advanced = Math.min(timeInterval, maxModelTimeInterval);
251          advanced = Math.min(advanced, timeToNextAlarm);
252          if (alarms.size() > 0 && currentTime - alarms.firstKey() == advanced) {
253            LinkedList<Alarm> alarmsToSetOff = alarms.remove(alarms.firstKey());
254            for (Alarm alarm : alarmsToSetOff) {
255              alarm.alarm();
256            }
257          }
258          canvas.updateSprites(advanced);
259          currentTime += advanced;
260          timeInterval -= advanced;
261          try {
262            // sometimes the game loses focus
263            // and stops responding to user input
264            // the following 2 lines fix this - Jam
265            if (!canvas.hasFocus())
266              canvas.requestFocusInWindow();
267            advanceFrame(advanced);
268            postAdvanceFrame(advanced);
269          } catch (Throwable e) {
270            Container container = getContentPane();
271            JPanel panel = new JPanel(new GridLayout(2, 1));
272            panel.add(container);
273            JTextArea errorMessage = new JTextArea();
274            errorMessage.setText("Cannot advanceFrame!\n");
275            errorMessage.append("Caused by Exception: " + e.toString() + "\n");
276            for (StackTraceElement ste : e.getStackTrace())
277              errorMessage.append("\t" + ste + "\n");
278            panel.add(new JScrollPane(errorMessage));
279            setContentPane(panel);
280            System.err.println("Error!");
281            e.printStackTrace();
282            ErrorConsole.addError("none", "none", e);
283            // this.pauseToggle();
284          }
285    
286        }
287      }
288    
289      /**
290       * sets the cursor for the game engine
291       * 
292       * @param filename
293       *          the image to display as the cursor
294       */
295      public void setCursor(URL url) {
296        canvas.setCursor(url);
297      }
298    
299      /** removes the cursor from the game canvas */
300      public void removeCursor() {
301        canvas.removeCursor();
302      }
303    
304      /** adds the default cursor back to the screen */
305      public void restoreCursor() {
306        canvas.restoreCursor();
307      }
308    
309      /**
310       * called after advanceFrame
311       * 
312       * @param timePassed
313       *          the duration since the last frame
314       */
315      public abstract void postAdvanceFrame(double timePassed);
316    
317      /**
318       * updates the AnimationCanvas, should only be called from the AWTEvent
319       * Thread.
320       */
321      public void refreshScreen() {
322        refreshTimes.removeFirst();
323        refreshTimes.addLast(currentTime);
324        canvas.paintImmediately();
325        // controlPanel.repaint();
326      }
327    
328      /**
329       * makes the current time zero
330       */
331      public void resetTime() {
332        timeOffset = currentTime;
333      }
334    }