1 /******************************************************************************* 2 * Copyright 2011 See AUTHORS file. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 ******************************************************************************/ 16 17 package com.badlogic.gdx.scenes.scene2d; 18 19 import static com.badlogic.gdx.utils.Align.*; 20 21 import com.badlogic.gdx.Gdx; 22 import com.badlogic.gdx.graphics.Color; 23 import com.badlogic.gdx.graphics.g2d.Batch; 24 import com.badlogic.gdx.graphics.glutils.ShapeRenderer; 25 import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; 26 import com.badlogic.gdx.math.MathUtils; 27 import com.badlogic.gdx.math.Rectangle; 28 import com.badlogic.gdx.math.Vector2; 29 import com.badlogic.gdx.scenes.scene2d.InputEvent.Type; 30 import com.badlogic.gdx.scenes.scene2d.utils.ActorGestureListener; 31 import com.badlogic.gdx.scenes.scene2d.utils.ClickListener; 32 import com.badlogic.gdx.scenes.scene2d.utils.ScissorStack; 33 import com.badlogic.gdx.utils.Align; 34 import com.badlogic.gdx.utils.Array; 35 import com.badlogic.gdx.utils.DelayedRemovalArray; 36 import com.badlogic.gdx.utils.Pools; 37 38 /** 2D scene graph node. An actor has a position, rectangular size, origin, scale, rotation, Z index, and color. The position 39 * corresponds to the unrotated, unscaled bottom left corner of the actor. The position is relative to the actor's parent. The 40 * origin is relative to the position and is used for scale and rotation. 41 * <p> 42 * An actor has a list of in progress {@link Action actions} that are applied to the actor (often over time). These are generally 43 * used to change the presentation of the actor (moving it, resizing it, etc). See {@link #act(float)}, {@link Action} and its 44 * many subclasses. 45 * <p> 46 * An actor has two kinds of listeners associated with it: "capture" and regular. The listeners are notified of events the actor 47 * or its children receive. The regular listeners are designed to allow an actor to respond to events that have been delivered. 48 * The capture listeners are designed to allow a parent or container actor to handle events before child actors. See {@link #fire} 49 * for more details. 50 * <p> 51 * An {@link InputListener} can receive all the basic input events. More complex listeners (like {@link ClickListener} and 52 * {@link ActorGestureListener}) can listen for and combine primitive events and recognize complex interactions like multi-touch 53 * or pinch. 54 * @author mzechner 55 * @author Nathan Sweet */ 56 public class Actor { 57 private Stage stage; 58 Group parent; 59 private final DelayedRemovalArray<EventListener> listeners = new DelayedRemovalArray(0); 60 private final DelayedRemovalArray<EventListener> captureListeners = new DelayedRemovalArray(0); 61 private final Array<Action> actions = new Array(0); 62 63 private String name; 64 private Touchable touchable = Touchable.enabled; 65 private boolean visible = true, debug; 66 float x, y; 67 float width, height; 68 float originX, originY; 69 float scaleX = 1, scaleY = 1; 70 float rotation; 71 final Color color = new Color(1, 1, 1, 1); 72 private Object userObject; 73 74 /** Draws the actor. The batch is configured to draw in the parent's coordinate system. 75 * {@link Batch#draw(com.badlogic.gdx.graphics.g2d.TextureRegion, float, float, float, float, float, float, float, float, float) 76 * This draw method} is convenient to draw a rotated and scaled TextureRegion. {@link Batch#begin()} has already been called on 77 * the batch. If {@link Batch#end()} is called to draw without the batch then {@link Batch#begin()} must be called before the 78 * method returns. 79 * <p> 80 * The default implementation does nothing. 81 * @param parentAlpha Should be multiplied with the actor's alpha, allowing a parent's alpha to affect all children. */ 82 public void draw (Batch batch, float parentAlpha) { 83 } 84 85 /** Updates the actor based on time. Typically this is called each frame by {@link Stage#act(float)}. 86 * <p> 87 * The default implementation calls {@link Action#act(float)} on each action and removes actions that are complete. 88 * @param delta Time in seconds since the last frame. */ 89 public void act (float delta) { 90 Array<Action> actions = this.actions; 91 if (actions.size > 0) { 92 if (stage != null && stage.getActionsRequestRendering()) Gdx.graphics.requestRendering(); 93 for (int i = 0; i < actions.size; i++) { 94 Action action = actions.get(i); 95 if (action.act(delta) && i < actions.size) { 96 Action current = actions.get(i); 97 int actionIndex = current == action ? i : actions.indexOf(action, true); 98 if (actionIndex != -1) { 99 actions.removeIndex(actionIndex); 100 action.setActor(null); 101 i--; 102 } 103 } 104 } 105 } 106 } 107 108 /** Sets this actor as the event {@link Event#setTarget(Actor) target} and propagates the event to this actor and ancestor 109 * actors as necessary. If this actor is not in the stage, the stage must be set before calling this method. 110 * <p> 111 * Events are fired in 2 phases: 112 * <ol> 113 * <li>The first phase (the "capture" phase) notifies listeners on each actor starting at the root and propagating downward to 114 * (and including) this actor.</li> 115 * <li>The second phase notifies listeners on each actor starting at this actor and, if {@link Event#getBubbles()} is true, 116 * propagating upward to the root.</li> 117 * </ol> 118 * If the event is {@link Event#stop() stopped} at any time, it will not propagate to the next actor. 119 * @return true if the event was {@link Event#cancel() cancelled}. */ 120 public boolean fire (Event event) { 121 if (event.getStage() == null) event.setStage(getStage()); 122 event.setTarget(this); 123 124 // Collect ancestors so event propagation is unaffected by hierarchy changes. 125 Array<Group> ancestors = Pools.obtain(Array.class); 126 Group parent = this.parent; 127 while (parent != null) { 128 ancestors.add(parent); 129 parent = parent.parent; 130 } 131 132 try { 133 // Notify all parent capture listeners, starting at the root. Ancestors may stop an event before children receive it. 134 Object[] ancestorsArray = ancestors.items; 135 for (int i = ancestors.size - 1; i >= 0; i--) { 136 Group currentTarget = (Group)ancestorsArray[i]; 137 currentTarget.notify(event, true); 138 if (event.isStopped()) return event.isCancelled(); 139 } 140 141 // Notify the target capture listeners. 142 notify(event, true); 143 if (event.isStopped()) return event.isCancelled(); 144 145 // Notify the target listeners. 146 notify(event, false); 147 if (!event.getBubbles()) return event.isCancelled(); 148 if (event.isStopped()) return event.isCancelled(); 149 150 // Notify all parent listeners, starting at the target. Children may stop an event before ancestors receive it. 151 for (int i = 0, n = ancestors.size; i < n; i++) { 152 ((Group)ancestorsArray[i]).notify(event, false); 153 if (event.isStopped()) return event.isCancelled(); 154 } 155 156 return event.isCancelled(); 157 } finally { 158 ancestors.clear(); 159 Pools.free(ancestors); 160 } 161 } 162 163 /** Notifies this actor's listeners of the event. The event is not propagated to any parents. Before notifying the listeners, 164 * this actor is set as the {@link Event#getListenerActor() listener actor}. The event {@link Event#setTarget(Actor) target} 165 * must be set before calling this method. If this actor is not in the stage, the stage must be set before calling this method. 166 * @param capture If true, the capture listeners will be notified instead of the regular listeners. 167 * @return true of the event was {@link Event#cancel() cancelled}. */ 168 public boolean notify (Event event, boolean capture) { 169 if (event.getTarget() == null) throw new IllegalArgumentException("The event target cannot be null."); 170 171 DelayedRemovalArray<EventListener> listeners = capture ? captureListeners : this.listeners; 172 if (listeners.size == 0) return event.isCancelled(); 173 174 event.setListenerActor(this); 175 event.setCapture(capture); 176 if (event.getStage() == null) event.setStage(stage); 177 178 listeners.begin(); 179 for (int i = 0, n = listeners.size; i < n; i++) { 180 EventListener listener = listeners.get(i); 181 if (listener.handle(event)) { 182 event.handle(); 183 if (event instanceof InputEvent) { 184 InputEvent inputEvent = (InputEvent)event; 185 if (inputEvent.getType() == Type.touchDown) { 186 event.getStage().addTouchFocus(listener, this, inputEvent.getTarget(), inputEvent.getPointer(), 187 inputEvent.getButton()); 188 } 189 } 190 } 191 } 192 listeners.end(); 193 194 return event.isCancelled(); 195 } 196 197 /** Returns the deepest actor that contains the specified point and is {@link #getTouchable() touchable} and 198 * {@link #isVisible() visible}, or null if no actor was hit. The point is specified in the actor's local coordinate system 199 * (0,0 is the bottom left of the actor and width,height is the upper right). 200 * <p> 201 * This method is used to delegate touchDown, mouse, and enter/exit events. If this method returns null, those events will not 202 * occur on this Actor. 203 * <p> 204 * The default implementation returns this actor if the point is within this actor's bounds. 205 * @param touchable If true, the hit detection will respect the {@link #setTouchable(Touchable) touchability}. 206 * @see Touchable */ 207 public Actor hit (float x, float y, boolean touchable) { 208 if (touchable && this.touchable != Touchable.enabled) return null; 209 return x >= 0 && x < width && y >= 0 && y < height ? this : null; 210 } 211 212 /** Removes this actor from its parent, if it has a parent. 213 * @see Group#removeActor(Actor) */ 214 public boolean remove () { 215 if (parent != null) return parent.removeActor(this, true); 216 return false; 217 } 218 219 /** Add a listener to receive events that {@link #hit(float, float, boolean) hit} this actor. See {@link #fire(Event)}. 220 * @see InputListener 221 * @see ClickListener */ 222 public boolean addListener (EventListener listener) { 223 if (listener == null) throw new IllegalArgumentException("listener cannot be null."); 224 if (!listeners.contains(listener, true)) { 225 listeners.add(listener); 226 return true; 227 } 228 return false; 229 } 230 231 public boolean removeListener (EventListener listener) { 232 if (listener == null) throw new IllegalArgumentException("listener cannot be null."); 233 return listeners.removeValue(listener, true); 234 } 235 236 public Array<EventListener> getListeners () { 237 return listeners; 238 } 239 240 /** Adds a listener that is only notified during the capture phase. 241 * @see #fire(Event) */ 242 public boolean addCaptureListener (EventListener listener) { 243 if (listener == null) throw new IllegalArgumentException("listener cannot be null."); 244 if (!captureListeners.contains(listener, true)) captureListeners.add(listener); 245 return true; 246 } 247 248 public boolean removeCaptureListener (EventListener listener) { 249 if (listener == null) throw new IllegalArgumentException("listener cannot be null."); 250 return captureListeners.removeValue(listener, true); 251 } 252 253 public Array<EventListener> getCaptureListeners () { 254 return captureListeners; 255 } 256 257 public void addAction (Action action) { 258 action.setActor(this); 259 actions.add(action); 260 261 if (stage != null && stage.getActionsRequestRendering()) Gdx.graphics.requestRendering(); 262 } 263 264 public void removeAction (Action action) { 265 if (actions.removeValue(action, true)) action.setActor(null); 266 } 267 268 public Array<Action> getActions () { 269 return actions; 270 } 271 272 /** Returns true if the actor has one or more actions. */ 273 public boolean hasActions () { 274 return actions.size > 0; 275 } 276 277 /** Removes all actions on this actor. */ 278 public void clearActions () { 279 for (int i = actions.size - 1; i >= 0; i--) 280 actions.get(i).setActor(null); 281 actions.clear(); 282 } 283 284 /** Removes all listeners on this actor. */ 285 public void clearListeners () { 286 listeners.clear(); 287 captureListeners.clear(); 288 } 289 290 /** Removes all actions and listeners on this actor. */ 291 public void clear () { 292 clearActions(); 293 clearListeners(); 294 } 295 296 /** Returns the stage that this actor is currently in, or null if not in a stage. */ 297 public Stage getStage () { 298 return stage; 299 } 300 301 /** Called by the framework when this actor or any parent is added to a group that is in the stage. 302 * @param stage May be null if the actor or any parent is no longer in a stage. */ 303 protected void setStage (Stage stage) { 304 this.stage = stage; 305 } 306 307 /** Returns true if this actor is the same as or is the descendant of the specified actor. */ 308 public boolean isDescendantOf (Actor actor) { 309 if (actor == null) throw new IllegalArgumentException("actor cannot be null."); 310 Actor parent = this; 311 while (true) { 312 if (parent == null) return false; 313 if (parent == actor) return true; 314 parent = parent.parent; 315 } 316 } 317 318 /** Returns true if this actor is the same as or is the ascendant of the specified actor. */ 319 public boolean isAscendantOf (Actor actor) { 320 if (actor == null) throw new IllegalArgumentException("actor cannot be null."); 321 while (true) { 322 if (actor == null) return false; 323 if (actor == this) return true; 324 actor = actor.parent; 325 } 326 } 327 328 /** Returns true if the actor's parent is not null. */ 329 public boolean hasParent () { 330 return parent != null; 331 } 332 333 /** Returns the parent actor, or null if not in a group. */ 334 public Group getParent () { 335 return parent; 336 } 337 338 /** Called by the framework when an actor is added to or removed from a group. 339 * @param parent May be null if the actor has been removed from the parent. */ 340 protected void setParent (Group parent) { 341 this.parent = parent; 342 } 343 344 /** Returns true if input events are processed by this actor. */ 345 public boolean isTouchable () { 346 return touchable == Touchable.enabled; 347 } 348 349 public Touchable getTouchable () { 350 return touchable; 351 } 352 353 /** Determines how touch events are distributed to this actor. Default is {@link Touchable#enabled}. */ 354 public void setTouchable (Touchable touchable) { 355 this.touchable = touchable; 356 } 357 358 public boolean isVisible () { 359 return visible; 360 } 361 362 /** If false, the actor will not be drawn and will not receive touch events. Default is true. */ 363 public void setVisible (boolean visible) { 364 this.visible = visible; 365 } 366 367 /** Returns an application specific object for convenience, or null. */ 368 public Object getUserObject () { 369 return userObject; 370 } 371 372 /** Sets an application specific object for convenience. */ 373 public void setUserObject (Object userObject) { 374 this.userObject = userObject; 375 } 376 377 /** Returns the X position of the actor's left edge. */ 378 public float getX () { 379 return x; 380 } 381 382 /** Returns the X position of the specified {@link Align alignment}. */ 383 public float getX (int alignment) { 384 float x = this.x; 385 if ((alignment & right) != 0) 386 x += width; 387 else if ((alignment & left) == 0) // 388 x += width / 2; 389 return x; 390 } 391 392 public void setX (float x) { 393 if (this.x != x) { 394 this.x = x; 395 positionChanged(); 396 } 397 } 398 399 /** Returns the Y position of the actor's bottom edge. */ 400 public float getY () { 401 return y; 402 } 403 404 public void setY (float y) { 405 if (this.y != y) { 406 this.y = y; 407 positionChanged(); 408 } 409 } 410 411 /** Returns the Y position of the specified {@link Align alignment}. */ 412 public float getY (int alignment) { 413 float y = this.y; 414 if ((alignment & top) != 0) 415 y += height; 416 else if ((alignment & bottom) == 0) // 417 y += height / 2; 418 return y; 419 } 420 421 /** Sets the position of the actor's bottom left corner. */ 422 public void setPosition (float x, float y) { 423 if (this.x != x || this.y != y) { 424 this.x = x; 425 this.y = y; 426 positionChanged(); 427 } 428 } 429 430 /** Sets the position using the specified {@link Align alignment}. Note this may set the position to non-integer 431 * coordinates. */ 432 public void setPosition (float x, float y, int alignment) { 433 if ((alignment & right) != 0) 434 x -= width; 435 else if ((alignment & left) == 0) // 436 x -= width / 2; 437 438 if ((alignment & top) != 0) 439 y -= height; 440 else if ((alignment & bottom) == 0) // 441 y -= height / 2; 442 443 if (this.x != x || this.y != y) { 444 this.x = x; 445 this.y = y; 446 positionChanged(); 447 } 448 } 449 450 /** Add x and y to current position */ 451 public void moveBy (float x, float y) { 452 if (x != 0 || y != 0) { 453 this.x += x; 454 this.y += y; 455 positionChanged(); 456 } 457 } 458 459 public float getWidth () { 460 return width; 461 } 462 463 public void setWidth (float width) { 464 if (this.width != width) { 465 this.width = width; 466 sizeChanged(); 467 } 468 } 469 470 public float getHeight () { 471 return height; 472 } 473 474 public void setHeight (float height) { 475 if (this.height != height) { 476 this.height = height; 477 sizeChanged(); 478 } 479 } 480 481 /** Returns y plus height. */ 482 public float getTop () { 483 return y + height; 484 } 485 486 /** Returns x plus width. */ 487 public float getRight () { 488 return x + width; 489 } 490 491 /** Called when the actor's position has been changed. */ 492 protected void positionChanged () { 493 } 494 495 /** Called when the actor's size has been changed. */ 496 protected void sizeChanged () { 497 } 498 499 /** Called when the actor's rotation has been changed. */ 500 protected void rotationChanged () { 501 } 502 503 /** Sets the width and height. */ 504 public void setSize (float width, float height) { 505 if (this.width != width || this.height != height) { 506 this.width = width; 507 this.height = height; 508 sizeChanged(); 509 } 510 } 511 512 /** Adds the specified size to the current size. */ 513 public void sizeBy (float size) { 514 if (size != 0) { 515 width += size; 516 height += size; 517 sizeChanged(); 518 } 519 } 520 521 /** Adds the specified size to the current size. */ 522 public void sizeBy (float width, float height) { 523 if (width != 0 || height != 0) { 524 this.width += width; 525 this.height += height; 526 sizeChanged(); 527 } 528 } 529 530 /** Set bounds the x, y, width, and height. */ 531 public void setBounds (float x, float y, float width, float height) { 532 if (this.x != x || this.y != y) { 533 this.x = x; 534 this.y = y; 535 positionChanged(); 536 } 537 if (this.width != width || this.height != height) { 538 this.width = width; 539 this.height = height; 540 sizeChanged(); 541 } 542 } 543 544 public float getOriginX () { 545 return originX; 546 } 547 548 public void setOriginX (float originX) { 549 this.originX = originX; 550 } 551 552 public float getOriginY () { 553 return originY; 554 } 555 556 public void setOriginY (float originY) { 557 this.originY = originY; 558 } 559 560 /** Sets the origin position which is relative to the actor's bottom left corner. */ 561 public void setOrigin (float originX, float originY) { 562 this.originX = originX; 563 this.originY = originY; 564 } 565 566 /** Sets the origin position to the specified {@link Align alignment}. */ 567 public void setOrigin (int alignment) { 568 if ((alignment & left) != 0) 569 originX = 0; 570 else if ((alignment & right) != 0) 571 originX = width; 572 else 573 originX = width / 2; 574 575 if ((alignment & bottom) != 0) 576 originY = 0; 577 else if ((alignment & top) != 0) 578 originY = height; 579 else 580 originY = height / 2; 581 } 582 583 public float getScaleX () { 584 return scaleX; 585 } 586 587 public void setScaleX (float scaleX) { 588 this.scaleX = scaleX; 589 } 590 591 public float getScaleY () { 592 return scaleY; 593 } 594 595 public void setScaleY (float scaleY) { 596 this.scaleY = scaleY; 597 } 598 599 /** Sets the scale for both X and Y */ 600 public void setScale (float scaleXY) { 601 this.scaleX = scaleXY; 602 this.scaleY = scaleXY; 603 } 604 605 /** Sets the scale X and scale Y. */ 606 public void setScale (float scaleX, float scaleY) { 607 this.scaleX = scaleX; 608 this.scaleY = scaleY; 609 } 610 611 /** Adds the specified scale to the current scale. */ 612 public void scaleBy (float scale) { 613 scaleX += scale; 614 scaleY += scale; 615 } 616 617 /** Adds the specified scale to the current scale. */ 618 public void scaleBy (float scaleX, float scaleY) { 619 this.scaleX += scaleX; 620 this.scaleY += scaleY; 621 } 622 623 public float getRotation () { 624 return rotation; 625 } 626 627 public void setRotation (float degrees) { 628 if (this.rotation != degrees) { 629 this.rotation = degrees; 630 rotationChanged(); 631 } 632 } 633 634 /** Adds the specified rotation to the current rotation. */ 635 public void rotateBy (float amountInDegrees) { 636 if (amountInDegrees != 0) { 637 rotation += amountInDegrees; 638 rotationChanged(); 639 } 640 } 641 642 public void setColor (Color color) { 643 this.color.set(color); 644 } 645 646 public void setColor (float r, float g, float b, float a) { 647 color.set(r, g, b, a); 648 } 649 650 /** Returns the color the actor will be tinted when drawn. The returned instance can be modified to change the color. */ 651 public Color getColor () { 652 return color; 653 } 654 655 /** @see #setName(String) 656 * @return May be null. */ 657 public String getName () { 658 return name; 659 } 660 661 /** Set the actor's name, which is used for identification convenience and by {@link #toString()}. 662 * @param name May be null. 663 * @see Group#findActor(String) */ 664 public void setName (String name) { 665 this.name = name; 666 } 667 668 /** Changes the z-order for this actor so it is in front of all siblings. */ 669 public void toFront () { 670 setZIndex(Integer.MAX_VALUE); 671 } 672 673 /** Changes the z-order for this actor so it is in back of all siblings. */ 674 public void toBack () { 675 setZIndex(0); 676 } 677 678 /** Sets the z-index of this actor. The z-index is the index into the parent's {@link Group#getChildren() children}, where a 679 * lower index is below a higher index. Setting a z-index higher than the number of children will move the child to the front. 680 * Setting a z-index less than zero is invalid. */ 681 public void setZIndex (int index) { 682 if (index < 0) throw new IllegalArgumentException("ZIndex cannot be < 0."); 683 Group parent = this.parent; 684 if (parent == null) return; 685 Array<Actor> children = parent.children; 686 if (children.size == 1) return; 687 index = Math.min(index, children.size - 1); 688 if (index == children.indexOf(this, true)) return; 689 if (!children.removeValue(this, true)) return; 690 children.insert(index, this); 691 } 692 693 /** Returns the z-index of this actor. 694 * @see #setZIndex(int) */ 695 public int getZIndex () { 696 Group parent = this.parent; 697 if (parent == null) return -1; 698 return parent.children.indexOf(this, true); 699 } 700 701 /** Calls {@link #clipBegin(float, float, float, float)} to clip this actor's bounds. */ 702 public boolean clipBegin () { 703 return clipBegin(x, y, width, height); 704 } 705 706 /** Clips the specified screen aligned rectangle, specified relative to the transform matrix of the stage's Batch. The 707 * transform matrix and the stage's camera must not have rotational components. Calling this method must be followed by a call 708 * to {@link #clipEnd()} if true is returned. 709 * @return false if the clipping area is zero and no drawing should occur. 710 * @see ScissorStack */ 711 public boolean clipBegin (float x, float y, float width, float height) { 712 if (width <= 0 || height <= 0) return false; 713 Rectangle tableBounds = Rectangle.tmp; 714 tableBounds.x = x; 715 tableBounds.y = y; 716 tableBounds.width = width; 717 tableBounds.height = height; 718 Stage stage = this.stage; 719 Rectangle scissorBounds = Pools.obtain(Rectangle.class); 720 stage.calculateScissors(tableBounds, scissorBounds); 721 if (ScissorStack.pushScissors(scissorBounds)) return true; 722 Pools.free(scissorBounds); 723 return false; 724 } 725 726 /** Ends clipping begun by {@link #clipBegin(float, float, float, float)}. */ 727 public void clipEnd () { 728 Pools.free(ScissorStack.popScissors()); 729 } 730 731 /** Transforms the specified point in screen coordinates to the actor's local coordinate system. */ 732 public Vector2 screenToLocalCoordinates (Vector2 screenCoords) { 733 Stage stage = this.stage; 734 if (stage == null) return screenCoords; 735 return stageToLocalCoordinates(stage.screenToStageCoordinates(screenCoords)); 736 } 737 738 /** Transforms the specified point in the stage's coordinates to the actor's local coordinate system. */ 739 public Vector2 stageToLocalCoordinates (Vector2 stageCoords) { 740 if (parent != null) parent.stageToLocalCoordinates(stageCoords); 741 parentToLocalCoordinates(stageCoords); 742 return stageCoords; 743 } 744 745 /** Transforms the specified point in the actor's coordinates to be in the stage's coordinates. 746 * @see Stage#toScreenCoordinates(Vector2, com.badlogic.gdx.math.Matrix4) */ 747 public Vector2 localToStageCoordinates (Vector2 localCoords) { 748 return localToAscendantCoordinates(null, localCoords); 749 } 750 751 /** Transforms the specified point in the actor's coordinates to be in the parent's coordinates. */ 752 public Vector2 localToParentCoordinates (Vector2 localCoords) { 753 final float rotation = -this.rotation; 754 final float scaleX = this.scaleX; 755 final float scaleY = this.scaleY; 756 final float x = this.x; 757 final float y = this.y; 758 if (rotation == 0) { 759 if (scaleX == 1 && scaleY == 1) { 760 localCoords.x += x; 761 localCoords.y += y; 762 } else { 763 final float originX = this.originX; 764 final float originY = this.originY; 765 localCoords.x = (localCoords.x - originX) * scaleX + originX + x; 766 localCoords.y = (localCoords.y - originY) * scaleY + originY + y; 767 } 768 } else { 769 final float cos = (float)Math.cos(rotation * MathUtils.degreesToRadians); 770 final float sin = (float)Math.sin(rotation * MathUtils.degreesToRadians); 771 final float originX = this.originX; 772 final float originY = this.originY; 773 final float tox = (localCoords.x - originX) * scaleX; 774 final float toy = (localCoords.y - originY) * scaleY; 775 localCoords.x = (tox * cos + toy * sin) + originX + x; 776 localCoords.y = (tox * -sin + toy * cos) + originY + y; 777 } 778 return localCoords; 779 } 780 781 /** Converts coordinates for this actor to those of a parent actor. The ascendant does not need to be a direct parent. */ 782 public Vector2 localToAscendantCoordinates (Actor ascendant, Vector2 localCoords) { 783 Actor actor = this; 784 while (actor != null) { 785 actor.localToParentCoordinates(localCoords); 786 actor = actor.parent; 787 if (actor == ascendant) break; 788 } 789 return localCoords; 790 } 791 792 /** Converts the coordinates given in the parent's coordinate system to this actor's coordinate system. */ 793 public Vector2 parentToLocalCoordinates (Vector2 parentCoords) { 794 final float rotation = this.rotation; 795 final float scaleX = this.scaleX; 796 final float scaleY = this.scaleY; 797 final float childX = x; 798 final float childY = y; 799 if (rotation == 0) { 800 if (scaleX == 1 && scaleY == 1) { 801 parentCoords.x -= childX; 802 parentCoords.y -= childY; 803 } else { 804 final float originX = this.originX; 805 final float originY = this.originY; 806 parentCoords.x = (parentCoords.x - childX - originX) / scaleX + originX; 807 parentCoords.y = (parentCoords.y - childY - originY) / scaleY + originY; 808 } 809 } else { 810 final float cos = (float)Math.cos(rotation * MathUtils.degreesToRadians); 811 final float sin = (float)Math.sin(rotation * MathUtils.degreesToRadians); 812 final float originX = this.originX; 813 final float originY = this.originY; 814 final float tox = parentCoords.x - childX - originX; 815 final float toy = parentCoords.y - childY - originY; 816 parentCoords.x = (tox * cos + toy * sin) / scaleX + originX; 817 parentCoords.y = (tox * -sin + toy * cos) / scaleY + originY; 818 } 819 return parentCoords; 820 } 821 822 /** Draws this actor's debug lines if {@link #getDebug()} is true. */ 823 public void drawDebug (ShapeRenderer shapes) { 824 drawDebugBounds(shapes); 825 } 826 827 /** Draws a rectange for the bounds of this actor if {@link #getDebug()} is true. */ 828 protected void drawDebugBounds (ShapeRenderer shapes) { 829 if (!debug) return; 830 shapes.set(ShapeType.Line); 831 shapes.setColor(stage.getDebugColor()); 832 shapes.rect(x, y, originX, originY, width, height, scaleX, scaleY, rotation); 833 } 834 835 /** If true, {@link #drawDebug(ShapeRenderer)} will be called for this actor. */ 836 public void setDebug (boolean enabled) { 837 debug = enabled; 838 if (enabled) Stage.debug = true; 839 } 840 841 public boolean getDebug () { 842 return debug; 843 } 844 845 /** Calls {@link #setDebug(boolean)} with {@code true}. */ 846 public Actor debug () { 847 setDebug(true); 848 return this; 849 } 850 851 public String toString () { 852 String name = this.name; 853 if (name == null) { 854 name = getClass().getName(); 855 int dotIndex = name.lastIndexOf('.'); 856 if (dotIndex != -1) name = name.substring(dotIndex + 1); 857 } 858 return name; 859 } 860 } 861