/******************************************************************************* * Copyright 2011 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package com.badlogic.gdx.utils; import com.badlogic.gdx.Application; import com.badlogic.gdx.Files; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.LifecycleListener; /** Executes tasks in the future on the main loop thread. * @author Nathan Sweet */ public class Timer { static private final int CANCELLED = -1, FOREVER = -2; // TimerThread access is synchronized using threadLock. // Timer access is synchronized using the Timer instance. // Task access is synchronized using the Task instance. static final Object threadLock = new Object(); static TimerThread thread; /** Timer instance singleton for general application wide usage. Static methods on {@link Timer} make convenient use of this * instance. */ static public Timer instance () { synchronized (threadLock) { TimerThread thread = thread(); if (thread.instance == null) thread.instance = new Timer(); return thread.instance; } } static private TimerThread thread () { synchronized (threadLock) { if (thread == null || thread.files != Gdx.files) { if (thread != null) thread.dispose(); thread = new TimerThread(); } return thread; } } private final Array<Task> tasks = new Array(false, 8); public Timer () { start(); } /** Schedules a task to occur once as soon as possible, but not sooner than the start of the next frame. */ public Task postTask (Task task) { return scheduleTask(task, 0, 0, 0); } /** Schedules a task to occur once after the specified delay. */ public Task scheduleTask (Task task, float delaySeconds) { return scheduleTask(task, delaySeconds, 0, 0); } /** Schedules a task to occur once after the specified delay and then repeatedly at the specified interval until cancelled. */ public Task scheduleTask (Task task, float delaySeconds, float intervalSeconds) { return scheduleTask(task, delaySeconds, intervalSeconds, FOREVER); } /** Schedules a task to occur once after the specified delay and then a number of additional times at the specified * interval. */ public Task scheduleTask (Task task, float delaySeconds, float intervalSeconds, int repeatCount) { synchronized (task) { if (task.repeatCount != CANCELLED) throw new IllegalArgumentException("The same task may not be scheduled twice."); task.executeTimeMillis = System.nanoTime() / 1000000 + (long)(delaySeconds * 1000); task.intervalMillis = (long)(intervalSeconds * 1000); task.repeatCount = repeatCount; } synchronized (this) { tasks.add(task); } synchronized (threadLock) { threadLock.notifyAll(); } return task; } /** Stops the timer, tasks will not be executed and time that passes will not be applied to the task delays. */ public void stop () { synchronized (threadLock) { thread().instances.removeValue(this, true); } } /** Starts the timer if it was stopped. */ public void start () { synchronized (threadLock) { TimerThread thread = thread(); Array<Timer> instances = thread.instances; if (instances.contains(this, true)) return; instances.add(this); threadLock.notifyAll(); } } /** Cancels all tasks. */ public synchronized void clear () { for (int i = 0, n = tasks.size; i < n; i++) tasks.get(i).cancel(); tasks.clear(); } /** Returns true if the timer has no tasks in the queue. Note that this can change at any time. Synchronize on the timer * instance to prevent tasks being added, removed, or updated. */ public synchronized boolean isEmpty () { return tasks.size == 0; } synchronized long update (long timeMillis, long waitMillis) { for (int i = 0, n = tasks.size; i < n; i++) { Task task = tasks.get(i); synchronized (task) { if (task.executeTimeMillis > timeMillis) { waitMillis = Math.min(waitMillis, task.executeTimeMillis - timeMillis); continue; } if (task.repeatCount != CANCELLED) { if (task.repeatCount == 0) task.repeatCount = CANCELLED; task.app.postRunnable(task); } if (task.repeatCount == CANCELLED) { tasks.removeIndex(i); i--; n--; } else { task.executeTimeMillis = timeMillis + task.intervalMillis; waitMillis = Math.min(waitMillis, task.intervalMillis); if (task.repeatCount > 0) task.repeatCount--; } } } return waitMillis; } /** Adds the specified delay to all tasks. */ public synchronized void delay (long delayMillis) { for (int i = 0, n = tasks.size; i < n; i++) { Task task = tasks.get(i); synchronized (task) { task.executeTimeMillis += delayMillis; } } } /** Schedules a task on {@link #instance}. * @see #postTask(Task) */ static public Task post (Task task) { return instance().postTask(task); } /** Schedules a task on {@link #instance}. * @see #scheduleTask(Task, float) */ static public Task schedule (Task task, float delaySeconds) { return instance().scheduleTask(task, delaySeconds); } /** Schedules a task on {@link #instance}. * @see #scheduleTask(Task, float, float) */ static public Task schedule (Task task, float delaySeconds, float intervalSeconds) { return instance().scheduleTask(task, delaySeconds, intervalSeconds); } /** Schedules a task on {@link #instance}. * @see #scheduleTask(Task, float, float, int) */ static public Task schedule (Task task, float delaySeconds, float intervalSeconds, int repeatCount) { return instance().scheduleTask(task, delaySeconds, intervalSeconds, repeatCount); } /** Runnable that can be scheduled on a {@link Timer}. * @author Nathan Sweet */ static abstract public class Task implements Runnable { final Application app; long executeTimeMillis, intervalMillis; int repeatCount = CANCELLED; public Task () { app = Gdx.app; // Store which app to postRunnable (eg for multiple LwjglAWTCanvas). if (app == null) throw new IllegalStateException("Gdx.app not available."); } /** If this is the last time the task will be ran or the task is first cancelled, it may be scheduled again in this * method. */ abstract public void run (); /** Cancels the task. It will not be executed until it is scheduled again. This method can be called at any time. */ public synchronized void cancel () { executeTimeMillis = 0; repeatCount = CANCELLED; } /** Returns true if this task is scheduled to be executed in the future by a timer. The execution time may be reached at any * time after calling this method, which may change the scheduled state. To prevent the scheduled state from changing, * synchronize on this task object, eg: * * <pre> * synchronized (task) { * if (!task.isScheduled()) { ... } * } * </pre> */ public synchronized boolean isScheduled () { return repeatCount != CANCELLED; } /** Returns the time in milliseconds when this task will be executed next. */ public synchronized long getExecuteTimeMillis () { return executeTimeMillis; } } /** Manages a single thread for updating timers. Uses libgdx application events to pause, resume, and dispose the thread. * @author Nathan Sweet */ static class TimerThread implements Runnable, LifecycleListener { final Files files; final Array<Timer> instances = new Array(1); Timer instance; private long pauseMillis; public TimerThread () { files = Gdx.files; Gdx.app.addLifecycleListener(this); resume(); Thread thread = new Thread(this, "Timer"); thread.setDaemon(true); thread.start(); } public void run () { while (true) { synchronized (threadLock) { if (thread != this || files != Gdx.files) break; long waitMillis = 5000; if (pauseMillis == 0) { long timeMillis = System.nanoTime() / 1000000; for (int i = 0, n = instances.size; i < n; i++) { try { waitMillis = instances.get(i).update(timeMillis, waitMillis); } catch (Throwable ex) { throw new GdxRuntimeException("Task failed: " + instances.get(i).getClass().getName(), ex); } } } if (thread != this || files != Gdx.files) break; try { if (waitMillis > 0) threadLock.wait(waitMillis); } catch (InterruptedException ignored) { } } } dispose(); } public void resume () { synchronized (threadLock) { long delayMillis = System.nanoTime() / 1000000 - pauseMillis; for (int i = 0, n = instances.size; i < n; i++) instances.get(i).delay(delayMillis); pauseMillis = 0; threadLock.notifyAll(); } } public void pause () { synchronized (threadLock) { pauseMillis = System.nanoTime() / 1000000; threadLock.notifyAll(); } } public void dispose () { // OK to call multiple times. synchronized (threadLock) { if (thread == this) thread = null; instances.clear(); threadLock.notifyAll(); } Gdx.app.removeLifecycleListener(this); } } }