How do you sort Actors in a libgdx Stage?
Asked Answered
P

8

9

I'm having trouble sorting Actors in a LibGdx Stage object. When the Stage gets rendered the images are rendered in the order they are added. Stage uses an Array to hold the Actors. I've tried setting the ZIndex of each Actor, but it still didn't sort. Then I tried creating a comparator object like this:

public class ActorComparator implements Comparator < Actor > {
    @Override
    public int compare(Actor arg0, Actor arg1) {
        if (arg0.getZIndex() < arg1.getZIndex()) {
            return -1;
        } else if (arg0.getZIndex() == arg1.getZIndex()) {
            return 0;
        } else {
            return 1;
        }
    }
}

and then when I want to do the actual comparison I did:

Collections.sort(Stage.getActors(), new ActorComparator());

It gives me the following error and won't compile:

The method sort(List<T>, Comparator<? super T>) in the type Collections 
is not applicable for the arguments (Array<Actor>, ActorComparator)

I have no clue what I'm doing wrong. Can someone explain this to me?

Photochronograph answered 21/4, 2013 at 9:51 Comment(1)
Glad I stumbled into this question. The z-index issue has had me going around in circlesBuchner
B
12

Looks like your code Stage.getActors() returns an Array of Actors instead of a List. Collections.sort() method accepts only Lists. Try:

Collections.sort(Arrays.asList(Stage.getActors().toArray()), new ActorComparator());

Update on sorting (by z-index in the question) from @James Holloway: z-index is overridden by the stage to be whatever the internal order of the Array is. So setting the Z-Index has no effect, except in the case that you set it as higher than the length of the list and then it just puts the image on top of the list (internal Stage does this). This is solved by sorting by name or ID.

Bacchanalia answered 21/4, 2013 at 9:57 Comment(4)
This doesn't throw any errors even when run but appears to have no effect. Its possible getActors() is sending a copy in which case I need to somehow get the sorted list and reset it into stage.Photochronograph
@JamesHolloway can u try overriding equals() & hashcode() in Actor class, if possible?Bacchanalia
I found a different problem. Apparently even though I'm setting the ZIndex its somehow resetting to -1. So your code probably works fine, but the problem is somewhere else.Photochronograph
Ok, so I looked around and apparently the z-index is overridden by the stage to be whatever the internal order of the Array<Actor> is. So setting the Z-Index has no effect, except in the case that you set it as higher than the length of the list and then it just puts the image on top of the list (internal Stage does this). So I solved it by sorting by the name I used. If there is an ID for each Actor I could probably do it by that too. In other words your answer is correct for sorting, but my comparator was wrong. If you could add that, it would help others that find this question.Photochronograph
L
19

Rather than the accepted answer, I like it better to prepare several "layer" groups, then adding to those group in whatever order it pleases me.

Group bg = new Group();
Group fg = new Group();
// the order is important in the following two lines
stage.addActor(bg); 
stage.addActor(fg); 

bg.addActor(whatever);
fg.addActor(whatever);
bg.addActor(whatever);
fg.addActor(whatever);

(Of course the addActor() order still matters among elements of bg, and among elements of fg, but not between an element of fg and an element of bg.)

This workflow works well in an inheritance scenario, where the base class owns protected Group layers (this.bg, this.fg, ...) and adds them in order to the stage. Then the derived class can add things to those layers without taking care of the order.

Leban answered 9/7, 2013 at 21:7 Comment(2)
This is a great answer, however i don't think it applies to what the OP needs. Your example works great for semi-static element like the HUD, background or other layers of effects. But what if you want to modify the order of the actors dynamicaly ? for example an actor will be behind a door if the Y position is greater than the door and be on front of it if the position is below the door. Maybe i'm wrong but i don't think your solution can take care of that.Mastic
This will work great until you move your camera. Actor coordinates are in world units, so you'll need to manually update the actor coordinates. For example, your background will move at the same rate as your foreground, and your UI layer will be left behind as the camera moves.Mange
B
12

Looks like your code Stage.getActors() returns an Array of Actors instead of a List. Collections.sort() method accepts only Lists. Try:

Collections.sort(Arrays.asList(Stage.getActors().toArray()), new ActorComparator());

Update on sorting (by z-index in the question) from @James Holloway: z-index is overridden by the stage to be whatever the internal order of the Array is. So setting the Z-Index has no effect, except in the case that you set it as higher than the length of the list and then it just puts the image on top of the list (internal Stage does this). This is solved by sorting by name or ID.

Bacchanalia answered 21/4, 2013 at 9:57 Comment(4)
This doesn't throw any errors even when run but appears to have no effect. Its possible getActors() is sending a copy in which case I need to somehow get the sorted list and reset it into stage.Photochronograph
@JamesHolloway can u try overriding equals() & hashcode() in Actor class, if possible?Bacchanalia
I found a different problem. Apparently even though I'm setting the ZIndex its somehow resetting to -1. So your code probably works fine, but the problem is somewhere else.Photochronograph
Ok, so I looked around and apparently the z-index is overridden by the stage to be whatever the internal order of the Array<Actor> is. So setting the Z-Index has no effect, except in the case that you set it as higher than the length of the list and then it just puts the image on top of the list (internal Stage does this). So I solved it by sorting by the name I used. If there is an ID for each Actor I could probably do it by that too. In other words your answer is correct for sorting, but my comparator was wrong. If you could add that, it would help others that find this question.Photochronograph
M
2

I have all my actors extend a DepthActor class that contains a depth value and implements Comparable<DepthActor>:

import com.badlogic.gdx.scenes.scene2d.Actor;

import java.util.Comparator;

/**
 * @author snd
 * @since May 02, 2017
 */
public class DepthActor extends Actor implements Comparable<DepthActor>
{
  public int depth;

  @Override
  public int compareTo(DepthActor o)
  {
    return depth < o.depth ? -1 : (depth > o.depth ? 1 : 0);
  }
}

Then sorting in place is trivial:

com.badlogic.gdx.utils.Sort.instance().sort( stage.getActors() );

Mange answered 2/5, 2017 at 20:0 Comment(0)
R
1

Not sure if this was recently added but what worked for me is simply calling sort on the Array of actors:

gameStage.getActors().sort(new ActorRenderComparator());

Then the ActorRenderComparator may look something like this:

public class ActorRenderComparator implements Comparator<Actor> {
    @Override
    public int compare(Actor a, Actor b) {
        if (a.getY() == b.getY()) {
            return 0;
        }

        if (a.getY() > b.getY()) {
            return -1;
        }

        return 1;
    }
}
Regenerative answered 14/8, 2020 at 15:8 Comment(0)
G
0

Probably not suitable for all cases, but I'm using LibGdx Actor.userObject for storing my z-index, and then auto sorting based on that. In your Screen or Game class:

class ActorZOrderComparator implements Comparator<Actor> {
    @Override
    public int compare(Actor o1, Actor o2) {
        if (o1 != null && o1.getUserObject() != null && o1.getUserObject() instanceof Integer
                && o2 != null && o2.getUserObject() != null && o2.getUserObject() instanceof Integer) {
            int z1 = (Integer) o1.getUserObject();
            int z2 = (Integer) o2.getUserObject();
            return z1 - z2;
        } else if (o1 != null && o2 != null) {
            return o1.getZIndex() - o2.getZIndex();
        } else if (o1 != null) {
            return -1;
        } else if (o2 != null) {
            return 1;
        } else {
            return 0;
        }
    }
}

protected ActorZOrderComparator zOrderComparator = new ActorZOrderComparator();
private boolean invalidZOrder;

protected void addActor(Actor actor) {
    stage.addActor(actor);
    if (actor.getUserObject() instanceof Integer) {
        invalidZOrder = true;
    }
}

@Override
public void render(float delta) {
    if (invalidZOrder) {
        Arrays.sort(stage.getRoot().getChildren().begin(), zOrderComparator);
        stage.getRoot().getChildren().end();
        invalidZOrder = false;
    }
}

and then you add actors using the new addActor method (not stage.addActor):

Image leftBar = new Image(skin, "leftBar");
    leftBar.setPosition(0, GameSettings.VIEWPORT_HEIGHT * 0.5f, Align.left);
    leftBar.setUserObject(1); // <-- this is the z-index, set it before calling addActor(..)
    addActor(leftBar);

This will also avoid sorting the array on each render call. Hope it helps!

Gravure answered 11/4, 2015 at 7:22 Comment(0)
P
0

I have used some float value to sort the my players. Larger the value higher the index of the player. Here is my snippet:-

    Map<Actor, Float> map = new HashMap<Actor, Float>();
    map.put(playerActor, playerArea);
    for (int i = 0; i < totalEnemies; i++) {
        if (i != playerImageNo) {
            map.put(enemyActor[i], enemyArea[i]);
        }
    }
    int noOfPlayers = map.size();

    // Sort Map based on values(size)
    Set<Entry<Actor, Float>> set = map.entrySet();
    List<Entry<Actor, Float>> list = new ArrayList<Entry<Actor, Float>>(set);
    Collections.sort(list, new Comparator<Map.Entry<Actor, Float>>() {
        @Override
        public int compare(Map.Entry<Actor, Float> o1,
                Map.Entry<Actor, Float> o2) {
            return (o2.getValue()).compareTo(o1.getValue());
        }
    });

    for (Map.Entry<Actor, Float> entry : list) {
        entry.getKey().setZIndex(noOfPlayers);          
        System.out.println("Player: " + entry.getKey() + ", index: " + noOfPlayers);
        noOfPlayers--;
    }
Parcenary answered 31/12, 2015 at 6:45 Comment(0)
P
0

My solution works for me in quick tests but there may still be some flaws. If people spot anything then please shout, but this works well for me and is fairly light.

I'm only running it on my cards (I'm writing a card game) so it only adjusts Card classes that are on the stage, and they will potentially be always above other things like Tables/Labels etc.

I've added a local int zIndex to my class which is what is used in the Comparator and is set at the beginning of the setZIndex method.

The beginning part of the method is lifted from the original in the Actor class.

@Override
public void setZIndex(int index) {
    this.zIndex = index;
    if (index < 0) throw new IllegalArgumentException("ZIndex cannot be < 0.");
    Group parent = this.getParent();
    if (parent == null) return;
    Array<Actor> children = parent.getChildren();
    if (children.size == 1) return;
    ArrayList<Card> allCards = new ArrayList<Card>();
    for(Actor child : children) {
        if(child instanceof Card) {
            allCards.add((Card)child);
        }
    }
    Collections.sort(allCards, new ZIndexComparator());
    for (Card card : allCards) {
        children.removeValue(card, false);
        children.add(card);
    }
}

class ZIndexComparator implements Comparator<Card> {
    public int compare(Card card1, Card card2) {
        return card1.zIndex - card2.zIndex;
    }
}
Paucker answered 3/1, 2016 at 14:34 Comment(0)
M
0

EASIEST SOLUTION!! I am ashamed it took me as long as it did to come up with this...

Make a "ZStage" class that extends Stage. Override the getActors method. Bubble-sort the actors based on their Y index to get their rendering order.

public class ZStage extends Stage {

public ZStage(Viewport vp){
    super(vp);
}

@Override
/** Returns the root's child actors.
 * @see Group#getChildren() */
public Array<Actor> getActors () {
    SnapshotArray<Actor> children = getRoot().getChildren();

    boolean sorting = true;
    while(sorting){
        sorting=false;
        for(int x=0; x< children.size-1; x++){
            if(children.get(x).getY() < children.get(x+1).getY()){
                children.swap(x,x+1);
                sorting=true;
            }
        }
    }

    return children;
}

}

Multifaceted answered 15/4, 2017 at 19:22 Comment(1)
If this needs to be called every frame, you are BubbleSorting every frame. That's expensive. Why not just use the gdx-provided Timsort?Mange

© 2022 - 2024 — McMap. All rights reserved.