Commits

Michael Ludwig committed eef1b60

Refactor and clean up how Results are passed between controllers.

  • Participants
  • Parent commits 4c8b372

Comments (0)

Files changed (5)

File src/main/java/com/lhkbob/entreri/Controller.java

      * @param c The component being removed
      */
     public void onComponentRemove(Component<?> c);
+    
+    /**
+     * Invoked when any other Controller reports a Result to the
+     * ControllerManager of this controller. Implementations may filter based on
+     * the type or value of the result. Results will only be reported during the
+     * processing of a phase. Singleton results will be reported at most once.
+     * 
+     * @param r The result being reported
+     */
+    public void report(Result r);
 
     /**
      * @return The current EntitySystem processed by this controller, or null if

File src/main/java/com/lhkbob/entreri/ControllerManager.java

 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * <p>
         PROCESS,
 
         /**
+         * <p>
          * The POSTPROCESS phase is invoked after PREPROCESS and POSTPROCESS.
          * All controllers in their manager will have their
-         * {@link Controller#postProcess(double)} method called
-         * before the frame is completed.
+         * {@link Controller#postProcess(double)} method called before the frame
+         * is completed.
+         * </p>
+         * <p>
+         * Singleton result tracking is reset after this phase completes so that
+         * singleton results can be reported at the start of the next frame.
+         * Execution timing is also completed at the end of this phase.
+         * </p>
          */
         POSTPROCESS
     }
     private final EntitySystem system;
     
     private long lastProcessTime;
+    private final Set<Class<?>> singletonResults;
+    
+    private Phase currentPhase;
 
     /**
      * Create a new ControllerManager that will store controllers and controller
         controllers = new ArrayList<Controller>();
         
         profile = new HashMap<Controller, ProfileData>();
+        singletonResults = new HashSet<Class<?>>();
         lastProcessTime = -1L;
+        
+        currentPhase = null;
     }
     
     /**
-     * Supply the given Result to all registered Controllers in this manager
-     * that listen to the type, T. Controllers declare their interest in results
-     * of type T by implementing the interface T.
+     * Report the given Result to all registered Controllers in this manager.
+     * This can only be called while a Phase is being processed.
      * 
      * @see Result
      * @param result The result to supply to registered controllers
      * @throws NullPointerException if result is null
+     * @throws IllegalStateException if not processing a phase, or if a
+     *             singleton result of the same type has already been reported
+     *             this frame
      */
-    public <T> void supply(Result<T> result) {
-        Class<T> type = result.getListenerType();
+    public void report(Result result) {
+        if (currentPhase == null)
+            throw new IllegalStateException("Can only report results while processing a phase"); 
+        if (result.isSingleton()) {
+            if (!singletonResults.add(result.getClass()))
+                throw new IllegalStateException("Singleton result of type " + result.getClass() + " already reported this frame");
+        }
+        
         int ct = controllers.size();
         for (int i = 0; i < ct; i++) {
-            if (type.isInstance(controllers.get(i))) {
-                result.supply(type.cast(controllers.get(i)));
-            }
+            controllers.get(i).report(result);
         }
     }
 
         if (phase == null)
             throw new NullPointerException("Phase cannot be null");
         
+        currentPhase = phase;
         switch(phase) {
         case PREPROCESS:
             firePreProcess(dt); break;
         case PROCESS:
             fireProcess(dt); break;
         case POSTPROCESS:
-            firePostProcess(dt); break;
+            firePostProcess(dt); 
+            break;
         }
+        currentPhase = null;
     }
 
     /**
         }
         
         lastProcessTime = System.nanoTime();
+        singletonResults.clear();
     }
     
     private static class ProfileData {

File src/main/java/com/lhkbob/entreri/Result.java

  */
 package com.lhkbob.entreri;
 
+import com.lhkbob.entreri.ControllerManager.Phase;
+
 /**
  * <p>
  * Result represents a computed result, preferably that of a bulk computation,
  * other controllers that might be interested in their computations.
  * </p>
  * <p>
- * Controllers that wish to expose results must define an interface that is
- * capable of receiving their computations. The signature of this method is
- * entirely up to the Controller. Then, the controller wraps the computed
- * results in an internal Result implementation that knows how to invoke the
- * listener interface defined previously with the just computed data. To provide
- * to all interested Controllers, the {@link ControllerManager#supply(Result)}
- * method can be used.
+ * Controllers that wish to expose results define classes that implement Result
+ * and expose the actual result data. During processing of a controller, result
+ * instances are created and supplied to all controllers by calling
+ * {@link ControllerManager#report(Result)}
  * </p>
  * <p>
- * To receive computed results, Controllers merely have to implement the
- * listener interfaces defined by the controllers of interest and store the
- * supplied results.
+ * To receive computed results, Controllers override their
+ * {@link Controller#report(Result)} and check incoming results for the
+ * desired type of result. Every controller is notified of all results, they are
+ * responsible for ignoring results they are not interested in.
  * </p>
  * 
  * @author Michael Ludwig
- * @param <T> The listener type
  */
-public interface Result<T> {
+public interface Result {
     /**
      * <p>
-     * Supply this result to the provided listener of type T. It is expected
-     * that a listener interface will define some method that enables compatible
-     * Result implementations to inject the computed data.
+     * Return true if this result is a "singleton" result. A singleton result is
+     * a type of result that is only supplied once during the processing of a
+     * frame (i.e. at the end of the {@link Phase#POSTPROCESS} phase, it is reset).The
+     * ControllerManager verifies that singleton results are supplied at most
+     * once. Most results should return false. The returned value should be the
+     * same for every instance of a type, it should not depend on the state of
+     * the instance.
      * </p>
      * <p>
-     * This injection or supplying is performed by this method. Controllers that
-     * compute results will define interfaces as appropriate to receive their
-     * results, and result implementations to provide those results.
+     * Singleton results should only be used when the computation of the result
+     * produces all of the result data. As an example, a 3D engine might assign
+     * entities to lights, and each unique configuration of lights on an entity
+     * is a "light group". It makes more sense to provide a single result that
+     * describes all light groups than individual results for each group. They
+     * are packed into a single result because each group is dependent on the
+     * others to guarantee its uniqueness.
+     * </p>
+     * <p>
+     * As a counter example, computing potentially visible sets for a 3D engine
+     * should not be a singleton result. A result that contains the view and set
+     * of visible entities is a self-contained result, other views or cameras do
+     * not affect the PVS results.
      * </p>
      * 
-     * @param listener The listener to receive the event
+     * @return True if this result should only be supplied at most once during
+     *         each frame
      */
-    public void supply(T listener);
-    
-    /**
-     * @return The listener interface this result is supplied to
-     */
-    public Class<T> getListenerType();
+    public boolean isSingleton();
 }

File src/main/java/com/lhkbob/entreri/SimpleController.java

     public void onComponentRemove(Component<?> c) {
         // do nothing in base class
     }
+    
+    @Override
+    public void report(Result result) {
+        // do nothing in base class
+    }
 
     @Override
     public EntitySystem getEntitySystem() {

File src/test/java/com/lhkbob/entreri/ControllerManagerTest.java

  */
 package com.lhkbob.entreri;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
 import org.junit.Assert;
 import org.junit.Test;
 
     }
     
     @Test
-    public void testSupplyResult() {
+    public void testReportResult() {
+        doTestReportResult(new ResultImpl());
+    }
+    
+    @Test
+    public void testReportMultipleResults() {
+        doTestReportResult(new ResultImpl(), new ResultImpl());
+    }
+    
+    @Test
+    public void testReportSingletonResult() {
+        doTestReportResult(new SingletonResultImpl());
+    }
+    
+    @Test
+    public void testReportMixedResults() {
+        doTestReportResult(new SingletonResultImpl(), new ResultImpl());
+    }
+    
+    @Test(expected=IllegalStateException.class)
+    public void testReportMultipleSingletonResults() {
+        doTestReportResult(new SingletonResultImpl(), new SingletonResultImpl());
+    }
+    
+    @Test(expected=IllegalStateException.class)
+    public void testReportOutOfPhase() {
+        Result result = new ResultImpl();
+        
         EntitySystem system = new EntitySystem();
-        ResultSupplyingController supplier = new ResultSupplyingController();
-        ListeningControler listener = new ListeningControler();
-        ControllerImpl ignored = new ControllerImpl();
+        ResultSupplyingController supplier = new ResultSupplyingController(result);
+        ControllerImpl receiver = new ControllerImpl();
         
         system.getControllerManager().addController(supplier);
-        system.getControllerManager().addController(listener);
-        system.getControllerManager().addController(ignored);
+        system.getControllerManager().addController(receiver);
         
-        Assert.assertNull(supplier.o);
-        Assert.assertNull(listener.o);
+        system.getControllerManager().report(result);
+    }
+    
+    @Test
+    public void testSingletonResultReset() {
+        Result result = new SingletonResultImpl();
+        
+        EntitySystem system = new EntitySystem();
+        ResultSupplyingController supplier = new ResultSupplyingController(result);
+        ControllerImpl receiver = new ControllerImpl();
+        
+        system.getControllerManager().addController(supplier);
+        system.getControllerManager().addController(receiver);
+        
+        Assert.assertArrayEquals(new Result[] { result }, supplier.resultsToReport);
+        Assert.assertTrue(receiver.reportedResults.isEmpty());
+        
+        // process twice
+        system.getControllerManager().process();
+        system.getControllerManager().process();
+        
+        // result list should contain 2 elements to the same result
+        Assert.assertEquals(2, receiver.reportedResults.size());
+        Assert.assertSame(result, receiver.reportedResults.get(0));
+        Assert.assertSame(result, receiver.reportedResults.get(1));
+    }
+    
+    private void doTestReportResult(Result... results) {
+        EntitySystem system = new EntitySystem();
+        ResultSupplyingController supplier = new ResultSupplyingController(results);
+        ControllerImpl receiver = new ControllerImpl();
+        
+        system.getControllerManager().addController(supplier);
+        system.getControllerManager().addController(receiver);
+        
+        Assert.assertArrayEquals(results, supplier.resultsToReport);
+        Assert.assertTrue(receiver.reportedResults.isEmpty());
         
         system.getControllerManager().process();
         
-        Assert.assertNotNull(supplier.o);
-        Assert.assertSame(supplier.o, listener.o);
+        Assert.assertEquals(Arrays.asList(results), receiver.reportedResults);
     }
     
     @Test
         Assert.assertTrue(i == ctrl.lastRemovedComponent);
     }
     
-    private static interface Listener {
-        public void setData(Object o);
-    }
-    
     private static class ResultSupplyingController extends SimpleController {
-        private Object o;
+        private final Result[] resultsToReport;
         
-        @Override
-        public void preProcess(double dt) {
-            o = new Object();
+        public ResultSupplyingController(Result... results) {
+            resultsToReport = results;
         }
         
         @Override
         public void process(double dt) {
-            getEntitySystem().getControllerManager().supply(new Result<Listener>() {
-                @Override
-                public void supply(Listener listener) {
-                    listener.setData(o);
-                }
-
-                @Override
-                public Class<Listener> getListenerType() {
-                    return Listener.class;
-                }
-            });
+            for (Result r: resultsToReport) {
+                getEntitySystem().getControllerManager().report(r);
+            }
         }
     }
     
-    private static class ListeningControler extends SimpleController implements Listener {
-        private Object o;
+    private static class ResultImpl implements Result {
         @Override
-        public void setData(Object o) {
-            this.o = o;
+        public boolean isSingleton() {
+            return false;
+        }
+    }
+    
+    private static class SingletonResultImpl implements Result {
+        @Override
+        public boolean isSingleton() {
+            return true;
         }
     }
     
         
         private EntitySystem system;
         
+        private final List<Result> reportedResults = new ArrayList<Result>();
+        
+        @Override
+        public void report(Result result) {
+            reportedResults.add(result);
+        }
+        
+        
         @Override
         public EntitySystem getEntitySystem() {
             return system;