/******************************************************************************* * Copyright 2011 See AUTHORS file. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the * specific language governing permissions and limitations under the License. ******************************************************************************/ package com.badlogic.gdx.scenes.scene2d; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.scenes.scene2d.InputEvent.Type; import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; import com.badlogic.gdx.scenes.scene2d.utils.ScissorStack; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.DelayedRemovalArray; import com.badlogic.gdx.utils.GdxRuntimeException; import com.badlogic.gdx.utils.Pools; /** * 2D scene graph node. An actor has a position, rectangular size, origin, scale, rotation, and color. The position * corresponds to the unrotated, unscaled bottom left corner of the actor. The position is relative to the actor's * parent. The origin is relative to the position and is used for scale and rotation. * <p> * An actor also has a list of actions that can manipulate the actor over time, and a list of listeners that are * notified of events the actor receives. * * @author mzechner * @author Nathan Sweet */ public class Actor { private Stage stage; private Group parent; private final DelayedRemovalArray<EventListener> listeners = new DelayedRemovalArray(0); private final DelayedRemovalArray<EventListener> captureListeners = new DelayedRemovalArray(0); private final Array<Action> actions = new Array(0); private String name; public Touchable touchable = Touchable.enabled; public boolean visible = true; public float x, y; protected float width, height; protected float originX, originY; protected float scaleX = 1, scaleY = 1; protected float rotation; protected final Color color = new Color(1, 1, 1, 1); /** * Draws the actor. The SpriteBatch is configured to draw in the parent's coordinate system. * {@link SpriteBatch#draw(com.badlogic.gdx.graphics.g2d.TextureRegion, float, float, float, float, float, float, float, float, float) * This draw method} is convenient to draw a rotated and scaled TextureRegion. {@link SpriteBatch#begin()} has * already been called on the SpriteBatch. If {@link SpriteBatch#end()} is called to draw without the SpriteBatch * then {@link SpriteBatch#begin()} must be called before the method returns. * <p> * The default implementation does nothing. * * @param parentAlpha * Should be multipied with the actor's alpha, allowing a parent's alpha to affect all children. */ public void draw(SpriteBatch batch, float parentAlpha) { } /** * Updates the actor based on time. Typically this is called each frame by {@link Stage#act(float)}. * <p> * The default implementation calls {@link Action#act(float)} on each action and removes actions that are complete. * * @param delta * Time in seconds since the last frame. */ public void act(float delta) { for (int i = 0, n = actions.size; i < n; i++) { Action action = actions.get(i); if (action.act(delta)) { actions.removeIndex(i); action.setActor(null); i--; n--; } } } /** * Sets this actor as the event {@link Event#setTarget(Actor) target} and propagates the event to this actor and * ancestor actors as necessary. If this actor is not in the stage, the stage must be set before calling this * method. * <p> * Events are fired in 2 phases. The first phase notifies listeners on each actor starting at the root and * propagating downward to (and including) this actor. The second phase notifes listeners on each actor starting at * this actor and, if {@link Event#getBubbles()} is true, propagating upward to the root. If the event is * {@link Event#stop() stopped} at any time, it will not propagate to the next actor. * * @return true of the event was {@link Event#cancel() cancelled}. */ public boolean fire(Event event) { if (event.getStage() == null) event.setStage(getStage()); event.setTarget(this); // Collect ancestors so event propagation is unaffected by hierarchy changes. Array<Group> ancestors = Pools.obtain(Array.class); Group parent = getParent(); while (parent != null) { ancestors.add(parent); parent = parent.getParent(); } try { // Notify all parent capture listeners, starting at the root. Ancestors may stop an event before children receive it. for (int i = ancestors.size - 1; i >= 0; i--) { Group currentTarget = ancestors.get(i); currentTarget.notify(event, true); if (event.isStopped()) return event.isCancelled(); } // Notify the target capture listeners. notify(event, true); if (event.isStopped()) return event.isCancelled(); // Notify the target listeners. notify(event, false); if (!event.getBubbles()) return event.isCancelled(); if (event.isStopped()) return event.isCancelled(); // Notify all parent listeners, starting at the target. Children may stop an event before ancestors receive it. for (int i = 0, n = ancestors.size; i < n; i++) { ancestors.get(i).notify(event, false); if (event.isStopped()) return event.isCancelled(); } return event.isCancelled(); } finally { ancestors.clear(); Pools.free(ancestors); } } /** * Notifies this actor's listeners of the event. The event is not propagated to any parents. Before notifying the * listeners, this actor is set as the {@link Event#getListenerActor() listener actor}. The event * {@link Event#setTarget(Actor) target} must be set before calling this method. If this actor is not in the stage, * the stage must be set before calling this method. * * @param capture * If true, the capture listeners will be notified instead of the regular listeners. * @return true of the event was {@link Event#cancel() cancelled}. */ public boolean notify(Event event, boolean capture) { if (event.getTarget() == null) throw new IllegalArgumentException("The event target cannot be null."); DelayedRemovalArray<EventListener> listeners = capture ? captureListeners : this.listeners; if (listeners.size == 0) return event.isCancelled(); event.setListenerActor(this); event.setCapture(capture); if (event.getStage() == null) event.setStage(stage); listeners.begin(); for (int i = 0, n = listeners.size; i < n; i++) { EventListener listener = listeners.get(i); if (listener.handle(event)) { event.handle(); if (event instanceof InputEvent) { InputEvent inputEvent = (InputEvent) event; if (inputEvent.getType() == Type.touchDown) { event.getStage().addTouchFocus(listener, this, inputEvent.getTarget(), inputEvent.getPointer(), inputEvent.getButton()); } } } } listeners.end(); return event.isCancelled(); } /** * Returns the deepest actor that contains the specified point and is {@link #getTouchable() touchable} and * {@link #isVisible() visible}, or null if no actor was hit. The point is specified in the actor's local coordinate * system (0,0 is the bottom left of the actor and width,height is the upper right). * <p> * This method is used to delegate touchDown events. If this method returns null, touchDown will not occur. * <p> * The default implementation returns this actor if the point is within this actor's bounds. * * @param touchable * If true, the hit detection will respect the {@link #setTouchable(Touchable) touchability}. * @see Touchable */ public Actor hit(float x, float y, boolean touchable) { if (touchable && this.touchable != Touchable.enabled) return null; return x >= 0 && x < width && y >= 0 && y < height ? this : null; } /** * Removes this actor from its parent, if it has a parent. * * @see Group#removeActor(Actor) */ public boolean remove() { if (parent != null) return parent.removeActor(this); return false; } /** * @see InputListener * @see ClickListener */ public boolean addListener(EventListener listener) { if (!listeners.contains(listener, true)) { listeners.add(listener); return true; } return false; } public boolean removeListener(EventListener listener) { return listeners.removeValue(listener, true); } public Array<EventListener> getListeners() { return listeners; } /** * Adds a listener that is only notified during the capture phase. * * @see #fire(Event) */ public boolean addCaptureListener(EventListener listener) { if (!captureListeners.contains(listener, true)) captureListeners.add(listener); return true; } public boolean removeCaptureListener(EventListener listener) { return captureListeners.removeValue(listener, true); } public Array<EventListener> getCaptureListeners() { return captureListeners; } public void addAction(Action action) { action.setActor(this); actions.add(action); } public void removeAction(Action action) { if (actions.removeValue(action, true)) action.setActor(null); } public Array<Action> getActions() { return actions; } /** Removes all actions on this actor. */ public void clearActions() { for (int i = actions.size - 1; i >= 0; i--) actions.get(i).setActor(null); actions.clear(); } /** Returns the stage that this actor is currently in, or null if not in a stage. */ public Stage getStage() { return stage; } /** * Called by the framework when this actor or any parent is added to a group that is in the stage. * * @param stage * May be null if the actor or any parent is no longer in a stage. */ protected void setStage(Stage stage) { this.stage = stage; } /** Returns true if this actor is the same as or is the descendant of the specified actor. */ public boolean isDescendantOf(Actor actor) { if (actor == null) throw new IllegalArgumentException("actor cannot be null."); Actor parent = this; while (true) { if (parent == null) return false; if (parent == actor) return true; parent = parent.getParent(); } } /** Returns true if this actor is the same as or is the ascendant of the specified actor. */ public boolean isAscendantOf(Actor actor) { if (actor == null) throw new IllegalArgumentException("actor cannot be null."); while (true) { if (actor == null) return false; if (actor == this) return true; actor = actor.getParent(); } } /** Returns true if the actor's parent is not null. */ public boolean hasParent() { return parent != null; } /** Returns the parent actor, or null if not in a stage. */ public Group getParent() { return parent; } /** * Called by the framework when an actor is added to or removed from a group. * * @param parent * May be null if the actor has been removed from the parent. */ protected void setParent(Group parent) { this.parent = parent; } public Touchable getTouchable() { return touchable; } /** Determines how touch events are distributed to this actor. Default is {@link Touchable#enabled}. */ public void setTouchable(Touchable touchable) { this.touchable = touchable; } public boolean isVisible() { return visible; } /** If false, the actor will not be drawn and will not receive touch events. Default is true. */ public void setVisible(boolean visible) { this.visible = visible; } public float getX() { return x; } public void setX(float x) { this.x = x; } public float getY() { return y; } public void setY(float y) { this.y = y; } /** Sets the x and y. */ public void setPosition(float x, float y) { setX(x); setY(y); } public void translate(float x, float y) { setX(this.x + x); setY(this.y + y); } public float getWidth() { return width; } public void setWidth(float width) { this.width = width; } public float getHeight() { return height; } public void setWidthAndHeight(float width, float height) { this.width = width; this.height = height; } public void setHeight(float height) { this.height = height; } /** Returns y plus height. */ public float getTop() { return getY() + getHeight(); } /** Returns x plus width. */ public float getRight() { return getX() + getWidth(); } /** Sets the width and height. */ public void setSize(float width, float height) { setWidth(width); setHeight(height); } /** Adds the specified size to the current size. */ public void size(float size) { setWidth(width + size); setHeight(height + size); } /** Adds the specified size to the current size. */ public void size(float width, float height) { setWidth(this.width + width); setHeight(this.height + height); } /** Set bounds the x, y, width, and height. */ public void setBounds(float x, float y, float width, float height) { setX(x); setY(y); setWidth(width); setHeight(height); } public float getOriginX() { return originX; } public void setOriginX(float originX) { this.originX = originX; } public float getOriginY() { return originY; } public void setOriginY(float originY) { this.originY = originY; } /** Sets the originx and originy. */ public void setOrigin(float originX, float originY) { setOriginX(originX); setOriginY(originY); } public float getScaleX() { return scaleX; } public void setScaleX(float scaleX) { this.scaleX = scaleX; } public float getScaleY() { return scaleY; } public void setScaleY(float scaleY) { this.scaleY = scaleY; } /** Sets the scalex and scaley. */ public void setScale(float scale) { setScaleX(scale); setScaleY(scale); } /** Sets the scalex and scaley. */ public void setScale(float scaleX, float scaleY) { setScaleX(scaleX); setScaleY(scaleY); } /** Adds the specified scale to the current scale. */ public void scale(float scale) { setScaleX(scaleX + scale); setScaleY(scaleY + scale); } /** Adds the specified scale to the current scale. */ public void scale(float scaleX, float scaleY) { setScaleX(this.scaleX + scaleX); setScaleY(this.scaleY + scaleY); } public float getRotation() { return rotation; } public void setRotation(float degrees) { this.rotation = degrees; } /** Adds the specified rotation to the current rotation. */ public void rotate(float amountInDegrees) { setRotation(rotation + amountInDegrees); } public void setColor(Color color) { this.color.set(color); } public void setColor(float r, float g, float b, float a) { color.set(r, g, b, a); } /** * Returns the color the actor will be tinted when drawn. The returned instance can be modified to change the color. */ public Color getColor() { return color; } public String getName() { return name; } /** * Sets a name for easier identification of the actor in application code. * * @see Group#findActor(String) */ public void setName(String name) { this.name = name; } /** Changes the z-order for this actor so it is in front of all siblings. */ public void toFront() { setZIndex(Integer.MAX_VALUE); } /** Changes the z-order for this actor so it is in back of all siblings. */ public void toBack() { setZIndex(0); } /** * Sets the z-index of this actor. The z-index is the index into the parent's {@link Group#getChildren() children}, * where a lower index is below a higher index. Setting a z-index higher than the number of children will move the * child to the front. Setting a z-index less than zero is invalid. */ public void setZIndex(int index) { if (index < 0) throw new IllegalArgumentException("ZIndex cannot be < 0."); Group parent = getParent(); if (parent == null) return; Array<Actor> children = parent.getChildren(); if (children.size == 1) return; if (!children.removeValue(this, true)) return; if (index >= children.size) children.add(this); else children.insert(index, this); } /** * Returns the z-index of this actor. * * @see #setZIndex(int) */ public int getZIndex() { Group parent = getParent(); if (parent == null) return -1; return parent.getChildren().indexOf(this, true); } /** Calls {@link #clipBegin(float, float, float, float)} to clip this actor's bounds. */ public boolean clipBegin() { return clipBegin(getX(), getY(), getWidth(), getHeight()); } /** * Clips the specified screen aligned rectangle, specified relative to the transform matrix of the stage's * SpriteBatch. The transform matrix and the stage's camera must not have rotational components. Calling this method * must be followed by a call to {@link #clipEnd()} if true is returned. * * @return false if the clipping area is zero and no drawing should occur. * @see ScissorStack */ public boolean clipBegin(float x, float y, float width, float height) { Rectangle tableBounds = Rectangle.tmp; tableBounds.x = x; tableBounds.y = y; tableBounds.width = width; tableBounds.height = height; Stage stage = getStage(); Rectangle scissorBounds = Pools.obtain(Rectangle.class); ScissorStack.calculateScissors(stage.getCamera(), stage.getSpriteBatch().getTransformMatrix(), tableBounds, scissorBounds); if (ScissorStack.pushScissors(scissorBounds)) return true; Pools.free(scissorBounds); return false; } /** Ends clipping begun by {@link #clipBegin(float, float, float, float)}. */ public void clipEnd() { Pools.free(ScissorStack.popScissors()); } /** Transforms the specified point in screen coordinates to the actor's local coordinate system. */ public Vector2 screenToLocalCoordinates(Vector2 screenCoords) { Stage stage = getStage(); if (stage == null) return screenCoords; return stageToLocalCoordinates(stage.screenToStageCoordinates(screenCoords)); } /** Transforms the specified point in the stage's coordinates to the actor's local coordinate system. */ public Vector2 stageToLocalCoordinates(Vector2 stageCoords) { if (parent == null) return stageCoords; parent.stageToLocalCoordinates(stageCoords); parentToLocalCoordinates(stageCoords); return stageCoords; } /** * Transforms the specified point in the actor's coordinates to be in the stage's coordinates. Note this method will * ONLY work for screen aligned, unrotated, unscaled actors! */ public Vector2 localToStageCoordinates(Vector2 localCoords) { Actor actor = this; while (actor != null) { if (actor.getRotation() != 0 || actor.getScaleX() != 1 || actor.getScaleY() != 1) throw new GdxRuntimeException("Only unrotated and unscaled actors may use this method."); localCoords.x += actor.getX(); localCoords.y += actor.getY(); actor = actor.getParent(); } return localCoords; } /** * Transforms the specified point in the actor's coordinates to be in the parent's coordinates. Note this method * will ONLY work for screen aligned, unrotated, unscaled actors! */ public Vector2 localToParentCoordinates(Vector2 localCoords) { if (getRotation() != 0 || getScaleX() != 1 || getScaleY() != 1) throw new GdxRuntimeException("Only unrotated and unscaled actors may use this method."); localCoords.x += getX(); localCoords.y += getY(); return localCoords; } /** * Converts coordinates for this actor to those of a parent actor. The ascendant does not need to be a direct * parent. */ public Vector2 localToAscendantCoordinates(Actor ascendant, Vector2 localCoords) { Actor actor = this; while (actor.getParent() != null) { actor.localToParentCoordinates(localCoords); actor = actor.getParent(); if (actor == ascendant) break; } return localCoords; } /** Converts the coordinates given in the parent's coordinate system to this actor's coordinate system. */ public Vector2 parentToLocalCoordinates(Vector2 parentCoords) { final float rotation = getRotation(); final float scaleX = getScaleX(); final float scaleY = getScaleY(); final float childX = getX(); final float childY = getY(); if (rotation == 0) { if (scaleX == 1 && scaleY == 1) { parentCoords.x -= childX; parentCoords.y -= childY; } else { final float originX = getOriginX(); final float originY = getOriginY(); if (originX == 0 && originY == 0) { parentCoords.x = (parentCoords.x - childX) / scaleX; parentCoords.y = (parentCoords.y - childY) / scaleY; } else { parentCoords.x = (parentCoords.x - childX - originX) / scaleX + originX; parentCoords.y = (parentCoords.y - childY - originY) / scaleY + originY; } } } else { final float cos = (float) Math.cos(rotation * MathUtils.degreesToRadians); final float sin = (float) Math.sin(rotation * MathUtils.degreesToRadians); final float originX = getOriginX(); final float originY = getOriginY(); if (scaleX == 1 && scaleY == 1) { if (originX == 0 && originY == 0) { float tox = parentCoords.x - childX; float toy = parentCoords.y - childY; parentCoords.x = tox * cos + toy * sin; parentCoords.y = tox * -sin + toy * cos; } else { final float worldOriginX = childX + originX; final float worldOriginY = childY + originY; final float fx = -originX; final float fy = -originY; final float x1 = cos * fx - sin * fy + worldOriginX; final float y1 = sin * fx + cos * fy + worldOriginY; final float tox = parentCoords.x - x1; final float toy = parentCoords.y - y1; parentCoords.x = tox * cos + toy * sin; parentCoords.y = tox * -sin + toy * cos; } } else { if (originX == 0 && originY == 0) { final float tox = parentCoords.x - childX; final float toy = parentCoords.y - childY; parentCoords.x = (tox * cos + toy * sin) / scaleX; parentCoords.y = (tox * -sin + toy * cos) / scaleY; } else { final float worldOriginX = childX + originX; final float worldOriginY = childY + originY; final float fx = -originX * scaleX; final float fy = -originY * scaleY; final float x1 = cos * fx - sin * fy + worldOriginX; final float y1 = sin * fx + cos * fy + worldOriginY; final float tox = parentCoords.x - x1; final float toy = parentCoords.y - y1; parentCoords.x = (tox * cos + toy * sin) / scaleX; parentCoords.y = (tox * -sin + toy * cos) / scaleY; } } } return parentCoords; } public String toString() { String name = this.name; if (name == null) { name = getClass().getName(); int dotIndex = name.lastIndexOf('.'); if (dotIndex != -1) name = name.substring(dotIndex + 1); } return name + " " + x + "," + y + " " + width + "x" + height; } }