diff --git a/jme3-core/src/main/java/com/jme3/scene/Geometry.java b/jme3-core/src/main/java/com/jme3/scene/Geometry.java
index 50ca01912..f56e71a57 100644
--- a/jme3-core/src/main/java/com/jme3/scene/Geometry.java
+++ b/jme3-core/src/main/java/com/jme3/scene/Geometry.java
@@ -77,15 +77,18 @@ public class Geometry extends Spatial {
* is managed by.
*/
protected GeometryGroupNode groupNode;
+
/**
* The start index of this Geometry's
inside
* the {@link GeometryGroupNode}.
*/
- protected int startIndex = -1;
+ protected int startIndex = -1;
+
/**
* Serialization only. Do not use.
*/
public Geometry() {
+ this(null);
}
/**
@@ -97,6 +100,12 @@ public class Geometry extends Spatial {
*/
public Geometry(String name) {
super(name);
+
+ // For backwards compatibility, only clear the "requires
+ // update" flag if we are not a subclass of Geometry.
+ // This prevents subclass from silently failing to receive
+ // updates when they upgrade.
+ setRequiresUpdates(Geometry.class != getClass());
}
/**
diff --git a/jme3-core/src/main/java/com/jme3/scene/Node.java b/jme3-core/src/main/java/com/jme3/scene/Node.java
index 3934b3e13..f085251bd 100644
--- a/jme3-core/src/main/java/com/jme3/scene/Node.java
+++ b/jme3-core/src/main/java/com/jme3/scene/Node.java
@@ -66,10 +66,27 @@ public class Node extends Spatial implements Savable {
*/
protected SafeArrayList children = new SafeArrayList(Spatial.class);
+ /**
+ * If this node is a root, this list will contain the current
+ * set of children (and children of children) that require
+ * updateLogicalState() to be called as indicated by their
+ * requiresUpdate() method.
+ */
+ private SafeArrayList updateList = null;
+
+ /**
+ * False if the update list requires rebuilding. This is Node.class
+ * specific and therefore not included as part of the Spatial update flags.
+ * A flag is used instead of nulling the updateList to avoid reallocating
+ * a whole list every time the scene graph changes.
+ */
+ private boolean updateListValid = false;
+
/**
* Serialization only. Do not use.
*/
public Node() {
+ this(null);
}
/**
@@ -81,6 +98,12 @@ public class Node extends Spatial implements Savable {
*/
public Node(String name) {
super(name);
+
+ // For backwards compatibility, only clear the "requires
+ // update" flag if we are not a subclass of Node.
+ // This prevents subclass from silently failing to receive
+ // updates when they upgrade.
+ setRequiresUpdates(Node.class != getClass());
}
/**
@@ -152,16 +175,72 @@ public class Node extends Spatial implements Savable {
this.worldBound = resultBound;
}
+ @Override
+ protected void setParent(Node parent) {
+ if( this.parent == null && parent != null ) {
+ // We were a root before and now we aren't... make sure if
+ // we had an updateList then we clear it completely to
+ // avoid holding the dead array.
+ updateList = null;
+ updateListValid = false;
+ }
+ super.setParent(parent);
+ }
+
+ private void addUpdateChildren( SafeArrayList results ) {
+ for( Spatial child : children.getArray() ) {
+ if( child.requiresUpdates() ) {
+ results.add(child);
+ }
+ if( child instanceof Node ) {
+ ((Node)child).addUpdateChildren(results);
+ }
+ }
+ }
+
+ /**
+ * Called to invalide the root node's update list. This is
+ * called whenever a spatial is attached/detached as well as
+ * when a control is added/removed from a Spatial in a way
+ * that would change state.
+ */
+ void invalidateUpdateList() {
+ updateListValid = false;
+ for( Node n = parent; n != null; n = n.getParent() ) {
+ n.invalidateUpdateList();
+ }
+ }
+
+ private SafeArrayList getUpdateList() {
+ if( updateListValid ) {
+ return updateList;
+ }
+ if( updateList == null ) {
+ updateList = new SafeArrayList(Spatial.class);
+ } else {
+ updateList.clear();
+ }
+
+ // Build the list
+ addUpdateChildren(updateList);
+ updateListValid = true;
+ return updateList;
+ }
+
@Override
public void updateLogicalState(float tpf){
super.updateLogicalState(tpf);
- if (children.isEmpty()) {
+ // Only perform updates on children if we are the
+ // root and then only peform updates on children we
+ // know to require updates.
+ // So if this isn't the root, abort.
+ if( parent != null ) {
return;
}
-
- for (Spatial child : children.getArray()) {
- child.updateLogicalState(tpf);
+
+ for( Spatial s : getUpdateList().getArray() ) {
+ s.updateLogicalState(tpf);
}
}
@@ -251,28 +330,7 @@ public class Node extends Spatial implements Savable {
* @throws IllegalArgumentException if child is null.
*/
public int attachChild(Spatial child) {
- if (child == null)
- throw new IllegalArgumentException("child cannot be null");
-
- if (child.getParent() != this && child != this) {
- if (child.getParent() != null) {
- child.getParent().detachChild(child);
- }
- child.setParent(this);
- children.add(child);
-
- // XXX: Not entirely correct? Forces bound update up the
- // tree stemming from the attached child. Also forces
- // transform update down the tree-
- child.setTransformRefresh();
- child.setLightListRefresh();
- if (logger.isLoggable(Level.FINE)) {
- logger.log(Level.FINE,"Child ({0}) attached to this node ({1})",
- new Object[]{child.getName(), getName()});
- }
- }
-
- return children.size();
+ return attachChildAt(child, children.size());
}
/**
@@ -298,12 +356,18 @@ public class Node extends Spatial implements Savable {
}
child.setParent(this);
children.add(index, child);
+
+ // XXX: Not entirely correct? Forces bound update up the
+ // tree stemming from the attached child. Also forces
+ // transform update down the tree-
child.setTransformRefresh();
child.setLightListRefresh();
if (logger.isLoggable(Level.FINE)) {
logger.log(Level.FINE,"Child ({0}) attached to this node ({1})",
new Object[]{child.getName(), getName()});
}
+
+ invalidateUpdateList();
}
return children.size();
@@ -380,6 +444,8 @@ public class Node extends Spatial implements Savable {
child.setTransformRefresh();
// lights are also inherited from parent
child.setLightListRefresh();
+
+ invalidateUpdateList();
}
return child;
}
@@ -390,6 +456,9 @@ public class Node extends Spatial implements Savable {
* node.
*/
public void detachAllChildren() {
+ // Note: this could be a bit more efficient if it delegated
+ // to a private method that avoided setBoundRefresh(), etc.
+ // for every child and instead did one in here at the end.
for ( int i = children.size() - 1; i >= 0; i-- ) {
detachChildAt(i);
}
diff --git a/jme3-core/src/main/java/com/jme3/scene/Spatial.java b/jme3-core/src/main/java/com/jme3/scene/Spatial.java
index 62add98ce..242aaf288 100644
--- a/jme3-core/src/main/java/com/jme3/scene/Spatial.java
+++ b/jme3-core/src/main/java/com/jme3/scene/Spatial.java
@@ -162,16 +162,24 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
protected transient int refreshFlags = 0;
/**
- * Serialization only. Do not use.
+ * Set to true if a subclass requires updateLogicalState() even
+ * if it doesn't have any controls. Defaults to true thus implementing
+ * the legacy behavior for any subclasses not specifically turning it
+ * off.
+ * This flag should be set during construction and never changed
+ * as it's supposed to be class-specific and not runtime state.
*/
- public Spatial() {
- localTransform = new Transform();
- worldTransform = new Transform();
-
- localLights = new LightList(this);
- worldLights = new LightList(this);
+ private boolean requiresUpdates = true;
- refreshFlags |= RF_BOUND;
+ /**
+ * Serialization only. Do not use.
+ * Not really. This class is never instantiated directly but the
+ * subclasses like to use the no-arg constructor for their own
+ * no-arg constructor... which is technically weaker than
+ * forward supplying defaults.
+ */
+ protected Spatial() {
+ this(null);
}
/**
@@ -182,9 +190,16 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
* the name of the scene element. This is required for
* identification and comparison purposes.
*/
- public Spatial(String name) {
- this();
+ protected Spatial(String name) {
this.name = name;
+
+ localTransform = new Transform();
+ worldTransform = new Transform();
+
+ localLights = new LightList(this);
+ worldLights = new LightList(this);
+
+ refreshFlags |= RF_BOUND;
}
public void setKey(AssetKey key) {
@@ -195,6 +210,54 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
return key;
}
+ /**
+ * Returns true if this spatial requires updateLogicalState() to
+ * be called, either because setRequiresUpdate(true) has been called
+ * or because the spatial has controls. This is package private to
+ * avoid exposing it to the public API since it is only used by Node.
+ */
+ boolean requiresUpdates() {
+ return requiresUpdates | !controls.isEmpty();
+ }
+
+ /**
+ * Subclasses can call this with true to denote that they require
+ * updateLogicalState() to be called even if they contain no controls.
+ * Setting this to false reverts to the default behavior of only
+ * updating if the spatial has controls. This is not meant to
+ * indicate dynamic state in any way and must be called while
+ * unattached or an IllegalStateException is thrown. It is designed
+ * to be called during object construction and then never changed, ie:
+ * it's meant to be subclass specific state and not runtime state.
+ * Subclasses of Node or Geometry that do not set this will get the
+ * old default behavior as if this was set to true. Subclasses should
+ * call setRequiresUpdate(false) in their constructors to receive
+ * optimal behavior if they don't require updateLogicalState() to be
+ * called even if there are no controls.
+ */
+ protected void setRequiresUpdates( boolean f ) {
+ // Note to explorers, the reason this was done as a protected setter
+ // instead of passed on construction is because it frees all subclasses
+ // from having to make sure to always pass the value up in case they
+ // are subclassed.
+ // The reason that requiresUpdates() isn't just a protected method to
+ // override (which would be more correct) is because the flag provides
+ // some flexibility in how we break subclasses. A protected method
+ // would require that all subclasses that required updates need implement
+ // this method or they would silently stop processing updates. A flag
+ // lets us set a default when a subclass is detected that is different
+ // than the internal "more efficient" default.
+ // Spatial's default is 'true' for this flag requiring subclasses to
+ // override it for more optimal behavior. Node and Geometry will override
+ // it to false if the class is Node.class or Geometry.class.
+ // This means that all subclasses will default to the old behavior
+ // unless they opt in.
+ if( parent != null ) {
+ throw new IllegalStateException("setRequiresUpdates() cannot be called once attached.");
+ }
+ this.requiresUpdates = f;
+ }
+
/**
* Indicate that the transform of this spatial has changed and that
* a refresh is required.
@@ -599,8 +662,17 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
* @see Spatial#removeControl(java.lang.Class)
*/
public void addControl(Control control) {
+ boolean before = requiresUpdates();
controls.add(control);
control.setSpatial(this);
+ boolean after = requiresUpdates();
+
+ // If the requirement to be updated has changed
+ // then we need to let the parent node know so it
+ // can rebuild its update list.
+ if( parent != null && before != after ) {
+ parent.invalidateUpdateList();
+ }
}
/**
@@ -609,12 +681,22 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
* @see Spatial#addControl(com.jme3.scene.control.Control)
*/
public void removeControl(Class extends Control> controlType) {
+ boolean before = requiresUpdates();
for (int i = 0; i < controls.size(); i++) {
if (controlType.isAssignableFrom(controls.get(i).getClass())) {
Control control = controls.remove(i);
control.setSpatial(null);
+ break; // added to match the javadoc -pspeed
}
}
+ boolean after = requiresUpdates();
+
+ // If the requirement to be updated has changed
+ // then we need to let the parent node know so it
+ // can rebuild its update list.
+ if( parent != null && before != after ) {
+ parent.invalidateUpdateList();
+ }
}
/**
@@ -627,11 +709,21 @@ public abstract class Spatial implements Savable, Cloneable, Collidable, Cloneab
* @see Spatial#addControl(com.jme3.scene.control.Control)
*/
public boolean removeControl(Control control) {
+ boolean before = requiresUpdates();
boolean result = controls.remove(control);
if (result) {
control.setSpatial(null);
}
+ boolean after = requiresUpdates();
+
+ // If the requirement to be updated has changed
+ // then we need to let the parent node know so it
+ // can rebuild its update list.
+ if( parent != null && before != after ) {
+ parent.invalidateUpdateList();
+ }
+
return result;
}