Commits

Michael Ludwig  committed 928c653

Improve controller data interface, add more event hooks for controllers, improve transient property handling, and improve default value support, as well as bug fixes.

  • Participants
  • Parent commits 0cff1dc

Comments (0)

Files changed (21)

File src/main/java/com/googlecode/entreri/AbstractController.java

+/*
+ * Entreri, an entity-component framework in Java
+ *
+ * Copyright (c) 2011, Michael Ludwig
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ *     Redistributions of source code must retain the above copyright notice,
+ *         this list of conditions and the following disclaimer.
+ *     Redistributions in binary form must reproduce the above copyright notice,
+ *         this list of conditions and the following disclaimer in the
+ *         documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.googlecode.entreri;
+
+/**
+ * AbstractController implements Controller by performing no action on each of
+ * Controller's process or event hooks. Subclasses can override just the methods
+ * they are interested in implementing.
+ * 
+ * @author Michael Ludwig
+ */
+public abstract class AbstractController implements Controller {
+
+    @Override
+    public void preProcess(EntitySystem system, float dt) {
+        // do nothing in base class
+    }
+
+    @Override
+    public void process(EntitySystem system, float dt) {
+        // do nothing in base class
+    }
+
+    @Override
+    public void postProcess(EntitySystem system, float dt) {
+        // do nothing in base class
+    }
+
+    @Override
+    public void addedToSystem(EntitySystem system) {
+        // do nothing in base class
+    }
+
+    @Override
+    public void removedFromSystem(EntitySystem system) {
+        // do nothing in base class
+    }
+
+    @Override
+    public void onEntityAdd(Entity e) {
+        // do nothing in base class
+    }
+
+    @Override
+    public void onEntityRemove(Entity e) {
+        // do nothing in base class
+    }
+
+    @Override
+    public void onComponentAdd(Component c) {
+        // do nothing in base class
+    }
+
+    @Override
+    public void onComponentRemove(Component c) {
+        // do nothing in base class
+    }
+}

File src/main/java/com/googlecode/entreri/Component.java

 
 import com.googlecode.entreri.property.IndexedDataStore;
 import com.googlecode.entreri.property.Property;
+import com.googlecode.entreri.property.PropertyFactory;
+import com.googlecode.entreri.property.Unmanaged;
 
 /**
  * <p>
      * component in the system.
      */
     int index;
-
+    
     final ComponentIndex<?> owner;
+    
     private final TypedId<? extends Component> typedId;
 
     /**
      * configured its declared properties. This is only called when the
      * Component is being added to an Entity. This is not called when a new
      * component instance is created for the purposes of a fast iterator
-     * (because it's just acting as a shell in that case).
+     * (because it's just acting as a shell in that case), or being cloned from
+     * a template.
      * </p>
      * <p>
      * The var-args initParams are the initial object parameters, in the same
      * sure to call super with arguments matching its super-type's InitParams
      * annotation.
      * </p>
+     * <p>
+     * Note that in many cases the default value (usually 0 or null), assigned
+     * by the {@link PropertyFactory} that created the component's properties
+     * could be sufficient.
+     * </p>
+     * 
      * @param initParams The initial parameters for the Component
      */
     protected abstract void init(Object... initParams) throws Exception;
     public final int getIndex() {
         return index;
     }
-    
+
+    /**
+     * @return True if this Component is still attached to an Entity, or false
+     *         if it has been removed
+     */
+    public final boolean isLive() {
+        return index != 0;
+    }
+
+    /**
+     * <p>
+     * Component's hashCode() returns the entity id of the component's owning
+     * entity. This means that any Component instance of this type that has the same index in
+     * the same EntitySystem will correctly use the same hash code.
+     * </p>
+     * <p>
+     * This means you can use the components created by a system's fast
+     * iterators to query a hash-based collections. However, you should never
+     * store fast components into a hash-based collection because their index
+     * (and thus hash code) will change each iteration.
+     * </p>
+     * <p>
+     * Additionally, a component's index is updated when it is removed from an
+     * entity or system. This means it is critical to remove components from
+     * collections before they a removed from a system. This can be done in
+     * {@link Controller#onComponentRemove(Component)}.
+     * </p>
+     * 
+     * @throws IllegalStateException if the component has already been removed
+     */
     @Override
     public int hashCode() {
-        return index;
+        if (!isLive())
+            throw new IllegalStateException("Component is not alive anymore");
+        return owner.getEntitySystem().getEntityId(owner.getEntityIndex(index));
     }
-    
+
+    /**
+     * <p>
+     * Component's equals() returns true if the object is another component of
+     * the same type, with the same owning Entity. This means that any component
+     * of this type that has the same index in the same entity system will
+     * correctly be treated as equal.
+     * </p>
+     * <p>
+     * This means you can use the components created by a system's fast
+     * iterators to query equals-based collections. However, you should never
+     * store fast components into a equals-based collections because their index
+     * (and thus definition of equality) will change with each iteration.
+     * </p>
+     * <p>
+     * Additionally, a component's index is updated when it is removed from an
+     * entity or system. This means it is critical to remove components from
+     * collections before they are removed from the system. This can be done in
+     * {@link Controller#onComponentRemove(Component)}.
+     * </p>
+     * 
+     * @param o The object to test equality with
+     * @return True if the two instances represent the same conceptual component
+     * @throws IllegalStateException if the component has already been removed
+     */
     @Override
     public boolean equals(Object o) {
+        if (!isLive())
+            throw new IllegalStateException("Component is not alive anymore");
+        
         if (!(o instanceof Component))
             return false;
         Component c = (Component) o;
-        if (c.owner == owner && c.typedId == typedId) {
-            // We can't use index because a canonical component might have it change,
-            // instead use its owning entity
-            int tei = owner.getEntitySystem().getEntityId(owner.getEntityIndex(index));
-            int cei = c.owner.getEntitySystem().getEntityId(c.owner.getEntityIndex(c.index));
-            return tei == cei;
+        if (c.owner == owner) {
+            // We use the hash code, since it is either the owner's id (unchanging)
+            // or the cached id from just before it was removed
+            return hashCode() == c.hashCode();
         } else {
             // type and owner don't match
             return false;
      * do not have this restriction.</li>
      * <li>Any non-static fields defined in a Component (abstract or concrete)
      * must implement Property and be declared private or protected, or be
-     * transient.</li>
+     * annotated with {@link Unmanaged} (in which case the field is ignored.</li>
      * </ul>
      * Additionally, abstract Component types cannot have a TypedId assigned to
      * them.
      * </p>
-     * <p>
-     * A note on transient fields when defining a component. Transient fields
-     * that are not properties are completely unmanaged. They are intended for
-     * caching at the level of Component instance, and not storing managed
-     * component data (i.e. the instance created for a fast iterator transient
-     * fields are different from the canonical instance). Transient fields that
-     * are Properties are still managed, and are intended for caching at the
-     * level of conceptual component. Their values are not copied when a
-     * Component is used as a template, but are otherwise like regular
-     * properties.
-     * </p>
      * 
      * @param <T> The Component class type
      * @param type The Class whose TypedId is fetched, which must be a subclass

File src/main/java/com/googlecode/entreri/ComponentBuilder.java

 import java.util.List;
 import java.util.Map;
 
+import com.googlecode.entreri.property.AbstractPropertyFactory;
 import com.googlecode.entreri.property.Factory;
 import com.googlecode.entreri.property.Parameter;
 import com.googlecode.entreri.property.Parameters;
 import com.googlecode.entreri.property.Property;
 import com.googlecode.entreri.property.PropertyFactory;
+import com.googlecode.entreri.property.Unmanaged;
 
 /**
  * <p>
     }
 
     /**
-     * Create a new map from field to property instances that can be shared by
-     * all instances of the Component type built by this builder for a single
-     * EntitySystem. It can be passed into
+     * Get the map from Fields of the component type to PropertyFactory
+     * implementations that will create valid property objects for each field.
+     * The created properties can then form a map valid with
      * {@link #newInstance(EntitySystem, int, Map)}.
      * 
-     * @return A new list of properties used to set the property fields in the
-     *         component
+     * @return A map from field to property factory for the type associated with
+     *         this builder
      */
-    public Map<Field, Property> createProperties() {
-        Map<Field, Property> props = new HashMap<Field, Property>(propertyFactories.size());
+    public Map<Field, PropertyFactory<?>> getPropertyFactories() {
+        Map<Field, PropertyFactory<?>> props = new HashMap<Field, PropertyFactory<?>>();
         for (int i = 0; i < propertyFactories.size(); i++)
-            props.put(fields.get(i), propertyFactories.get(i).create());
+            props.put(fields.get(i), propertyFactories.get(i));
         return Collections.unmodifiableMap(props);
     }
 
      * is used to assign values to the declared property fields of the type.
      * </p>
      * <p>
-     * It is assumed that the map was previously returned from a call to
-     * {@link #createProperties()}.
+     * It is assumed that the map was created by the factories returned from
+     * {@link #getPropertyFactories()}. Additionally, it is assumed that
+     * {@link PropertyFactory#setValue(Property, int)} is invoked by the caller
+     * as appropriate (no initialization is performed by the builder).
      * </p>
      * 
      * @param system The owning EntitySystem
             if (Modifier.isStatic(modifiers))
                 continue; // ignore static fields
             
+            if (declared[i].isAnnotationPresent(Unmanaged.class))
+                continue; // ignore the field
+            
             if (!Property.class.isAssignableFrom(declared[i].getType())) {
-                if (Modifier.isTransient(modifiers))
-                    continue; // ignore this field
-                else
-                    throw new IllegalComponentDefinitionException(type, "Component has non-Property field that is not transient: " + declared[i]);
+                throw new IllegalComponentDefinitionException(type, "Component has non-Property field that is not transient: " + declared[i]);
             }
             
             if (!Modifier.isPrivate(modifiers) && !Modifier.isProtected(modifiers))
         return nonTransientFields;
     }
     
-    private static class ReflectionPropertyFactory<P extends Property> implements PropertyFactory<P> {
+    private static class ReflectionPropertyFactory<P extends Property> extends AbstractPropertyFactory<P> {
         private final Constructor<P> ctor;
         private final Object[] values;
         

File src/main/java/com/googlecode/entreri/ComponentIndex.java

 package com.googlecode.entreri;
 
 import java.lang.reflect.Field;
-import java.lang.reflect.Modifier;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
     
     private int componentInsert;
 
-    private final List<PropertyStore> declaredProperties;
-    private final List<PropertyStore> decoratedProperties;
+    private final List<PropertyStore<?>> declaredProperties;
+    private final List<PropertyStore<?>> decoratedProperties;
     
     private final ComponentBuilder<T> builder;
     private final Map<Field, Property> builderProperties; // Properties from declaredProperties, cached for newInstance()
-    private final Class<?>[] initParams; // all primitives will be boxed at this point
+    private final Class<?>[] initParamTypes; // all primitives will be boxed at this point
     
     private final EntitySystem system;
     
      * @param type The type of component
      * @throws NullPointerException if system or type are null
      */
+    @SuppressWarnings({ "rawtypes", "unchecked" })
     public ComponentIndex(EntitySystem system, TypedId<T> type) {
         if (system == null || type == null)
             throw new NullPointerException("Arguments cannot be null");
         
         this.system = system;
-        initParams = getInitParams(type.getType());
+        initParamTypes = getInitParams(type.getType());
         
         builder = Component.getBuilder(type);
         
-        builderProperties = builder.createProperties();
+        Map<Field, PropertyFactory<?>> builderPropertyFactories = builder.getPropertyFactories();
+        builderProperties = new HashMap<Field, Property>();
         
-        declaredProperties = new ArrayList<PropertyStore>();
-        decoratedProperties = new ArrayList<PropertyStore>(); // empty for now
-        for (Entry<Field, Property> e: builderProperties.entrySet()) {
-            boolean isTransient = Modifier.isTransient(e.getKey().getModifiers());
-            declaredProperties.add(new PropertyStore(e.getValue(), isTransient));
+        declaredProperties = new ArrayList<PropertyStore<?>>();
+        decoratedProperties = new ArrayList<PropertyStore<?>>(); // empty for now
+        for (Entry<Field, PropertyFactory<?>> e: builderPropertyFactories.entrySet()) {
+            PropertyStore store = new PropertyStore(e.getValue());
+            declaredProperties.add(store);
+            builderProperties.put(e.getKey(), store.property);
         }
         
         entityIndexToComponentIndex = new int[1]; // holds default 0 value in 0th index
      * Convenience to create a new data store for each property with the given
      * size, copy the old data over, and assign it back to the property.
      */
-    private void resizePropertyStores(List<PropertyStore> properties, int size) {
+    private void resizePropertyStores(List<PropertyStore<?>> properties, int size) {
         int ct = properties.size();
         for (int i = 0; i < ct; i++) {
             IndexedDataStore oldStore = properties.get(i).property.getDataStore();
      * @return A new component of type T
      * @throws NullPointerException if fromTemplate is null
      */
+    @SuppressWarnings({ "unchecked", "rawtypes" })
     public T addComponent(int entityIndex, T fromTemplate) {
         T instance = addComponent(entityIndex);
 
         // Copy values from fromTemplate's properties to the new instances
-        List<PropertyStore> templateProps = fromTemplate.owner.declaredProperties;
+        List<PropertyStore<?>> templateProps = fromTemplate.owner.declaredProperties;
         for (int i = 0; i < templateProps.size(); i++) {
-            if (!templateProps.get(i).transientProperty) {
-                templateProps.get(i).property.getDataStore().copy(fromTemplate.index, 1,
-                                                                  declaredProperties.get(i).property.getDataStore(), 
-                                                                  instance.getIndex());
-            }
+            PropertyStore src = templateProps.get(i);
+            PropertyStore dst = declaredProperties.get(i);
+            
+            src.clone(fromTemplate.index, dst.property, instance.index);
         }
         
+        // fire add-event listener after cloning is completed
+        system.getControllerManager().fireComponentAdd(instance);
         return instance;
     }
 
             initParams = new Object[0];
         
         boolean valid = true;
-        if (initParams.length == this.initParams.length) {
+        if (initParams.length == this.initParamTypes.length) {
             for (int i = 0; i < initParams.length; i++) {
-                if (!this.initParams[i].isInstance(initParams[i])) {
+                if (!this.initParamTypes[i].isInstance(initParams[i])) {
                     valid = false;
                     break;
                 }
         }
 
         if (!valid)
-            throw new IllegalArgumentException("Must provide init params in the order: " + Arrays.toString(this.initParams));
+            throw new IllegalArgumentException("Must provide init params in the order: " + Arrays.toString(this.initParamTypes));
 
         // We know the arguments types match, so continue
         T instance = addComponent(entityIndex);
             throw new IllegalArgumentException("Init parameters failed validation", e);
         }
         
+        // fire add-event listener after initialization is completed
+        system.getControllerManager().fireComponentAdd(instance);
         return instance;
     }
     
     /*
      * Allocate and store a new component, but don't initialize it yet.
      */
-    @SuppressWarnings("unchecked")
     private T addComponent(int entityIndex) {
         if (entityIndexToComponentIndex[entityIndex] != 0)
             removeComponent(entityIndex);
         componentIndexToEntityIndex[componentIndex] = entityIndex;
         entityIndexToComponentIndex[entityIndex] = componentIndex;
 
-        // Copy default value for declared and decorated properties
+        // Set default value for declared and decorated properties,
+        // this is needed because we might be overwriting a previously removed
+        // component, or the factory might be doing something tricky
         for (int i = 0; i < declaredProperties.size(); i++) {
-            PropertyStore p = declaredProperties.get(i);
-            p.defaultData.copy(0, 1, p.property.getDataStore(), componentIndex);
+            declaredProperties.get(i).setValue(componentIndex);
         }
         
         for (int i = 0; i < decoratedProperties.size(); i++) {
-            PropertyStore p = decoratedProperties.get(i);
-            p.defaultData.copy(0, 1, p.property.getDataStore(), componentIndex);
+            decoratedProperties.get(i).setValue(componentIndex);
         }
         
-        return (T) components[componentIndex];
+        return instance;
     }
 
     /**
 
         // This code works even if componentIndex is 0
         Component oldComponent = components[componentIndex];
+        if (oldComponent != null) {
+            // perform component clean up before data is invalidated
+            system.getControllerManager().fireComponentRemove(oldComponent);
+            oldComponent.index = 0;
+        }
 
         components[componentIndex] = null;
         entityIndexToComponentIndex[entityIndex] = 0; // entity does not have component
         componentIndexToEntityIndex[componentIndex] = 0; // component does not have entity
         
-        // Make all removed component instances point to the 0th index
-        if (oldComponent != null)
-            oldComponent.index = 0;
-        
         return oldComponent != null;
     }
 
      * Update all component data in the list of properties. If possible the data
      * store in swap is reused.
      */
-    private void update(List<PropertyStore> properties, Component[] newToOldMap) {
+    private void update(List<PropertyStore<?>> properties, Component[] newToOldMap) {
         for (int i = 0; i < properties.size(); i++) {
-            PropertyStore p = properties.get(i);
+            PropertyStore<?> p = properties.get(i);
             IndexedDataStore origStore = p.property.getDataStore();
             
             p.property.setDataStore(update(origStore, p.swap, newToOldMap));
         return dst;
     }
     
-    private void notifyCompactAwareProperties(List<PropertyStore> props) {
-        PropertyStore p;
+    private void notifyCompactAwareProperties(List<PropertyStore<?>> props) {
+        PropertyStore<?> p;
         for (int i = 0; i < props.size(); i++) {
             p = props.get(i);
             if (p.property instanceof CompactAwareProperty)
      * @return The property decorated onto the type of the index
      */
     public <P extends Property> P decorate(PropertyFactory<P> factory) {
-        P prop = factory.create();
-        
-        int size = (declaredProperties.isEmpty() ? componentInsert + 1 
+        int size = (declaredProperties.isEmpty() ? componentInsert
                                                  : declaredProperties.get(0).property.getDataStore().size());
         
-        PropertyStore pstore = new PropertyStore(prop, true);
+        PropertyStore<P> pstore = new PropertyStore<P>(factory);
 
-        // Copy original values from factory property over to all component slots
-        IndexedDataStore newStore = prop.getDataStore().create(size);
+        // Set values from factory to all component slots
+        IndexedDataStore newStore = pstore.property.getDataStore().create(size);
+        pstore.property.setDataStore(newStore);
         for (int i = 1; i < size; i++) {
-            // This assumes that the property stores its data in the 0th index
-            pstore.defaultData.copy(0, 1, newStore, i);
+            pstore.setValue(i);
         }
-        prop.setDataStore(newStore);
         
         decoratedProperties.add(pstore);
-        return prop;
+        return pstore.property;
     }
 
     /**
      * @param p The property to remove
      */
     public void undecorate(Property p) {
-        Iterator<PropertyStore> it = decoratedProperties.iterator();
+        Iterator<PropertyStore<?>> it = decoratedProperties.iterator();
         while(it.hasNext()) {
             if (it.next().property == p) {
                 it.remove();
         }
     }
     
-    private static class PropertyStore {
-        final Property property;
-        final boolean transientProperty;
-        final IndexedDataStore defaultData; // if not null, has a single component
-
+    private static class PropertyStore<P extends Property> {
+        final P property;
+        final PropertyFactory<P> creator;
         IndexedDataStore swap; // may be null
         
         
-        public PropertyStore(Property p, boolean isTransient) {
-            property = p;
-            transientProperty = isTransient;
-            
-            defaultData = property.getDataStore().create(1);
-            property.getDataStore().copy(0, 1, defaultData, 0);
+        public PropertyStore(PropertyFactory<P> creator) {
+            this.creator = creator;
+            property = creator.create();
+        }
+        
+        private void clone(int srcIndex, P dst, int dstIndex) {
+            creator.clone(property, srcIndex, dst, dstIndex);
+        }
+        
+        private void setValue(int index) {
+            creator.setValue(property, index);
         }
     }
 }

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

 package com.googlecode.entreri;
 
 /**
+ * <p>
  * Controllers are functional processors of the entities and components within
  * an EntitySystem. Different Controller implementations have different
  * purposes, such as updating transforms, computing physics or AI, or rendering
  * a scene. Generally controller implementations should be as small and as
  * independent as possible to allow for reuse and easy composability.
+ * </p>
+ * <p>
+ * Controllers also have event listener method hooks so that they can be
+ * notified when they are added or removed from an EntitySystem, or when an
+ * Entity or component in a system they're attached to is added or removed.
+ * </p>
  * 
  * @author Michael Ludwig
  */
      * @param dt The elapsed time since the last processing
      */
     public void postProcess(EntitySystem system, float dt);
+    
+    public void addedToSystem(EntitySystem system);
+    
+    public void removedFromSystem(EntitySystem system);
+    
+    public void onEntityAdd(Entity e);
+    
+    public void onEntityRemove(Entity e);
+    
+    public void onComponentAdd(Component c);
+    
+    public void onComponentRemove(Component c);
 }

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

  */
 package com.googlecode.entreri;
 
-import java.lang.annotation.Annotation;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.ConcurrentHashMap;
  */
 public class ControllerManager {
     /**
+     * Key represents a typed key into controller data managed by a
+     * ControllerManager. Equality is defined by reference, so it is generally
+     * best to store keys for reuse in private or public fields (possibly static
+     * if the key is shared).
+     * 
+     * @param <T> The type of data associated with the key
+     */
+    public static class Key<T> { }
+    
+    /**
      * The Phase enum represents the different phases of
      * processing that an EntitySystem can go through during
      * what is often considered a "frame".
     
     // This is a concurrent map so that parallel controllers can access it efficiently
     // - the rest of the class is assumed to be single-threaded
-    private final ConcurrentHashMap<Class<? extends Annotation>, Object> controllerData;
-    private final ConcurrentHashMap<Object, Object> privateData;
+    private final ConcurrentHashMap<Key<?>, Object> controllerData;
 
     private final EntitySystem system;
 
             throw new NullPointerException("EntitySystem cannot be null");
         
         this.system = system;
-        controllerData = new ConcurrentHashMap<Class<? extends Annotation>, Object>();
-        privateData = new ConcurrentHashMap<Object, Object>();
+        controllerData = new ConcurrentHashMap<Key<?>, Object>();
         controllers = new ArrayList<Controller>();
         
         fixedDelta = 1 / 60f; // 60fps
     public float getFixedDelta() {
         return fixedDelta;
     }
-    
+
     /**
-     * Return the controller data that has been mapped to the given annotation
-     * <tt>key</tt>. This will return if there has been no assigned data. This
-     * can be used to store arbitrary data that must be shared between related
-     * controllers.
+     * Return the controller data that has been mapped to the given <tt>key</tt>
+     * . This will return null if there has been no assigned data. The getData()
+     * and setData() methods should be used by controllers to share data between
+     * themselves, and to store system-dependent state so that their
+     * implementations are properly indepdent of the system.
      * 
      * @param key The annotation key
      * @return The object previously mapped to the annotation with
      *         {@link #setControllerData(Class, Object)}
      * @throws NullPointerException if key is null
      */
-    public Object getControllerData(Class<? extends Annotation> key) {
+    @SuppressWarnings("unchecked")
+    public <T> T getData(Key<T> key) {
         if (key == null)
             throw new NullPointerException("Key cannot be null");
-        return controllerData.get(key);
+        return (T) controllerData.get(key);
     }
 
     /**
-     * Map <tt>value</tt> to the given annotation <tt>key</tt> so that future
-     * calls to {@link #getControllerData(Class)} with the same key will return
+     * Store <tt>value</tt> to the given <tt>key</tt> so that future
+     * calls to {@link #getData(Key)} with the same key will return
      * the new value. If the value is null, any previous mapping is removed.
      * 
-     * @param key The annotation key
+     * @param key The key
      * @param value The new value to store
      * @throws NullPointerException if key is null
      */
-    public void setControllerData(Class<? extends Annotation> key, Object value) {
+    public <T> void setData(Key<T> key, T value) {
         if (key == null)
             throw new NullPointerException("Key cannot be null");
         if (value == null)
     }
 
     /**
-     * Retrieve a privately stored instance from this manager's cache. It is
-     * similar to {@link #getControllerData(Class)} except the key is intended
-     * to be something known only to the owner so the data is effectively hidden
-     * from other controllers (unlike the annotation key which facilitates
-     * cross-controller communication).
-     * 
-     * @param key The key to lookup
-     * @return The value associated with the key, or null
-     * @throws NullPointerException if key is null
-     */
-    public Object getPrivateData(Object key) {
-        if (key == null)
-            throw new NullPointerException("Key cannot be null");
-        return privateData.get(key);
-    }
-
-    /**
-     * Store the given value into this manager's cache so that it can be
-     * retrieved by the given key. It is intended that the key is not accessible
-     * to other controllers so that this represents private data. If sharing is
-     * desired, create an annotation description and use
-     * {@link #setControllerData(Class, Object)}.
-     * 
-     * @param key The key to store the value to
-     * @param value The value being stored, or null to remove the old value
-     * @throws NullPointerException if key is null
-     */
-    public void setPrivateData(Object key, Object value) {
-        if (key == null)
-            throw new NullPointerException("Key cannot be null");
-        if (value == null)
-            privateData.remove(key);
-        else
-            privateData.put(key, value);
-    }
-
-    /**
      * <p>
      * Add a Controller to this manager so that subsequent calls to
      * {@link #process()} and its varieties will invoke the process hooks on the
             throw new NullPointerException("Controller cannot be null");
         
         // remove it first - which does nothing if not in the list
-        controllers.remove(controller);
+        boolean removed = controllers.remove(controller);
         // now add it to the end
         controllers.add(controller);
+        
+        if (!removed)
+            controller.addedToSystem(system);
     }
 
     /**
     public void removeController(Controller controller) {
         if (controller == null)
             throw new NullPointerException("Controller cannot be null");
-        controllers.remove(controller);
+        boolean removed = controllers.remove(controller);
+        if (removed)
+            controller.removedFromSystem(system);
     }
     
     /**
         
         switch(phase) {
         case PREPROCESS:
-            doPreProcess(dt); break;
+            firePreProcess(dt); break;
         case PROCESS:
-            doProcess(dt); break;
+            fireProcess(dt); break;
         case POSTPROCESS:
-            doPostProcess(dt); break;
+            firePostProcess(dt); break;
         case ALL:
-            // Perform all stages in one go
-            doPreProcess(dt);
-            doProcess(dt);
-            doPostProcess(dt);
+            // Perform all phases in one go
+            firePreProcess(dt);
+            fireProcess(dt);
+            firePostProcess(dt);
             break;
         }
     }
+
+    /**
+     * Invoke onEntityAdd() for all controllers.
+     * 
+     * @param e The entity being added
+     */
+    void fireEntityAdd(Entity e) {
+        for (int i = 0; i < controllers.size(); i++)
+            controllers.get(i).onEntityAdd(e);
+    }
     
-    private void doPreProcess(float dt) {
+    /**
+     * Invoke onEntityRemove() for all controllers.
+     * 
+     * @param e The entity being removed
+     */
+    void fireEntityRemove(Entity e) {
+        for (int i = 0; i < controllers.size(); i++)
+            controllers.get(i).onEntityRemove(e);
+    }
+    
+    /**
+     * Invoke onComponentAdd() for all controllers.
+     * 
+     * @param c The component being added
+     */
+    void fireComponentAdd(Component c) {
+        for (int i = 0; i < controllers.size(); i++)
+            controllers.get(i).onComponentAdd(c);
+    }
+    
+    /**
+     * Invoke onComponentRemove() for all controllers.
+     * 
+     * @param c The component being removed
+     */
+    void fireComponentRemove(Component c) {
+        for (int i = 0; i < controllers.size(); i++)
+            controllers.get(i).onComponentRemove(c);
+    }
+    
+    private void firePreProcess(float dt) {
         for (int i = 0; i < controllers.size(); i++)
             controllers.get(i).preProcess(system, dt);
     }
     
-    private void doProcess(float dt) {
+    private void fireProcess(float dt) {
         for (int i = 0; i < controllers.size(); i++)
             controllers.get(i).process(system, dt);
     }
     
-    private void doPostProcess(float dt) {
+    private void firePostProcess(float dt) {
         for (int i = 0; i < controllers.size(); i++)
             controllers.get(i).postProcess(system, dt);
     }

File src/main/java/com/googlecode/entreri/Entity.java

 import java.util.Iterator;
 import java.util.NoSuchElementException;
 
+import com.googlecode.entreri.property.Property;
+import com.googlecode.entreri.property.PropertyFactory;
+
 /**
  * <p>
  * An Entity represents a collection of Components within an EntitySystem.
      *         has been removed
      */
     public boolean isLive() {
-        return getId() != 0;
+        // index == 0 is equivalent to getId() == 0, given the way the system
+        // manages index's, but this is faster
+        return index != 0;
     }
 
     /**
     }
 
     /**
+     * <p>
      * Add a Component of type T to this Entity, but clone its state from the
      * existing component of type T. The existing component must still be
      * attached to an Entity other than this entity, but it could be from a
      * different EntitySystem. If there already exists a component of type T
      * added to this entity, it is removed first, and a new one is instantiated.
+     * </p>
+     * <p>
+     * The new component is initialized by cloning the property values from
+     * <tt>toClone</tt> into the values of the new instance. This is performed
+     * by invoking {@link PropertyFactory#clone(Property, int, Property, int)}
+     * with the factory that created each property. All default property
+     * factories perform a copy by value (or copy by reference for object
+     * types).
+     * </p>
      * 
      * @param <T> The parameterized type of component to add
      * @param toClone The existing T to clone when attaching to this component
     public Iterator<Component> iterator() {
         return new ComponentIterator(system, index);
     }
-    
+
+    /**
+     * <p>
+     * Entity's hashCode() returns its entity id.
+     * </p>
+     * <p>
+     * This means you can use the entities created by a system's fast iterators
+     * to query a hash-based collections. However, you should never store fast
+     * entities into a hash-based collection because their id (and thus hash
+     * code) will change each iteration.
+     * </p>
+     * <p>
+     * Additionally, an entity's id is updated when it is removed from an system.
+     * This means it is critical to remove entities from collections before they
+     * a removed from a system. This can be done in
+     * {@link Controller#onEntityRemove(Entity)}.
+     * </p>
+     * 
+     * @throws IllegalStateException if the entity has already been removed
+     */
     @Override
     public int hashCode() {
+        if (!isLive())
+            throw new IllegalStateException("Entity is no longer alive");
         return getId();
     }
-    
+
+    /**
+     * <p>
+     * Entity's equals() returns true if the object is another entity in the
+     * same EntitySystem, with the same id.
+     * </p>
+     * <p>
+     * This means you can use the entities created by a system's fast iterators
+     * to query equals-based collections. However, you should never store fast
+     * entities into a equals-based collections because their id (and thus
+     * definition of equality) will change with each iteration.
+     * </p>
+     * <p>
+     * Additionally, an entity's index is updated when it is removed from an
+     * entity or system. This means it is critical to remove entities from
+     * collections before they are removed from the system. This can be done in
+     * {@link Controller#onEntityRemove(Entity)}.
+     * </p>
+     * 
+     * @param o The object to test equality with
+     * @return True if the two instances represent the same conceptual entity
+     * @throws IllegalStateException if the entity has already been removed
+     */
     @Override
     public boolean equals(Object o) {
+        if (!isLive())
+            throw new IllegalStateException("Entity is no longer alive");
+        
         if (!(o instanceof Entity))
             return false;
         Entity e = (Entity) o;

File src/main/java/com/googlecode/entreri/EntitySystem.java

         entities[entityIndex] = newEntity;
         entityIds[entityIndex] = entityIdSeq++;
         
+        // invoke add-event listeners now before we invoke any listeners
+        // due to templating so events have proper sequencing
+        manager.fireEntityAdd(newEntity);
+        
         if (template != null) {
             for (Component c: template) {
                 addFromTemplate(entityIndex, c.getTypedId(), c);
         
         // clear out id and canonical entity
         Entity old = entities[index];
-        entityIds[index] = 0;
-        entities[index] = null;
-        
         if (old != null) {
-            // update its index
+            // fire remove-event listener
+            manager.fireEntityRemove(old);
+            
+            entityIds[index] = 0;
+            entities[index] = null;
             old.index = 0;
+            
             return true;
-        } else
+        } else {
             return false;
+        }
     }
     
     @SuppressWarnings({ "unchecked", "rawtypes" })

File src/main/java/com/googlecode/entreri/property/AbstractPropertyFactory.java

+/*
+ * Entreri, an entity-component framework in Java
+ *
+ * Copyright (c) 2011, Michael Ludwig
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ *     Redistributions of source code must retain the above copyright notice,
+ *         this list of conditions and the following disclaimer.
+ *     Redistributions in binary form must reproduce the above copyright notice,
+ *         this list of conditions and the following disclaimer in the
+ *         documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.googlecode.entreri.property;
+
+/**
+ * AbstractPropertyFactory is an abstract PropertyFactory implementation that
+ * implements {@link #setValue(Property, int)} in terms of
+ * {@link IndexedDataStore#setDefault(int)} and implements
+ * {@link #clone(Property, int, Property, int)} in terms of
+ * {@link IndexedDataStore#copy(int, int, IndexedDataStore, int)}. In many cases
+ * this is sufficient for most PropertyFactories.
+ * 
+ * @author Michael Ludwig
+ * @param <P>
+ */
+public abstract class AbstractPropertyFactory<P extends Property> implements PropertyFactory<P> {
+    @Override
+    public void setValue(P property, int index) {
+        property.getDataStore().setDefault(index);
+    }
+
+    @Override
+    public void clone(P src, int srcIndex, P dst, int dstIndex) {
+        src.getDataStore().copy(srcIndex, 1, dst.getDataStore(), dstIndex);
+    }
+}

File src/main/java/com/googlecode/entreri/property/FloatProperty.java

      * @return A PropertyFactory for FloatProperty
      */
     public static PropertyFactory<FloatProperty> factory(final int elementSize) {
-        return new PropertyFactory<FloatProperty>() {
+        return new AbstractPropertyFactory<FloatProperty>() {
             @Override
             public FloatProperty create() {
                 return new FloatProperty(elementSize);
         protected int getArrayLength(float[] array) {
             return array.length;
         }
+
+        @Override
+        public void setDefault(int offset) {
+            for (int i = offset * elementSize; i < (offset + 1) * elementSize; i++)
+                array[i] = 0f;
+        }
     }
 }

File src/main/java/com/googlecode/entreri/property/IndexedDataStore.java

      *             out-of-bounds exceptions
      */
     public void copy(int srcOffset, int len, IndexedDataStore dest, int destOffset);
+
+    /**
+     * Update the values of the data store at the given component offset to
+     * reflect the default value. The default value is dependent on the
+     * IndexedDataStore (and in that sense, the Property creating the data
+     * store), but will usually be null or 0.
+     * 
+     * @param offset The component offset into the data store
+     * @throws IndexOutOfBoundsException if the offset is invalid
+     */
+    public void setDefault(int offset);
 }

File src/main/java/com/googlecode/entreri/property/IntProperty.java

      * @return A PropertyFactory for IntProperty
      */
     public static PropertyFactory<IntProperty> factory(final int elementSize) {
-        return new PropertyFactory<IntProperty>() {
+        return new AbstractPropertyFactory<IntProperty>() {
             @Override
             public IntProperty create() {
                 return new IntProperty(elementSize);
         protected int getArrayLength(int[] array) {
             return array.length;
         }
+        
+        @Override
+        public void setDefault(int offset) {
+            for (int i = offset * elementSize; i < (offset + 1) * elementSize; i++)
+                array[i] = 0;
+        }
     }
 }

File src/main/java/com/googlecode/entreri/property/ObjectProperty.java

      * @return A PropertyFactory for ObjectProperty
      */
     public static <T> PropertyFactory<ObjectProperty<T>> factory(final int elementSize) {
-        return new PropertyFactory<ObjectProperty<T>>() {
+        return new AbstractPropertyFactory<ObjectProperty<T>>() {
             @Override
             public ObjectProperty<T> create() {
                 return new ObjectProperty<T>(elementSize);
         protected int getArrayLength(Object[] array) {
             return array.length;
         }
+        
+        @Override
+        public void setDefault(int offset) {
+            for (int i = offset * elementSize; i < (offset + 1) * elementSize; i++)
+                array[i] = null;
+        }
     }
 }

File src/main/java/com/googlecode/entreri/property/PropertyFactory.java

  */
 package com.googlecode.entreri.property;
 
+import com.googlecode.entreri.Entity;
+
 /**
  * <p>
  * A PropertyFactory is a simple factory that can be used to create Property
      * @return A new Property of type T
      */
     public T create();
+
+    /**
+     * Set the default value that the component at the specified <tt>index</tt>
+     * will see before it's init() method is invoked. In some cases, this could
+     * be used in-place of initializing in init() method.
+     * 
+     * @param property The property whose value will be updated
+     * @param index The component index to be updated
+     */
+    public void setValue(T property, int index);
+
+    /**
+     * Copy the value from <tt>src</tt> at component index, <tt>srcIndex</tt> to
+     * <tt>dst</tt> at <tt>dstIndex</tt>. This is used when a component is
+     * created and cloned from a template with
+     * {@link Entity#add(com.googlecode.entreri.Component)}. For many cases a
+     * plain copy-by-value or copy-by-reference is sufficient, but some
+     * component types might require more complicated cloning rules.
+     * 
+     * @param src The source property that is being cloned
+     * @param srcIndex The index into src of the component being cloned
+     * @param dst The destination property created from the template
+     * @param dstIndex The index into dst of the component being created
+     */
+    public void clone(T src, int srcIndex, T dst, int dstIndex);
 }

File src/main/java/com/googlecode/entreri/property/Unmanaged.java

+/*
+ * Entreri, an entity-component framework in Java
+ *
+ * Copyright (c) 2011, Michael Ludwig
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ *     Redistributions of source code must retain the above copyright notice,
+ *         this list of conditions and the following disclaimer.
+ *     Redistributions in binary form must reproduce the above copyright notice,
+ *         this list of conditions and the following disclaimer in the
+ *         documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.googlecode.entreri.property;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import com.googlecode.entreri.IllegalComponentDefinitionException;
+
+/**
+ * Unmanaged is an annotation that can be applied to fields in a Component
+ * definition to make the field completely ignored by the EntitySystem creating
+ * or managing the component. This can be used to store per-instance cached data
+ * in the component without triggering
+ * {@link IllegalComponentDefinitionException exceptions}. However, it makes
+ * little sense to declare a Property field as unmanaged because then its data
+ * store will not be kept in sync with the component's other indexed data.
+ * 
+ * @author Michael Ludwig
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+public @interface Unmanaged { }

File src/test/java/com/googlecode/entreri/ComponentTest.java

 import com.googlecode.entreri.component.ObjectComponent;
 import com.googlecode.entreri.component.PublicConstructorComponent;
 import com.googlecode.entreri.component.PublicPropertyComponent;
-import com.googlecode.entreri.component.TransientFieldComponent;
+import com.googlecode.entreri.component.UnmanagedFieldComponent;
 import com.googlecode.entreri.property.FloatProperty;
 import com.googlecode.entreri.property.FloatPropertyFactory;
 import com.googlecode.entreri.property.MultiParameterProperty;
 import com.googlecode.entreri.property.NoParameterProperty;
 import com.googlecode.entreri.property.Property;
+import com.googlecode.entreri.property.PropertyFactory;
 
 public class ComponentTest {
     @Test
         doGetTypedIdTest(IntComponent.class);
         doGetTypedIdTest(ObjectComponent.class);
         doGetTypedIdTest(MultiPropertyComponent.class);
-        doGetTypedIdTest(TransientFieldComponent.class);
+        doGetTypedIdTest(UnmanagedFieldComponent.class);
     }
     
     private void doGetTypedIdTest(Class<? extends Component> type) {
     }
     
     @Test
+    public void testFactorySetValue() {
+        EntitySystem system = new EntitySystem();
+        MultiPropertyComponent c = system.addEntity().add(Component.getTypedId(MultiPropertyComponent.class));
+        Assert.assertEquals(FloatPropertyFactory.DEFAULT, c.getFactoryFloat(), .0001f);
+    }
+    
+    @Test
     public void testPropertyLookup() {
         ComponentBuilder<MultiPropertyComponent> builder = Component.getBuilder(Component.getTypedId(MultiPropertyComponent.class));
-        Collection<Property> props = builder.createProperties().values();
+        Collection<Property> props = new HashSet<Property>();
+        for (PropertyFactory<?> factory: builder.getPropertyFactories().values()) {
+            props.add(factory.create());
+        }
         
         Set<Class<? extends Property>> propTypeSet = new HashSet<Class<? extends Property>>();
         for (Property p: props) {
     }
     
     @Test
-    public void testTransientField() {
-        TypedId<TransientFieldComponent> id = Component.getTypedId(TransientFieldComponent.class);
+    public void testUnmanagedField() {
+        TypedId<UnmanagedFieldComponent> id = Component.getTypedId(UnmanagedFieldComponent.class);
         
         EntitySystem system = new EntitySystem();
         for (int i = 0; i < 4; i++) {
         }
         
         int i = 0;
-        Iterator<TransientFieldComponent> it = system.iterator(id);
+        Iterator<UnmanagedFieldComponent> it = system.iterator(id);
         while(it.hasNext()) {
             float f = it.next().getFloat();
             Assert.assertEquals(i, f, .0001f);

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

 import org.junit.Assert;
 import org.junit.Test;
 
-import com.googlecode.entreri.Controller;
-import com.googlecode.entreri.EntitySystem;
+import com.googlecode.entreri.ControllerManager.Key;
 import com.googlecode.entreri.ControllerManager.Phase;
+import com.googlecode.entreri.component.IntComponent;
 
 public class ControllerManagerTest {
     @Test
     
     @Test
     public void testControllerData() {
+        Key<Object> key = new Key<Object>();
         EntitySystem system = new EntitySystem();
         Object data = new Object();
-        system.getControllerManager().setControllerData(Key.class, data);
+        system.getControllerManager().setData(key, data);
         
-        Assert.assertEquals(data, system.getControllerManager().getControllerData(Key.class));
+        Assert.assertEquals(data, system.getControllerManager().getData(key));
         
-        system.getControllerManager().setControllerData(Key.class, null);
-        Assert.assertNull(system.getControllerManager().getControllerData(Key.class));
+        system.getControllerManager().setData(key, null);
+        Assert.assertNull(system.getControllerManager().getData(key));
     }
     
     @Test
-    public void testPrivateData() {
+    public void testControllerAddRemoveListener() {
         EntitySystem system = new EntitySystem();
-        Object data = new Object();
-        Object key = new Object();
+        ControllerImpl ctrl = new ControllerImpl();
         
-        system.getControllerManager().setPrivateData(key, data);
-        
-        Assert.assertEquals(data, system.getControllerManager().getPrivateData(key));
-        
-        system.getControllerManager().setPrivateData(key, null);
-        Assert.assertNull(system.getControllerManager().getPrivateData(key));
+        system.getControllerManager().addController(ctrl);
+        Assert.assertTrue(ctrl.added);
+        system.getControllerManager().removeController(ctrl);
+        Assert.assertTrue(ctrl.removed);
     }
     
-    private static @interface Key {
+    @Test
+    public void testEntityAddRemoveListener() {
+        EntitySystem system = new EntitySystem();
+        ControllerImpl ctrl = new ControllerImpl();
+        system.getControllerManager().addController(ctrl);
         
+        Entity e = system.addEntity();
+        Assert.assertEquals(e, ctrl.lastAddedEntity);
+        system.removeEntity(e);
+        Assert.assertTrue(e == ctrl.lastRemovedEntity);
+    }
+    
+    @Test
+    public void testComponentAddRemoveListener() {
+        EntitySystem system = new EntitySystem();
+        ControllerImpl ctrl = new ControllerImpl();
+        system.getControllerManager().addController(ctrl);
+        
+        Entity e = system.addEntity();
+        IntComponent i = e.add(Component.getTypedId(IntComponent.class));
+        
+        Assert.assertEquals(i, ctrl.lastAddedComponent);
+        e.remove(Component.getTypedId(IntComponent.class));
+        Assert.assertTrue(i == ctrl.lastRemovedComponent);
     }
     
     private static class ControllerImpl implements Controller {
         
         private float dt;
         
+        private boolean added;
+        private boolean removed;
+        
+        private Entity lastAddedEntity;
+        private Entity lastRemovedEntity;
+        
+        private Component lastAddedComponent;
+        private Component lastRemovedComponent;
+        
         @Override
         public void preProcess(EntitySystem system, float dt) {
             preprocessed = true;
             postprocessed = true;
             this.dt = dt;
         }
+
+        @Override
+        public void addedToSystem(EntitySystem system) {
+            added = true;
+        }
+
+        @Override
+        public void removedFromSystem(EntitySystem system) {
+            removed = true;
+        }
+
+        @Override
+        public void onEntityAdd(Entity e) {
+            lastAddedEntity = e;
+        }
+
+        @Override
+        public void onEntityRemove(Entity e) {
+            lastRemovedEntity = e;
+        }
+
+        @Override
+        public void onComponentAdd(Component c) {
+            lastAddedComponent = c;
+        }
+
+        @Override
+        public void onComponentRemove(Component c) {
+            lastRemovedComponent = c;
+        }
     }
 }

File src/test/java/com/googlecode/entreri/component/MultiPropertyComponent.java

         return noparams;
     }
     
+    public void setFactoryFloat(float f) {
+        fromFactory.set(f, getIndex(), 0);
+    }
+    
+    public float getFactoryFloat() {
+        return fromFactory.get(getIndex(), 0);
+    }
+    
     @Override
     protected void init(Object... initParams) throws Exception {
     }

File src/test/java/com/googlecode/entreri/component/TransientFieldComponent.java

-/*
- * Entreri, an entity-component framework in Java
- *
- * Copyright (c) 2011, Michael Ludwig
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without modification,
- * are permitted provided that the following conditions are met:
- *
- *     Redistributions of source code must retain the above copyright notice,
- *         this list of conditions and the following disclaimer.
- *     Redistributions in binary form must reproduce the above copyright notice,
- *         this list of conditions and the following disclaimer in the
- *         documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
- * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
- * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
- * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-package com.googlecode.entreri.component;
-
-import com.googlecode.entreri.Component;
-import com.googlecode.entreri.EntitySystem;
-import com.googlecode.entreri.property.ObjectProperty;
-
-public class TransientFieldComponent extends Component {
-    private transient ObjectProperty<Object> transientProperty;
-    private transient float field;
-    
-    protected TransientFieldComponent(EntitySystem system, int index) {
-        super(system, index);
-    }
-
-    @Override
-    protected void init(Object... initParams) throws Exception {
-        
-    }
-
-    public void setObject(Object v) {
-        transientProperty.set(v, getIndex(), 0);
-    }
-    
-    public Object getObject() {
-        return transientProperty.get(getIndex(), 0);
-    }
-    
-    public float getFloat() {
-        return field;
-    }
-    
-    public void setFloat(float f) {
-        field = f;
-    }
-}

File src/test/java/com/googlecode/entreri/component/UnmanagedFieldComponent.java

+/*
+ * Entreri, an entity-component framework in Java
+ *
+ * Copyright (c) 2011, Michael Ludwig
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without modification,
+ * are permitted provided that the following conditions are met:
+ *
+ *     Redistributions of source code must retain the above copyright notice,
+ *         this list of conditions and the following disclaimer.
+ *     Redistributions in binary form must reproduce the above copyright notice,
+ *         this list of conditions and the following disclaimer in the
+ *         documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.googlecode.entreri.component;
+
+import com.googlecode.entreri.Component;
+import com.googlecode.entreri.EntitySystem;
+import com.googlecode.entreri.property.ObjectProperty;
+import com.googlecode.entreri.property.Unmanaged;
+
+public class UnmanagedFieldComponent extends Component {
+    private transient ObjectProperty<Object> transientProperty;
+    
+    @Unmanaged
+    private transient float field;
+    
+    protected UnmanagedFieldComponent(EntitySystem system, int index) {
+        super(system, index);
+    }
+
+    @Override
+    protected void init(Object... initParams) throws Exception {
+        
+    }
+
+    public void setObject(Object v) {
+        transientProperty.set(v, getIndex(), 0);
+    }
+    
+    public Object getObject() {
+        return transientProperty.get(getIndex(), 0);
+    }
+    
+    public float getFloat() {
+        return field;
+    }
+    
+    public void setFloat(float f) {
+        field = f;
+    }
+}

File src/test/java/com/googlecode/entreri/property/FloatPropertyFactory.java

  */
 package com.googlecode.entreri.property;
 
-import com.googlecode.entreri.property.FloatProperty;
-import com.googlecode.entreri.property.PropertyFactory;
 
-public class FloatPropertyFactory implements PropertyFactory<FloatProperty> {
+public class FloatPropertyFactory extends AbstractPropertyFactory<FloatProperty> {
+    public static final float DEFAULT = 5f;
+    
     @Override
     public FloatProperty create() {
         return new FloatProperty(1);
     }
+    
+    @Override
+    public void setValue(FloatProperty p, int index) {
+        p.set(DEFAULT, index, 0);
+    }
 }