package edu.gatech.cs2340.trydent; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import javafx.geometry.Point2D; import javafx.scene.Group; import javafx.scene.Node; import javafx.scene.paint.Paint; import javafx.scene.shape.Shape; import javafx.scene.transform.NonInvertibleTransformException; import javafx.scene.transform.Rotate; import javafx.scene.transform.Transform; import edu.gatech.cs2340.trydent.animation.Animation; import edu.gatech.cs2340.trydent.animation.AnimationEvent; import edu.gatech.cs2340.trydent.animation.AnimationListener; import edu.gatech.cs2340.trydent.animation.DispatchAnimationListener; import edu.gatech.cs2340.trydent.internal.TrydentInternalException; import edu.gatech.cs2340.trydent.math.BaseVector; import edu.gatech.cs2340.trydent.math.MathTools; import edu.gatech.cs2340.trydent.math.Orientation; import edu.gatech.cs2340.trydent.math.Position; import edu.gatech.cs2340.trydent.math.Scale; import edu.gatech.cs2340.trydent.math.curve.IndexWrapMode; import edu.gatech.cs2340.trydent.math.curve.TimeWrapMode; /** * Basic GameObject all visual elements of a game should either use directly or * extend. * * <p> * Core functionality of GameObjects includes arbitrary positioning, scaling, * and rotating in the scene graph, display of JavaFXNodes, and animation. * * <p> * <strong>GameObject generic `Features'</strong> * <p> * Features are similar to `Components' in similar game engines; essentially, * they are objects that can be looked up by the classes they are instances of. * This can be used to extend GameObjects with generic functionality. * <p> * For example, say you want to add to GameObject the ability to serialize * themselves -- that is, the ability to store a String representation of * themselves, for use in a save-game feature. You might do the following: * * <pre> * public interface Serializer { * public String serialize(); * } * ... (in some other file) ... * GameObject bulletGameObject = ... * Serializer s = new BulletSerializer(); * bulletGameObject.addFeature(s); * </pre> * <p> * Then later on, you might use the serializer as such: * * <pre> * public void saveGameObjects(Collection<GameObject> objects, PrintStream file) { * for (GameObject g : objects) { * if (g.hasFeature(Serializer.class)) { * file.println(g.getFeature(Serializer.class).serialize()); * } * } * } * </pre> * * @author Garrett Malmquist * */ public class GameObject { private String name = "Untitled GameObject"; private Group fxNode; private GameObject parent; // Only used for deletion. private List<GameObject> children; private Transform localRotate; private Transform localScale; private Transform localTranslate; private Animation animation; private double animationStartTime; private int animationLoopCounter = 0; private DispatchAnimationListener animationListener; private volatile boolean animationPaused = false; private boolean isDestroyed = false; private Map<Class<?>, Set<Object>> features; /** * Creates a new GameObject with the given name. * * @param name * the name of the GameObject. */ public GameObject(String name) { this(); this.name = name; } /** * Creates a new unnamed GameObject. */ public GameObject() { if (!TrydentEngine.isRunning()) throw new TrydentException("Cannot instantiate GameObject before TrydentEngine has been started."); this.children = new LinkedList<>(); this.features = new HashMap<>(); // We initialize all GameObjects to be groups, so that they can easily // have children. GameObjects which are more than just empty will simply // have children that include the "actual" thing they seem to represent // (eg, an image view), but the user need not know that. this.fxNode = new Group(); // By default, our javafx Node parent is the scene root, which // corresponds // to a 'null' GameObject parent. TrydentEngine.getRootNode().getChildren().add(fxNode); // Identity localRotate = Transform.affine(1, 0, 0, 1, 0, 0); localScale = Transform.affine(1, 0, 0, 1, 0, 0); localTranslate = Transform.affine(1, 0, 0, 1, 0, 0); setMatrices(localTranslate, localRotate, localScale); createAnimationBehavior(); animationListener = new DispatchAnimationListener(); } /** * Creates a new GameObject displaying the given JavaFX node. * * @param javaFXObject * the JavaFX node to display */ public GameObject(Node javaFXObject) { this(); this.fxNode.getChildren().add(javaFXObject); } /** * Creates a new GameObject with the given name and JavaFX node. * * @param name * the name of the GameObject * @param javaFXObject * the JavaFX node to display */ public GameObject(String name, Node javaFXObject) { this(javaFXObject); this.name = name; } /** * Creates a new GameObject displaying the JavaFX node generated from the * parameter. * * @param node * the object which can generate javafx nodes. */ public GameObject(JavaFxConvertable node) { this(node.toJavaFxNode()); } /** * Creates a new GameObject displaying the JavaFX node generated from the * parameter. * * @param name * the name of this GameObject. * @param node * the object which can generate javafx nodes. */ public GameObject(String name, JavaFxConvertable node) { this(name, node.toJavaFxNode()); } /** * Associates a new `feature' with this GameObject. * <p> * @see edu.gatech.cs2340.trydent.GameObject for a discussion of what * features are and how to use them. * * @param feature * the Object to add as a feature. * @param <T> the type of the feature. */ public <T> void addFeature(T feature) { Class<?> key = feature.getClass(); if (!features.containsKey(key)) { features.put(key, new HashSet<Object>()); } features.get(key).add(feature); } /** * Retrieves a `feature' of the given type associated with this GameObject. * <p> * @see edu.gatech.cs2340.trydent.GameObject for a discussion of what * features are and how to use them. * * @param type * the type (or supertype) of the feature to retrieve. * @param <T> the type of the feature. * @return the feature if it exists, null otherwise. */ @SuppressWarnings("unchecked") public <T> T getFeature(Class<T> type) { if (features.containsKey(type)) { for (Object o : features.get(type)) { return (T) o; } } for (Class<?> key : features.keySet()) { if (type.isAssignableFrom(key)) { for (Object o : features.get(key)) { return (T) o; } } } return null; } /** * Retrieves all `features' of the given type associated with this * GameObject. * <p> * @see edu.gatech.cs2340.trydent.GameObject for a discussion of what * features are and how to use them. * * @param type * the type (or supertype) of the features to retrieve. E.g., * passing in `Object.class' will retrieve <i>all</i> features on * this game objects, because all features are subclasses of the * java Object superclass. * @param <T> the type of the feature. * @return an Iterable over all features of the given type that this object * contains. */ @SuppressWarnings("unchecked") public <T> Iterable<T> getFeatures(Class<T> type) { Set<T> iterable = new HashSet<>(); for (Class<?> key : features.keySet()) { if (type.isAssignableFrom(key)) { for (Object o : features.get(key)) { iterable.add((T) o); } } } return iterable; } /** * Checks whether this object contains a `feature' of the given type * associated with this GameObject. * <p> * @see edu.gatech.cs2340.trydent.GameObject for a discussion of what * features are and how to use them. * * @param type * the type (or supertype) of the feature to check for. * @param <T> the type of the feature. * @return true if this object has a feature of the given type, false * otherwise. */ public <T> boolean hasFeature(Class<T> type) { return getFeature(type) != null; } /** * Removes all `features' of (or subclasses of) the given type associated * with this GameObject. * <p> * @see edu.gatech.cs2340.trydent.GameObject for a discussion of what * features are and how to use them. * * @param type * the type (or supertype) of the features to remove. * @param <T> the type of the feature. */ public <T> void removeAllFeatures(Class<T> type) { Set<Class<?>> toRemove = new HashSet<>(); for (Class<?> key : features.keySet()) { if (type.isAssignableFrom(key)) { toRemove.add(key); } } for (Class<?> key : toRemove) { features.remove(key); } } /** * Removes the `feature' of (or a subclass of) the given type associated * with this GameObject. * <p> * @see edu.gatech.cs2340.trydent.GameObject for a discussion of what * features are and how to use them. * * @param feature * the type (or supertype) of the feature to remove. * @param <T> the type of the feature. * @return true if the feature was removed, false if there was no feature * to remove. */ public <T> boolean removeFeature(T feature) { for (Class<?> key : features.keySet()) { if (feature.getClass().isAssignableFrom(key)) { if (features.get(key).remove(feature)) { return true; } } } return false; } /** * Sets the fill of the underlying javafx node, if applicable. * * @param paint the fill color as a javaFx paint. */ public void setFill(Paint paint) { if (fxNode != null && fxNode.getChildren().size() > 0) { Node child = fxNode.getChildren().get(0); if (child != null && child instanceof Shape) { ((Shape) child).setFill(paint); } } } /** * Adds the animation listener. * * @param listener * AnimationListener object to receive AnimationEvents. */ public void addAnimationListener(AnimationListener listener) { animationListener.addAnimationListener(listener); } /** * Removes the animation listener. * * @param listener * AnimationListener to remove. */ public void removeAnimationListener(AnimationListener listener) { animationListener.removeAnimationListener(listener); } /** * Removes all animation listeners. */ public void clearAnimationListeners() { animationListener.clearAnimationListeners(); } /** * Plays the given animation, stopping any currently playing animations. * Animations modify the position, rotate, and scale of an object over time. * <p> * * @param animation * the animation to play */ public void playAnimation(Animation animation) { if (this.animation != null) { animationListener.animationInterrupted(new AnimationEvent(this, this.animation)); } this.animation = animation; this.animationStartTime = Time.getTime(); this.animationLoopCounter = 1; animation.setTimeWrap(TimeWrapMode.CLAMP); animationPaused = false; animationListener.animationStarted(new AnimationEvent(this, animation)); } /** * Plays the given animation, stopping any currently playing animations, and * loops it count number of times. * <p> * Animations modify the position, rotate, and scale of an object over time. * * @param animation * the animation to loop * @param count * The number of times to play the animation. count = 0 will not * play anything, count = 1 will play the animation exactly once. * A negative value for count will cause the animation to loop * infinitely. */ public void loopAnimation(Animation animation, int count) { if (this.animation != null) { animationListener.animationInterrupted(new AnimationEvent(this, this.animation)); } this.animation = animation; this.animationStartTime = Time.getTime(); this.animationLoopCounter = count; animation.setTimeWrap(TimeWrapMode.WRAP); animation.setIndexWrap(IndexWrapMode.WRAP); animationPaused = false; animationListener.animationStarted(new AnimationEvent(this, animation)); } /** * Plays the given animation in a loop forever. * <p> * Animations modify the position, rotation, and scale of an object over * time. * * @param animation * the animation to loop */ public void loopAnimation(Animation animation) { loopAnimation(animation, -1); } /** * Pauses or unpauses the currently playing animation (if any). * * @param paused * whether to pause or play the animation. */ public void setAnimationPaused(boolean paused) { if (this.animationPaused == paused) return; this.animationPaused = paused; if (this.animation != null) { if (paused) { animationListener.animationPaused(new AnimationEvent(this, animation)); } else { animationListener.animationUnpaused(new AnimationEvent(this, animation)); } } } /** * Stops the currently playing animation. */ public void stopAnimation() { if (this.animation != null) { animationListener.animationStopped(new AnimationEvent(this, animation)); } this.animation = null; } private void createAnimationBehavior() { new Behavior(this) { @Override public void onUpdate() { GameObject g = getGameObject(); if (g.animation == null) { return; } if (g.animationPaused) { // Simulate pausing by moving the start-time forward. g.animationStartTime += Time.getTimePassed(); return; // No updates should occur, so don't bother. } double time = Time.getTime() - g.animationStartTime; if (g.animationLoopCounter >= 0 && time > g.animationLoopCounter * g.animation.getDuration()) { animationListener.animationEnded(new AnimationEvent(g, animation)); g.animation = null; return; } else { int lastLoopIndex = (int) (Math.max(0, time - Time.getTimePassed()) / g.animation.getDuration()); int currLoopIndex = (int) (Math.max(0, time) / g.animation.getDuration()); if (currLoopIndex > lastLoopIndex) { animationListener.animationLooped(new AnimationEvent(g, animation)); } } g.setLocalOrientation(g.animation.sample(time)); } }; } private void setMatrices(Transform translate, Transform rotate, Transform scale) { List<Transform> transforms = fxNode.getTransforms(); if (transforms.size() >= 3) { // This should happen every time except for initialization. for (int i = 0; i < 3; i++) { transforms.remove(transforms.size() - 1); } } transforms.add(localTranslate = translate); transforms.add(localRotate = rotate); transforms.add(localScale = scale); } private void setTranslateMatrix(Transform translate) { setMatrices(translate, localRotate, localScale); } private void setRotateMatrix(Transform rotate) { setMatrices(localTranslate, rotate, localScale); } private void setScaleMatrix(Transform scale) { setMatrices(localTranslate, localRotate, scale); } /** * Sets the global orientation of this object. * * @param orientation * orientation object with position, rotation, and scale. */ public void setOrientation(Orientation orientation) { setPosition(orientation.getPosition()); setRotation(orientation.getRotation()); setScale(orientation.getScale()); } /** * Sets the local orientation of this object. * * @param orientation * orientation object with position, rotation, and scale. */ public void setLocalOrientation(Orientation orientation) { setLocalPosition(orientation.getPosition()); setLocalRotation(orientation.getRotation()); setLocalScale(orientation.getScale()); } /** * Returns the global orientation of this object. * * @return the orientation object containing position, rotation, and scale */ public Orientation getOrientation() { return new Orientation(getPosition(), getRotation(), getScale()); } /** * Returns the local orientation of this object. * * @return the orientation object containing position, rotation, and scale */ public Orientation getLocalOrientation() { return new Orientation(getLocalPosition(), getLocalRotation(), getLocalScale()); } /** * Sets the global position. * * @param position * the 2D position */ public void setPosition(Position position) { try { Point2D p = getParentFxNode().getLocalToSceneTransform().inverseTransform(position.getX(), position.getY()); setTranslateMatrix(Transform.translate(p.getX(), p.getY())); } catch (NonInvertibleTransformException e) { throw new TrydentInternalException("Local -> Scene not invertable! " + e); } } /** * Sets the global rotation in degrees. * * @param rotation * the rotation about the Z axis */ public void setRotation(double rotation) { double delta = rotation - getRotation(); setLocalRotation(delta + getLocalRotation()); } /** * Sets the global scale. * * @param scale * the 2D scale */ public void setScale(Scale scale) { if (scale.getX() == 0 || scale.getY() == 0) { throw new TrydentException("Setting the x or y scale to 0 is not a good idea (tried to set scale to " + scale + ")."); } Scale old = getScale(); Scale change = scale.copy().scale(1f / old.getX(), 1f / old.getY()); setLocalScale(change.scale(getLocalScale())); } /** * Sets the position of this object relative to its parent's rotation, * position, and scale. * * @param position * the 2D position */ public void setLocalPosition(Position position) { setTranslateMatrix(Transform.translate(position.getX(), position.getY())); } /** * Sets the rotation of this object relative to its parent's rotation in * degrees. * * @param rotation * the rotation about the z axis */ public void setLocalRotation(double rotation) { setRotateMatrix(new Rotate(rotation)); } /** * Sets the scale of this object relative to its parent's scale. * * @param scale * the 2D scale */ public void setLocalScale(Scale scale) { if (scale.getX() == 0 || scale.getY() == 0) { throw new TrydentException("Setting the x or y scale to 0 is not a good idea (tried to set scale to " + scale + ")."); } setScaleMatrix(Transform.scale(scale.getX(), scale.getY())); } /** * Rotates this object in global space by the given amount. * * @param rotation * - rotation in degrees. */ public void rotate(double rotation) { setRotation(rotation + getRotation()); } /** * Translates (aka moves) this object in global space by the given amount. * * @param x * the x displacement * @param y * the y displacement */ public void translate(double x, double y) { setPosition(getPosition().add(x, y)); } /** * Translates (aka moves) this object in global space by the given amount. * * @param by * the 2D displacement */ public void translate(BaseVector<?> by) { translate(by.getX(), by.getY()); } /** * Scales this object by the given amount. Non-uniform scalings (ie not * scaling sx and sy by the same amount) are not recommended as they can * have undesirable effects (shearing) if this object has rotated children. * * @param sx * the x scale * @param sy * the y scale */ public void scale(double sx, double sy) { fxNode.getTransforms().add(Transform.scale(sx, sy)); } /** * Scales this object by the given amount. Non-uniform scalings (ie not * scaling sx and sy by the same amount) are not recommended as they can * have undesirable effects (shearing) if this object has rotated children. * * @param scale * the 2D scale */ public void scale(Scale scale) { scale(scale.getX(), scale.getY()); } /** * Returns this objects global (x, y) position. * * @return the current position (changing the returned value will not affect * the position of this object) */ public Position getPosition() { return MathTools.getTranslation(fxNode.getLocalToSceneTransform()); } /** * Returns this object's global rotation in degrees. * * @return the 2D rotation */ public double getRotation() { return MathTools.getRotation(fxNode.getLocalToSceneTransform()); } /** * Returns this object's x,y scale in global space. * * @return the 2D scale (changing the returned value will not affect the * scale of this object) */ public Scale getScale() { return MathTools.getScale(fxNode.getLocalToSceneTransform()); } /** * Return's this object's position relative to its parent's position, * rotation, and scale. * * @return the 2D position (changing the returned value will not affect the * position of this object) */ public Position getLocalPosition() { return MathTools.getTranslation(fxNode.getLocalToParentTransform()); } /** * Returns this object's rotation relative to its parent's rotation in * degrees. * * @return the 2D rotation */ public double getLocalRotation() { return MathTools.getRotation(fxNode.getLocalToParentTransform()); } /** * Returns this object's local (sx, sy) scale. * * @return the 2D scale (changing the returned value will not affect the * scale of this object) */ public Scale getLocalScale() { return MathTools.getScale(fxNode.getLocalToParentTransform()); } /** * Returns this object's underlying JavaFX Group object. This allows child * classes to modify some properties directly, such as the children list. * This should be used with caution, to avoid breaking Trydent's scene * graph. * * @return the underyling JavaFX Group. */ protected Group getFxNode() { return fxNode; } public boolean isChildOf(GameObject object) { for (GameObject obj = parent; obj != null; obj = obj.parent) { if (obj == object) { return true; } } return false; } public void setParent(GameObject object) { if (object != null && (object == this || object.isChildOf(this))) throw new TrydentException( "Time-travel paradox detected: GameObjects cannot have their descendents as their parents."); if (fxNode == null) return; Position oldPos = getPosition(); double oldRot = getRotation(); Scale oldScale = getScale(); getParentFxNode().getChildren().remove(fxNode); if (this.parent != null) { this.parent.children.remove(this); } this.parent = object; getParentFxNode().getChildren().add(fxNode); setScale(oldScale); setPosition(oldPos); setRotation(oldRot); if (this.parent != null) { this.parent.children.add(this); } } /** * Marks this object and its children for deletion. */ public void destroy() { for (GameObject child : this.children) { child.destroy(); } setParent(null); getParentFxNode().getChildren().remove(fxNode); isDestroyed = true; this.fxNode = null; } /** * Returns whether this object has been destroyed. * * @return true if this object has been destroyed. */ public boolean isDestroyed() { return isDestroyed; } public GameObject getParent() { return parent; } public void setName(String name) { this.name = name; } public String getName() { return name; } @Override public String toString() { return "GameObject[" + name + "]"; } private Group getParentFxNode() { if (this.parent == null) return TrydentEngine.getRootNode(); return this.parent.fxNode; } }