/* * Copyright 2013 MovingBlocks * * 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 org.terasology.rendering.logic; import com.google.common.collect.Lists; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.terasology.entitySystem.entity.EntityRef; import org.terasology.logic.location.DistanceComparator; import org.terasology.logic.location.LocationComponent; import org.terasology.rendering.cameras.Camera; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Timer; import java.util.TimerTask; /** * This data structure takes Entities with a location in the world and sorts * them based on their distance to an other entity. * <br><br> * The sorting is done in a background thread. * <br><br> * When retrieving Entities from this container, no guarantees are given on the * sorting of the entities. This class only tries to keep the elements sorted, * but does not guarantee it. * <br><br> * It it therefor use full for graphics purposes, to keep track of the nearest * entities to draw. * */ public class NearestSortingList implements Iterable<EntityRef> { private static final Logger logger = LoggerFactory.getLogger(NearestSortingList.class); private List<EntityRef> entities = Lists.newLinkedList(); private final List<Command> commands = Lists.newArrayList(); private Timer timer; private SortTask sortingTask; private Thread sortingThread; /** * True while the background sorting process is active. */ private boolean sorting; /** * The delay in ms to wait between each sorting run. */ private long sortPeriod = 50; /** * Default value is 50 milliseconds. * * @return the amount of milliseconds between the start of two consecutive * sorting runs. (Unless sorting takes longer than this value in ms.) */ public long getSortPeriod() { return sortPeriod; } /** * @return the amount of elements in this list. */ public int size() { return entities.size(); } /** * @return true if there are no elements in this container. */ public boolean isEmpty() { return entities.isEmpty(); } public boolean contains(EntityRef e) { return entities.contains(e); } /** * Add an Entity with a LocationComponent to this container. Note that it * will be inserted, rather than appended. So until a new sorting pass has * been made, this new entity is returned whenever entities are requested * from this container. * * @param e The entity to add. Must have a LocationComponent or an * IlligalArgumentException is thrown. */ public synchronized void add(EntityRef e) { if (e.getComponent(LocationComponent.class) == null) { logger.warn("Adding entity without LocationComponent to container that sorts on location. Entity: {}", e); } //new entities are inserted to make sure that new entities are drawn first. //Since it is likely the players wants to see new entities over existing ones //And it is likely new entities spawn near the player. entities.add(0, e); if (sorting) { commands.add(new AddCommand(e)); } } /** * Remove an entity from this container. * * @param e the entity to remove. Must have a LocationComponent or an * IllegalArgumentException is thrown. If the entity does not have a * LocationComponent it cannot reside in this container. */ public synchronized void remove(EntityRef e) { entities.remove(e); if (sorting) { commands.add(new RemoveCommand(e)); } } /** * Removes all elements from this container. */ public synchronized void clear() { entities.clear(); if (sorting) { //There is no need to execute all additions and removals if the list //will be cleared, so we can safely clearn the pending commands. commands.clear(); commands.add(new ClearCommand()); } } /** * Warning: this method is memory intensive, as the list is copied. The * copying is required to ensure thread safety. Returns a normal iterator * over all Entities in this collection. While this class attempts to keep * the elements sorted based on the distance to the player. This is not * guaranteed. The sorting tries to put closer objects on a lower index, * hence they will returned first by this iterator. * * @return An Iterator over all Entities in this collection. */ @Override public Iterator<EntityRef> iterator() { return cloneEntities().iterator(); } /** * Warning: this method is memory intensive, as the list is copied. The * copying is required to ensure thread safety. Similar to iterator(), but * this version returns a ListIterator, which has some additional * functionality. * * @return A ListIterator over all Entities in this collection. */ public ListIterator<EntityRef> listIterator() { return cloneEntities().listIterator(); } /** * Returns a copy of the entities in this container. Although it is not * guaranteed the list is sorted, attempts have been made to put entities * nearer to the player at a lower index. * * @return a list with all entities in this container. */ public List<EntityRef> getEntities() { return cloneEntities(); } /** * Fills the given array with Entities from this container. Attempts are * made to put the Entities nearest to the player in this array and nearer * entities are expected, but not guaranteed to be at a lower index. * <br><br> * This is the most memory friendly way to obtain elements from this * container. * * @param output The array to fill with entities from this container. * @return The amount of entities that were put into the array. If there are * less entities in this container than the size of output, this * number will be this.size(). Otherwise it will be output.length */ public synchronized int getNearest(EntityRef[] output) { int size = Math.min(size(), output.length); Iterator<EntityRef> iter = entities.iterator(); for (int x = 0; x < size; x++) { output[x] = iter.next(); } return size; } /** * Returns the entities that are expected to be the nearest to the player. * It is not guaranteed they are the nearest entities though. * * @param count the number of entities to return. * @return An array with Entities. Attempts have been made to put the * Entities that are closer to the player at a lower index. The size * of this array equals min(count, size()). */ public EntityRef[] getNearest(int count) { EntityRef[] output = new EntityRef[Math.min(count, size())]; getNearest(output); return output; } /** * Calling this method starts the background sorting. If never called, the * elements in this container are never sorted! * * @param origin The entity to sort around. When using the getNearest * methods, this container has tried to put entities nearer to the entity * given here at a lower index. */ public synchronized void initialise(Camera origin) { initialise(origin, 50, 0); } /** * Initializes the background sorting without starting the sorting * yet. This can later be done with the continueSorting() method. * * @param origin The entity to sort around. When using the getNearest * methods, this container has tried to put entities nearer to the entity * given here at a lower index. */ public synchronized void initialiseAndPause(Camera origin) { if (sortingTask != null || timer != null) { logger.error("Mis-usages of initialise detected! Initialising again" + " before stopping the sorting process. Sorting is " + "stopped now, but it should be done by the user of " + "this class."); stop(); } sortingTask = new SortTask(origin); } /** * Same as initialise(), but allows the user to specify an amount * of milliseconds to wait before the first sorting run. * * @param origin * @param period The minimum time between sorts. * @param initialDelay delay before the first sorting run. The frequency of * sorting runs can be set with the setSortDelayMS(long) method. */ public synchronized void initialise(Camera origin, long period, long initialDelay) { if (sortingTask != null || timer != null) { logger.error("Mis-usages of initialise detected! Initialising again" + " before stopping the sorting process. Sorting is " + "stopped now, but it should be done by the user of " + "this class."); stop(); } sortPeriod = period; timer = new Timer(); sortingTask = new SortTask(origin); timer.scheduleAtFixedRate(sortingTask, initialDelay, sortPeriod); } /** * @return true if this container has been initialised, false otherwise. Initialised containers have started sorting. */ public boolean isInitialised() { return sortingTask != null; } /** * Stops the background sorting without deleting clearing this container. * This is required for proper clean-up. * <br><br> * Note that if a sorting process is running while this method is called, * the sorting process finishes sorting this method will wait for it to * finish. Afterwards the sorting is not scheduled again until the * initialize method is called again. * <br><br> * Note that calling stop() and clear() can be done in any order and the * specified behaviour will be exactly the same. If there is a difference it * is an insignificant performance loss or win if. */ public synchronized void stop() { if (timer != null) { //stopping a paused instance timer.cancel(); timer.purge(); timer = null; } if (sortingThread != null) { try { sortingThread.join(); } catch (InterruptedException ex) { logger.error("Joining of sorting thread was interrupted!"); } } sortingThread = null; sortingTask = null; } /** * Although it has the exact same function as getEntries(), it reads easier * inside this class when the word 'clone' is used, rather than 'get'. * * @return A copy of the entities in this container. */ private synchronized List<EntityRef> cloneEntities() { return Lists.newLinkedList(entities); } /** * These two actions needed to happen atomically and the easier method was * to put them in a synchronized method. * * @return cloneEntities() */ private synchronized List<EntityRef> cloneAndSetSorting() { sorting = true; return cloneEntities(); } /** * Updates the sorted list with all changes made while sorting before * swapping the lists. * * @param newEntities the newly sorted list with entities. All changes will * be applied to this list before the entities of this * collection are set to this list. */ private synchronized void processQueueAndSetEntities(List<EntityRef> newEntities) { //Note that the commands are executed in the order they are added to the list. for (Command c : commands) { c.executeOn(newEntities); } commands.clear(); entities = newEntities; sorting = false; } /** * Clear the command queue in a synchronized way. Used when the sorting * fails. */ private synchronized void clearQueue() { commands.clear(); } /** * The commands are used to store addition, removal and clear operations * when the background process is sorting the entities. */ private interface Command { void executeOn(List<EntityRef> entities); } private static class AddCommand implements Command { private EntityRef toAdd; AddCommand(EntityRef toAdd) { this.toAdd = toAdd; } @Override public void executeOn(List<EntityRef> entities) { entities.add(0, toAdd); } } private static class RemoveCommand implements Command { private EntityRef toRem; RemoveCommand(EntityRef toRemove) { toRem = toRemove; } @Override public void executeOn(List<EntityRef> entities) { entities.remove(toRem); } } private static class ClearCommand implements Command { @Override public void executeOn(List<EntityRef> entities) { entities.clear(); } } /** * The TimerTask that does the sorting work. */ private class SortTask extends TimerTask { private DistanceComparator comparator = new DistanceComparator(); private Camera originCamera; /** * @param origin The entities of a NearestSortingCollection will be * sorted based on their distance to this entity. */ SortTask(Camera origin) { originCamera = origin; } @Override public void run() { sortingThread = Thread.currentThread(); try { sort(); } catch (Exception ex) { /** * We don't want the failure of the sorter to cause the entire * game to crash. Instead we shall output an error to the logger * and continue. */ //for ArrayIndexOutOfBoundsException see issue #2742 logger.error("Uncaught exception in sorting thread", ex); } } /** * Sorts the entities of this container. Can be executed concurrently * with the other operations on this container. */ private void sort() { comparator.setOrigin(originCamera.getPosition()); if (!commands.isEmpty()) { logger.warn("The commands list was not emptied properly!"); commands.clear(); } /** * Note that while cloneAndSetSorting() and * processQueueAndSetEntities() are synchronized, this method itself * and the sorting are not. This means that the actual sorting can * be done concurrently with any other operations. */ List<EntityRef> newEnts = cloneAndSetSorting(); try { Collections.sort(newEnts, comparator); } catch (IllegalArgumentException ex) { logger.warn("Entities destroyed during sorting process. Sorting is skipped this round."); clearQueue(); return; } processQueueAndSetEntities(newEnts); } } }